Compare commits

..

70 Commits

Author SHA1 Message Date
Joe Savona
ee90fab4a3 [compiler] Infer types for properties after holes in array patterns
In InferTypes when we infer types for properties during destructuring, we were breaking out of the loop when we encounter a hole in the array. Instead we should just skip that element and continue inferring later properties.

Closes #34748
2025-10-14 15:35:12 -07:00
Eugene Choi
e7984651e4 [playground] Allow accordion tabs to open on error (#34844)
There was a bug where the other output passes (aside from the "Output"
tab) were unable to open on compiler error. This PR still allows for the
"Output" tab to automatically open on error, but also allows other tabs
to be opened.


https://github.com/user-attachments/assets/157bf5d6-c289-46fd-bafb-073c2e0ff52b
2025-10-14 15:07:27 -04:00
Sebastian Markbåge
5f2b571878 [DevTools] Filter out built-in stack frames (#34828)
Treat fake eval anonymous stacks as built-in. Hide built-in stack frames
unless they're used to call into a non-ignored stack frame.

The two main things to fix here is that 1) we're showing a linkified
stack for fake anonymous and 2) we're showing only built-ins when the
stack is completely internal. Meaning framework code is all noise.
2025-10-14 09:34:57 -04:00
Sebastian Markbåge
56e846921d [Flight] Exclude RSC Stream if the stream resolves in a task (#34838) 2025-10-14 14:28:47 +02:00
Sebastian Markbåge
19b71673b1 [Flight] Forward the current environment when forwarding I/O entries (#34836) 2025-10-14 13:57:48 +02:00
Sebastian "Sebbie" Silbermann
73507ec457 [DevTools] Exclude Suspense boundaries in hidden Activity (#34756) 2025-10-14 13:57:08 +02:00
Sebastian Markbåge
03a62b20fd [Flight] Look for moved debugInfo when logging component performance track (#34839) 2025-10-14 13:21:12 +02:00
Ruslan Lesiutin
b9ec735de2 [Perf Tracks]: Clear potentially large measures (#34803)
Fixes https://github.com/facebook/react/issues/34770.

We need to clear measures at some point, otherwise all these copies of
props that we end up recording will allocate too much memory in
Chromium. This adds `performance.clearMeasures(...)` calls to such cases
in DEV.

Validated that entries are still shown on Performance panel timeline.
2025-10-13 17:42:13 -04:00
Ruslan Lesiutin
47905a7950 Fix/add missing else branch for renders with no props change (#34837)
Stacked on https://github.com/facebook/react/pull/34822.
Fixes a bug introduced in https://github.com/facebook/react/pull/34370.

Just copying the lower else branch to the `properties.length` else
branch at the top.
2025-10-13 17:23:04 -04:00
Sebastian "Sebbie" Silbermann
7b971c0a55 Current behavior for excluding Component render with unchanged props from Components track (#34822)
If we rerender with the same props, the render time will not be
accounted for in the Components track. The attached test reproduces the
behavior observed in
https://codesandbox.io/p/sandbox/patient-fast-j94f2g:
<img width="1118" height="354" alt="CleanShot 2025-10-13 at 00 13 41@2x"
src="https://github.com/user-attachments/assets/4be10ee9-d529-4d98-9035-4f26f9587f52"
/>
2025-10-13 17:14:51 -04:00
Sebastian Markbåge
83ea655a0b [DevTools] Group consecutive suspended by rows by the same name (#34830)
Stacked on #34829.

This lets you get an overview more easily when there's lots of things
like scripts downloading. Pluralized the name. E.g. `script` ->
`scripts` or `fetch` -> `fetches`.

This only groups them consecutively when they'd have the same place in
the list anyway because otherwise it might cover up some kind of
waterfall effects.

<img width="404" height="225" alt="Screenshot 2025-10-13 at 12 06 51 AM"
src="https://github.com/user-attachments/assets/da204a8e-d5f7-4eb0-8c51-4cc5bfd184c4"
/>

Expanded:

<img width="407" height="360" alt="Screenshot 2025-10-13 at 12 07 00 AM"
src="https://github.com/user-attachments/assets/de3c3de9-f314-4c87-b606-31bc49eb4aba"
/>
2025-10-13 13:07:39 -04:00
Sebastian Markbåge
026abeaa5f [Flight] Respect displayName of Promise instances on the server (#34825)
This lets you assign a name to a Promise that's passed into first party
code from third party since it otherwise would have no other stack frame
to indicate its name since the whole creation stack would be in third
party.

We already respect the `displayName` on the client but it's more
complicated on the server because we don't only consider the exact
instance passed to `use()` but the whole await sequence and we can pick
any Promise along the way for consideration. Therefore this also adds a
change where we pick the Promise node for consideration if it has a name
but no stack. Where we otherwise would've picked the I/O node.

Another thing that this PR does is treat anonymous stack frames (empty
url) as third party for purposes of heuristics like "hasUnfilteredFrame"
and the name assignment. This lets you include these in the actual
generated stacks (by overriding `filterStackFrame`) but we don't
actually want them to be considered first party code in the heuristics
since it ends up favoring those stacks and using internals like
`Function.all` in name assignment.
2025-10-13 12:29:00 -04:00
Sebastian Markbåge
d7215b4970 [DevTools] Preserve the original index when sorting suspended by (#34829)
The index is both used as the key and for hydration purposes. Previously
we didn't preserve the index when sorting so the index didn't line up
which caused hydration to be the wrong slot when sorted.
2025-10-13 12:12:12 -04:00
Sebastian Markbåge
e2ce64acb9 [DevTools] Don't show the root as being non-compliant (#34827)
`isStrictModeNonCompliant` on the root just means that it supports
strict mode. It's inherited by other nodes.

It's not possible to opt-in to strict mode on the root itself but rather
right below it. So we should not mark the root as being non-compliant.

This lets you select the root in the suspense tab and it shouldn't show
as red with a warning.
2025-10-13 12:11:52 -04:00
Sebastian Markbåge
34b1567427 [DevTools] Ignore suspense boundaries, without visual representation, in the timeline (#34824)
This ignore a Suspense boundary from the timeline when it has no visual
representation. No rect. In effect, this is not blocking the user
experience.

Technically it could be an effect that mounts which can have a
side-effect which is visible.

It could also be a meta-data tag like `<title>` which is visible. We
could hoistables a virtual representation by giving them a virtual rect.
E.g. at the top of the page. This could be added after the fact.
2025-10-13 12:10:54 -04:00
Sebastian Markbåge
b467c6e949 [DevTools] Explicitly say which id to scroll to and only once (#34823)
This ensures that we don't scroll on changes to the timeline such as
when loading a new page or while the timeline is still loading.

We only auto scroll to a boundary when we perform an explicit operation
from the user.
2025-10-13 12:09:45 -04:00
Sebastian "Sebbie" Silbermann
93d4458fdc [Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components (#34831) 2025-10-13 17:58:43 +02:00
Sebastian Markbåge
1d68bce19c [Fiber] Don't unhide a node if a direct parent offscreen is still hidden (#34821)
If an inner Offscreen commits an unhide, but an outer Offscreen is still
hidden but they're controlling the same DOM node then we shouldn't
unhide the DOM node yet.

This keeps track of whether we're directly inside a hidden offscreen. It
might be better to just do the tree search instead of keeping the stack
state since it's a rare case. Although this hide/unhide path does
trigger a lot of times even when there's no change.

This was technically a bug with Suspense too but it doesn't appear
because a suspended Suspense boundary never commits its partial state.
If it did, it would trigger this same path. But it can happen with an
outer Activity and inner Suspense.
2025-10-12 19:50:06 -04:00
Hendrik Liebau
ead92181bd [Flight] Avoid unnecessary indirection when serializing debug info (#34797)
When a debug channel is hooked up, and we're serializing debug models,
if the result is an already outlined reference, we can emit it directly,
without also outlining the reference. This would create an unnecessary
indirection.

Before:

```
:N1760023808330.2688
0:D"$2"
0:D"$3"
0:D"$4"
0:"hi"

1:{"name":"Component","key":null,"env":"Server","stack":[],"props":{}}
2:{"time":3.0989999999999327}
3:"$1"
4:{"time":3.261792000000014}
```

After:

```
:N1760023786873.8916
0:D"$2"
0:D"$1"
0:D"$3"
0:"hi"

1:{"name":"Component","key":null,"env":"Server","stack":[],"props":{}}
2:{"time":2.4145829999999933}
3:{"time":2.5488749999999527}
```

Notice how the second debug info chunk is now directly referencing chunk
`1` in the debug channel, without outlining and referencing `"$1"` as
its own debug chunk `3`.

This not only simplifies the RSC payload, and reduces overhead. But more
importantly it helps the client resolve cyclic references when a model
has debug info that has a reference back to the model. The client is
currently not able to resolve such a cycle when those chunk indirections
are involved. Ideally, it would also be able to resolve them regardless,
but that requires more work. In the meantime, this fixes an immediate
issue.
2025-10-10 21:44:28 +02:00
Hendrik Liebau
d44659744f [Flight] Fix preload as attribute for stylesheets (#34760)
Follow-up to #34604. For a stylesheet, we need to render `<link
rel="preload" as="style" ...>`, and not `<link rel="preload"
as="stylesheet" ...>`.
([ref](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload#what_types_of_content_can_be_preloaded))

fixes vercel/next.js#84569
2025-10-10 21:40:56 +02:00
Sophie Alpert
8454a32f3c devtools: fix ellipsis truncation for key values (#34796)
before
<img width="349" height="73" alt="Screenshot 2025-10-09 at 11 38 03"
src="https://github.com/user-attachments/assets/93fec45d-4ef2-498f-9550-36ff807b63f9"
/>

after
<img width="349" height="73" alt="Screenshot 2025-10-09 at 11 38 39"
src="https://github.com/user-attachments/assets/cb279384-4229-4d56-a803-93c2df897754"
/>
2025-10-10 14:05:49 -04:00
Ian Duvall
06fcc8f380 [playground] Fix syntax error from crashing the Compiler playground (#34623)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

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

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

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

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

Fixes a syntax error causing the Compiler playground to crash. Resolves
https://github.com/facebook/react/issues/34622.

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

Tested locally and added a test.

<img width="1470" height="836" alt="Screenshot 2025-09-27 at 8 13 07 AM"
src="https://github.com/user-attachments/assets/29473682-94c3-49dc-9ee9-c2004062aaea"
/>
2025-10-09 12:02:55 -07:00
Anatole-Godard
91e5c3daf1 fix(devtools): remove duplicated "Display density" field in General settings (#34792)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

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

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

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

This pull request fixes a small UI issue in the React Developer Tools
settings panel.
The “Display density” field was appearing twice in the General tab.

Fix : https://github.com/facebook/react/issues/34791
2025-10-09 10:38:23 -07:00
lauren
4b3e662e4c [compiler] Add VoidUseMemo rule to RecommendedLatest (#34783)
Adds a new error category VoidUseMemo which is only enabled in the
RecommendedLatest preset for now.
2025-10-08 15:55:13 -04:00
lauren
3e1b34dc51 [compiler] Setup RecommendedLatest preset (#34782)
Renames the `recommended` property on LintRule to `preset`, to allow
exporting rules for different presets. For now the `Recommended` and
`RecommendedLatest` presets are the same, but in the next PR I will
enable more rules for the latest preset.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34782).
* #34783
* __->__ #34782
2025-10-08 15:45:22 -04:00
lauren
7568e71854 [eprh] Prepare for 7.0.0 (#34757)
For 7.0.0:

Slim down presets to just 2 configurations:

- `recommended`: legacy and flat config with all recommended rules, and
- `recommended-latest`: legacy and flat config with all recommended
rules plus new bleeding edge experimental compiler rules

Removed:
- `recommended-latest-legacy`
- `flat/recommended`

Please see the README for new install instructions.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34757).
* #34783
* #34782
* __->__ #34757
2025-10-08 15:17:31 -04:00
Ruslan Lesiutin
9724e3e66e [DevTools] Gate SuspenseTab (#34754) 2025-10-08 05:47:50 -07:00
lauren
848e0e3a4f [eprh] Update plugin config to be compatible with flat and legacy (#34762)
This has been incredibly frustrating as [ESLint's own
docs](https://eslint.org/docs/latest/extend/plugins#backwards-compatibility-for-legacy-configs)
are clearly wrong (see #34679).

This PR uses [eslint-plugin-react's
setup](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/index.js)
as a reference, where the presets are assigned to `configs.flat` (not
documented by eslint).
2025-10-07 16:23:56 -04:00
lauren
5c15c1cd34 [ci] Dry run with debug mode (#34767)
Adds `--debug` to our dry run command so we can see the npm dry run
output
2025-10-07 15:16:18 -04:00
lauren
69b4cb8df4 [ci] Allow dry run (#34765)
Allow running the compiler release script as dry run.
2025-10-07 14:44:46 -04:00
lauren
a664f5f2ee [compiler] Fix incorrect version name (#34764)
Script was using the wrong version name.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34764).
* #34765
* __->__ #34764
2025-10-07 14:32:51 -04:00
lauren
1384ea8230 [compiler] Update release script for latest tag (#34763)
Updates our release script so we can publish to the `latest` tag.
2025-10-07 14:14:50 -04:00
Sebastian Markbåge
3025aa3964 [Flight] Don't serialize toJSON in Debug path and omit wide arrays (#34759)
There's a couple of issues with serializing Buffer in the debug renders.

For one, the Node.js Buffer has a `toJSON` on it which turns the binary
data into a JSON array which is very inefficient to serialize compared
to the real buffer. For debug info we never really want to resolve these
and unlike the regular render we can't error. So this uses the trick
where we read the original value. It's still unfortunate that this
intermediate gets created at all but at least now we're not serializing
it.

Second, we have a limit on depth of objects but we didn't have a limit
on width like large arrays or typed arrays. This omits large arrays from
the payload when possible and make them deferred when there's a debug
channel.
2025-10-07 06:59:34 -07:00
Sebastian "Sebbie" Silbermann
a4eb2dfa6f Release Fragment refs to Canary (#34720)
## Overview

This PR adds the `ref` prop to `<Fragment>` in `react@canary`.

This means this API is ready for final feedback and prepared for a
semver stable release.

## What this means

Shipping Fragment refs to canary means they have gone through extensive
testing in production, we are confident in the stability of the APIs,
and we are preparing to release it in a future semver stable version.

Libraries and frameworks following the [Canary
Workflow](https://react.dev/blog/2023/05/03/react-canaries) should begin
implementing and testing these features.

## Why we follow the Canary Workflow

To prepare for semver stable, libraries should test canary features like
Fragment refs with `react@canary` to confirm compatibility and prepare
for the next semver release in a myriad of environments and
configurations used throughout the React ecosystem. This provides
libraries with ample time to catch any issues we missed before slamming
them with problems in the wider semver release.

Since these features have already gone through extensive production
testing, and we are confident they are stable, frameworks following the
[Canary Workflow](https://react.dev/blog/2023/05/03/react-canaries) can
also begin adopting canary features like Fragment refs.

This adoption is similar to how different Browsers implement new
proposed browser features before they are added to the standard. If a
frameworks adopts a canary feature, they are committing to stability for
their users by ensuring any API changes before a semver stable release
are opaque and non-breaking to their users.

Apps not using a framework are also free to adopt canary features like
Fragment refs as long as they follow the [Canary
Workflow](https://react.dev/blog/2023/05/03/react-canaries), but we
generally recommend waiting for a semver stable release unless you have
the capacity to commit to following along with the canary changes and
debugging library compatibility issues.

Waiting for semver stable means you're able to benefit from libraries
testing and confirming support, and use semver as signal for which
version of a library you can use with support of the feature.

## Docs 

Check out the ["React Labs: View Transitions, Activity, and
more"](https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more#fragment-refs)
blog post, and [the new docs for Fragment
refs`](https://react.dev/reference/react/Fragment#fragmentinstance) for
more info.
2025-10-06 21:24:24 -07:00
Sebastian "Sebbie" Silbermann
6a8c7fb6f1 Release <ViewTransition /> to Canary (#34712)
## Overview

This PR ships the View Transition APIs to `react@canary`:
- [`<ViewTransition
/>`](https://react.dev/reference/react/ViewTransition)
-
[`addTransitionType`](https://react.dev/reference/react/addTransitionType)

This means these APIs are ready for final feedback and prepare for
semver stable release.

## What this means

Shipping `<ViewTransition />` and `addTransitionType` to canary means
they have gone through extensive testing in production, we are confident
in the stability of the APIs, and we are preparing to release it in a
future semver stable version.

Libraries and frameworks following the [Canary
Workflow](https://react.dev/blog/2023/05/03/react-canaries) should begin
implementing and testing these features.

## Why we follow the Canary Workflow

To prepare for semver stable, libraries should test canary features like
`<ViewTransition />` with `react@canary` to confirm compatibility and
prepare for the next semver release in a myriad of environments and
configurations used throughout the React ecosystem. This provides
libraries with ample time to catch any issues we missed before slamming
them with problems in the wider semver release.

Since these features have already gone through extensive production
testing, and we are confident they are stable, frameworks following the
[Canary Workflow](https://react.dev/blog/2023/05/03/react-canaries) can
also begin adopting canary features like `<ViewTransition />`.

This adoption is similar to how different Browsers implement new
proposed browser features before they are added to the standard. If a
frameworks adopts a canary feature, they are committing to stability for
their users by ensuring any API changes before a semver stable release
are opaque and non-breaking to their users.

Apps not using a framework are also free to adopt canary features like
`<ViewTransition>` as long as they follow the [Canary
Workflow](https://react.dev/blog/2023/05/03/react-canaries), but we
generally recommend waiting for a semver stable release unless you have
the capacity to commit to following along with the canary changes and
debugging library compatibility issues.

Waiting for semver stable means you're able to benefit from libraries
testing and confirming support, and use semver as signal for which
version of a library you can use with support of the feature.

## Docs 

Check out the ["React Labs: View Transitions, Activity, and
more"](https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more#view-transitions)
blog post, and [the new docs for `<ViewTransition
/>`](https://react.dev/reference/react/ViewTransition) and
[`addTransitionType`](https://react.dev/reference/react/addTransitionType)
for more info.
2025-10-06 21:23:34 -07:00
lauren
b65e6fc58b Revert [eprh] Remove hermes-parser (#34747)
Adds back HermesParser to eslint-plugin-react-hooks. There are still
[external users of
Flow](https://github.com/facebook/react/pull/34719#issuecomment-3368137743)
using the plugin, so we shouldn't break the plugin for them. However, we
still have the problem of double parsing: once from eslint (which we
discard) and then another via babel/hermes parser.

In the long run we should investigate a translation layer from estree to
babel (or alternatively, update the compiler to take estree as input).
But for now, I am reverting the PR.

This does mean that [Sandpack in
react.dev](11cb6b5915/src/components/MDX/Sandpack/runESLint.tsx (L31))
cannot update to the latest eprh as HermesParser does not appear to be
able to be run in a browser. I discovered this while trying to update
eprh on react.dev last week, but didn't investigate deeply. I'll need to
double check that again to find out more.
2025-10-06 12:43:39 -04:00
lauren
c786258422 [eprh] Fix config type not being exported correctly (#34746)
Another attempt to fix #34745. I updated our fixture for eslint-v9 to
include running tsc. I believe there were 2 issues:

1. `export * from './cjs/eslint-plugin-react-hooks'` in npm/index.d.ts
was no longer correct as we updated index.ts to export default instead
of named exports
2. After fixing ^ there was a typescript error which I fixed by making
some small tweaks
2025-10-06 00:53:21 -04:00
Sebastian "Sebbie" Silbermann
1be3ce9996 [Fiber] Bail out of diffing wide objects and arrays (#34742) 2025-10-06 01:13:22 +02:00
Ruslan Lesiutin
3b2a398106 [DevTools] Bump version of react-devtools-core for react-devtools (#34740)
This one was overlooked and yarn.lock was not synced.

Related:
- https://github.com/facebook/react/pull/34692
- https://github.com/facebook/react/pull/34723
2025-10-05 13:45:41 +01:00
Ruslan Lesiutin
62ff1e61fc Revert "[DevTools] Always include the root in the timeline and select it by default" (#34739)
Reverts facebook/react#34654
2025-10-05 13:35:07 +01:00
Sebastian Markbåge
0e79784702 [DevTools] Use documentElement to override cmd+F (#34734)
We override Cmd+F to jump to our search input instead of searching
through the HTML. This is ofc critical since our view virtualized.

However, Chrome DevTools installs its own listener on the document as
well (in the bubble phase) so if we prevent it at the document level
it's too late and it ends up stealing the focus instead. If we instead
listen at the documentElement it works as intended.
2025-10-05 13:13:22 +01:00
lauren
a2329c10ff [eprh] 6.1.1 changelog (#34726)
Update changelog for 6.1.1
2025-10-03 17:58:06 -04:00
Ruslan Lesiutin
d3f84a433a [DevTools] Bump version for extensions (#34723)
`./scripts/devtools/prepare-release.js` actually does automate the
version bump, but only path / minor ones.
2025-10-03 22:03:48 +01:00
lauren
bc2356176b [ci] Fix incorrect filtering logic for prereleases (#34725)
The workflow was correctly publishing the package(s) specified in
`only`, but due to incorrect logic it would also run the 'Publish all
packages' step.
2025-10-03 16:37:55 -04:00
lauren
4fdf7cf249 [ci] Fix runtime_prereleases (#34722)
When using the "only" or "skip" option in the workflow, we were adding
the `--skipTests` param, but that isn't an actual option:
1de32a5e75/scripts/release/publish-commands/parse-params.js
2025-10-03 14:41:34 -04:00
Sebastian "Sebbie" Silbermann
614a945d9d React DevTools 7.0.0 (#34692)
[Preview](https://github.com/eps1lon/react/blob/sebbie/09-28-react_devtools_7.0.0/packages/react-devtools/CHANGELOG.md#700)

Suspense tab is omitted since that's gated on Canary or 19.3.

Will draft a separate blog post for suspended by and open-in-editor
instructions while the extension is in review.
2025-10-03 18:48:28 +01:00
Joseph Savona
d6eb735938 [compiler] Update for Zod v3/v4 compatibility (#34717)
Partial redo of #34710. The changes there tried to use `z.function(args,
return)` to be compatible across Zod v3 and v4, but Zod 4's function API
has completely changed. Instead, I've updated to just use `z.any()`
where we expect a function, and manually validate that it's a function
before we call the value. We already have validation of the return type
(also using Zod).

Co-authored-by: kolvian <eliot@pontarelli.com>
2025-10-03 10:08:20 -07:00
lauren
71753ac90a [eprh] Remove hermes-parser (#34719)
We will be focusing eslint-plugin-react-hooks as the primary OSS-only
package for our lint plugin. eslint-plugin-react-compiler will remain as
a Meta only package as some limitations of our internal infra require us
to use packages that aren't widely adopted by the rest of the industry.

This PR removes `hermes-parser`, which was meant to support parsing Flow
syntax.
2025-10-03 12:58:00 -04:00
Joseph Savona
f24d3bbc70 Update readme for eprh (#34714) 2025-10-03 09:47:34 -07:00
Joseph Savona
85c427d822 [compiler] Remove @babel/plugin-proposal-private-methods (#34715)
redo of #34458 but fixing up prettier

Co-authored-by: Arnaud Barré <arnaud.barre@carbometrix.com>
2025-10-03 09:13:55 -07:00
Sebastian Markbåge
02bd4458f7 [DevTools] Double clicking the root should jump to the beginning of the timeline (#34704)
Unlike the rects, this never toggles. It just jumps.
2025-10-03 11:52:44 -04:00
Eugene Choi
0eebd37041 [playground] Config panel quality fixes (#34611)
Fixed two small issues with the config panel in the compiler playground:
1. Object descriptions were being confined in the config box and most of
it would not be visible upon hover
2. Changed it so that "Applied Configs" would only display a valid set
of configs, rather than switching between "Invalid Configs" and the set
of options. This would be less visually jarring for users as the Output
panel already displays errors. Additionally, if users want to see the
list of config options but have a currently broken config, they would
previously not know how to fix it.

Object hover before: 
<img width="702" height="481" alt="Screenshot 2025-09-26 at 10 41 03 AM"
src="https://github.com/user-attachments/assets/b2ddec2f-16ba-41a1-be1f-96211f46764c"
/>
Hover after:
<img width="702" height="481" alt="Screenshot 2025-09-26 at 10 40 37 AM"
src="https://github.com/user-attachments/assets/dc713a22-4710-46a8-a5d7-485060cc9074"
/>

Applied Configs always displays the last valid set of configs:


https://github.com/user-attachments/assets/2fb9232f-7388-4488-9b7a-bb48bf09e4ca
2025-10-03 10:52:36 -04:00
Jack Pope
74dee8ef64 Add getClientRects to fabric fragment instance (#34545)
Stacked on #34544 

We only have getBoundingClientRect available from RN currently. This
should work as a substitute for this case because the equivalent of
multi-rect elements in RN is a nested Text component. We only include
the rects of top-level host components here so we can assume that
calling getBoundingClientRect on each child is the same result.

Tested in react-native with Fantom.
2025-10-03 09:54:33 -04:00
Jack Pope
e866b1d1e9 Add getRootNode to fabric fragment instance (#34544)
Stacked on #34533 for root fragment handling

This is the same approach as DOM, where we call getRootNode on the
parent.
    
Tests are in react-native using Fantom.
2025-10-03 09:48:37 -04:00
lauren
19f65ff179 [eprh] Remove NoUnusedOptOutDirectives (#34703)
This rule was a leftover from a while ago and doesn't actually lint
anything useful. Specifically, you get a lint error if you try to opt
out a component that isn't already bailing out. If there's a bailout the
compiler already safely skips over it, so adding `'use no memo'` there
is unnecessary.

Fixes #31407

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34703).
* __->__ #34703
* #34700
2025-10-02 19:19:01 -04:00
lauren
26b177bc5e [eprh] Fix recommended config for flat config compatibility (#34700)
Previously, the `recommended` config used the legacy ESLint format
(plugins as an array of strings). This causes errors when used with
ESLint v9's `defineConfig()` helper. This was following [eslint's own
docs](https://eslint.org/docs/latest/extend/plugins#backwards-compatibility-for-legacy-configs):

> With this approach, both configuration systems recognize
"recommended". The old config system uses the recommended key while the
current config system uses the flat/recommended key. The defineConfig()
helper first looks at the recommended key, and if that is not in the
correct format, it looks for the flat/recommended key. This allows you
an upgrade path if you’d later like to rename flat/recommended to
recommended when you no longer need to support the old config system.

However,
[`isLegacyConfig()`](https://github.com/eslint/rewrite/blob/main/packages/config-helpers/src/define-config.js#L73-L81)
(also see
[`eslintrcKeys`](https://github.com/eslint/rewrite/blob/main/packages/config-helpers/src/define-config.js#L24-L35))
function doesn't check for the `plugins` key, so our config was
incorrectly treated as flat config despite being in legacy format.

This PR fixes the issue, along with a few other fixes combined:

1. Convert `recommended` to flat config format
2. Separate basic rules (exhaustive-deps, rules-of-hooks) from compiler
rules
3. Add `recommended-latest-legacy` config for non-flat config users who
want all recommended rules (including compiler rules)
4. Adding more types for the exported config

Our shipped presets in 6.x.x will essentially be:
- `recommended-legacy`: legacy (non-flat), with basic rules only
- `recommended-latest-legacy`: legacy (non-flat), all rules (basic +
compiler)
- `flat/recommended`: flat, basic rules only (now the same as
recommended, but to avoid making a breaking change we'll just keep it
around in 6.x.x)
- `recommended-latest`: flat, all rules (basic + compiler)
- `recommended`: flat, basic rules only

In the next breaking release 7.x.x, we will collapse down the presets
into three:

- `recommended-legacy`: all recommended rules
- `recommended`: all recommended rules
- `recommended-experimental`: all recommended rules + new bleeding edge
experimental rules

Closes #34679

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34700).
* #34703
* __->__ #34700
2025-10-02 18:52:52 -04:00
lauren
056a586928 [fixtures] Update eslint fixture lockfiles (#34699)
Updates the eslint fixture lockfiles.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34699).
* #34703
* #34700
* __->__ #34699
* #34675
2025-10-02 18:42:43 -04:00
lauren
5cc3d49f72 [eprh] Add compiler rules to recommended preset (#34675)
Adds back the compiler rules to the recommended preset, intended for the
next release.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34675).
* #34703
* #34700
* #34699
* __->__ #34675
2025-10-02 18:42:30 -04:00
Eugene Choi
289f070d64 [playground] Improve DiffEditor scrollbar + view (#34691)
The previous DiffEditor view of the playground looked broken and not
cohesive. There would be parts of the scrollbar appearing on the left
side for some reason, along with two scrollbars on the right side. This
PR makes the DiffEditor look more cohesive.

Previous:


https://github.com/user-attachments/assets/1aa1c775-5940-43b2-a75a-9b46452fb78b

After:


https://github.com/user-attachments/assets/b5c04998-6a6c-4b52-b3c5-b2fef21729e0
2025-10-02 17:41:29 -04:00
Sebastian "Sebbie" Silbermann
6a8a8ef326 [Flight] Add <Activity> (#34697) 2025-10-02 23:14:52 +02:00
Sebastian Markbåge
f89ed71ddf [DevTools] Track whether to auto select when new timeline entries come on (#34698)
This auto updates to select the last entry in the timeline until we make
the first selection. That way when new content loads in, we show the
last timeline of what is visible.
2025-10-02 17:06:52 -04:00
Josh Story
7d9f876cbc [Fizz] Detatch boundary after flushing segment with boundary (#34694)
When we flush a Suspense boundary we might not flush the fallback
segment, it might only flush a placeholder instead. In this case the
segment can flush again but we do not want to flush the boundary itself
a second time. We now detach the boundary after flushing it.

better solution to: https://github.com/facebook/react/pull/34668
2025-10-02 13:21:57 -07:00
Sebastian "Sebbie" Silbermann
df3562dc7f Fix DevTools regression tests (#34696) 2025-10-02 21:52:50 +02:00
Sebastian Markbåge
b56907db51 [DevTools] Show Props as Read-only for Suspense/Activity but below (#34695)
Somehow my last commit didn't make it in #34630.
2025-10-02 15:29:38 -04:00
Sebastian Markbåge
c825f03067 [DevTools] Hide State and Props in the Sidebar for Suspense (#34630)
We're showing too much noise in the side-panel when selecting a Suspense
boundary. The interesting thing to see directly is the "Suspended by".

The "props" are mostly useless because the `"name"` prop is already in
the tree. I'm now also showing it in the title bar of the selected
element panel. The "children" and "fallback" props are just the thing
that you can see in the tree view anyway.

The "state" is this weird section with just one field in it, which we
already have duplicated in the top toolbar as well. We can just delete
this. I make sure to show the icon and a "suspended..." section while
the boundary is still loading but now yet resuspended by force
suspending.

While still loading:

<img width="600" height="193" alt="Screenshot 2025-09-27 at 11 54 37 PM"
src="https://github.com/user-attachments/assets/1c3f3a96-46e0-4b11-806f-032569c7d5b5"
/>

After loading:

<img width="602" height="266" alt="Screenshot 2025-09-27 at 11 54 53 PM"
src="https://github.com/user-attachments/assets/c43cc4cb-036f-4ced-9b0d-226c6320cd76"
/>

Resuspended after loading:

<img width="602" height="300" alt="Screenshot 2025-09-27 at 11 55 07 PM"
src="https://github.com/user-attachments/assets/0be01735-48a7-47dc-b5cf-e72ec71e0148"
/>
2025-10-02 15:18:41 -04:00
Sebastian Markbåge
2e68dc76a4 [DevTools] Give a distinct color to the root (#34690)
Stacked on #34654.

The root is special since it represents "Initial Paint" (or a
"Transition" when an Activity is selected). This gives it a different
color in the timeline as well as gives it an outline that's clickable.
Hovering the timeline now shows "Initial Paint" or "Suspense".

Also made the cursor a pointer to invite you to try to click things and
some rounded corners.

<img width="1219" height="420" alt="Screenshot 2025-10-02 at 1 26 38 PM"
src="https://github.com/user-attachments/assets/12451f93-8917-4f3b-8f01-930129e5fc13"
/>

<img width="1217" height="419" alt="Screenshot 2025-10-02 at 1 26 54 PM"
src="https://github.com/user-attachments/assets/02b5e94c-3fbe-488d-b0f2-225b73578608"
/>

<img width="1215" height="419" alt="Screenshot 2025-10-02 at 1 27 24 PM"
src="https://github.com/user-attachments/assets/c24e8861-e74a-4ccc-8643-ee9d04bef43c"
/>

<img width="1216" height="419" alt="Screenshot 2025-10-02 at 1 27 10 PM"
src="https://github.com/user-attachments/assets/d5cc2b62-fa64-41bf-b485-116b1cd67467"
/>
2025-10-02 14:37:03 -04:00
Sebastian Markbåge
ced705d756 [DevTools] Always include the root in the timeline and select it by default (#34654)
Rebased on #34454.

Always include the root in the timeline even if it has no unique
suspenders, since even if it won't suspend, we have to be able to see
that and step to one step before the next boundary to see the first
boundary that does suspend in its fallback state.

Also, if there's no current selection on initial mount, select the last
entry in the timeline. We usually do this with `selectedSuspenseID` but
that doesn't happen on initial load. So this does it on initial load if
nothing else is selected by then. That way when you reload you get the
initial root selected.

There's a problem here because we should really use one source of truth
and `selectedSuspenseID` doesn't really do anything now. Either it
should be its separate source of truth and you can't show components in
the side-panel or it should be derived from the other state.

If it's derived, once there's a selection, e.g. in the root, then even
if new timelines load it will never change but that's probably a good
thing.
2025-10-02 14:20:02 -04:00
Joseph Savona
70b52beca6 [compiler] @enablePreserveExistingMemoizationGuarantees on by default (#34689)
This enables `@enablePreserveExistingMemoizationGuarantees` by default.
As of the previous PR (#34503), this mode now enables the following
behaviors:

- Treating variables referenced within a `useMemo()` or `useCallback()`
as "frozen" (immutable) as of the start of the call. Ie, the compiler
will assume that the values you reference are not mutated by the body of
the useMemo, not are they mutated later. Directly modifying them (eg
`var.property = true`) will be an error.
- Similarly, the results of the useMemo/useCallback are treated as
frozen (immutable) after the call.

These two rules match the behavior for other hooks: this means that
developers will see similar behavior to swapping out `useMemo()` for a
custom `useMyMemo()` wrapper/alias.

Additionally, as of #34503 the compiler uses information from the manual
dependencies to know which variables are non-nullable. Even if a useMemo
block conditionally accesses a nested property — `if (cond) { log(x.y.z)
}` — where the compiler would not usually know that `x` is non-nullable,
if the user specifies `x.y.z` as a manual dependency then the compiler
knows that `x` and `x.y` are non-nullable and can infer a more precise
dependency.

Finally, this mode also ensures that we always memoize function calls
that return primitives. See #34343 for more details.

For now, I've explicitly opted out of this feature in all test fixtures
where the behavior changed.
2025-10-02 10:25:00 -07:00
Sebastian "Sebbie" Silbermann
4a28227960 [DevTools] Inspect the Initial Paint when inspecting a Root (#34454) 2025-10-02 19:18:15 +02:00
Sebastian "Sebbie" Silbermann
e4a27db283 [DevTools] Defer Suspense tab to 19.3.0-canary (#34688) 2025-10-02 19:13:52 +02:00
150 changed files with 4626 additions and 3203 deletions

View File

@@ -19,6 +19,9 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
secrets:
NPM_TOKEN:
required: true
@@ -55,7 +58,13 @@ jobs:
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Publish packages to npm
- if: inputs.dry_run == true
name: Publish packages to npm (dry run)
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
- if: inputs.dry_run != true
name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}

View File

@@ -17,6 +17,9 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
permissions: {}
@@ -33,5 +36,6 @@ jobs:
dist_tag: ${{ inputs.dist_tag }}
version_name: ${{ inputs.version_name }}
tag_version: ${{ inputs.tag_version }}
dry_run: ${{ inputs.dry_run }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -19,5 +19,6 @@ jobs:
release_channel: experimental
dist_tag: experimental
version_name: '0.0.0'
dry_run: false
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -82,7 +82,6 @@ jobs:
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
@@ -91,11 +90,10 @@ jobs:
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \

View File

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

View File

@@ -314,6 +314,36 @@ test('disableMemoizationForDebugging flag works as expected', async ({
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
});
test('error is displayed when source has syntax error', async ({page}) => {
const syntaxErrorSource = `function TestComponent(props) {
const oops = props.
return (
<>{oops}</>
);
}`;
const store: Store = {
source: syntaxErrorSource,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`);
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/08-source-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain(
'Expected identifier to be defined before being used',
);
});
TEST_CASE_INPUTS.forEach((t, idx) =>
test(`playground compiles: ${t.name}`, async ({page}) => {
const store: Store = {

View File

@@ -7,7 +7,6 @@
import {Resizable} from 're-resizable';
import React, {
useCallback,
useId,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
@@ -23,7 +22,6 @@ export default function AccordionWindow(props: {
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
changedPasses: Set<string>;
isFailure: boolean;
}): React.ReactElement {
return (
<div className="flex-1 min-w-[550px] sm:min-w-0">
@@ -37,7 +35,6 @@ export default function AccordionWindow(props: {
tabsOpen={props.tabsOpen}
setTabsOpen={props.setTabsOpen}
hasChanged={props.changedPasses.has(name)}
isFailure={props.isFailure}
/>
);
})}
@@ -52,7 +49,6 @@ function AccordionWindowItem({
tabsOpen,
setTabsOpen,
hasChanged,
isFailure,
}: {
name: string;
tabs: TabsRecord;
@@ -62,11 +58,11 @@ function AccordionWindowItem({
isFailure: boolean;
}): React.ReactElement {
const id = useId();
const isShow = isFailure ? name === 'Output' : tabsOpen.has(name);
const isShow = tabsOpen.has(name);
const transitionName = `accordion-window-item-${id}`;
const toggleTabs = () => {
const toggleTabs = (): void => {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
const nextState = new Set(tabsOpen);

View File

@@ -6,7 +6,6 @@
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {PluginOptions} from 'babel-plugin-react-compiler';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import React, {
@@ -18,9 +17,8 @@ import React, {
} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
import {monacoConfigOptions} from './monacoOptions';
import {IconChevron} from '../Icons/IconChevron';
import prettyFormat from 'pretty-format';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
@@ -29,9 +27,9 @@ import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
loader.config({monaco});
export default function ConfigEditor({
appliedOptions,
formattedAppliedConfig,
}: {
appliedOptions: PluginOptions | null;
formattedAppliedConfig: string;
}): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
@@ -49,7 +47,7 @@ export default function ConfigEditor({
setIsExpanded(false);
});
}}
appliedOptions={appliedOptions}
formattedAppliedConfig={formattedAppliedConfig}
/>
</div>
<div
@@ -71,10 +69,10 @@ export default function ConfigEditor({
function ExpandedEditor({
onToggle,
appliedOptions,
formattedAppliedConfig,
}: {
onToggle: () => void;
appliedOptions: PluginOptions | null;
onToggle: (expanded: boolean) => void;
formattedAppliedConfig: string;
}): React.ReactElement {
const store = useStore();
const dispatchStore = useStoreDispatch();
@@ -122,13 +120,6 @@ function ExpandedEditor({
});
};
const formattedAppliedOptions = appliedOptions
? prettyFormat(appliedOptions, {
printFunctionName: false,
printBasicPrototype: false,
})
: 'Invalid configs';
return (
<ViewTransition
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
@@ -158,7 +149,7 @@ function ExpandedEditor({
Config Overrides
</h2>
</div>
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'config.ts'}
language={'typescript'}
@@ -167,16 +158,7 @@ function ExpandedEditor({
onChange={handleChange}
loading={''}
className="monaco-editor-config"
options={{
...monacoOptions,
lineNumbers: 'off',
renderLineHighlight: 'none',
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
scrollBeyondLastLine: false,
glyphMargin: false,
}}
options={monacoConfigOptions}
/>
</div>
</div>
@@ -186,23 +168,16 @@ function ExpandedEditor({
Applied Configs
</h2>
</div>
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'applied-config.js'}
language={'javascript'}
value={formattedAppliedOptions}
value={formattedAppliedConfig}
loading={''}
className="monaco-editor-applied-config"
options={{
...monacoOptions,
lineNumbers: 'off',
renderLineHighlight: 'none',
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
scrollBeyondLastLine: false,
...monacoConfigOptions,
readOnly: true,
glyphMargin: false,
}}
/>
</div>

View File

@@ -5,316 +5,17 @@
* LICENSE file in the root directory of this source tree.
*/
import {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
import {
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorCategory,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
type LoggerEvent,
} from 'babel-plugin-react-compiler';
import {
useDeferredValue,
useMemo,
unstable_ViewTransition as ViewTransition,
} from 'react';
import {useDeferredValue, useMemo, useState} from 'react';
import {useStore} from '../StoreContext';
import ConfigEditor from './ConfigEditor';
import Input from './Input';
import {
CompilerOutput,
CompilerTransformOutput,
default as Output,
PrintedCompilerPipelineValue,
} from './Output';
import {transformFromAstSync} from '@babel/core';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
function parseOptions(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): PluginOptions {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
environment:
mode === 'linter'
? {
// enabled in compiler
validateRefAccessDuringRender: false,
// enabled in linter
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
validateNoVoidUseMemo: true,
}
: {
/* use defaults for compiler mode */
},
});
// Parse config overrides from config editor
let configOverrideOptions: any = {};
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
if (configOverrides.trim()) {
if (configMatch && configMatch[1]) {
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
configOverrideOptions = new Function(`return (${configString})`)();
} else {
throw new Error('Invalid override format');
}
}
const opts: PluginOptions = parsePluginOptions({
...parsedPragmaOptions,
...configOverrideOptions,
environment: {
...parsedPragmaOptions.environment,
...configOverrideOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
});
return opts;
}
function compile(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
let baseOpts: PluginOptions | null = null;
try {
baseOpts = parseOptions(source, mode, configOverrides);
} catch (err) {
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Config,
reason: `Unexpected failure when transforming configs! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
if (baseOpts) {
try {
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
// Add logger options to the parsed options
const opts = {
...baseOpts,
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent): void => {
if (event.kind === 'CompileError') {
otherErrors.push(event.detail);
}
},
},
};
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Invariant,
reason: `Unexpected failure when transforming input! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
}
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.details.push(e));
}
if (error.hasErrors()) {
return [{kind: 'err', results, error}, language, baseOpts];
}
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
baseOpts,
];
}
import {CompilerOutput, default as Output} from './Output';
import {compile} from '../../lib/compilation';
import prettyFormat from 'pretty-format';
export default function Editor(): JSX.Element {
const store = useStore();
@@ -327,6 +28,7 @@ export default function Editor(): JSX.Element {
() => compile(deferredStore.source, 'linter', deferredStore.config),
[deferredStore.source, deferredStore.config],
);
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
let mergedOutput: CompilerOutput;
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
@@ -340,11 +42,22 @@ export default function Editor(): JSX.Element {
mergedOutput = compilerOutput;
errors = compilerOutput.error.details;
}
if (appliedOptions) {
const formatted = prettyFormat(appliedOptions, {
printFunctionName: false,
printBasicPrototype: false,
});
if (formatted !== formattedAppliedConfig) {
setFormattedAppliedConfig(formatted);
}
}
return (
<>
<div className="relative flex top-14">
<div className="flex-shrink-0">
<ConfigEditor appliedOptions={appliedOptions} />
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
</div>
<div className="flex flex-1 min-w-0">
<Input language={language} errors={errors} />

View File

@@ -27,6 +27,8 @@ import {
useState,
Suspense,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import AccordionWindow from '../AccordionWindow';
import TabbedWindow from '../TabbedWindow';
@@ -35,6 +37,7 @@ import {BabelFileResult} from '@babel/core';
import {
CONFIG_PANEL_TRANSITION,
TOGGLE_INTERNALS_TRANSITION,
EXPAND_ACCORDION_TRANSITION,
} from '../../lib/transitionTypes';
import {LRUCache} from 'lru-cache';
@@ -265,8 +268,22 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
* Update the active tab back to the output or errors tab when the compilation state
* changes between success/failure.
*/
const [previousOutputKind, setPreviousOutputKind] = useState(
compilerOutput.kind,
);
const isFailure = compilerOutput.kind !== 'ok';
if (compilerOutput.kind !== previousOutputKind) {
setPreviousOutputKind(compilerOutput.kind);
if (isFailure) {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
setTabsOpen(prev => new Set(prev).add('Output'));
setActiveTab('Output');
});
}
}
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
for (const [passName, results] of compilerOutput.results) {
@@ -295,8 +312,6 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
// Display the Output tab on compilation failure
activeTabOverride={isFailure ? 'Output' : undefined}
/>
</ViewTransition>
);
@@ -315,7 +330,6 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
tabsOpen={tabsOpen}
tabs={tabs}
changedPasses={changedPasses}
isFailure={isFailure}
/>
</ViewTransition>
);
@@ -372,12 +386,18 @@ function TextTabContent({
loading={''}
options={{
...monacoOptions,
scrollbar: {
vertical: 'hidden',
},
dimension: {
width: 0,
height: 0,
},
readOnly: true,
lineNumbers: 'off',
glyphMargin: false,
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
overviewRulerLanes: 0,
}}
/>
) : (

View File

@@ -32,3 +32,14 @@ export const monacoOptions: Partial<EditorProps['options']> = {
tabSize: 2,
};
export const monacoConfigOptions: Partial<EditorProps['options']> = {
...monacoOptions,
lineNumbers: 'off',
renderLineHighlight: 'none',
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
scrollBeyondLastLine: false,
glyphMargin: false,
};

View File

@@ -17,15 +17,11 @@ export default function TabbedWindow({
tabs,
activeTab,
onTabChange,
activeTabOverride,
}: {
tabs: Map<string, React.ReactNode>;
activeTab: string;
onTabChange: (tab: string) => void;
activeTabOverride?: string;
}): React.ReactElement {
const currentActiveTab = activeTabOverride ? activeTabOverride : activeTab;
const id = useId();
const transitionName = `tab-highlight-${id}`;
@@ -41,7 +37,7 @@ export default function TabbedWindow({
<div className="flex flex-col h-full max-w-full">
<div className="flex p-2 flex-shrink-0">
{Array.from(tabs.keys()).map(tab => {
const isActive = currentActiveTab === tab;
const isActive = activeTab === tab;
return (
<button
key={tab}
@@ -77,7 +73,7 @@ export default function TabbedWindow({
})}
</div>
<div className="flex-1 overflow-hidden w-full h-full">
{tabs.get(currentActiveTab)}
{tabs.get(activeTab)}
</div>
</div>
</div>

View File

@@ -0,0 +1,308 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorCategory,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
type LoggerEvent,
} from 'babel-plugin-react-compiler';
import {transformFromAstSync} from '@babel/core';
import type {
CompilerOutput,
CompilerTransformOutput,
PrintedCompilerPipelineValue,
} from '../components/Editor/Output';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
function parseOptions(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): PluginOptions {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
environment:
mode === 'linter'
? {
// enabled in compiler
validateRefAccessDuringRender: false,
// enabled in linter
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
validateNoVoidUseMemo: true,
}
: {
/* use defaults for compiler mode */
},
});
// Parse config overrides from config editor
let configOverrideOptions: any = {};
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
if (configOverrides.trim()) {
if (configMatch && configMatch[1]) {
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
configOverrideOptions = new Function(`return (${configString})`)();
} else {
throw new Error('Invalid override format');
}
}
const opts: PluginOptions = parsePluginOptions({
...parsedPragmaOptions,
...configOverrideOptions,
environment: {
...parsedPragmaOptions.environment,
...configOverrideOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
});
return opts;
}
export function compile(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
let baseOpts: PluginOptions | null = null;
try {
baseOpts = parseOptions(source, mode, configOverrides);
} catch (err) {
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Config,
reason: `Unexpected failure when transforming configs! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
if (baseOpts) {
try {
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
// Add logger options to the parsed options
const opts = {
...baseOpts,
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent): void => {
if (event.kind === 'CompileError') {
otherErrors.push(event.detail);
}
},
},
};
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Invariant,
reason: `Unexpected failure when transforming input! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
}
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.details.push(e));
}
if (error.hasErrors() || !transformOutput) {
return [{kind: 'err', results, error}, language, baseOpts];
}
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
baseOpts,
];
}

View File

@@ -124,3 +124,11 @@
::view-transition-group(.expand-accordion) {
overflow: clip;
}
/**
* For some reason, the original Monaco editor is still visible to the
* left of the DiffEditor. This is a workaround for better visual clarity.
*/
.monaco-diff-editor .editor.original{
visibility: hidden !important;
}

View File

@@ -52,8 +52,8 @@
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"zod": "^3.22.4",
"zod-validation-error": "^2.1.0"
"zod": "^3.22.4 || ^4.0.0",
"zod-validation-error": "^3.0.3 || ^4.0.0"
},
"resolutions": {
"./**/@babel/parser": "7.7.4",

View File

@@ -536,7 +536,8 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
case ErrorCategory.StaticComponents:
case ErrorCategory.Suppression:
case ErrorCategory.Syntax:
case ErrorCategory.UseMemo: {
case ErrorCategory.UseMemo:
case ErrorCategory.VoidUseMemo: {
heading = 'Error';
break;
}
@@ -582,6 +583,10 @@ export enum ErrorCategory {
* Checking for valid usage of manual memoization
*/
UseMemo = 'UseMemo',
/**
* Checking that useMemos always return a value
*/
VoidUseMemo = 'VoidUseMemo',
/**
* Checking for higher order functions acting as factories for components/hooks
*/
@@ -669,6 +674,21 @@ export enum ErrorCategory {
FBT = 'FBT',
}
export enum LintRulePreset {
/**
* Rules that are stable and included in the `recommended` preset.
*/
Recommended = 'recommended',
/**
* Rules that are more experimental and only included in the `recommended-latest` preset.
*/
RecommendedLatest = 'recommended-latest',
/**
* Rules that are disabled.
*/
Off = 'off',
}
export type LintRule = {
// Stores the category the rule corresponds to, used to filter errors when reporting
category: ErrorCategory;
@@ -689,15 +709,14 @@ export type LintRule = {
description: string;
/**
* If true, this rule will automatically appear in the default, "recommended" ESLint
* rule set. Otherwise it will be part of an `allRules` export that developers can
* use to opt-in to showing output of all possible rules.
* Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
* any preset.
*
* NOTE: not all validations are enabled by default! Setting this flag only affects
* whether a given rule is part of the recommended set. The corresponding validation
* also should be enabled by default if you want the error to actually show up!
*/
recommended: boolean;
preset: LintRulePreset;
};
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
@@ -720,7 +739,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'automatic-effect-dependencies',
description:
'Verifies that automatic effect dependencies are compiled if opted-in',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.CapitalizedCalls: {
@@ -730,7 +749,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'capitalized-calls',
description:
'Validates against calling capitalized functions/methods instead of using JSX',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.Config: {
@@ -739,7 +758,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'config',
description: 'Validates the compiler configuration options',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.EffectDependencies: {
@@ -748,7 +767,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'memoized-effect-dependencies',
description: 'Validates that effect dependencies are memoized',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.EffectDerivationsOfState: {
@@ -758,7 +777,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'no-deriving-state-in-effects',
description:
'Validates against deriving values from state in an effect',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.EffectSetState: {
@@ -768,7 +787,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'set-state-in-effect',
description:
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.ErrorBoundaries: {
@@ -778,7 +797,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'error-boundaries',
description:
'Validates usage of error boundaries instead of try/catch for errors in child components',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.Factories: {
@@ -789,7 +808,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
description:
'Validates against higher order functions defining nested components or hooks. ' +
'Components and hooks should be defined at the module level',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.FBT: {
@@ -798,7 +817,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'fbt',
description: 'Validates usage of fbt',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.Fire: {
@@ -807,7 +826,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'fire',
description: 'Validates usage of `fire`',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.Gating: {
@@ -817,7 +836,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'gating',
description:
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.Globals: {
@@ -828,7 +847,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
description:
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.Hooks: {
@@ -842,7 +861,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
* this rule.
*/
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.Immutability: {
@@ -852,7 +871,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'immutability',
description:
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.Invariant: {
@@ -861,7 +880,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'invariant',
description: 'Internal invariants',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.PreserveManualMemo: {
@@ -873,7 +892,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
'Validates that existing manual memoized is preserved by the compiler. ' +
'React Compiler will only compile components and hooks if its inference ' +
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.Purity: {
@@ -883,7 +902,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'purity',
description:
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.Refs: {
@@ -893,7 +912,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'refs',
description:
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.RenderSetState: {
@@ -903,7 +922,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'set-state-in-render',
description:
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.StaticComponents: {
@@ -913,7 +932,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'static-components',
description:
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.Suppression: {
@@ -922,7 +941,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'rule-suppression',
description: 'Validates against suppression of other rules',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.Syntax: {
@@ -931,7 +950,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'syntax',
description: 'Validates against invalid syntax',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.Todo: {
@@ -940,7 +959,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Hint,
name: 'todo',
description: 'Unimplemented features',
recommended: false,
preset: LintRulePreset.Off,
};
}
case ErrorCategory.UnsupportedSyntax: {
@@ -950,7 +969,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'unsupported-syntax',
description:
'Validates against syntax that we do not plan to support in React Compiler',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.UseMemo: {
@@ -960,7 +979,17 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'use-memo',
description:
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.VoidUseMemo: {
return {
category,
severity: ErrorSeverity.Error,
name: 'void-use-memo',
description:
'Validates that useMemos always return a value. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
preset: LintRulePreset.RecommendedLatest,
};
}
case ErrorCategory.IncompatibleLibrary: {
@@ -970,7 +999,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'incompatible-library',
description:
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
recommended: true,
preset: LintRulePreset.Recommended,
};
}
default: {

View File

@@ -159,7 +159,7 @@ export const EnvironmentConfigSchema = z.object({
* A function that, given the name of a module, can optionally return a description
* of that module's type signature.
*/
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
moduleTypeProvider: z.nullable(z.any()).default(null),
/**
* A list of functions which the application compiles as macros, where
@@ -249,7 +249,7 @@ export const EnvironmentConfigSchema = z.object({
* Allows specifying a function that can populate HIR with type information from
* Flow
*/
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
flowTypeProvider: z.nullable(z.any()).default(null),
/**
* Enables inference of optional dependency chains. Without this flag
@@ -906,6 +906,12 @@ export class Environment {
if (moduleTypeProvider == null) {
return null;
}
if (typeof moduleTypeProvider !== 'function') {
CompilerError.throwInvalidConfig({
reason: `Expected a function for \`moduleTypeProvider\``,
loc,
});
}
const unparsedModuleConfig = moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);

View File

@@ -454,7 +454,7 @@ export function dropManualMemoization(
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() callbacks must return a value',
description: `This ${
manualMemo.loadInstr.value.kind === 'PropertyLoad'

View File

@@ -393,7 +393,7 @@ function* generateInstructionTypes(
shapeId: BuiltInArrayId,
});
} else {
break;
continue;
}
}
} else {

View File

@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
validateHooksUsage: 1,
} as any);
}).toThrowErrorMatchingInlineSnapshot(
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage"."`,
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Invalid input: expected boolean, received number at "validateHooksUsage"."`,
);
});
@@ -38,7 +38,7 @@ describe('parseConfigPragma()', () => {
],
} as any);
}).toThrowErrorMatchingInlineSnapshot(
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: AutodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
);
});

View File

@@ -0,0 +1,41 @@
## Input
```javascript
function Component(props) {
// Intentionally don't bind state, this repros a bug where we didn't
// infer the type of destructured properties after a hole in the array
let [, setState] = useState();
setState(1);
return props.foo;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: ['TodoAdd'],
isComponent: 'TodoAdd',
};
```
## Error
```
Found 1 error:
Error: Calling setState during render may trigger an infinite loop
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
error.invalid-setState-in-render-unbound-state.ts:5:2
3 | // infer the type of destructured properties after a hole in the array
4 | let [, setState] = useState();
> 5 | setState(1);
| ^^^^^^^^ Found setState() in render
6 | return props.foo;
7 | }
8 |
```

View File

@@ -0,0 +1,13 @@
function Component(props) {
// Intentionally don't bind state, this repros a bug where we didn't
// infer the type of destructured properties after a hole in the array
let [, setState] = useState();
setState(1);
return props.foo;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: ['TodoAdd'],
isComponent: 'TodoAdd',
};

View File

@@ -1,35 +0,0 @@
## Input
```javascript
function t(props) {
let [, setstate] = useState();
setstate(1);
return props.foo;
}
export const FIXTURE_ENTRYPOINT = {
fn: t,
params: ['TodoAdd'],
isComponent: 'TodoAdd',
};
```
## Code
```javascript
function t(props) {
const [, setstate] = useState();
setstate(1);
return props.foo;
}
export const FIXTURE_ENTRYPOINT = {
fn: t,
params: ["TodoAdd"],
isComponent: "TodoAdd",
};
```

View File

@@ -1,11 +0,0 @@
function t(props) {
let [, setstate] = useState();
setstate(1);
return props.foo;
}
export const FIXTURE_ENTRYPOINT = {
fn: t,
params: ['TodoAdd'],
isComponent: 'TodoAdd',
};

View File

@@ -0,0 +1,52 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useCallback, useTransition} from 'react';
function useFoo() {
const [, /* isPending intentionally not captured */ start] = useTransition();
return useCallback(() => {
start();
}, []);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { useCallback, useTransition } from "react";
function useFoo() {
const $ = _c(1);
const [, start] = useTransition();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
start();
};
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
### Eval output
(kind: ok) "[[ function params=0 ]]"

View File

@@ -0,0 +1,15 @@
// @validatePreserveExistingMemoizationGuarantees
import {useCallback, useTransition} from 'react';
function useFoo() {
const [, /* isPending intentionally not captured */ start] = useTransition();
return useCallback(() => {
start();
}, []);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};

View File

@@ -0,0 +1,55 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {useCallback, useTransition} from 'react';
function useFoo() {
const [, /* state value intentionally not captured */ setState] = useState();
return useCallback(() => {
setState(x => x + 1);
}, []);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { useCallback, useTransition } from "react";
function useFoo() {
const $ = _c(1);
const [, setState] = useState();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
setState(_temp);
};
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp(x) {
return x + 1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};
```
### Eval output
(kind: exception) useState is not defined

View File

@@ -0,0 +1,15 @@
// @validatePreserveExistingMemoizationGuarantees
import {useCallback, useTransition} from 'react';
function useFoo() {
const [, /* state value intentionally not captured */ setState] = useState();
return useCallback(() => {
setState(x => x + 1);
}, []);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [],
};

View File

@@ -14,6 +14,7 @@ export {
ErrorSeverity,
ErrorCategory,
LintRules,
LintRulePreset,
type CompilerErrorDetailOptions,
type CompilerDiagnosticOptions,
type CompilerDiagnosticDetail,

View File

@@ -1,58 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {NoUnusedDirectivesRule} from '../src/rules/ReactCompilerRule';
import {normalizeIndent, testRule} from './shared-utils';
testRule('no unused directives rule', NoUnusedDirectivesRule, {
valid: [],
invalid: [
{
name: "Unused 'use no forget' directive is reported when no errors are present on components",
code: normalizeIndent`
function Component() {
'use no forget';
return <div>Hello world</div>
}
`,
errors: [
{
message: "Unused 'use no forget' directive",
suggestions: [
{
output:
// yuck
'\nfunction Component() {\n \n return <div>Hello world</div>\n}\n',
},
],
},
],
},
{
name: "Unused 'use no forget' directive is reported when no errors are present on non-components or hooks",
code: normalizeIndent`
function notacomponent() {
'use no forget';
return 1 + 1;
}
`,
errors: [
{
message: "Unused 'use no forget' directive",
suggestions: [
{
output:
// yuck
'\nfunction notacomponent() {\n \n return 1 + 1;\n}\n',
},
],
},
],
},
],
});

View File

@@ -10,6 +10,5 @@ module.exports = {
plugins: [
['@babel/plugin-transform-private-property-in-object', {loose: true}],
['@babel/plugin-transform-class-properties', {loose: true}],
['@babel/plugin-transform-private-methods', {loose: true}],
],
};

View File

@@ -14,10 +14,9 @@
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"hermes-parser": "^0.25.1",
"zod": "^3.22.4",
"zod-validation-error": "^3.0.3"
"zod": "^3.22.4 || ^4.0.0",
"zod-validation-error": "^3.0.3 || ^4.0.0"
},
"devDependencies": {
"@babel/preset-env": "^7.22.4",

View File

@@ -15,6 +15,7 @@ import type {Linter, Rule} from 'eslint';
import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler';
import {
ErrorSeverity,
LintRulePreset,
LintRules,
type LintRule,
} from 'babel-plugin-react-compiler/src/CompilerError';
@@ -150,7 +151,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
type: 'problem',
docs: {
description: rule.description,
recommended: rule.recommended,
recommended: rule.preset === LintRulePreset.Recommended,
},
fixable: 'code',
hasSuggestions: true,
@@ -161,69 +162,30 @@ function makeRule(rule: LintRule): Rule.RuleModule {
};
}
export const NoUnusedDirectivesRule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
recommended: true,
},
fixable: 'code',
hasSuggestions: true,
// validation is done at runtime with zod
schema: [{type: 'object', additionalProperties: true}],
},
create(context: Rule.RuleContext): Rule.RuleListener {
const results = getReactCompilerResult(context);
for (const directive of results.unusedOptOutDirectives) {
context.report({
message: `Unused '${directive.directive}' directive`,
loc: directive.loc,
suggest: [
{
desc: 'Remove the directive',
fix(fixer): Rule.Fix {
return fixer.removeRange(directive.range);
},
},
],
});
}
return {};
},
};
type RulesConfig = {
[name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
};
export const allRules: RulesConfig = LintRules.reduce(
(acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
},
{
'no-unused-directives': {
rule: NoUnusedDirectivesRule,
severity: ErrorSeverity.Error,
},
} as RulesConfig,
);
export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export const recommendedRules: RulesConfig = LintRules.filter(
rule => rule.recommended,
).reduce(
(acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
},
{
'no-unused-directives': {
rule: NoUnusedDirectivesRule,
severity: ErrorSeverity.Error,
},
} as RulesConfig,
);
rule => rule.preset === LintRulePreset.Recommended,
).reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export const recommendedLatestRules: RulesConfig = LintRules.filter(
rule =>
rule.preset === LintRulePreset.Recommended ||
rule.preset === LintRulePreset.RecommendedLatest,
).reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export function mapErrorSeverityToESlint(
severity: ErrorSeverity,

View File

@@ -5,20 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/
import {transformFromAstSync, traverse} from '@babel/core';
import {transformFromAstSync} from '@babel/core';
import {parse as babelParse} from '@babel/parser';
import {Directive, File} from '@babel/types';
// @ts-expect-error: no types available
import PluginProposalPrivateMethods from '@babel/plugin-proposal-private-methods';
import {File} from '@babel/types';
import BabelPluginReactCompiler, {
parsePluginOptions,
validateEnvironmentConfig,
OPT_OUT_DIRECTIVES,
type PluginOptions,
} from 'babel-plugin-react-compiler/src';
import {Logger, LoggerEvent} from 'babel-plugin-react-compiler/src/Entrypoint';
import type {SourceCode} from 'eslint';
import {SourceLocation} from 'estree';
// @ts-expect-error: no types available
import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
@@ -45,17 +41,11 @@ const COMPILER_OPTIONS: PluginOptions = {
}),
};
export type UnusedOptOutDirective = {
loc: SourceLocation;
range: [number, number];
directive: string;
};
export type RunCacheEntry = {
sourceCode: string;
filename: string;
userOpts: PluginOptions;
flowSuppressions: Array<{line: number; code: string}>;
unusedOptOutDirectives: Array<UnusedOptOutDirective>;
events: Array<LoggerEvent>;
};
@@ -87,25 +77,6 @@ function getFlowSuppressions(
return results;
}
function filterUnusedOptOutDirectives(
directives: ReadonlyArray<Directive>,
): Array<UnusedOptOutDirective> {
const results: Array<UnusedOptOutDirective> = [];
for (const directive of directives) {
if (
OPT_OUT_DIRECTIVES.has(directive.value.value) &&
directive.loc != null
) {
results.push({
loc: directive.loc,
directive: directive.value.value,
range: [directive.start!, directive.end!],
});
}
}
return results;
}
function runReactCompilerImpl({
sourceCode,
filename,
@@ -125,7 +96,6 @@ function runReactCompilerImpl({
filename,
userOpts,
flowSuppressions: [],
unusedOptOutDirectives: [],
events: [],
};
const userLogger: Logger | null = options.logger;
@@ -173,37 +143,11 @@ function runReactCompilerImpl({
filename,
highlightCode: false,
retainLines: true,
plugins: [
[PluginProposalPrivateMethods, {loose: true}],
[BabelPluginReactCompiler, options],
],
plugins: [[BabelPluginReactCompiler, options]],
sourceType: 'module',
configFile: false,
babelrc: false,
});
if (results.events.filter(e => e.kind === 'CompileError').length === 0) {
traverse(babelAST, {
FunctionDeclaration(path) {
path.node;
results.unusedOptOutDirectives.push(
...filterUnusedOptOutDirectives(path.node.body.directives),
);
},
ArrowFunctionExpression(path) {
if (path.node.body.type === 'BlockStatement') {
results.unusedOptOutDirectives.push(
...filterUnusedOptOutDirectives(path.node.body.directives),
);
}
},
FunctionExpression(path) {
results.unusedOptOutDirectives.push(
...filterUnusedOptOutDirectives(path.node.body.directives),
);
},
});
}
} catch (err) {
/* errors handled by injected logger */
}

View File

@@ -10,13 +10,7 @@ import {defineConfig} from 'tsup';
export default defineConfig({
entry: ['./src/index.ts'],
outDir: './dist',
external: [
'@babel/core',
'@babel/plugin-proposal-private-methods',
'hermes-parser',
'zod',
'zod-validation-error',
],
external: ['@babel/core', 'hermes-parser', 'zod', 'zod-validation-error'],
splitting: false,
sourcemap: false,
dts: false,

View File

@@ -17,8 +17,8 @@
"fast-glob": "^3.3.2",
"ora": "5.4.1",
"yargs": "^17.7.2",
"zod": "^3.22.4",
"zod-validation-error": "^3.0.3"
"zod": "^3.22.4 || ^4.0.0",
"zod-validation-error": "^3.0.3 || ^4.0.0"
},
"devDependencies": {},
"engines": {

View File

@@ -24,7 +24,7 @@
"html-to-text": "^9.0.5",
"prettier": "^3.3.3",
"puppeteer": "^24.7.2",
"zod": "^3.23.8"
"zod": "^3.22.4 || ^4.0.0"
},
"devDependencies": {
"@types/html-to-text": "^9.0.4",

View File

@@ -68,7 +68,7 @@ async function main() {
.option('tag', {
description: 'Tag to publish to npm',
type: 'choices',
choices: ['experimental', 'beta', 'rc'],
choices: ['experimental', 'beta', 'rc', 'latest'],
default: 'experimental',
})
.option('tag-version', {
@@ -145,10 +145,15 @@ async function main() {
files: {exclude: ['.DS_Store']},
});
const truncatedHash = hash.slice(0, 7);
let newVersion =
argv.tagVersion == null || argv.tagVersion === ''
? `${argv.versionName}-${argv.tag}`
: `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
let newVersion;
if (argv.tag === 'latest') {
newVersion = argv.versionName;
} else {
newVersion =
argv.tagVersion == null || argv.tagVersion === ''
? `${argv.versionName}-${argv.tag}`
: `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
}
if (argv.tag === 'experimental' || argv.tag === 'beta') {
newVersion = `${newVersion}-${truncatedHash}-${dateString}`;
}
@@ -181,21 +186,9 @@ async function main() {
if (otp != null) {
opts.push(`--otp=${otp}`);
}
/**
* Typically, the `latest` tag is reserved for stable package versions. Since the the compiler
* is still pre-release, until we have a stable release let's only add the
* `latest` tag to non-experimental releases.
*
* `latest` is added by default, so we only override it for experimental releases so that
* those don't get the `latest` tag.
*
* TODO: Update this when we have a stable release.
*/
if (argv.tag === 'experimental') {
opts.push('--tag=experimental');
} else {
opts.push('--tag=latest');
}
opts.push(`--tag=${argv.tag}`);
try {
await spawnHelper(
'npm',

View File

@@ -326,7 +326,7 @@
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.25.9", "@babel/helper-create-class-features-plugin@^7.27.0":
"@babel/helper-create-class-features-plugin@^7.25.9", "@babel/helper-create-class-features-plugin@^7.27.0":
version "7.27.0"
resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz"
integrity sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==
@@ -706,14 +706,6 @@
"@babel/helper-plugin-utils" "^7.25.9"
"@babel/traverse" "^7.25.9"
"@babel/plugin-proposal-private-methods@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz"
integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2":
version "7.21.0-placeholder-for-preset-env.2"
resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz"
@@ -10494,16 +10486,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -10576,14 +10559,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -11360,7 +11336,7 @@ workerpool@^6.5.1:
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -11378,15 +11354,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
@@ -11538,17 +11505,17 @@ zod-to-json-schema@^3.24.1:
resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz"
integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
zod-validation-error@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-2.1.0.tgz"
integrity sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==
"zod-validation-error@^3.0.3 || ^4.0.0":
version "4.0.2"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
zod-validation-error@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.0.3.tgz"
integrity sha512-cETTrcMq3Ze58vhdR0zD37uJm/694I6mAxcf/ei5bl89cC++fBNxrC2z8lkFze/8hVMPwrbtrwXHR2LB50fpHw==
"zod@^3.22.4 || ^4.0.0":
version "4.1.11"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5"
integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==
zod@^3.22.4, zod@^3.23.8, zod@^3.24.1:
zod@^3.23.8, zod@^3.24.1:
version "3.24.3"
resolved "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz"
integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==

View File

@@ -55,11 +55,11 @@ export default function ScrollIntoViewCase() {
const scrollContainerRef = useRef(null);
const scrollVertical = () => {
fragmentRef.current.experimental_scrollIntoView(alignToTop);
fragmentRef.current.scrollIntoView(alignToTop);
};
const scrollVerticalNoChildren = () => {
noChildRef.current.experimental_scrollIntoView(alignToTop);
noChildRef.current.scrollIntoView(alignToTop);
};
useEffect(() => {

View File

@@ -1,14 +1,11 @@
{
"root": true,
"extends": ["plugin:react-hooks/recommended-legacy"],
"extends": ["plugin:react-hooks/recommended"],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"react-hooks/exhaustive-deps": "error"
}
}

View File

@@ -141,3 +141,27 @@ function useHookInLoops() {
useHook4();
}
}
/**
* Compiler Rules
*/
// Invalid: component factory
function InvalidComponentFactory() {
const DynamicComponent = () => <div>Hello</div>;
// eslint-disable-next-line react-hooks/static-components
return <DynamicComponent />;
}
// Invalid: mutating globals
function InvalidGlobals() {
// eslint-disable-next-line react-hooks/immutability
window.myGlobal = 42;
return <div>Done</div>;
}
// Invalid: useMemo with wrong deps - triggers preserve-manual-memoization
function InvalidUseMemo({items}) {
// eslint-disable-next-line react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps
const sorted = useMemo(() => [...items].sort(), []);
return <div>{sorted.length}</div>;
}

View File

@@ -2,15 +2,7 @@
# yarn lockfile v1
"@ampproject/remapping@^2.2.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.26.2":
"@babel/code-frame@^7.0.0":
version "7.26.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
@@ -19,228 +11,11 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/compat-data@^7.26.5":
version "7.26.8"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367"
integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==
"@babel/core@^7.24.4":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9"
integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.10"
"@babel/helper-compilation-targets" "^7.26.5"
"@babel/helper-module-transforms" "^7.26.0"
"@babel/helpers" "^7.26.10"
"@babel/parser" "^7.26.10"
"@babel/template" "^7.26.9"
"@babel/traverse" "^7.26.10"
"@babel/types" "^7.26.10"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.26.10":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.10.tgz#a60d9de49caca16744e6340c3658dfef6138c3f7"
integrity sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==
dependencies:
"@babel/parser" "^7.26.10"
"@babel/types" "^7.26.10"
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4"
integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==
dependencies:
"@babel/types" "^7.25.9"
"@babel/helper-compilation-targets@^7.26.5":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8"
integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==
dependencies:
"@babel/compat-data" "^7.26.5"
"@babel/helper-validator-option" "^7.25.9"
browserslist "^4.24.0"
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.25.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz#d6f83e3039547fbb39967e78043cd3c8b7820c71"
integrity sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==
dependencies:
"@babel/helper-annotate-as-pure" "^7.25.9"
"@babel/helper-member-expression-to-functions" "^7.25.9"
"@babel/helper-optimise-call-expression" "^7.25.9"
"@babel/helper-replace-supers" "^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers" "^7.25.9"
"@babel/traverse" "^7.26.9"
semver "^6.3.1"
"@babel/helper-member-expression-to-functions@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3"
integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-module-imports@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715"
integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-module-transforms@^7.26.0":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae"
integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==
dependencies:
"@babel/helper-module-imports" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/traverse" "^7.25.9"
"@babel/helper-optimise-call-expression@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e"
integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==
dependencies:
"@babel/types" "^7.25.9"
"@babel/helper-plugin-utils@^7.25.9":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35"
integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==
"@babel/helper-replace-supers@^7.26.5":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz#6cb04e82ae291dae8e72335dfe438b0725f14c8d"
integrity sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==
dependencies:
"@babel/helper-member-expression-to-functions" "^7.25.9"
"@babel/helper-optimise-call-expression" "^7.25.9"
"@babel/traverse" "^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9"
integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/helper-validator-option@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==
"@babel/helpers@^7.26.10":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384"
integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==
dependencies:
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.10"
"@babel/parser@^7.24.4", "@babel/parser@^7.26.10", "@babel/parser@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749"
integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==
dependencies:
"@babel/types" "^7.26.10"
"@babel/plugin-transform-private-methods@^7.24.4":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57"
integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.25.9"
"@babel/helper-plugin-utils" "^7.25.9"
"@babel/template@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2"
integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/parser" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380"
integrity sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.10"
"@babel/parser" "^7.26.10"
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.10"
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.10.tgz#396382f6335bd4feb65741eacfc808218f859259"
integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@jridgewell/gen-mapping@^0.3.5":
version "0.3.8"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142"
integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==
dependencies:
"@jridgewell/set-array" "^1.2.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/set-array@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
acorn-jsx@^5.2.0:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -317,26 +92,11 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
browserslist@^4.24.0:
version "4.24.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
dependencies:
caniuse-lite "^1.0.30001688"
electron-to-chromium "^1.5.73"
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001688:
version "1.0.30001703"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz#977cb4920598c158f491ecf4f4f2cfed9e354718"
integrity sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==
chalk@^2.1.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -400,11 +160,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
cross-spawn@^6.0.5:
version "6.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57"
@@ -416,7 +171,7 @@ cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
debug@^4.0.1, debug@^4.1.0, debug@^4.3.1:
debug@^4.0.1:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
@@ -435,11 +190,6 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
electron-to-chromium@^1.5.73:
version "1.5.114"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz#f2bb4fda80a7db4ea273565e75b0ebbe19af0ac3"
integrity sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==
emoji-regex@^7.0.1:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@@ -450,11 +200,6 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -632,11 +377,6 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
glob-parent@^5.0.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -656,11 +396,6 @@ glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
globals@^12.1.0:
version "12.4.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
@@ -678,18 +413,6 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
hermes-estree@0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
hermes-parser@^0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
dependencies:
hermes-estree "0.25.1"
iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -787,11 +510,6 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
jsesc@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -802,11 +520,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
levn@^0.3.0, levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -820,13 +533,6 @@ lodash@^4.17.14, lodash@^4.17.19:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
dependencies:
yallist "^3.0.2"
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@@ -871,11 +577,6 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-releases@^2.0.19:
version "2.0.19"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -924,7 +625,7 @@ path-key@^2.0.1:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
picocolors@^1.0.0, picocolors@^1.1.1:
picocolors@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
@@ -991,7 +692,7 @@ semver@^5.5.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
semver@^6.1.2, semver@^6.3.1:
semver@^6.1.2:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
@@ -1127,14 +828,6 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
update-browserslist-db@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
dependencies:
escalade "^3.2.0"
picocolors "^1.1.1"
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@@ -1170,18 +863,3 @@ write@1.0.3:
integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
dependencies:
mkdirp "^0.5.1"
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
zod-validation-error@^3.0.3:
version "3.4.0"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.4.0.tgz#3a8a1f55c65579822d7faa190b51336c61bee2a6"
integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==
zod@^3.22.4:
version "3.24.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==

View File

@@ -1,14 +1,11 @@
{
"root": true,
"extends": ["plugin:react-hooks/recommended-legacy"],
"extends": ["plugin:react-hooks/recommended"],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"react-hooks/exhaustive-deps": "error"
}
}

View File

@@ -141,3 +141,27 @@ function useHookInLoops() {
useHook4();
}
}
/**
* Compiler Rules
*/
// Invalid: component factory
function InvalidComponentFactory() {
const DynamicComponent = () => <div>Hello</div>;
// eslint-disable-next-line react-hooks/static-components
return <DynamicComponent />;
}
// Invalid: mutating globals
function InvalidGlobals() {
// eslint-disable-next-line react-hooks/immutability
window.myGlobal = 42;
return <div>Done</div>;
}
// Invalid: useMemo with wrong deps - triggers preserve-manual-memoization
function InvalidUseMemo({items}) {
// eslint-disable-next-line react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps
const sorted = useMemo(() => [...items].sort(), []);
return <div>{sorted.length}</div>;
}

View File

@@ -2,14 +2,6 @@
# yarn lockfile v1
"@ampproject/remapping@^2.2.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@babel/code-frame@7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
@@ -17,160 +9,11 @@
dependencies:
"@babel/highlight" "^7.10.4"
"@babel/code-frame@^7.26.2":
version "7.26.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
dependencies:
"@babel/helper-validator-identifier" "^7.25.9"
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/compat-data@^7.26.5":
version "7.26.8"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367"
integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==
"@babel/core@^7.24.4":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9"
integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.10"
"@babel/helper-compilation-targets" "^7.26.5"
"@babel/helper-module-transforms" "^7.26.0"
"@babel/helpers" "^7.26.10"
"@babel/parser" "^7.26.10"
"@babel/template" "^7.26.9"
"@babel/traverse" "^7.26.10"
"@babel/types" "^7.26.10"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.26.10":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.10.tgz#a60d9de49caca16744e6340c3658dfef6138c3f7"
integrity sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==
dependencies:
"@babel/parser" "^7.26.10"
"@babel/types" "^7.26.10"
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4"
integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==
dependencies:
"@babel/types" "^7.25.9"
"@babel/helper-compilation-targets@^7.26.5":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8"
integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==
dependencies:
"@babel/compat-data" "^7.26.5"
"@babel/helper-validator-option" "^7.25.9"
browserslist "^4.24.0"
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.25.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz#d6f83e3039547fbb39967e78043cd3c8b7820c71"
integrity sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==
dependencies:
"@babel/helper-annotate-as-pure" "^7.25.9"
"@babel/helper-member-expression-to-functions" "^7.25.9"
"@babel/helper-optimise-call-expression" "^7.25.9"
"@babel/helper-replace-supers" "^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers" "^7.25.9"
"@babel/traverse" "^7.26.9"
semver "^6.3.1"
"@babel/helper-member-expression-to-functions@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3"
integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-module-imports@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715"
integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-module-transforms@^7.26.0":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae"
integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==
dependencies:
"@babel/helper-module-imports" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/traverse" "^7.25.9"
"@babel/helper-optimise-call-expression@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e"
integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==
dependencies:
"@babel/types" "^7.25.9"
"@babel/helper-plugin-utils@^7.25.9":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35"
integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==
"@babel/helper-replace-supers@^7.26.5":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz#6cb04e82ae291dae8e72335dfe438b0725f14c8d"
integrity sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==
dependencies:
"@babel/helper-member-expression-to-functions" "^7.25.9"
"@babel/helper-optimise-call-expression" "^7.25.9"
"@babel/traverse" "^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9"
integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/helper-validator-option@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==
"@babel/helpers@^7.26.10":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384"
integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==
dependencies:
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.10"
"@babel/highlight@^7.10.4":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6"
@@ -181,51 +24,6 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/parser@^7.24.4", "@babel/parser@^7.26.10", "@babel/parser@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749"
integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==
dependencies:
"@babel/types" "^7.26.10"
"@babel/plugin-transform-private-methods@^7.24.4":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57"
integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.25.9"
"@babel/helper-plugin-utils" "^7.25.9"
"@babel/template@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2"
integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/parser" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380"
integrity sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.10"
"@babel/parser" "^7.26.10"
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.10"
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.10.tgz#396382f6335bd4feb65741eacfc808218f859259"
integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@eslint/eslintrc@^0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
@@ -255,38 +53,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@jridgewell/gen-mapping@^0.3.5":
version "0.3.8"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142"
integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==
dependencies:
"@jridgewell/set-array" "^1.2.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/set-array@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
acorn-jsx@^5.3.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -366,26 +132,11 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
browserslist@^4.24.0:
version "4.24.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
dependencies:
caniuse-lite "^1.0.30001688"
electron-to-chromium "^1.5.73"
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001688:
version "1.0.30001703"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz#977cb4920598c158f491ecf4f4f2cfed9e354718"
integrity sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==
chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -432,11 +183,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
cross-spawn@^7.0.2:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -446,7 +192,7 @@ cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
debug@^4.0.1, debug@^4.1.1:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
@@ -465,11 +211,6 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
electron-to-chromium@^1.5.73:
version "1.5.114"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz#f2bb4fda80a7db4ea273565e75b0ebbe19af0ac3"
integrity sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -483,11 +224,6 @@ enquirer@^2.3.5:
ansi-colors "^4.1.1"
strip-ansi "^6.0.1"
escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -667,11 +403,6 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -691,11 +422,6 @@ glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
globals@^13.6.0, globals@^13.9.0:
version "13.24.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
@@ -713,18 +439,6 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
hermes-estree@0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
hermes-parser@^0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
dependencies:
hermes-estree "0.25.1"
ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
@@ -791,11 +505,6 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
jsesc@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
json-buffer@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
@@ -816,11 +525,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
keyv@^4.5.3:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -846,13 +550,6 @@ lodash.truncate@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
dependencies:
yallist "^3.0.2"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -870,11 +567,6 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
node-releases@^2.0.19:
version "2.0.19"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -911,7 +603,7 @@ path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
picocolors@^1.0.0, picocolors@^1.1.1:
picocolors@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
@@ -953,11 +645,6 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.2.1:
version "7.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
@@ -1052,14 +739,6 @@ type-fest@^0.20.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
update-browserslist-db@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
dependencies:
escalade "^3.2.0"
picocolors "^1.1.1"
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@@ -1088,18 +767,3 @@ wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
zod-validation-error@^3.0.3:
version "3.4.0"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.4.0.tgz#3a8a1f55c65579822d7faa190b51336c61bee2a6"
integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==
zod@^3.22.4:
version "3.24.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==

View File

@@ -1,14 +1,11 @@
{
"root": true,
"extends": ["plugin:react-hooks/recommended-legacy"],
"extends": ["plugin:react-hooks/recommended"],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"react-hooks/exhaustive-deps": "error"
}
}

View File

@@ -141,3 +141,27 @@ function useHookInLoops() {
useHook4();
}
}
/**
* Compiler Rules
*/
// Invalid: component factory
function InvalidComponentFactory() {
const DynamicComponent = () => <div>Hello</div>;
// eslint-disable-next-line react-hooks/static-components
return <DynamicComponent />;
}
// Invalid: mutating globals
function InvalidGlobals() {
// eslint-disable-next-line react-hooks/immutability
window.myGlobal = 42;
return <div>Done</div>;
}
// Invalid: useMemo with wrong deps - triggers preserve-manual-memoization
function InvalidUseMemo({items}) {
// eslint-disable-next-line react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps
const sorted = useMemo(() => [...items].sort(), []);
return <div>{sorted.length}</div>;
}

View File

@@ -2,213 +2,6 @@
# yarn lockfile v1
"@ampproject/remapping@^2.2.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@babel/code-frame@^7.26.2":
version "7.26.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
dependencies:
"@babel/helper-validator-identifier" "^7.25.9"
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/compat-data@^7.26.5":
version "7.26.8"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367"
integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==
"@babel/core@^7.24.4":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9"
integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.10"
"@babel/helper-compilation-targets" "^7.26.5"
"@babel/helper-module-transforms" "^7.26.0"
"@babel/helpers" "^7.26.10"
"@babel/parser" "^7.26.10"
"@babel/template" "^7.26.9"
"@babel/traverse" "^7.26.10"
"@babel/types" "^7.26.10"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.26.10":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.10.tgz#a60d9de49caca16744e6340c3658dfef6138c3f7"
integrity sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==
dependencies:
"@babel/parser" "^7.26.10"
"@babel/types" "^7.26.10"
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4"
integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==
dependencies:
"@babel/types" "^7.25.9"
"@babel/helper-compilation-targets@^7.26.5":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8"
integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==
dependencies:
"@babel/compat-data" "^7.26.5"
"@babel/helper-validator-option" "^7.25.9"
browserslist "^4.24.0"
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.25.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz#d6f83e3039547fbb39967e78043cd3c8b7820c71"
integrity sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==
dependencies:
"@babel/helper-annotate-as-pure" "^7.25.9"
"@babel/helper-member-expression-to-functions" "^7.25.9"
"@babel/helper-optimise-call-expression" "^7.25.9"
"@babel/helper-replace-supers" "^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers" "^7.25.9"
"@babel/traverse" "^7.26.9"
semver "^6.3.1"
"@babel/helper-member-expression-to-functions@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3"
integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-module-imports@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715"
integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-module-transforms@^7.26.0":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae"
integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==
dependencies:
"@babel/helper-module-imports" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/traverse" "^7.25.9"
"@babel/helper-optimise-call-expression@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e"
integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==
dependencies:
"@babel/types" "^7.25.9"
"@babel/helper-plugin-utils@^7.25.9":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35"
integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==
"@babel/helper-replace-supers@^7.26.5":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz#6cb04e82ae291dae8e72335dfe438b0725f14c8d"
integrity sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==
dependencies:
"@babel/helper-member-expression-to-functions" "^7.25.9"
"@babel/helper-optimise-call-expression" "^7.25.9"
"@babel/traverse" "^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9"
integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/helper-validator-option@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==
"@babel/helpers@^7.26.10":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384"
integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==
dependencies:
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.10"
"@babel/parser@^7.24.4", "@babel/parser@^7.26.10", "@babel/parser@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749"
integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==
dependencies:
"@babel/types" "^7.26.10"
"@babel/plugin-transform-private-methods@^7.24.4":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57"
integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.25.9"
"@babel/helper-plugin-utils" "^7.25.9"
"@babel/template@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2"
integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/parser" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380"
integrity sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.10"
"@babel/parser" "^7.26.10"
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.10"
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.10.tgz#396382f6335bd4feb65741eacfc808218f859259"
integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@eslint-community/eslint-utils@^4.2.0":
version "4.4.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56"
@@ -260,38 +53,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@jridgewell/gen-mapping@^0.3.5":
version "0.3.8"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142"
integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==
dependencies:
"@jridgewell/set-array" "^1.2.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/set-array@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -368,26 +129,11 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
browserslist@^4.24.0:
version "4.24.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
dependencies:
caniuse-lite "^1.0.30001688"
electron-to-chromium "^1.5.73"
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001688:
version "1.0.30001703"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz#977cb4920598c158f491ecf4f4f2cfed9e354718"
integrity sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==
chalk@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@@ -413,11 +159,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
cross-spawn@^7.0.2:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -427,7 +168,7 @@ cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
debug@^4.1.0, debug@^4.3.1, debug@^4.3.2:
debug@^4.3.1, debug@^4.3.2:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
@@ -446,16 +187,6 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
electron-to-chromium@^1.5.73:
version "1.5.114"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz#f2bb4fda80a7db4ea273565e75b0ebbe19af0ac3"
integrity sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==
escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -611,11 +342,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
glob-parent@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
@@ -635,11 +361,6 @@ glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
globals@^13.19.0:
version "13.24.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
@@ -657,18 +378,6 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
hermes-estree@0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
hermes-parser@^0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
dependencies:
hermes-estree "0.25.1"
ignore@^5.2.0:
version "5.3.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
@@ -722,11 +431,6 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
@@ -734,11 +438,6 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
jsesc@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
json-buffer@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
@@ -754,11 +453,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
keyv@^4.5.3:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -786,13 +480,6 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
dependencies:
yallist "^3.0.2"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -817,11 +504,6 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
node-releases@^2.0.19:
version "2.0.19"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -877,11 +559,6 @@ path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
picocolors@^1.0.0, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -921,11 +598,6 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -974,14 +646,6 @@ type-fest@^0.20.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
update-browserslist-db@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
dependencies:
escalade "^3.2.0"
picocolors "^1.1.1"
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@@ -1006,22 +670,7 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod-validation-error@^3.0.3:
version "3.4.0"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.4.0.tgz#3a8a1f55c65579822d7faa190b51336c61bee2a6"
integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==
zod@^3.22.4:
version "3.24.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==

View File

@@ -1,9 +1,8 @@
import {defineConfig} from 'eslint/config';
import reactHooks from 'eslint-plugin-react-hooks';
console.log(reactHooks.configs['recommended-latest']);
export default defineConfig([
reactHooks.configs.flat['recommended-latest'],
{
languageOptions: {
ecmaVersion: 'latest',
@@ -14,10 +13,6 @@ export default defineConfig([
},
},
},
plugins: {
'react-hooks': reactHooks,
},
extends: ['react-hooks/recommended-latest'],
rules: {
'react-hooks/exhaustive-deps': 'error',
},

View File

@@ -141,3 +141,27 @@ function useHookInLoops() {
useHook4();
}
}
/**
* Compiler Rules
*/
// Invalid: component factory
function InvalidComponentFactory() {
const DynamicComponent = () => <div>Hello</div>;
// eslint-disable-next-line react-hooks/static-components
return <DynamicComponent />;
}
// Invalid: mutating globals
function InvalidGlobals() {
// eslint-disable-next-line react-hooks/immutability
window.myGlobal = 42;
return <div>Done</div>;
}
// Invalid: useMemo with wrong deps - triggers preserve-manual-memoization
function InvalidUseMemo({items}) {
// eslint-disable-next-line react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps
const sorted = useMemo(() => [...items].sort(), []);
return <div>{sorted.length}</div>;
}

View File

@@ -8,6 +8,9 @@
},
"scripts": {
"build": "node build.mjs && yarn",
"lint": "eslint index.js --report-unused-disable-directives"
"lint": "tsc --noEmit && eslint index.js --report-unused-disable-directives"
},
"devDependencies": {
"typescript": "^5.4.3"
}
}

View File

@@ -1,2 +1,20 @@
{
"compilerOptions": {
"lib": [
"es2022"
],
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es2022",
"typeRoots": [
"./node_modules/@types"
],
"skipLibCheck": true
},
"exclude": [
"node_modules",
"**/node_modules",
"../node_modules",
"../../node_modules"
]
}

View File

@@ -2,212 +2,144 @@
# yarn lockfile v1
"@ampproject/remapping@^2.2.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
"@babel/code-frame@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@babel/code-frame@^7.26.2":
version "7.26.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
dependencies:
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/helper-validator-identifier" "^7.27.1"
js-tokens "^4.0.0"
picocolors "^1.0.0"
picocolors "^1.1.1"
"@babel/compat-data@^7.26.5":
version "7.26.8"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367"
integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==
"@babel/compat-data@^7.27.2":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.4.tgz#96fdf1af1b8859c8474ab39c295312bfb7c24b04"
integrity sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==
"@babel/core@^7.24.4":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9"
integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.4.tgz#12a550b8794452df4c8b084f95003bce1742d496"
integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.10"
"@babel/helper-compilation-targets" "^7.26.5"
"@babel/helper-module-transforms" "^7.26.0"
"@babel/helpers" "^7.26.10"
"@babel/parser" "^7.26.10"
"@babel/template" "^7.26.9"
"@babel/traverse" "^7.26.10"
"@babel/types" "^7.26.10"
"@babel/code-frame" "^7.27.1"
"@babel/generator" "^7.28.3"
"@babel/helper-compilation-targets" "^7.27.2"
"@babel/helper-module-transforms" "^7.28.3"
"@babel/helpers" "^7.28.4"
"@babel/parser" "^7.28.4"
"@babel/template" "^7.27.2"
"@babel/traverse" "^7.28.4"
"@babel/types" "^7.28.4"
"@jridgewell/remapping" "^2.3.5"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.26.10":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.10.tgz#a60d9de49caca16744e6340c3658dfef6138c3f7"
integrity sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==
"@babel/generator@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e"
integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==
dependencies:
"@babel/parser" "^7.26.10"
"@babel/types" "^7.26.10"
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
"@babel/parser" "^7.28.3"
"@babel/types" "^7.28.2"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4"
integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==
"@babel/helper-compilation-targets@^7.27.2":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d"
integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==
dependencies:
"@babel/types" "^7.25.9"
"@babel/helper-compilation-targets@^7.26.5":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8"
integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==
dependencies:
"@babel/compat-data" "^7.26.5"
"@babel/helper-validator-option" "^7.25.9"
"@babel/compat-data" "^7.27.2"
"@babel/helper-validator-option" "^7.27.1"
browserslist "^4.24.0"
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.25.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz#d6f83e3039547fbb39967e78043cd3c8b7820c71"
integrity sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==
"@babel/helper-globals@^7.28.0":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674"
integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
"@babel/helper-module-imports@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204"
integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==
dependencies:
"@babel/helper-annotate-as-pure" "^7.25.9"
"@babel/helper-member-expression-to-functions" "^7.25.9"
"@babel/helper-optimise-call-expression" "^7.25.9"
"@babel/helper-replace-supers" "^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers" "^7.25.9"
"@babel/traverse" "^7.26.9"
semver "^6.3.1"
"@babel/traverse" "^7.27.1"
"@babel/types" "^7.27.1"
"@babel/helper-member-expression-to-functions@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3"
integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==
"@babel/helper-module-transforms@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6"
integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-module-imports" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@babel/traverse" "^7.28.3"
"@babel/helper-module-imports@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715"
integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==
"@babel/helper-string-parser@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-validator-identifier@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
"@babel/helper-validator-option@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f"
integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
"@babel/helpers@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827"
integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.4"
"@babel/helper-module-transforms@^7.26.0":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae"
integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==
"@babel/parser@^7.24.4", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8"
integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==
dependencies:
"@babel/helper-module-imports" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.28.4"
"@babel/helper-optimise-call-expression@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e"
integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==
"@babel/template@^7.27.2":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
dependencies:
"@babel/types" "^7.25.9"
"@babel/code-frame" "^7.27.1"
"@babel/parser" "^7.27.2"
"@babel/types" "^7.27.1"
"@babel/helper-plugin-utils@^7.25.9":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35"
integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==
"@babel/helper-replace-supers@^7.26.5":
version "7.26.5"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz#6cb04e82ae291dae8e72335dfe438b0725f14c8d"
integrity sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==
"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.4.tgz#8d456101b96ab175d487249f60680221692b958b"
integrity sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==
dependencies:
"@babel/helper-member-expression-to-functions" "^7.25.9"
"@babel/helper-optimise-call-expression" "^7.25.9"
"@babel/traverse" "^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9"
integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/helper-validator-option@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==
"@babel/helpers@^7.26.10":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384"
integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==
dependencies:
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.10"
"@babel/parser@^7.24.4", "@babel/parser@^7.26.10", "@babel/parser@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749"
integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==
dependencies:
"@babel/types" "^7.26.10"
"@babel/plugin-transform-private-methods@^7.24.4":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57"
integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.25.9"
"@babel/helper-plugin-utils" "^7.25.9"
"@babel/template@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2"
integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/parser" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380"
integrity sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.10"
"@babel/parser" "^7.26.10"
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.10"
"@babel/code-frame" "^7.27.1"
"@babel/generator" "^7.28.3"
"@babel/helper-globals" "^7.28.0"
"@babel/parser" "^7.28.4"
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.4"
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.10.tgz#396382f6335bd4feb65741eacfc808218f859259"
integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==
"@babel/types@^7.27.1", "@babel/types@^7.28.2", "@babel/types@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a"
integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@eslint-community/eslint-utils@^4.2.0":
version "4.4.1"
@@ -303,13 +235,20 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.2.tgz#1860473de7dfa1546767448f333db80cb0ff2161"
integrity sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==
"@jridgewell/gen-mapping@^0.3.5":
version "0.3.8"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142"
integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==
"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5":
version "0.3.13"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
dependencies:
"@jridgewell/set-array" "^1.2.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/sourcemap-codec" "^1.5.0"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/remapping@^2.3.5":
version "2.3.5"
resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1"
integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/resolve-uri@^3.1.0":
@@ -317,20 +256,15 @@
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/set-array@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
@@ -387,6 +321,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
baseline-browser-mapping@^2.8.9:
version "2.8.10"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz#32eb5e253d633fa3fa3ffb1685fabf41680d9e8a"
integrity sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -396,24 +335,25 @@ brace-expansion@^1.1.7:
concat-map "0.0.1"
browserslist@^4.24.0:
version "4.24.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
version "4.26.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.26.3.tgz#40fbfe2d1cd420281ce5b1caa8840049c79afb56"
integrity sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==
dependencies:
caniuse-lite "^1.0.30001688"
electron-to-chromium "^1.5.73"
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
baseline-browser-mapping "^2.8.9"
caniuse-lite "^1.0.30001746"
electron-to-chromium "^1.5.227"
node-releases "^2.0.21"
update-browserslist-db "^1.1.3"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001688:
version "1.0.30001703"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz#977cb4920598c158f491ecf4f4f2cfed9e354718"
integrity sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==
caniuse-lite@^1.0.30001746:
version "1.0.30001746"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz#199d20f04f5369825e00ff7067d45d5dfa03aee7"
integrity sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==
chalk@^4.0.0:
version "4.1.2"
@@ -454,7 +394,14 @@ cross-spawn@^7.0.6:
shebang-command "^2.0.0"
which "^2.0.1"
debug@^4.1.0, debug@^4.3.1, debug@^4.3.2:
debug@^4.1.0:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
ms "^2.1.3"
debug@^4.3.1, debug@^4.3.2:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
@@ -466,10 +413,10 @@ deep-is@^0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
electron-to-chromium@^1.5.73:
version "1.5.114"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz#f2bb4fda80a7db4ea273565e75b0ebbe19af0ac3"
integrity sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==
electron-to-chromium@^1.5.227:
version "1.5.228"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.228.tgz#38b849bc8714bd21fb64f5ad56bf8cfd8638e1e9"
integrity sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==
escalade@^3.2.0:
version "3.2.0"
@@ -646,11 +593,6 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
@@ -661,18 +603,6 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
hermes-estree@0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
hermes-parser@^0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
dependencies:
hermes-estree "0.25.1"
ignore@^5.2.0:
version "5.3.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
@@ -801,10 +731,10 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
node-releases@^2.0.19:
version "2.0.19"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
node-releases@^2.0.21:
version "2.0.21"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.21.tgz#f59b018bc0048044be2d4c4c04e4c8b18160894c"
integrity sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==
optionator@^0.9.3:
version "0.9.4"
@@ -849,7 +779,7 @@ path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
picocolors@^1.0.0, picocolors@^1.1.1:
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
@@ -905,7 +835,12 @@ type-check@^0.4.0, type-check@~0.4.0:
dependencies:
prelude-ls "^1.2.1"
update-browserslist-db@^1.1.1:
typescript@^5.4.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
update-browserslist-db@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
@@ -942,12 +877,12 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod-validation-error@^3.0.3:
version "3.4.0"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.4.0.tgz#3a8a1f55c65579822d7faa190b51336c61bee2a6"
integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==
"zod-validation-error@^3.0.3 || ^4.0.0":
version "4.0.2"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
zod@^3.22.4:
version "3.24.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==
"zod@^3.22.4 || ^4.0.0":
version "4.1.11"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5"
integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==

View File

@@ -3,7 +3,7 @@ import React, {
useLayoutEffect,
useEffect,
useState,
unstable_addTransitionType as addTransitionType,
addTransitionType,
} from 'react';
import Chrome from './Chrome.js';

View File

@@ -1,6 +1,6 @@
import React, {
unstable_addTransitionType as addTransitionType,
unstable_ViewTransition as ViewTransition,
addTransitionType,
ViewTransition,
Activity,
useLayoutEffect,
useEffect,

View File

@@ -1,6 +1,23 @@
## 7.0.0
This release slims down presets to just 2 configurations (`recommended` and `recommended-latest`), and all compiler rules are enabled by default.
- **Breaking:** Removed `recommended-latest-legacy` and `flat/recommended` configs. The plugin now provides `recommended` (legacy and flat configs with all recommended rules), and `recommended-latest` (legacy and flat configs with all recommended rules plus new bleeding edge experimental compiler rules). ([@poteto](https://github.com/poteto) in [#34757](https://github.com/facebook/react/pull/34757))
## 6.1.1
**Note:** 6.1.0 accidentally allowed use of `recommended` without flat config, causing errors when used with ESLint v9's `defineConfig()` helper. This has been fixed in 6.1.1.
- Fix `recommended` config for flat config compatibility. The `recommended` config has been converted to flat config format. Non-flat config users should use `recommended-legacy` instead. ([@poteto](https://github.com/poteto) in [#34700](https://github.com/facebook/react/pull/34700))
- Add `recommended-latest` and `recommended-latest-legacy` configs that include React Compiler rules. ([@poteto](https://github.com/poteto) in [#34675](https://github.com/facebook/react/pull/34675))
- Remove unused `NoUnusedOptOutDirectives` rule. ([@poteto](https://github.com/poteto) in [#34703](https://github.com/facebook/react/pull/34703))
- Remove `hermes-parser` and dependency. ([@poteto](https://github.com/poteto) in [#34719](https://github.com/facebook/react/pull/34719))
- Remove `@babel/plugin-proposal-private-methods` dependency. ([@ArnaudBarre](https://github.com/ArnaudBarre) and [@josephsavona](https://github.com/josephsavona) in [#34715](https://github.com/facebook/react/pull/34715))
- Update for Zod v3/v4 compatibility. ([@kolian](https://github.com/kolvian) and [@josephsavona](https://github.com/josephsavona) in [#34717](https://github.com/facebook/react/pull/34717))
## 6.1.0
**Note:** Version 6.0.0 was mistakenly released and immediately deprecated and untagged on npm. This is the first official 6.x major release and includes breaking changes.
**Note:** Version 6.0.0 was mistakenly released and immediately deprecated and untagged on npm. This is the first official 6.x major release and includes breaking changes.
- **Breaking:** Require Node.js 18 or newer. ([@michaelfaith](https://github.com/michaelfaith) in [#32458](https://github.com/facebook/react/pull/32458))
- **Breaking:** Flat config is now the default `recommended` preset. Legacy config moved to `recommended-legacy`. ([@michaelfaith](https://github.com/michaelfaith) in [#32457](https://github.com/facebook/react/pull/32457))

View File

@@ -1,13 +1,9 @@
# `eslint-plugin-react-hooks`
This ESLint plugin enforces the [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks).
It is a part of the [Hooks API](https://react.dev/reference/react/hooks) for React.
The official ESLint plugin for [React](https://react.dev) which enforces the [Rules of React](https://react.dev/reference/eslint-plugin-react-hooks) and other best practices.
## Installation
**Note: If you're using Create React App, please use `react-scripts` >= 3 instead of adding it directly.**
Assuming you already have ESLint installed, run:
```sh
@@ -20,9 +16,7 @@ yarn add eslint-plugin-react-hooks --dev
### Flat Config (eslint.config.js|ts)
#### >= 6.0.0
For users of 6.0 and beyond, add the `recommended` config.
Add the `recommended` config for all recommended rules:
```js
// eslint.config.js
@@ -30,71 +24,42 @@ import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([
{
files: ["src/**/*.{js,jsx,ts,tsx}"],
plugins: {
'react-hooks': reactHooks,
},
extends: ['react-hooks/recommended'],
},
reactHooks.configs.flat.recommended,
]);
```
#### 5.2.0
For users of 5.2.0 (the first version with flat config support), add the `recommended-latest` config.
If you want to try bleeding edge experimental compiler rules, use `recommended-latest`.
```js
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([
{
files: ["src/**/*.{js,jsx,ts,tsx}"],
plugins: {
'react-hooks': reactHooks,
},
extends: ['react-hooks/recommended-latest'],
},
reactHooks.configs.flat['recommended-latest'],
]);
```
### Legacy Config (.eslintrc)
#### >= 5.2.0
If you are still using ESLint below 9.0.0, you can use `recommended-legacy` for accessing a legacy version of the recommended config.
If you are still using ESLint below 9.0.0, the `recommended` preset can also be used to enable all recommended rules.
```js
{
"extends": [
// ...
"plugin:react-hooks/recommended-legacy"
]
"extends": ["plugin:react-hooks/recommended"],
// ...
}
```
#### < 5.2.0
If you're using a version earlier than 5.2.0, the legacy config was simply `recommended`.
```js
{
"extends": [
// ...
"plugin:react-hooks/recommended"
]
}
```
### Custom Configuration
If you want more fine-grained configuration, you can instead add a snippet like this to your ESLint configuration file:
If you want more fine-grained configuration, you can instead choose to enable specific rules. However, we strongly encourage using the recommended presets — see above — so that you will automatically receive new recommended rules as we add them in future versions of the plugin.
#### Flat Config (eslint.config.js|ts)
```js
import * as reactHooks from 'eslint-plugin-react-hooks';
import reactHooks from 'eslint-plugin-react-hooks';
export default [
{
@@ -102,8 +67,26 @@ export default [
plugins: { 'react-hooks': reactHooks },
// ...
rules: {
// Core hooks rules
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// React Compiler rules
'react-hooks/config': 'error',
'react-hooks/error-boundaries': 'error',
'react-hooks/component-hook-factories': 'error',
'react-hooks/gating': 'error',
'react-hooks/globals': 'error',
'react-hooks/immutability': 'error',
'react-hooks/preserve-manual-memoization': 'error',
'react-hooks/purity': 'error',
'react-hooks/refs': 'error',
'react-hooks/set-state-in-effect': 'error',
'react-hooks/set-state-in-render': 'error',
'react-hooks/static-components': 'error',
'react-hooks/unsupported-syntax': 'warn',
'react-hooks/use-memo': 'error',
'react-hooks/incompatible-library': 'warn',
}
},
];
@@ -118,8 +101,26 @@ export default [
],
"rules": {
// ...
// Core hooks rules
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
"react-hooks/exhaustive-deps": "warn",
// React Compiler rules
"react-hooks/config": "error",
"react-hooks/error-boundaries": "error",
"react-hooks/component-hook-factories": "error",
"react-hooks/gating": "error",
"react-hooks/globals": "error",
"react-hooks/immutability": "error",
"react-hooks/preserve-manual-memoization": "error",
"react-hooks/purity": "error",
"react-hooks/refs": "error",
"react-hooks/set-state-in-effect": "error",
"react-hooks/set-state-in-render": "error",
"react-hooks/static-components": "error",
"react-hooks/unsupported-syntax": "warn",
"react-hooks/use-memo": "error",
"react-hooks/incompatible-library": "warn"
}
}
```

View File

@@ -5,4 +5,4 @@
* LICENSE file in the root directory of this source tree.
*/
export * from './cjs/eslint-plugin-react-hooks';
export {default} from './cjs/eslint-plugin-react-hooks';

View File

@@ -1,7 +1,7 @@
{
"name": "eslint-plugin-react-hooks",
"description": "ESLint rules for React Hooks",
"version": "5.2.0",
"version": "7.0.0",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
@@ -41,24 +41,23 @@
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"hermes-parser": "^0.25.1",
"zod": "^3.22.4",
"zod-validation-error": "^3.0.3"
"zod": "^3.22.4 || ^4.0.0",
"zod-validation-error": "^3.0.3 || ^4.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.11.4",
"@babel/preset-typescript": "^7.26.0",
"@babel/types": "^7.19.0",
"@tsconfig/strictest": "^2.0.5",
"@types/eslint": "^9.6.1",
"@types/estree": "^1.0.6",
"@types/estree-jsx": "^1.0.5",
"@types/node": "^20.2.5",
"@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",
"@typescript-eslint/parser-v5": "npm:@typescript-eslint/parser@^5.62.0",
"@types/eslint": "^8.56.12",
"@types/estree": "^1.0.6",
"@types/estree-jsx": "^1.0.5",
"@types/node": "^20.2.5",
"babel-eslint": "^10.0.3",
"eslint-v7": "npm:eslint@^7.7.0",
"eslint-v8": "npm:eslint@^8.57.1",

View File

@@ -7,10 +7,14 @@
import type {Linter, Rule} from 'eslint';
import ExhaustiveDeps from './rules/ExhaustiveDeps';
import {allRules} from './shared/ReactCompiler';
import {
allRules,
mapErrorSeverityToESlint,
recommendedRules,
recommendedLatestRules,
} from './shared/ReactCompiler';
import RulesOfHooks from './rules/RulesOfHooks';
// All rules
const rules = {
'exhaustive-deps': ExhaustiveDeps,
'rules-of-hooks': RulesOfHooks,
@@ -19,47 +23,74 @@ const rules = {
),
} satisfies Record<string, Rule.RuleModule>;
// Config rules
const ruleConfigs = {
const basicRuleConfigs = {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
} satisfies Linter.RulesRecord;
} as const satisfies Linter.RulesRecord;
const recommendedCompilerRuleConfigs = Object.fromEntries(
Object.entries(recommendedRules).map(([name, ruleConfig]) => {
return [
`react-hooks/${name}` as const,
mapErrorSeverityToESlint(ruleConfig.severity),
] as const;
}),
) as Record<`react-hooks/${string}`, Linter.RuleEntry>;
const recommendedLatestCompilerRuleConfigs = Object.fromEntries(
Object.entries(recommendedLatestRules).map(([name, ruleConfig]) => {
return [
`react-hooks/${name}` as const,
mapErrorSeverityToESlint(ruleConfig.severity),
] as const;
}),
) as Record<`react-hooks/${string}`, Linter.RuleEntry>;
const recommendedRuleConfigs: Linter.RulesRecord = {
...basicRuleConfigs,
...recommendedCompilerRuleConfigs,
};
const recommendedLatestRuleConfigs: Linter.RulesRecord = {
...basicRuleConfigs,
...recommendedLatestCompilerRuleConfigs,
};
const plugins = ['react-hooks'];
type ReactHooksFlatConfig = {
plugins: {react: any};
rules: Linter.RulesRecord;
};
const configs = {
recommended: {
plugins,
rules: recommendedRuleConfigs,
},
'recommended-latest': {
plugins,
rules: recommendedLatestRuleConfigs,
},
flat: {} as Record<string, ReactHooksFlatConfig>,
};
const plugin = {
meta: {
name: 'eslint-plugin-react-hooks',
version: '7.0.0',
},
configs: {},
rules,
configs,
};
Object.assign(plugin.configs, {
'recommended-legacy': {
plugins: ['react-hooks'],
rules: ruleConfigs,
Object.assign(configs.flat, {
'recommended-latest': {
plugins: {'react-hooks': plugin},
rules: configs['recommended-latest'].rules,
},
'flat/recommended': [
{
plugins: {
'react-hooks': plugin,
},
rules: ruleConfigs,
},
],
'recommended-latest': [
{
plugins: {
'react-hooks': plugin,
},
rules: ruleConfigs,
},
],
recommended: {
plugins: ['react-hooks'],
rules: ruleConfigs,
plugins: {'react-hooks': plugin},
rules: configs.recommended.rules,
},
});

View File

@@ -14,6 +14,7 @@ import {
LintRules,
type LintRule,
ErrorSeverity,
LintRulePreset,
} from 'babel-plugin-react-compiler';
import {type Linter, type Rule} from 'eslint';
import runReactCompiler, {RunCacheEntry} from './RunReactCompiler';
@@ -149,7 +150,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
type: 'problem',
docs: {
description: rule.description,
recommended: rule.recommended,
recommended: rule.preset === LintRulePreset.Recommended,
},
fixable: 'code',
hasSuggestions: true,
@@ -160,69 +161,30 @@ function makeRule(rule: LintRule): Rule.RuleModule {
};
}
export const NoUnusedDirectivesRule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
recommended: true,
},
fixable: 'code',
hasSuggestions: true,
// validation is done at runtime with zod
schema: [{type: 'object', additionalProperties: true}],
},
create(context: Rule.RuleContext): Rule.RuleListener {
const results = getReactCompilerResult(context);
for (const directive of results.unusedOptOutDirectives) {
context.report({
message: `Unused '${directive.directive}' directive`,
loc: directive.loc,
suggest: [
{
desc: 'Remove the directive',
fix(fixer): Rule.Fix {
return fixer.removeRange(directive.range);
},
},
],
});
}
return {};
},
};
type RulesConfig = {
[name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
};
export const allRules: RulesConfig = LintRules.reduce(
(acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
},
{
'no-unused-directives': {
rule: NoUnusedDirectivesRule,
severity: ErrorSeverity.Error,
},
} as RulesConfig,
);
export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export const recommendedRules: RulesConfig = LintRules.filter(
rule => rule.recommended,
).reduce(
(acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
},
{
'no-unused-directives': {
rule: NoUnusedDirectivesRule,
severity: ErrorSeverity.Error,
},
} as RulesConfig,
);
rule => rule.preset === LintRulePreset.Recommended,
).reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export const recommendedLatestRules: RulesConfig = LintRules.filter(
rule =>
rule.preset === LintRulePreset.Recommended ||
rule.preset === LintRulePreset.RecommendedLatest,
).reduce((acc, rule) => {
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
return acc;
}, {} as RulesConfig);
export function mapErrorSeverityToESlint(
severity: ErrorSeverity,

View File

@@ -6,21 +6,17 @@
*/
/* eslint-disable no-for-of-loops/no-for-of-loops */
import {transformFromAstSync, traverse} from '@babel/core';
import {transformFromAstSync} from '@babel/core';
import {parse as babelParse} from '@babel/parser';
import {Directive, File} from '@babel/types';
// @ts-expect-error: no types available
import PluginProposalPrivateMethods from '@babel/plugin-proposal-private-methods';
import {File} from '@babel/types';
import BabelPluginReactCompiler, {
parsePluginOptions,
validateEnvironmentConfig,
OPT_OUT_DIRECTIVES,
type PluginOptions,
Logger,
LoggerEvent,
} from 'babel-plugin-react-compiler';
import type {SourceCode} from 'eslint';
import {SourceLocation} from 'estree';
import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
@@ -46,17 +42,11 @@ const COMPILER_OPTIONS: PluginOptions = {
},
};
export type UnusedOptOutDirective = {
loc: SourceLocation;
range: [number, number];
directive: string;
};
export type RunCacheEntry = {
sourceCode: string;
filename: string;
userOpts: PluginOptions;
flowSuppressions: Array<{line: number; code: string}>;
unusedOptOutDirectives: Array<UnusedOptOutDirective>;
events: Array<LoggerEvent>;
};
@@ -88,25 +78,6 @@ function getFlowSuppressions(
return results;
}
function filterUnusedOptOutDirectives(
directives: ReadonlyArray<Directive>,
): Array<UnusedOptOutDirective> {
const results: Array<UnusedOptOutDirective> = [];
for (const directive of directives) {
if (
OPT_OUT_DIRECTIVES.has(directive.value.value) &&
directive.loc != null
) {
results.push({
loc: directive.loc,
directive: directive.value.value,
range: [directive.start!, directive.end!],
});
}
}
return results;
}
function runReactCompilerImpl({
sourceCode,
filename,
@@ -126,7 +97,6 @@ function runReactCompilerImpl({
filename,
userOpts,
flowSuppressions: [],
unusedOptOutDirectives: [],
events: [],
};
const userLogger: Logger | null = options.logger;
@@ -144,6 +114,7 @@ function runReactCompilerImpl({
}
let babelAST: ParseResult<File> | null = null;
if (filename.endsWith('.tsx') || filename.endsWith('.ts')) {
try {
babelAST = babelParse(sourceCode.text, {
@@ -174,36 +145,11 @@ function runReactCompilerImpl({
filename,
highlightCode: false,
retainLines: true,
plugins: [
[PluginProposalPrivateMethods, {loose: true}],
[BabelPluginReactCompiler, options],
],
plugins: [[BabelPluginReactCompiler, options]],
sourceType: 'module',
configFile: false,
babelrc: false,
});
if (results.events.filter(e => e.kind === 'CompileError').length === 0) {
traverse(babelAST, {
FunctionDeclaration(path) {
results.unusedOptOutDirectives.push(
...filterUnusedOptOutDirectives(path.node.body.directives),
);
},
ArrowFunctionExpression(path) {
if (path.node.body.type === 'BlockStatement') {
results.unusedOptOutDirectives.push(
...filterUnusedOptOutDirectives(path.node.body.directives),
);
}
},
FunctionExpression(path) {
results.unusedOptOutDirectives.push(
...filterUnusedOptOutDirectives(path.node.body.directives),
);
},
});
}
} catch (err) {
/* errors handled by injected logger */
}

View File

@@ -367,6 +367,7 @@ type Response = {
_debugRootStack?: null | Error, // DEV-only
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugStartTime: number, // DEV-only
_debugIOStarted: boolean, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
_debugChannel?: void | DebugChannel, // DEV-only
_blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
@@ -500,7 +501,7 @@ function createErrorChunk<T>(
}
function moveDebugInfoFromChunkToInnerValue<T>(
chunk: InitializedChunk<T>,
chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
value: T,
): void {
// Remove the debug info from the initialized chunk, and add it to the inner
@@ -1569,6 +1570,10 @@ function fulfillReference(
initializedChunk.reason = handler.reason; // Used by streaming chunks
if (resolveListeners !== null) {
wakeChunk(resolveListeners, handler.value, initializedChunk);
} else {
if (__DEV__) {
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
}
}
}
}
@@ -1818,6 +1823,10 @@ function loadServerReference<A: Iterable<any>, T>(
initializedChunk.value = handler.value;
if (resolveListeners !== null) {
wakeChunk(resolveListeners, handler.value, initializedChunk);
} else {
if (__DEV__) {
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
}
}
}
}
@@ -2536,6 +2545,10 @@ function missingCall() {
);
}
function markIOStarted(this: Response) {
this._debugIOStarted = true;
}
function ResponseInstance(
this: $FlowFixMe,
bundlerConfig: ServerConsumerModuleMap,
@@ -2609,6 +2622,10 @@ function ResponseInstance(
// where as if you use createFromReadableStream from the body of the fetch
// then the start time is when the headers resolved.
this._debugStartTime = performance.now();
this._debugIOStarted = false;
// We consider everything before the first setTimeout task to be cached data
// and is not considered I/O required to load the stream.
setTimeout(markIOStarted.bind(this), 0);
}
this._debugFindSourceMapURL = findSourceMapURL;
this._debugChannel = debugChannel;
@@ -2762,7 +2779,7 @@ function incrementChunkDebugInfo(
}
}
function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
function addAsyncInfo(chunk: SomeChunk<any>, asyncInfo: ReactAsyncInfo): void {
const value = resolveLazy(chunk.value);
if (
typeof value === 'object' &&
@@ -2774,34 +2791,39 @@ function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
) {
if (isArray(value._debugInfo)) {
// $FlowFixMe[method-unbinding]
value._debugInfo.push.apply(value._debugInfo, debugInfo);
value._debugInfo.push(asyncInfo);
} else {
Object.defineProperty((value: any), '_debugInfo', {
configurable: false,
enumerable: false,
writable: true,
value: debugInfo,
value: [asyncInfo],
});
}
} else {
// $FlowFixMe[method-unbinding]
chunk._debugInfo.push.apply(chunk._debugInfo, debugInfo);
chunk._debugInfo.push(asyncInfo);
}
}
function resolveChunkDebugInfo(
response: Response,
streamState: StreamState,
chunk: SomeChunk<any>,
): void {
if (__DEV__ && enableAsyncDebugInfo) {
// Add the currently resolving chunk's debug info representing the stream
// to the Promise that was waiting on the stream, or its underlying value.
const debugInfo: ReactDebugInfo = [{awaited: streamState._debugInfo}];
if (chunk.status === PENDING || chunk.status === BLOCKED) {
const boundAddDebugInfo = addDebugInfo.bind(null, chunk, debugInfo);
chunk.then(boundAddDebugInfo, boundAddDebugInfo);
} else {
addDebugInfo(chunk, debugInfo);
// Only include stream information after a macrotask. Any chunk processed
// before that is considered cached data.
if (response._debugIOStarted) {
// Add the currently resolving chunk's debug info representing the stream
// to the Promise that was waiting on the stream, or its underlying value.
const asyncInfo: ReactAsyncInfo = {awaited: streamState._debugInfo};
if (chunk.status === PENDING || chunk.status === BLOCKED) {
const boundAddAsyncInfo = addAsyncInfo.bind(null, chunk, asyncInfo);
chunk.then(boundAddAsyncInfo, boundAddAsyncInfo);
} else {
addAsyncInfo(chunk, asyncInfo);
}
}
}
}
@@ -2837,12 +2859,12 @@ function resolveModel(
model,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
resolveModelChunk(response, chunk, model);
}
@@ -2869,7 +2891,7 @@ function resolveText(
}
const newChunk = createInitializedTextChunk(response, text);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
}
@@ -2895,7 +2917,7 @@ function resolveBuffer(
}
const newChunk = createInitializedBufferChunk(response, buffer);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
}
@@ -2942,7 +2964,7 @@ function resolveModule(
blockedChunk.status = BLOCKED;
}
if (__DEV__) {
resolveChunkDebugInfo(streamState, blockedChunk);
resolveChunkDebugInfo(response, streamState, blockedChunk);
}
promise.then(
() => resolveModuleChunk(response, blockedChunk, clientReference),
@@ -2952,12 +2974,12 @@ function resolveModule(
if (!chunk) {
const newChunk = createResolvedModuleChunk(response, clientReference);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
// This can't actually happen because we don't have any forward
// references to modules.
@@ -2978,13 +3000,13 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
if (!chunk) {
const newChunk = createInitializedStreamChunk(response, stream, controller);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
return;
}
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
if (chunk.status !== PENDING) {
// We already resolved. We didn't expect to see this.
@@ -3034,6 +3056,10 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
resolvedChunk.reason = controller;
if (resolveListeners !== null) {
wakeChunk(resolveListeners, chunk.value, (chunk: any));
} else {
if (__DEV__) {
moveDebugInfoFromChunkToInnerValue(resolvedChunk, stream);
}
}
}
@@ -3433,12 +3459,12 @@ function resolvePostponeDev(
postponeInstance,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
triggerErrorOnChunk(response, chunk, postponeInstance);
}
@@ -3467,12 +3493,12 @@ function resolveErrorModel(
errorWithDigest,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
triggerErrorOnChunk(response, chunk, errorWithDigest);
}
@@ -4318,7 +4344,26 @@ function flushComponentPerformance(
// First find the start time of the first component to know if it was running
// in parallel with the previous.
const debugInfo = __DEV__ && root._debugInfo;
let debugInfo = null;
if (__DEV__) {
debugInfo = root._debugInfo;
if (debugInfo.length === 0 && root.status === 'fulfilled') {
const resolvedValue = resolveLazy(root.value);
if (
typeof resolvedValue === 'object' &&
resolvedValue !== null &&
(isArray(resolvedValue) ||
typeof resolvedValue[ASYNC_ITERATOR] === 'function' ||
resolvedValue.$$typeof === REACT_ELEMENT_TYPE ||
resolvedValue.$$typeof === REACT_LAZY_TYPE) &&
isArray(resolvedValue._debugInfo)
) {
// It's possible that the value has been given the debug info.
// In that case we need to look for it on the resolved value.
debugInfo = resolvedValue._debugInfo;
}
}
}
if (debugInfo) {
let startTime = 0;
for (let i = 0; i < debugInfo.length; i++) {

View File

@@ -102,6 +102,7 @@ export function logComponentRender(
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
const debugTask = componentInfo.debugTask;
const measureName = '\u200b' + entryName;
if (__DEV__ && debugTask) {
const properties: Array<[string, string]> = [];
if (componentInfo.key != null) {
@@ -110,9 +111,10 @@ export function logComponentRender(
if (componentInfo.props != null) {
addObjectToProperties(componentInfo.props, properties, 0, '');
}
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, '\u200b' + entryName, {
performance.measure.bind(performance, measureName, {
start: startTime < 0 ? 0 : startTime,
end: childrenEndTime,
detail: {
@@ -125,9 +127,10 @@ export function logComponentRender(
},
}),
);
performance.clearMeasures(measureName);
} else {
console.timeStamp(
'\u200b' + entryName,
measureName,
startTime < 0 ? 0 : startTime,
childrenEndTime,
trackNames[trackIdx],
@@ -152,6 +155,7 @@ export function logComponentAborted(
const isPrimaryEnv = env === rootEnv;
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
const measureName = '\u200b' + entryName;
if (__DEV__) {
const properties: Array<[string, string]> = [
[
@@ -165,7 +169,8 @@ export function logComponentAborted(
if (componentInfo.props != null) {
addObjectToProperties(componentInfo.props, properties, 0, '');
}
performance.measure('\u200b' + entryName, {
performance.measure(measureName, {
start: startTime < 0 ? 0 : startTime,
end: childrenEndTime,
detail: {
@@ -178,9 +183,10 @@ export function logComponentAborted(
},
},
});
performance.clearMeasures(measureName);
} else {
console.timeStamp(
entryName,
measureName,
startTime < 0 ? 0 : startTime,
childrenEndTime,
trackNames[trackIdx],
@@ -206,6 +212,7 @@ export function logComponentErrored(
const isPrimaryEnv = env === rootEnv;
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
const measureName = '\u200b' + entryName;
if (__DEV__) {
const message =
typeof error === 'object' &&
@@ -222,7 +229,8 @@ export function logComponentErrored(
if (componentInfo.props != null) {
addObjectToProperties(componentInfo.props, properties, 0, '');
}
performance.measure('\u200b' + entryName, {
performance.measure(measureName, {
start: startTime < 0 ? 0 : startTime,
end: childrenEndTime,
detail: {
@@ -235,9 +243,10 @@ export function logComponentErrored(
},
},
});
performance.clearMeasures(measureName);
} else {
console.timeStamp(
entryName,
measureName,
startTime < 0 ? 0 : startTime,
childrenEndTime,
trackNames[trackIdx],
@@ -397,6 +406,7 @@ export function logComponentAwaitAborted(
},
}),
);
performance.clearMeasures(entryName);
} else {
console.timeStamp(
entryName,
@@ -453,6 +463,7 @@ export function logComponentAwaitErrored(
},
}),
);
performance.clearMeasures(entryName);
} else {
console.timeStamp(
entryName,
@@ -514,6 +525,7 @@ export function logComponentAwait(
},
}),
);
performance.clearMeasures(entryName);
} else {
console.timeStamp(
entryName,
@@ -538,6 +550,7 @@ export function logIOInfoErrored(
const description = getIODescription(error);
const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv);
const debugTask = ioInfo.debugTask;
const measureName = '\u200b' + entryName;
if (__DEV__ && debugTask) {
const message =
typeof error === 'object' &&
@@ -550,9 +563,10 @@ export function logIOInfoErrored(
const properties = [['rejected with', message]];
const tooltipText =
getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected';
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, '\u200b' + entryName, {
performance.measure.bind(performance, measureName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
@@ -565,9 +579,10 @@ export function logIOInfoErrored(
},
}),
);
performance.clearMeasures(measureName);
} else {
console.timeStamp(
entryName,
measureName,
startTime < 0 ? 0 : startTime,
endTime,
IO_TRACK,
@@ -590,6 +605,7 @@ export function logIOInfo(
const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv);
const color = getIOColor(entryName);
const debugTask = ioInfo.debugTask;
const measureName = '\u200b' + entryName;
if (__DEV__ && debugTask) {
const properties: Array<[string, string]> = [];
if (typeof value === 'object' && value !== null) {
@@ -605,7 +621,7 @@ export function logIOInfo(
);
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, '\u200b' + entryName, {
performance.measure.bind(performance, measureName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
@@ -618,9 +634,10 @@ export function logIOInfo(
},
}),
);
performance.clearMeasures(measureName);
} else {
console.timeStamp(
entryName,
measureName,
startTime < 0 ? 0 : startTime,
endTime,
IO_TRACK,

View File

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

View File

@@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Chrome Developer Tools.",
"version": "6.1.5",
"version_name": "6.1.5",
"version": "7.0.0",
"version_name": "7.0.0",
"minimum_chrome_version": "114",
"icons": {
"16": "icons/16-production.png",

View File

@@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
"version": "6.1.5",
"version_name": "6.1.5",
"version": "7.0.0",
"version_name": "7.0.0",
"minimum_chrome_version": "114",
"icons": {
"16": "icons/16-production.png",

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Firefox Developer Tools.",
"version": "6.1.5",
"version": "7.0.0",
"browser_specific_settings": {
"gecko": {
"id": "@react-devtools",

View File

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

View File

@@ -850,6 +850,7 @@ describe('Timeline profiler', () => {
`);
});
// @reactVersion >= 19.1
// @reactVersion < 19.2
it('should process a sample createRoot render sequence', async () => {
function App() {
@@ -2158,6 +2159,7 @@ describe('Timeline profiler', () => {
`);
});
// @reactVersion >= 19.1
// @reactVersion < 19.2
it('should process a sample createRoot render sequence', async () => {
function App() {

View File

@@ -974,12 +974,8 @@ describe('Store', () => {
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
`);
const rendererID = getRendererID();
const rootID = store.getRootIDForElement(store.getElementIDAtIndex(0));
await actAsync(() => {
agent.overrideSuspenseMilestone({
rendererID,
rootID,
suspendedSet: [
store.getElementIDAtIndex(4),
store.getElementIDAtIndex(8),
@@ -1009,8 +1005,6 @@ describe('Store', () => {
await actAsync(() => {
agent.overrideSuspenseMilestone({
rendererID,
rootID,
suspendedSet: [],
});
});
@@ -3148,4 +3142,105 @@ describe('Store', () => {
await actAsync(() => render(null));
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 19
it('should keep suspended boundaries in the Suspense tree but not hidden Activity', async () => {
const Activity = React.Activity || React.unstable_Activity;
const never = new Promise(() => {});
function Never() {
readValue(never);
return null;
}
function Component({children}) {
return <div>{children}</div>;
}
function App({hidden}) {
return (
<>
<Activity mode={hidden ? 'hidden' : 'visible'}>
<React.Suspense name="inside-activity">
<Component key="inside-activity">inside Activity</Component>
</React.Suspense>
</Activity>
<React.Suspense name="outer-suspense">
<React.Suspense name="inner-suspense">
<Component key="inside-suspense">inside Suspense</Component>
</React.Suspense>
{hidden ? <Never /> : null}
</React.Suspense>
</>
);
}
await actAsync(() => {
render(<App hidden={true} />);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Activity>
<Suspense name="outer-suspense">
[suspense-root] rects={[{x:1,y:2,width:15,height:1}]}
<Suspense name="outer-suspense" rects={null}>
`);
// mount as visible
await actAsync(() => {
render(null);
});
await actAsync(() => {
render(<App hidden={false} />);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Activity>
▾ <Suspense name="inside-activity">
<Component key="inside-activity">
▾ <Suspense name="outer-suspense">
▾ <Suspense name="inner-suspense">
<Component key="inside-suspense">
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
<Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
`);
await actAsync(() => {
render(<App hidden={true} />);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Activity>
<Suspense name="outer-suspense">
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
`);
await actAsync(() => {
render(<App hidden={false} />);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Activity>
▾ <Suspense name="inside-activity">
<Component key="inside-activity">
▾ <Suspense name="outer-suspense">
▾ <Suspense name="inner-suspense">
<Component key="inside-suspense">
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
<Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
`);
});
});

View File

@@ -251,7 +251,8 @@ describe('Store component filters', () => {
});
it('should filter ViewTransition', async () => {
const ViewTransition = React.unstable_ViewTransition;
const ViewTransition =
React.ViewTransition || React.unstable_ViewTransition;
if (ViewTransition != null) {
await actAsync(async () =>

View File

@@ -8,7 +8,11 @@
*/
import EventEmitter from '../events';
import {SESSION_STORAGE_LAST_SELECTION_KEY, __DEBUG__} from '../constants';
import {
SESSION_STORAGE_LAST_SELECTION_KEY,
UNKNOWN_SUSPENDERS_NONE,
__DEBUG__,
} from '../constants';
import setupHighlighter from './views/Highlighter';
import {
initialize as setupTraceUpdates,
@@ -26,8 +30,13 @@ import type {
RendererID,
RendererInterface,
DevToolsHookSettings,
InspectedElement,
} from './types';
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
import type {
ComponentFilter,
DehydratedData,
ElementType,
} from 'react-devtools-shared/src/frontend/types';
import type {GroupItem} from './views/TraceUpdates/canvas';
import {gte, isReactNativeEnvironment} from './utils';
import {
@@ -73,6 +82,13 @@ type InspectElementParams = {
requestID: number,
};
type InspectScreenParams = {
forceFullData: boolean,
id: number,
path: Array<string | number> | null,
requestID: number,
};
type OverrideHookParams = {
id: number,
hookID: number,
@@ -131,8 +147,6 @@ type OverrideSuspenseParams = {
};
type OverrideSuspenseMilestoneParams = {
rendererID: number,
rootID: number,
suspendedSet: Array<number>,
};
@@ -141,6 +155,111 @@ type PersistedSelection = {
path: Array<PathFrame>,
};
function createEmptyInspectedScreen(
arbitraryRootID: number,
type: ElementType,
): InspectedElement {
const suspendedBy: DehydratedData = {
cleaned: [],
data: [],
unserializable: [],
};
return {
// invariants
id: arbitraryRootID,
type: type,
// Properties we merge
isErrored: false,
errors: [],
warnings: [],
suspendedBy,
suspendedByRange: null,
// TODO: How to merge these?
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
// Properties where merging doesn't make sense so we ignore them entirely in the UI
rootType: null,
plugins: {stylex: null},
nativeTag: null,
env: null,
source: null,
stack: null,
rendererPackageName: null,
rendererVersion: null,
// These don't make sense for a Root. They're just bottom values.
key: null,
canEditFunctionProps: false,
canEditHooks: false,
canEditFunctionPropsDeletePaths: false,
canEditFunctionPropsRenamePaths: false,
canEditHooksAndDeletePaths: false,
canEditHooksAndRenamePaths: false,
canToggleError: false,
canToggleSuspense: false,
isSuspended: false,
hasLegacyContext: false,
context: null,
hooks: null,
props: null,
state: null,
owners: null,
};
}
function mergeRoots(
left: InspectedElement,
right: InspectedElement,
suspendedByOffset: number,
): void {
const leftSuspendedByRange = left.suspendedByRange;
const rightSuspendedByRange = right.suspendedByRange;
if (right.isErrored) {
left.isErrored = true;
}
for (let i = 0; i < right.errors.length; i++) {
left.errors.push(right.errors[i]);
}
for (let i = 0; i < right.warnings.length; i++) {
left.warnings.push(right.warnings[i]);
}
const leftSuspendedBy: DehydratedData = left.suspendedBy;
const {data, cleaned, unserializable} = (right.suspendedBy: DehydratedData);
const leftSuspendedByData = ((leftSuspendedBy.data: any): Array<mixed>);
const rightSuspendedByData = ((data: any): Array<mixed>);
for (let i = 0; i < rightSuspendedByData.length; i++) {
leftSuspendedByData.push(rightSuspendedByData[i]);
}
for (let i = 0; i < cleaned.length; i++) {
leftSuspendedBy.cleaned.push(
[suspendedByOffset + cleaned[i][0]].concat(cleaned[i].slice(1)),
);
}
for (let i = 0; i < unserializable.length; i++) {
leftSuspendedBy.unserializable.push(
[suspendedByOffset + unserializable[i][0]].concat(
unserializable[i].slice(1),
),
);
}
if (rightSuspendedByRange !== null) {
if (leftSuspendedByRange === null) {
left.suspendedByRange = [
rightSuspendedByRange[0],
rightSuspendedByRange[1],
];
} else {
if (rightSuspendedByRange[0] < leftSuspendedByRange[0]) {
leftSuspendedByRange[0] = rightSuspendedByRange[0];
}
if (rightSuspendedByRange[1] > leftSuspendedByRange[1]) {
leftSuspendedByRange[1] = rightSuspendedByRange[1];
}
}
}
}
export default class Agent extends EventEmitter<{
hideNativeHighlight: [],
showNativeHighlight: [HostInstance],
@@ -201,6 +320,7 @@ export default class Agent extends EventEmitter<{
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
bridge.addListener('getOwnersList', this.getOwnersList);
bridge.addListener('inspectElement', this.inspectElement);
bridge.addListener('inspectScreen', this.inspectScreen);
bridge.addListener('logElementToConsole', this.logElementToConsole);
bridge.addListener('overrideError', this.overrideError);
bridge.addListener('overrideSuspense', this.overrideSuspense);
@@ -531,6 +651,138 @@ export default class Agent extends EventEmitter<{
}
};
inspectScreen: InspectScreenParams => void = ({
requestID,
id,
forceFullData,
path: screenPath,
}) => {
let inspectedScreen: InspectedElement | null = null;
let found = false;
// the suspendedBy index will be from the previously merged roots.
// We need to keep track of how many suspendedBy we've already seen to know
// to which renderer the index belongs.
let suspendedByOffset = 0;
let suspendedByPathIndex: number | null = null;
// The path to hydrate for a specific renderer
let rendererPath: InspectElementParams['path'] = null;
if (screenPath !== null && screenPath.length > 1) {
const secondaryCategory = screenPath[0];
if (secondaryCategory !== 'suspendedBy') {
throw new Error(
'Only hydrating suspendedBy paths is supported. This is a bug.',
);
}
if (typeof screenPath[1] !== 'number') {
throw new Error(
`Expected suspendedBy index to be a number. Received '${screenPath[1]}' instead. This is a bug.`,
);
}
suspendedByPathIndex = screenPath[1];
rendererPath = screenPath.slice(2);
}
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
let path: InspectElementParams['path'] = null;
if (suspendedByPathIndex !== null && rendererPath !== null) {
const suspendedByPathRendererIndex =
suspendedByPathIndex - suspendedByOffset;
const rendererHasRequestedSuspendedByPath =
renderer.getElementAttributeByPath(id, [
'suspendedBy',
suspendedByPathRendererIndex,
]) !== undefined;
if (rendererHasRequestedSuspendedByPath) {
path = ['suspendedBy', suspendedByPathRendererIndex].concat(
rendererPath,
);
}
}
const inspectedRootsPayload = renderer.inspectElement(
requestID,
id,
path,
forceFullData,
);
switch (inspectedRootsPayload.type) {
case 'hydrated-path':
// The path will be relative to the Roots of this renderer. We adjust it
// to be relative to all Roots of this implementation.
inspectedRootsPayload.path[1] += suspendedByOffset;
// TODO: Hydration logic is flawed since the Frontend path is not based
// on the original backend data but rather its own representation of it (e.g. due to reorder).
// So we can receive null here instead when hydration fails
if (inspectedRootsPayload.value !== null) {
for (
let i = 0;
i < inspectedRootsPayload.value.cleaned.length;
i++
) {
inspectedRootsPayload.value.cleaned[i][1] += suspendedByOffset;
}
}
this._bridge.send('inspectedScreen', inspectedRootsPayload);
// If we hydrated a path, it must've been in a specific renderer so we can stop here.
return;
case 'full-data':
const inspectedRoots = inspectedRootsPayload.value;
if (inspectedScreen === null) {
inspectedScreen = createEmptyInspectedScreen(
inspectedRoots.id,
inspectedRoots.type,
);
}
mergeRoots(inspectedScreen, inspectedRoots, suspendedByOffset);
const dehydratedSuspendedBy: DehydratedData =
inspectedRoots.suspendedBy;
const suspendedBy = ((dehydratedSuspendedBy.data: any): Array<mixed>);
suspendedByOffset += suspendedBy.length;
found = true;
break;
case 'no-change':
found = true;
const rootsSuspendedBy: Array<mixed> =
(renderer.getElementAttributeByPath(id, ['suspendedBy']): any);
suspendedByOffset += rootsSuspendedBy.length;
break;
case 'not-found':
break;
case 'error':
// bail out and show the error
// TODO: aggregate errors
this._bridge.send('inspectedScreen', inspectedRootsPayload);
return;
}
}
if (inspectedScreen === null) {
if (found) {
this._bridge.send('inspectedScreen', {
type: 'no-change',
responseID: requestID,
id,
});
} else {
this._bridge.send('inspectedScreen', {
type: 'not-found',
responseID: requestID,
id,
});
}
} else {
this._bridge.send('inspectedScreen', {
type: 'full-data',
responseID: requestID,
id,
value: inspectedScreen,
});
}
};
logElementToConsole: ElementAndRendererID => void = ({id, rendererID}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
@@ -567,17 +819,15 @@ export default class Agent extends EventEmitter<{
};
overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({
rendererID,
rootID,
suspendedSet,
}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(
`Invalid renderer id "${rendererID}" to override suspense milestone`,
);
} else {
renderer.overrideSuspenseMilestone(rootID, suspendedSet);
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
if (renderer.supportsTogglingSuspense) {
renderer.overrideSuspenseMilestone(suspendedSet);
}
}
};
@@ -739,7 +989,7 @@ export default class Agent extends EventEmitter<{
if (renderer !== null) {
const devRenderer = renderer.bundleType === 1;
const enableSuspenseTab =
devRenderer && gte(renderer.version, '19.2.0-canary');
devRenderer && gte(renderer.version, '19.3.0-canary');
if (enableSuspenseTab) {
this._bridge.send('enableSuspenseTab');
}

View File

@@ -2420,7 +2420,6 @@ export function attach(
!isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0,
);
pushOperation(hasOwnerMetadata ? 1 : 0);
pushOperation(supportsTogglingSuspense ? 1 : 0);
if (isProfiling) {
if (displayNamesByRootID !== null) {
@@ -3145,12 +3144,30 @@ export function attach(
}
}
/**
* Offscreen of suspended Suspense
*/
function isSuspendedOffscreen(fiber: Fiber): boolean {
switch (fiber.tag) {
case LegacyHiddenComponent:
// fallthrough since all published implementations currently implement the same state as Offscreen.
case OffscreenComponent:
return (
fiber.memoizedState !== null &&
fiber.return !== null &&
fiber.return.tag === SuspenseComponent
);
default:
return false;
}
}
function unmountRemainingChildren() {
if (
reconcilingParent !== null &&
(reconcilingParent.kind === FIBER_INSTANCE ||
reconcilingParent.kind === FILTERED_FIBER_INSTANCE) &&
isHiddenOffscreen(reconcilingParent.data) &&
isSuspendedOffscreen(reconcilingParent.data) &&
!isInDisconnectedSubtree
) {
// This is a hidden offscreen, we need to execute this in the context of a disconnected subtree.
@@ -4027,7 +4044,7 @@ export function attach(
trackDebugInfoFromHostComponent(nearestInstance, fiber);
}
if (isHiddenOffscreen(fiber)) {
if (isSuspendedOffscreen(fiber)) {
// If an Offscreen component is hidden, mount its children as disconnected.
const stashedDisconnected = isInDisconnectedSubtree;
isInDisconnectedSubtree = true;
@@ -4038,6 +4055,9 @@ export function attach(
} finally {
isInDisconnectedSubtree = stashedDisconnected;
}
} else if (isHiddenOffscreen(fiber)) {
// hidden Activity is noisy.
// Including it may show overlapping Suspense rects
} else if (fiber.tag === SuspenseComponent && OffscreenComponent === -1) {
// Legacy Suspense without the Offscreen wrapper. For the modern Suspense we just handle the
// Offscreen wrapper itself specially.
@@ -4902,7 +4922,11 @@ export function attach(
fiberInstance.data = nextFiber;
if (
mostRecentlyInspectedElement !== null &&
mostRecentlyInspectedElement.id === fiberInstance.id &&
(mostRecentlyInspectedElement.id === fiberInstance.id ||
// If we're inspecting a Root, we inspect the Screen.
// Invalidating any Root invalidates the Screen too.
(mostRecentlyInspectedElement.type === ElementTypeRoot &&
nextFiber.tag === HostRoot)) &&
didFiberRender(prevFiber, nextFiber)
) {
// If this Fiber has updated, clear cached inspected data.
@@ -4978,6 +5002,8 @@ export function attach(
const prevWasHidden = isHiddenOffscreen(prevFiber);
const nextIsHidden = isHiddenOffscreen(nextFiber);
const prevWasSuspended = isSuspendedOffscreen(prevFiber);
const nextIsSuspended = isSuspendedOffscreen(nextFiber);
if (isLegacySuspense) {
if (
@@ -5055,8 +5081,8 @@ export function attach(
);
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
}
} else if (nextIsHidden) {
if (!prevWasHidden) {
} else if (nextIsSuspended) {
if (!prevWasSuspended) {
// We're hiding the children. Disconnect them from the front end but keep state.
if (fiberInstance !== null && !isInDisconnectedSubtree) {
disconnectChildrenRecursively(remainingReconcilingChildren);
@@ -5074,7 +5100,7 @@ export function attach(
} finally {
isInDisconnectedSubtree = stashedDisconnected;
}
} else if (prevWasHidden && !nextIsHidden) {
} else if (prevWasSuspended && !nextIsSuspended) {
// We're revealing the hidden children. We now need to update them to the latest state.
// We do this while still in the disconnected state and then we reconnect the new ones.
// This avoids reconnecting things that are about to be removed anyway.
@@ -5100,6 +5126,13 @@ export function attach(
// Children may have reordered while they were hidden.
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
}
} else if (nextIsHidden) {
if (prevWasHidden) {
// still hidden. Nothing to do.
} else {
// We're hiding the children. Remove them from the Frontend
unmountRemainingChildren();
}
} else if (
nextFiber.tag === SuspenseComponent &&
OffscreenComponent !== -1 &&
@@ -5256,7 +5289,7 @@ export function attach(
// We need to crawl the subtree for closest non-filtered Fibers
// so that we can display them in a flat children set.
if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) {
if (!nextIsHidden && !isInDisconnectedSubtree) {
if (!nextIsSuspended && !isInDisconnectedSubtree) {
recordResetChildren(fiberInstance);
}
@@ -5332,7 +5365,7 @@ export function attach(
if (
(child.kind === FIBER_INSTANCE ||
child.kind === FILTERED_FIBER_INSTANCE) &&
isHiddenOffscreen(child.data)
isSuspendedOffscreen(child.data)
) {
// This instance's children are already disconnected.
} else {
@@ -6422,7 +6455,10 @@ export function attach(
return inspectVirtualInstanceRaw(devtoolsInstance);
}
if (devtoolsInstance.kind === FIBER_INSTANCE) {
return inspectFiberInstanceRaw(devtoolsInstance);
const isRoot = devtoolsInstance.parent === null;
return isRoot
? inspectRootsRaw(devtoolsInstance.id)
: inspectFiberInstanceRaw(devtoolsInstance);
}
(devtoolsInstance: FilteredFiberInstance); // assert exhaustive
throw new Error('Unsupported instance kind');
@@ -6590,9 +6626,6 @@ export function attach(
rootType = fiberRoot._debugRootType;
}
const isTimedOutSuspense =
tag === SuspenseComponent && memoizedState !== null;
let isErrored = false;
if (isErrorBoundary(fiber)) {
// if the current inspected element is an error boundary,
@@ -6664,7 +6697,7 @@ export function attach(
if (
fiberInstance.suspenseNode !== null &&
fiberInstance.suspenseNode.hasUnknownSuspenders &&
!isTimedOutSuspense
!isSuspended
) {
// Something unknown threw to suspended this boundary. Let's figure out why that might be.
if (renderer.bundleType === 0) {
@@ -6702,7 +6735,7 @@ export function attach(
supportsTogglingSuspense &&
hasSuspenseBoundary &&
// If it's showing the real content, we can always flip fallback.
(!isTimedOutSuspense ||
(!isSuspended ||
// If it's showing fallback because we previously forced it to,
// allow toggling it back to remove the fallback override.
forceFallbackForFibers.has(fiber) ||
@@ -6875,10 +6908,24 @@ export function attach(
let currentlyInspectedPaths: Object = {};
function isMostRecentlyInspectedElement(id: number): boolean {
return (
mostRecentlyInspectedElement !== null &&
mostRecentlyInspectedElement.id === id
);
if (mostRecentlyInspectedElement === null) {
return false;
}
if (mostRecentlyInspectedElement.id === id) {
return true;
}
if (mostRecentlyInspectedElement.type === ElementTypeRoot) {
// we inspected the screen recently. If we're inspecting another root, we're
// still inspecting the screen.
const instance = idToDevToolsInstanceMap.get(id);
return (
instance !== undefined &&
instance.kind === FIBER_INSTANCE &&
instance.parent === null
);
}
return false;
}
function isMostRecentlyInspectedElementCurrent(id: number): boolean {
@@ -7060,8 +7107,8 @@ export function attach(
if (!hasElementUpdatedSinceLastInspected) {
if (path !== null) {
let secondaryCategory: 'suspendedBy' | 'hooks' | null = null;
if (path[0] === 'hooks') {
secondaryCategory = 'hooks';
if (path[0] === 'hooks' || path[0] === 'suspendedBy') {
secondaryCategory = path[0];
}
// If this element has not been updated since it was last inspected,
@@ -7209,6 +7256,99 @@ export function attach(
};
}
function inspectRootsRaw(arbitraryRootID: number): InspectedElement | null {
const roots = hook.getFiberRoots(rendererID);
if (roots.size === 0) {
return null;
}
const inspectedRoots: InspectedElement = {
// invariants
id: arbitraryRootID,
type: ElementTypeRoot,
// Properties we merge
isErrored: false,
errors: [],
warnings: [],
suspendedBy: [],
suspendedByRange: null,
// TODO: How to merge these?
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
// Properties where merging doesn't make sense so we ignore them entirely in the UI
rootType: null,
plugins: {stylex: null},
nativeTag: null,
env: null,
source: null,
stack: null,
rendererPackageName: null,
rendererVersion: null,
// These don't make sense for a Root. They're just bottom values.
key: null,
canEditFunctionProps: false,
canEditHooks: false,
canEditFunctionPropsDeletePaths: false,
canEditFunctionPropsRenamePaths: false,
canEditHooksAndDeletePaths: false,
canEditHooksAndRenamePaths: false,
canToggleError: false,
canToggleSuspense: false,
isSuspended: false,
hasLegacyContext: false,
context: null,
hooks: null,
props: null,
state: null,
owners: null,
};
let minSuspendedByRange = Infinity;
let maxSuspendedByRange = -Infinity;
roots.forEach(root => {
const rootInstance = rootToFiberInstanceMap.get(root);
if (rootInstance === undefined) {
throw new Error(
'Expected a root instance to exist for this Fiber root',
);
}
const inspectedRoot = inspectFiberInstanceRaw(rootInstance);
if (inspectedRoot === null) {
return;
}
if (inspectedRoot.isErrored) {
inspectedRoots.isErrored = true;
}
for (let i = 0; i < inspectedRoot.errors.length; i++) {
inspectedRoots.errors.push(inspectedRoot.errors[i]);
}
for (let i = 0; i < inspectedRoot.warnings.length; i++) {
inspectedRoots.warnings.push(inspectedRoot.warnings[i]);
}
for (let i = 0; i < inspectedRoot.suspendedBy.length; i++) {
inspectedRoots.suspendedBy.push(inspectedRoot.suspendedBy[i]);
}
const suspendedByRange = inspectedRoot.suspendedByRange;
if (suspendedByRange !== null) {
if (suspendedByRange[0] < minSuspendedByRange) {
minSuspendedByRange = suspendedByRange[0];
}
if (suspendedByRange[1] > maxSuspendedByRange) {
maxSuspendedByRange = suspendedByRange[1];
}
}
});
if (minSuspendedByRange !== Infinity || maxSuspendedByRange !== -Infinity) {
inspectedRoots.suspendedByRange = [
minSuspendedByRange,
maxSuspendedByRange,
];
}
return inspectedRoots;
}
function logElementToConsole(id: number) {
const result = isMostRecentlyInspectedElementCurrent(id)
? mostRecentlyInspectedElement
@@ -7867,13 +8007,9 @@ export function attach(
/**
* Resets the all other roots of this renderer.
* @param rootID The root that contains this milestone
* @param suspendedSet List of IDs of SuspenseComponent Fibers
*/
function overrideSuspenseMilestone(
rootID: FiberInstance['id'],
suspendedSet: Array<FiberInstance['id']>,
) {
function overrideSuspenseMilestone(suspendedSet: Array<FiberInstance['id']>) {
if (
typeof setSuspenseHandler !== 'function' ||
typeof scheduleUpdate !== 'function'
@@ -7883,8 +8019,6 @@ export function attach(
);
}
// TODO: Allow overriding the timeline for the specified root.
const unsuspendedSet: Set<Fiber> = new Set(forceFallbackForFibers);
let resuspended = false;

View File

@@ -412,7 +412,6 @@ export function attach(
pushOperation(0); // Profiling flag
pushOperation(0); // StrictMode supported?
pushOperation(hasOwnerMetadata ? 1 : 0);
pushOperation(supportsTogglingSuspense ? 1 : 0);
pushOperation(SUSPENSE_TREE_OPERATION_ADD);
pushOperation(id);
@@ -800,6 +799,20 @@ export function attach(
return null;
}
const rootID = internalInstanceToRootIDMap.get(internalInstance);
if (rootID === undefined) {
throw new Error('Expected to find root ID.');
}
const isRoot = rootID === id;
return isRoot
? inspectRootsRaw(rootID)
: inspectInternalInstanceRaw(id, internalInstance);
}
function inspectInternalInstanceRaw(
id: number,
internalInstance: InternalInstance,
): InspectedElement | null {
const {key} = getData(internalInstance);
const type = getElementType(internalInstance);
@@ -903,6 +916,98 @@ export function attach(
};
}
function inspectRootsRaw(arbitraryRootID: number): InspectedElement | null {
const roots =
renderer.Mount._instancesByReactRootID ||
renderer.Mount._instancesByContainerID;
const inspectedRoots: InspectedElement = {
// invariants
id: arbitraryRootID,
type: ElementTypeRoot,
// Properties we merge
isErrored: false,
errors: [],
warnings: [],
suspendedBy: [],
suspendedByRange: null,
// TODO: How to merge these?
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
// Properties where merging doesn't make sense so we ignore them entirely in the UI
rootType: null,
plugins: {stylex: null},
nativeTag: null,
env: null,
source: null,
stack: null,
// TODO: We could make the Frontend accept an array to display
// a list of unique renderers contributing to this Screen.
rendererPackageName: null,
rendererVersion: null,
// These don't make sense for a Root. They're just bottom values.
key: null,
canEditFunctionProps: false,
canEditHooks: false,
canEditFunctionPropsDeletePaths: false,
canEditFunctionPropsRenamePaths: false,
canEditHooksAndDeletePaths: false,
canEditHooksAndRenamePaths: false,
canToggleError: false,
canToggleSuspense: false,
isSuspended: false,
hasLegacyContext: false,
context: null,
hooks: null,
props: null,
state: null,
owners: null,
};
let minSuspendedByRange = Infinity;
let maxSuspendedByRange = -Infinity;
for (const rootKey in roots) {
const internalInstance = roots[rootKey];
const id = getID(internalInstance);
const inspectedRoot = inspectInternalInstanceRaw(id, internalInstance);
if (inspectedRoot === null) {
return null;
}
if (inspectedRoot.isErrored) {
inspectedRoots.isErrored = true;
}
for (let i = 0; i < inspectedRoot.errors.length; i++) {
inspectedRoots.errors.push(inspectedRoot.errors[i]);
}
for (let i = 0; i < inspectedRoot.warnings.length; i++) {
inspectedRoots.warnings.push(inspectedRoot.warnings[i]);
}
for (let i = 0; i < inspectedRoot.suspendedBy.length; i++) {
inspectedRoots.suspendedBy.push(inspectedRoot.suspendedBy[i]);
}
const suspendedByRange = inspectedRoot.suspendedByRange;
if (suspendedByRange !== null) {
if (suspendedByRange[0] < minSuspendedByRange) {
minSuspendedByRange = suspendedByRange[0];
}
if (suspendedByRange[1] > maxSuspendedByRange) {
maxSuspendedByRange = suspendedByRange[1];
}
}
}
if (minSuspendedByRange !== Infinity || maxSuspendedByRange !== -Infinity) {
inspectedRoots.suspendedByRange = [
minSuspendedByRange,
maxSuspendedByRange,
];
}
return inspectedRoots;
}
function logElementToConsole(id: number): void {
const result = inspectElementRaw(id);
if (result === null) {

View File

@@ -450,10 +450,7 @@ export type RendererInterface = {
onErrorOrWarning?: OnErrorOrWarning,
overrideError: (id: number, forceError: boolean) => void,
overrideSuspense: (id: number, forceFallback: boolean) => void,
overrideSuspenseMilestone: (
rootID: number,
suspendedSet: Array<number>,
) => void,
overrideSuspenseMilestone: (suspendedSet: Array<number>) => void,
overrideValueAtPath: (
type: Type,
id: number,

View File

@@ -10,6 +10,7 @@
import Agent from 'react-devtools-shared/src/backend/agent';
import {hideOverlay, showOverlay} from './Highlighter';
import type {HostInstance} from 'react-devtools-shared/src/backend/types';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
import type {RendererInterface} from '../../types';
@@ -26,6 +27,7 @@ export default function setupHighlighter(
): void {
bridge.addListener('clearHostInstanceHighlight', clearHostInstanceHighlight);
bridge.addListener('highlightHostInstance', highlightHostInstance);
bridge.addListener('highlightHostInstances', highlightHostInstances);
bridge.addListener('scrollToHostInstance', scrollToHostInstance);
bridge.addListener('shutdown', stopInspectingHost);
bridge.addListener('startInspectingHost', startInspectingHost);
@@ -157,6 +159,52 @@ export default function setupHighlighter(
hideOverlay(agent);
}
function highlightHostInstances({
displayName,
hideAfterTimeout,
elements,
scrollIntoView,
}: {
displayName: string | null,
hideAfterTimeout: boolean,
elements: Array<{rendererID: number, id: number}>,
scrollIntoView: boolean,
}) {
const nodes: Array<HostInstance> = [];
for (let i = 0; i < elements.length; i++) {
const {id, rendererID} = elements[i];
const renderer = agent.rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
continue;
}
// In some cases fiber may already be unmounted
if (!renderer.hasElementWithId(id)) {
continue;
}
const hostInstances = renderer.findHostInstancesForElementID(id);
if (hostInstances !== null) {
for (let j = 0; j < hostInstances.length; j++) {
nodes.push(hostInstances[j]);
}
}
}
if (nodes.length > 0) {
const node = nodes[0];
// $FlowFixMe[method-unbinding]
if (scrollIntoView && typeof node.scrollIntoView === 'function') {
// If the node isn't visible show it before highlighting it.
// We may want to reconsider this; it might be a little disruptive.
node.scrollIntoView({block: 'nearest', inline: 'nearest'});
}
}
showOverlay(nodes, displayName, agent, hideAfterTimeout);
}
function attemptScrollToHostInstance(
renderer: RendererInterface,
id: number,

View File

@@ -95,7 +95,7 @@ export function inspectElement(
id: number,
path: InspectedElementPath | null,
rendererID: number,
shouldListenToPauseEvents: boolean = false,
shouldListenToPauseEvents: boolean,
): Promise<InspectedElementPayload> {
const requestID = requestCounter++;
const promise = getPromiseForRequestID<InspectedElementPayload>(
@@ -117,6 +117,32 @@ export function inspectElement(
return promise;
}
export function inspectScreen(
bridge: FrontendBridge,
forceFullData: boolean,
arbitraryRootID: number,
path: InspectedElementPath | null,
shouldListenToPauseEvents: boolean,
): Promise<InspectedElementPayload> {
const requestID = requestCounter++;
const promise = getPromiseForRequestID<InspectedElementPayload>(
requestID,
'inspectedScreen',
bridge,
`Timed out while inspecting screen.`,
shouldListenToPauseEvents,
);
bridge.send('inspectScreen', {
requestID,
id: arbitraryRootID,
path,
forceFullData,
});
return promise;
}
let storeAsGlobalCount = 0;
export function storeAsGlobal({

View File

@@ -65,12 +65,6 @@ export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
{
version: 2,
minNpmVersion: '4.22.0',
maxNpmVersion: '6.2.0',
},
// Version 3 adds supports-toggling-suspense bit to add-root
{
version: 3,
minNpmVersion: '6.2.0',
maxNpmVersion: null,
},
];
@@ -92,6 +86,12 @@ type HighlightHostInstance = {
openBuiltinElementsPanel: boolean,
scrollIntoView: boolean,
};
type HighlightHostInstances = {
elements: Array<ElementAndRendererID>,
displayName: string | null,
hideAfterTimeout: boolean,
scrollIntoView: boolean,
};
type ScrollToHostInstance = {
...ElementAndRendererID,
@@ -145,8 +145,6 @@ type OverrideSuspense = {
};
type OverrideSuspenseMilestone = {
rendererID: number,
rootID: number,
suspendedSet: Array<number>,
};
@@ -167,6 +165,13 @@ type InspectElementParams = {
requestID: number,
};
type InspectScreenParams = {
requestID: number,
id: number,
forceFullData: boolean,
path: Array<number | string> | null,
};
type StoreAsGlobalParams = {
...ElementAndRendererID,
count: number,
@@ -199,6 +204,7 @@ export type BackendEvents = {
fastRefreshScheduled: [],
getSavedPreferences: [],
inspectedElement: [InspectedElementPayload],
inspectedScreen: [InspectedElementPayload],
isReloadAndProfileSupportedByBackend: [boolean],
operations: [Array<number>],
ownersList: [OwnersList],
@@ -243,7 +249,9 @@ type FrontendEvents = {
getProfilingData: [{rendererID: RendererID}],
getProfilingStatus: [],
highlightHostInstance: [HighlightHostInstance],
highlightHostInstances: [HighlightHostInstances],
inspectElement: [InspectElementParams],
inspectScreen: [InspectScreenParams],
logElementToConsole: [ElementAndRendererID],
overrideError: [OverrideError],
overrideSuspense: [OverrideSuspense],

View File

@@ -51,6 +51,7 @@ import type {
ComponentFilter,
ElementType,
SuspenseNode,
Rect,
} from 'react-devtools-shared/src/frontend/types';
import type {
FrontendBridge,
@@ -96,10 +97,13 @@ export type Capabilities = {
supportsBasicProfiling: boolean,
hasOwnerMetadata: boolean,
supportsStrictMode: boolean,
supportsTogglingSuspense: boolean,
supportsAdvancedProfiling: AdvancedProfiling,
};
function isNonZeroRect(rect: Rect) {
return rect.width > 0 || rect.height > 0 || rect.x > 0 || rect.y > 0;
}
/**
* The store is the single source of truth for updates from the backend.
* ContextProviders can subscribe to the Store for specific things they want to provide.
@@ -506,14 +510,6 @@ export default class Store extends EventEmitter<{
);
}
supportsTogglingSuspense(rootID: Element['id']): boolean {
const capabilities = this._rootIDToCapabilities.get(rootID);
if (capabilities === undefined) {
throw new Error(`No capabilities registered for root ${rootID}`);
}
return capabilities.supportsTogglingSuspense;
}
// This build of DevTools supports the Timeline profiler.
// This is a static flag, controlled by the Store config.
get supportsTimeline(): boolean {
@@ -898,38 +894,56 @@ export default class Store extends EventEmitter<{
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
*/
getSuspendableDocumentOrderSuspense(
rootID: Element['id'] | void,
uniqueSuspendersOnly: boolean,
): $ReadOnlyArray<SuspenseNode['id']> {
if (rootID === undefined) {
return [];
}
const root = this.getElementByID(rootID);
if (root === null) {
return [];
}
if (!this.supportsTogglingSuspense(rootID)) {
const roots = this.roots;
if (roots.length === 0) {
return [];
}
const list: SuspenseNode['id'][] = [];
const suspense = this.getSuspenseByID(rootID);
if (suspense !== null) {
const stack = [suspense];
while (stack.length > 0) {
const current = stack.pop();
if (current === undefined) {
continue;
for (let i = 0; i < roots.length; i++) {
const rootID = roots[i];
const root = this.getElementByID(rootID);
if (root === null) {
continue;
}
// TODO: This includes boundaries that can't be suspended due to no support from the renderer.
const suspense = this.getSuspenseByID(rootID);
if (suspense !== null) {
if (list.length === 0) {
// start with an arbitrary root that will allow inspection of the Screen
list.push(suspense.id);
}
// Include the root even if we won't show it suspended (because that's just blank).
// You should be able to see what suspended the shell.
if (!uniqueSuspendersOnly || current.hasUniqueSuspenders) {
list.push(current.id);
}
// Add children in reverse order to maintain document order
for (let j = current.children.length - 1; j >= 0; j--) {
const childSuspense = this.getSuspenseByID(current.children[j]);
if (childSuspense !== null) {
stack.push(childSuspense);
const stack = [suspense];
while (stack.length > 0) {
const current = stack.pop();
if (current === undefined) {
continue;
}
// Ignore any suspense boundaries that has no visual representation as this is not
// part of the visible loading sequence.
// TODO: Consider making visible meta data and other side-effects get virtual rects.
const hasRects =
current.rects !== null &&
current.rects.length > 0 &&
current.rects.some(isNonZeroRect);
if (
hasRects &&
(!uniqueSuspendersOnly || current.hasUniqueSuspenders) &&
// Roots are already included as part of the Screen
current.id !== rootID
) {
list.push(current.id);
}
// Add children in reverse order to maintain document order
for (let j = current.children.length - 1; j >= 0; j--) {
const childSuspense = this.getSuspenseByID(current.children[j]);
if (childSuspense !== null) {
stack.push(childSuspense);
}
}
}
}
@@ -1191,7 +1205,6 @@ export default class Store extends EventEmitter<{
let supportsStrictMode = false;
let hasOwnerMetadata = false;
let supportsTogglingSuspense = false;
// If we don't know the bridge protocol, guess that we're dealing with the latest.
// If we do know it, we can take it into consideration when parsing operations.
@@ -1204,9 +1217,6 @@ export default class Store extends EventEmitter<{
hasOwnerMetadata = operations[i] > 0;
i++;
supportsTogglingSuspense = operations[i] > 0;
i++;
}
this._roots = this._roots.concat(id);
@@ -1215,7 +1225,6 @@ export default class Store extends EventEmitter<{
supportsBasicProfiling,
hasOwnerMetadata,
supportsStrictMode,
supportsTogglingSuspense,
supportsAdvancedProfiling,
});
@@ -1561,7 +1570,12 @@ export default class Store extends EventEmitter<{
if (name === null) {
// The boundary isn't explicitly named.
// Pick a sensible default.
name = this._guessSuspenseName(element);
if (parentID === 0) {
// For Roots we use their display name.
name = element.displayName;
} else {
name = this._guessSuspenseName(element);
}
}
}

View File

@@ -181,9 +181,7 @@ export default function Element({data, index, style}: Props): React.Node {
className={styles.KeyValue}
title={key}
onDoubleClick={handleKeyDoubleClick}>
<pre>
<IndexableDisplayName displayName={key} id={id} />
</pre>
<IndexableDisplayName displayName={key} id={id} />
</span>
"
</Fragment>
@@ -196,9 +194,7 @@ export default function Element({data, index, style}: Props): React.Node {
className={styles.KeyValue}
title={nameProp}
onDoubleClick={handleKeyDoubleClick}>
<pre>
<IndexableDisplayName displayName={nameProp} id={id} />
</pre>
<IndexableDisplayName displayName={nameProp} id={id} />
</span>
"
</Fragment>

View File

@@ -194,7 +194,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
}
let strictModeBadge = null;
if (element.isStrictModeNonCompliant) {
if (element.isStrictModeNonCompliant && element.parentID !== 0) {
strictModeBadge = (
<Tooltip label="This component is not running in StrictMode. Click to learn more.">
<a
@@ -237,7 +237,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
<div className={styles.SelectedComponentName}>
<div
className={
element.isStrictModeNonCompliant
element.isStrictModeNonCompliant && element.parentID !== 0
? `${styles.ComponentName} ${styles.StrictModeNonCompliantComponentName}`
: styles.ComponentName
}
@@ -269,18 +269,21 @@ export default function InspectedElementWrapper(_: Props): React.Node {
<ButtonIcon type="error" />
</Toggle>
)}
{canToggleSuspense && (
{canToggleSuspense || isSuspended ? (
<Toggle
isChecked={isSuspended}
isDisabled={!canToggleSuspense}
onChange={toggleSuspended}
title={
isSuspended
? 'Unsuspend the selected component'
? canToggleSuspense
? 'Unsuspend the selected component'
: 'This boundary is still suspended'
: 'Suspend the selected component'
}>
<ButtonIcon type="suspend" />
</Toggle>
)}
) : null}
{store.supportsInspectMatchingDOMElement && (
<Button
onClick={highlightElement}

View File

@@ -17,7 +17,11 @@ import NewKeyValue from './NewKeyValue';
import {alphaSortEntries, serializeDataForCopy} from '../utils';
import Store from '../../store';
import styles from './InspectedElementSharedStyles.css';
import {ElementTypeClass} from 'react-devtools-shared/src/frontend/types';
import {
ElementTypeClass,
ElementTypeSuspense,
ElementTypeActivity,
} from 'react-devtools-shared/src/frontend/types';
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
@@ -50,7 +54,12 @@ export default function InspectedElementPropsTree({
const canDeletePaths =
type === ElementTypeClass || canEditFunctionPropsDeletePaths;
const canEditValues =
!readOnly && (type === ElementTypeClass || canEditFunctionProps);
!readOnly &&
(type === ElementTypeClass || canEditFunctionProps) &&
// Make it read-only for Suspense to make it a bit cleaner. It's not
// useful to edit children anyway.
type !== ElementTypeSuspense &&
type !== ElementTypeActivity;
const canRenamePaths =
type === ElementTypeClass || canEditFunctionPropsRenamePaths;

View File

@@ -13,7 +13,7 @@ import {useState, useTransition} from 'react';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import KeyValue from './KeyValue';
import {serializeDataForCopy} from '../utils';
import {serializeDataForCopy, pluralize} from '../utils';
import Store from '../../store';
import styles from './InspectedElementSharedStyles.css';
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
@@ -44,6 +44,7 @@ type RowProps = {
index: number,
minTime: number,
maxTime: number,
skipName?: boolean,
};
function getShortDescription(name: string, description: string): string {
@@ -99,6 +100,7 @@ function SuspendedByRow({
index,
minTime,
maxTime,
skipName,
}: RowProps) {
const [isOpen, setIsOpen] = useState(false);
const [openIsPending, startOpenTransition] = useTransition();
@@ -166,8 +168,10 @@ function SuspendedByRow({
className={styles.CollapsableHeaderIcon}
type={isOpen ? 'expanded' : 'collapsed'}
/>
<span className={styles.CollapsableHeaderTitle}>{name}</span>
{shortDescription === '' ? null : (
<span className={styles.CollapsableHeaderTitle}>
{skipName ? shortDescription : name}
</span>
{skipName || shortDescription === '' ? null : (
<>
<span className={styles.CollapsableHeaderSeparator}>{' ('}</span>
<span className={styles.CollapsableHeaderTitle}>
@@ -300,15 +304,141 @@ type Props = {
store: Store,
};
function compareTime(a: SerializedAsyncInfo, b: SerializedAsyncInfo): number {
const ioA = a.awaited;
const ioB = b.awaited;
function withIndex(
value: SerializedAsyncInfo,
index: number,
): {
index: number,
value: SerializedAsyncInfo,
} {
return {
index,
value,
};
}
function compareTime(
a: {
index: number,
value: SerializedAsyncInfo,
},
b: {
index: number,
value: SerializedAsyncInfo,
},
): number {
const ioA = a.value.awaited;
const ioB = b.value.awaited;
if (ioA.start === ioB.start) {
return ioA.end - ioB.end;
}
return ioA.start - ioB.start;
}
type GroupProps = {
bridge: FrontendBridge,
element: Element,
inspectedElement: InspectedElement,
store: Store,
name: string,
suspendedBy: Array<{
index: number,
value: SerializedAsyncInfo,
}>,
minTime: number,
maxTime: number,
};
function SuspendedByGroup({
bridge,
element,
inspectedElement,
store,
name,
suspendedBy,
minTime,
maxTime,
}: GroupProps) {
const [isOpen, setIsOpen] = useState(false);
let start = Infinity;
let end = -Infinity;
let isRejected = false;
for (let i = 0; i < suspendedBy.length; i++) {
const asyncInfo: SerializedAsyncInfo = suspendedBy[i].value;
const ioInfo = asyncInfo.awaited;
if (ioInfo.start < start) {
start = ioInfo.start;
}
if (ioInfo.end > end) {
end = ioInfo.end;
}
const value: any = ioInfo.value;
if (
value !== null &&
typeof value === 'object' &&
value[meta.name] === 'rejected Thenable'
) {
isRejected = true;
}
}
const timeScale = 100 / (maxTime - minTime);
let left = (start - minTime) * timeScale;
let width = (end - start) * timeScale;
if (width < 5) {
// Use at least a 5% width to avoid showing too small indicators.
width = 5;
if (left > 95) {
left = 95;
}
}
const pluralizedName = pluralize(name);
return (
<div className={styles.CollapsableRow}>
<Button
className={styles.CollapsableHeader}
onClick={() => {
setIsOpen(prevIsOpen => !prevIsOpen);
}}
title={pluralizedName}>
<ButtonIcon
className={styles.CollapsableHeaderIcon}
type={isOpen ? 'expanded' : 'collapsed'}
/>
<span className={styles.CollapsableHeaderTitle}>{pluralizedName}</span>
<div className={styles.CollapsableHeaderFiller} />
{isOpen ? null : (
<div className={styles.TimeBarContainer}>
<div
className={
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
}
style={{
left: left.toFixed(2) + '%',
width: width.toFixed(2) + '%',
}}
/>
</div>
)}
</Button>
{isOpen &&
suspendedBy.map(({value, index}) => (
<SuspendedByRow
key={index}
index={index}
asyncInfo={value}
bridge={bridge}
element={element}
inspectedElement={inspectedElement}
store={store}
minTime={minTime}
maxTime={maxTime}
skipName={true}
/>
))}
</div>
);
}
export default function InspectedElementSuspendedBy({
bridge,
element,
@@ -322,6 +452,17 @@ export default function InspectedElementSuspendedBy({
(suspendedBy == null || suspendedBy.length === 0) &&
inspectedElement.unknownSuspenders === UNKNOWN_SUSPENDERS_NONE
) {
if (inspectedElement.isSuspended) {
// If we're still suspended, show a place holder until the data loads.
// We don't know what we're suspended by until it has loaded.
return (
<div>
<div className={styles.HeaderRow}>
<div className={styles.Header}>suspended...</div>
</div>
</div>
);
}
return null;
}
@@ -353,9 +494,31 @@ export default function InspectedElementSuspendedBy({
minTime = maxTime - 25;
}
const sortedSuspendedBy = suspendedBy === null ? [] : suspendedBy.slice(0);
const sortedSuspendedBy =
suspendedBy === null ? [] : suspendedBy.map(withIndex);
sortedSuspendedBy.sort(compareTime);
// Organize into groups of consecutive entries with the same name.
const groups = [];
let currentGroup = null;
let currentGroupName = null;
for (let i = 0; i < sortedSuspendedBy.length; i++) {
const entry = sortedSuspendedBy[i];
const name = entry.value.awaited.name;
if (
currentGroupName !== name ||
!name ||
name === 'Promise' ||
currentGroup === null
) {
// Create a new group.
currentGroupName = name;
currentGroup = [];
groups.push(currentGroup);
}
currentGroup.push(entry);
}
let unknownSuspenders = null;
switch (inspectedElement.unknownSuspenders) {
case UNKNOWN_SUSPENDERS_REASON_PRODUCTION:
@@ -396,19 +559,48 @@ export default function InspectedElementSuspendedBy({
<ButtonIcon type="copy" />
</Button>
</div>
{sortedSuspendedBy.map((asyncInfo, index) => (
<SuspendedByRow
key={index}
index={index}
asyncInfo={asyncInfo}
bridge={bridge}
element={element}
inspectedElement={inspectedElement}
store={store}
minTime={minTime}
maxTime={maxTime}
/>
))}
{groups.length === 1
? // If it's only one type of suspender we can flatten it.
groups[0].map(entry => (
<SuspendedByRow
key={entry.index}
index={entry.index}
asyncInfo={entry.value}
bridge={bridge}
element={element}
inspectedElement={inspectedElement}
store={store}
minTime={minTime}
maxTime={maxTime}
/>
))
: groups.map((entries, index) =>
entries.length === 1 ? (
<SuspendedByRow
key={entries[0].index}
index={entries[0].index}
asyncInfo={entries[0].value}
bridge={bridge}
element={element}
inspectedElement={inspectedElement}
store={store}
minTime={minTime}
maxTime={maxTime}
/>
) : (
<SuspendedByGroup
key={entries[0].index}
name={entries[0].value.awaited.name}
suspendedBy={entries}
bridge={bridge}
element={element}
inspectedElement={inspectedElement}
store={store}
minTime={minTime}
maxTime={maxTime}
/>
),
)}
{unknownSuspenders}
</div>
);

View File

@@ -1,72 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {OptionsContext} from '../context';
import EditableValue from './EditableValue';
import Store from '../../store';
import {ElementTypeSuspense} from 'react-devtools-shared/src/frontend/types';
import styles from './InspectedElementSharedStyles.css';
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
type Props = {
bridge: FrontendBridge,
inspectedElement: InspectedElement,
store: Store,
};
export default function InspectedElementSuspenseToggle({
bridge,
inspectedElement,
store,
}: Props): React.Node {
const {readOnly} = React.useContext(OptionsContext);
const {id, isSuspended, type} = inspectedElement;
const canToggleSuspense = !readOnly && inspectedElement.canToggleSuspense;
if (type !== ElementTypeSuspense) {
return null;
}
const toggleSuspense = (path: any, value: boolean) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('overrideSuspense', {
id,
rendererID,
forceFallback: value,
});
}
};
return (
<div>
<div className={styles.HeaderRow}>
<div className={styles.Header}>suspense</div>
</div>
<div className={styles.ToggleSuspenseRow}>
<span className={styles.Name}>Suspended</span>
{canToggleSuspense ? (
// key is required to keep <EditableValue> and header row toggle button in sync
<EditableValue
key={isSuspended}
overrideValue={toggleSuspense}
path={['suspense', 'Suspended']}
value={isSuspended}
/>
) : (
<span className={styles.Value}>{isSuspended ? 'true' : 'false'}</span>
)}
</div>
</div>
);
}

View File

@@ -17,7 +17,6 @@ import InspectedElementHooksTree from './InspectedElementHooksTree';
import InspectedElementPropsTree from './InspectedElementPropsTree';
import InspectedElementStateTree from './InspectedElementStateTree';
import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
import InspectedElementSuspendedBy from './InspectedElementSuspendedBy';
import NativeStyleEditor from './NativeStyleEditor';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
@@ -25,6 +24,10 @@ import InspectedElementSourcePanel from './InspectedElementSourcePanel';
import StackTraceView from './StackTraceView';
import OwnerView from './OwnerView';
import Skeleton from './Skeleton';
import {
ElementTypeSuspense,
ElementTypeActivity,
} from 'react-devtools-shared/src/frontend/types';
import styles from './InspectedElementView.css';
@@ -61,6 +64,7 @@ export default function InspectedElementView({
rootType,
source,
nativeTag,
type,
} = inspectedElement;
const bridge = useContext(BridgeContext);
@@ -75,6 +79,17 @@ export default function InspectedElementView({
const showRenderedBy =
showStack || showOwnersList || rendererLabel !== null || rootType !== null;
const propsSection = (
<div className={styles.InspectedElementSection}>
<InspectedElementPropsTree
bridge={bridge}
element={element}
inspectedElement={inspectedElement}
store={store}
/>
</div>
);
return (
<Fragment>
<div className={styles.InspectedElement}>
@@ -86,22 +101,12 @@ export default function InspectedElementView({
/>
</div>
<div className={styles.InspectedElementSection}>
<InspectedElementPropsTree
bridge={bridge}
element={element}
inspectedElement={inspectedElement}
store={store}
/>
</div>
<div className={styles.InspectedElementSection}>
<InspectedElementSuspenseToggle
bridge={bridge}
inspectedElement={inspectedElement}
store={store}
/>
</div>
{
// For Suspense and Activity we show the props further down.
type !== ElementTypeSuspense && type !== ElementTypeActivity
? propsSection
: null
}
<div className={styles.InspectedElementSection}>
<InspectedElementStateTree
@@ -166,6 +171,13 @@ export default function InspectedElementView({
/>
</div>
{
// For Suspense and Activity we show the props below suspended by to give that more priority.
type !== ElementTypeSuspense && type !== ElementTypeActivity
? null
: propsSection
}
{showRenderedBy && (
<div
className={styles.InspectedElementSection}

View File

@@ -2,13 +2,17 @@
padding: 0.25rem;
}
.CallSite, .IgnoredCallSite {
.CallSite {
display: block;
padding-left: 1rem;
}
.IgnoredCallSite {
opacity: 0.5;
.IgnoredCallSite, .BuiltInCallSite {
display: none;
}
.CallSite + .BuiltInCallSite {
display: block;
}
.Link {

View File

@@ -60,16 +60,22 @@ export function CallSiteView({
symbolicatedCallSite !== null ? symbolicatedCallSite.location : callSite;
const ignored =
symbolicatedCallSite !== null ? symbolicatedCallSite.ignored : false;
if (ignored) {
// TODO: Make an option to be able to toggle the display of ignore listed rows.
// Ideally this UI should be higher than a single Stack Trace so that there's not
// multiple buttons in a single inspection taking up space.
return null;
}
// TODO: Make an option to be able to toggle the display of ignore listed rows.
// Ideally this UI should be higher than a single Stack Trace so that there's not
// multiple buttons in a single inspection taking up space.
const isBuiltIn = url === '' || url.startsWith('<anonymous>'); // This looks like a fake anonymous through eval.
return (
<div className={ignored ? styles.IgnoredCallSite : styles.CallSite}>
<div
className={
ignored
? styles.IgnoredCallSite
: isBuiltIn
? styles.BuiltInCallSite
: styles.CallSite
}>
{functionName || virtualFunctionName}
{url !== '' && (
{!isBuiltIn && (
<>
{' @ '}
<span

View File

@@ -361,15 +361,17 @@ export default function DevTools({
}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'suspense'}>
<SuspenseTab
portalContainer={
suspensePortalContainer
}
/>
</div>
{enableSuspenseTab && (
<div
className={styles.TabContent}
hidden={tab !== 'suspense'}>
<SuspenseTab
portalContainer={
suspensePortalContainer
}
/>
</div>
)}
</div>
{editorPortalContainer ? (
<EditorPane

View File

@@ -209,7 +209,6 @@ function updateTree(
i++; // Profiling flag
i++; // supportsStrictMode flag
i++; // hasOwnerMetadata flag
i++; // supportsTogglingSuspense flag
if (__DEBUG__) {
debug('Add', `new root fiber ${id}`);

View File

@@ -64,8 +64,9 @@ export default function SearchInput({
const handleKeyDown = (event: KeyboardEvent) => {
const {key, metaKey} = event;
if (key === 'f' && metaKey) {
if (inputRef.current !== null) {
inputRef.current.focus();
const inputElement = inputRef.current;
if (inputElement !== null) {
inputElement.focus();
event.preventDefault();
event.stopPropagation();
}
@@ -75,10 +76,14 @@ export default function SearchInput({
// It's important to listen to the ownerDocument to support the browser extension.
// Here we use portals to render individual tabs (e.g. Profiler),
// and the root document might belong to a different window.
const ownerDocument = inputRef.current.ownerDocument;
ownerDocument.addEventListener('keydown', handleKeyDown);
const ownerDocumentElement = inputRef.current.ownerDocument.documentElement;
if (ownerDocumentElement === null) {
return;
}
ownerDocumentElement.addEventListener('keydown', handleKeyDown);
return () => ownerDocument.removeEventListener('keydown', handleKeyDown);
return () =>
ownerDocumentElement.removeEventListener('keydown', handleKeyDown);
}, []);
return (

View File

@@ -104,18 +104,6 @@ export default function GeneralSettings(_: {}): React.Node {
) : null}
</div>
<div className={styles.SettingWrapper}>
<div className={styles.RadioLabel}>Display density</div>
<select
value={displayDensity}
onChange={({currentTarget}) =>
setDisplayDensity(currentTarget.value)
}>
<option value="compact">Compact</option>
<option value="comfortable">Comfortable</option>
</select>
</div>
{supportsTraceUpdates && (
<div className={styles.SettingWrapper}>
<label className={styles.SettingRow}>

View File

@@ -25,7 +25,7 @@ export default function SuspenseBreadcrumbs(): React$Node {
const store = useContext(StoreContext);
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
const {selectedSuspenseID, selectedRootID, lineage} = useContext(
const {selectedSuspenseID, lineage, roots} = useContext(
SuspenseTreeStateContext,
);
@@ -45,13 +45,13 @@ export default function SuspenseBreadcrumbs(): React$Node {
// that rendered the whole screen. In laymans terms this is really "Initial Paint".
// TODO: Once we add subtree selection, then the equivalent should be called
// "Transition" since in that case it's really about a Transition within the page.
selectedRootID !== null ? (
roots.length > 0 ? (
<li
className={styles.SuspenseBreadcrumbsListItem}
aria-current={selectedSuspenseID === selectedRootID}>
aria-current="true">
<button
className={styles.SuspenseBreadcrumbsButton}
onClick={handleClick.bind(null, selectedRootID)}
onClick={handleClick.bind(null, roots[0])}
type="button">
Initial Paint
</button>

View File

@@ -1,5 +1,12 @@
.SuspenseRectsContainer {
padding: .25rem;
cursor: pointer;
outline: 1px solid var(--color-component-name);
border-radius: 0.25rem;
}
.SuspenseRectsContainer[data-highlighted='true'] {
background: var(--color-dimmest);
}
.SuspenseRectsViewBox {
@@ -28,6 +35,8 @@
pointer-events: all;
outline-style: solid;
outline-width: 1px;
border-radius: 0.125rem;
cursor: pointer;
}
.SuspenseRectsScaledRect {
@@ -42,7 +51,7 @@
/* highlight this boundary */
.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
background-color: var(--color-background-hover);
background-color: var(--color-background-hover);
}
.SuspenseRectsRect[data-highlighted='true'] {

View File

@@ -278,11 +278,7 @@ function getDocumentBoundingRect(
};
}
function SuspenseRectsShell({
rootID,
}: {
rootID: SuspenseNode['id'],
}): React$Node {
function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {
const store = useContext(StoreContext);
const root = store.getSuspenseByID(rootID);
if (root === null) {
@@ -299,6 +295,9 @@ const ViewBox = createContext<Rect>((null: any));
function SuspenseRectsContainer(): React$Node {
const store = useContext(StoreContext);
const {inspectedElementID} = useContext(TreeStateContext);
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
const {roots} = useContext(SuspenseTreeStateContext);
@@ -312,14 +311,51 @@ function SuspenseRectsContainer(): React$Node {
const width = '100%';
const aspectRatio = `1 / ${heightScale}`;
function handleClick(event: SyntheticMouseEvent) {
if (event.defaultPrevented) {
// Already clicked on an inner rect
return;
}
if (roots.length === 0) {
// Nothing to select
return;
}
const arbitraryRootID = roots[0];
event.preventDefault();
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: arbitraryRootID});
suspenseTreeDispatch({
type: 'SET_SUSPENSE_LINEAGE',
payload: arbitraryRootID,
});
}
function handleDoubleClick(event: SyntheticMouseEvent) {
if (event.defaultPrevented) {
// Already clicked on an inner rect
return;
}
event.preventDefault();
suspenseTreeDispatch({
type: 'SUSPENSE_SET_TIMELINE_INDEX',
payload: 0,
});
}
const isRootSelected = roots.includes(inspectedElementID);
return (
<div className={styles.SuspenseRectsContainer}>
<div
className={styles.SuspenseRectsContainer}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
data-highlighted={isRootSelected}>
<ViewBox.Provider value={boundingBox}>
<div
className={styles.SuspenseRectsViewBox}
style={{aspectRatio, width}}>
{roots.map(rootID => {
return <SuspenseRectsShell key={rootID} rootID={rootID} />;
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
})}
</div>
</ViewBox.Provider>

View File

@@ -37,7 +37,7 @@
padding-right: 0;
}
.SuspenseScrubberBead, .SuspenseScrubberBeadSelected {
.SuspenseScrubberBead {
flex: 1;
height: 0.5rem;
background: var(--color-background-selected);
@@ -51,9 +51,11 @@
background: var(--color-background-selected);
}
.SuspenseScrubberBeadTransition {
background: var(--color-component-name);
}
.SuspenseScrubberStepHighlight > .SuspenseScrubberBead,
.SuspenseScrubberStepHighlight > .SuspenseScrubberBeadSelected,
.SuspenseScrubberStep:hover > .SuspenseScrubberBead,
.SuspenseScrubberStep:hover > .SuspenseScrubberBeadSelected {
.SuspenseScrubberStep:hover > .SuspenseScrubberBead {
height: 0.75rem;
}

View File

@@ -14,6 +14,8 @@ import {useRef} from 'react';
import styles from './SuspenseScrubber.css';
import Tooltip from '../Components/reach-ui/tooltip';
export default function SuspenseScrubber({
min,
max,
@@ -53,24 +55,38 @@ export default function SuspenseScrubber({
const steps = [];
for (let index = min; index <= max; index++) {
steps.push(
<div
<Tooltip
key={index}
className={
styles.SuspenseScrubberStep +
(highlight === index
? ' ' + styles.SuspenseScrubberStepHighlight
: '')
}
onPointerDown={handlePress.bind(null, index)}
onMouseEnter={onHoverSegment.bind(null, index)}>
label={
index === min
? // The first step in the timeline is always a Transition (Initial Paint).
// TODO: Support multiple environments.
'Initial Paint'
: // TODO: Consider adding the name of this specific boundary if this step has only one.
'Suspense'
}>
<div
className={
index <= value
? styles.SuspenseScrubberBeadSelected
: styles.SuspenseScrubberBead
styles.SuspenseScrubberStep +
(highlight === index
? ' ' + styles.SuspenseScrubberStepHighlight
: '')
}
/>
</div>,
onPointerDown={handlePress.bind(null, index)}
onMouseEnter={onHoverSegment.bind(null, index)}>
<div
className={
styles.SuspenseScrubberBead +
(index === min
? // The first step in the timeline is always a Transition (Initial Paint).
// TODO: Support multiple environments.
' ' + styles.SuspenseScrubberBeadTransition
: '') +
(index <= value ? ' ' + styles.SuspenseScrubberBeadSelected : '')
}
/>
</div>
</Tooltip>,
);
}

View File

@@ -34,13 +34,9 @@ import {
SuspenseTreeStateContext,
} from './SuspenseTreeContext';
import {StoreContext, OptionsContext} from '../context';
import {TreeDispatcherContext} from '../Components/TreeContext';
import Button from '../Button';
import Toggle from '../Toggle';
import typeof {
SyntheticEvent,
SyntheticPointerEvent,
} from 'react-dom-bindings/src/events/SyntheticEvent';
import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
@@ -71,20 +67,14 @@ function ToggleUniqueSuspenders() {
const store = useContext(StoreContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
const {selectedRootID: rootID, uniqueSuspendersOnly} = useContext(
SuspenseTreeStateContext,
);
const {uniqueSuspendersOnly} = useContext(SuspenseTreeStateContext);
function handleToggleUniqueSuspenders() {
const nextUniqueSuspendersOnly = !uniqueSuspendersOnly;
const nextTimeline =
rootID === null
? []
: // TODO: Handle different timeline modes (e.g. random order)
store.getSuspendableDocumentOrderSuspense(
rootID,
nextUniqueSuspendersOnly,
);
// TODO: Handle different timeline modes (e.g. random order)
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
nextUniqueSuspendersOnly,
);
suspenseTreeDispatch({
type: 'SET_SUSPENSE_TIMELINE',
payload: [nextTimeline, null, nextUniqueSuspendersOnly],
@@ -101,55 +91,6 @@ function ToggleUniqueSuspenders() {
);
}
function SelectRoot() {
const store = useContext(StoreContext);
const {roots, selectedRootID, uniqueSuspendersOnly} = useContext(
SuspenseTreeStateContext,
);
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
function handleChange(event: SyntheticEvent) {
const newRootID = +event.currentTarget.value;
// TODO: scrollIntoView both suspense rects and host instance.
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
newRootID,
uniqueSuspendersOnly,
);
suspenseTreeDispatch({
type: 'SET_SUSPENSE_TIMELINE',
payload: [nextTimeline, newRootID, uniqueSuspendersOnly],
});
if (nextTimeline.length > 0) {
const milestone = nextTimeline[nextTimeline.length - 1];
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: milestone});
}
}
return (
roots.length > 0 && (
<select
aria-label="Select Suspense Root"
className={styles.SuspenseTimelineRootSwitcher}
onChange={handleChange}
value={selectedRootID === null ? -1 : selectedRootID}>
<option disabled={true} value={-1}>
----
</option>
{roots.map(rootID => {
// TODO: Use name
const name = '#' + rootID;
// TODO: Highlight host on hover
return (
<option key={rootID} value={rootID}>
{name}
</option>
);
})}
</select>
)
);
}
function ToggleTreeList({
dispatch,
state,
@@ -427,7 +368,6 @@ function SuspenseTab(_: {}) {
<div className={styles.SuspenseBreadcrumbs}>
<SuspenseBreadcrumbs />
</div>
<SelectRoot />
<div className={styles.VRule} />
<ToggleUniqueSuspenders />
{!hideSettings && <SettingsModalContextToggle />}

View File

@@ -8,8 +8,8 @@
*/
import * as React from 'react';
import {useContext, useEffect, useRef} from 'react';
import {BridgeContext, StoreContext} from '../context';
import {useContext, useEffect} from 'react';
import {BridgeContext} from '../context';
import {TreeDispatcherContext} from '../Components/TreeContext';
import {useHighlightHostInstance, useScrollToHostInstance} from '../hooks';
import {
@@ -23,20 +23,14 @@ import ButtonIcon from '../ButtonIcon';
function SuspenseTimelineInput() {
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
const {highlightHostInstance, clearHighlightHostInstance} =
useHighlightHostInstance();
const scrollToHostInstance = useScrollToHostInstance();
const {
selectedRootID: rootID,
timeline,
timelineIndex,
hoveredTimelineIndex,
playing,
} = useContext(SuspenseTreeStateContext);
const {timeline, timelineIndex, hoveredTimelineIndex, playing, autoScroll} =
useContext(SuspenseTreeStateContext);
const min = 0;
const max = timeline.length > 0 ? timeline.length - 1 : 0;
@@ -107,47 +101,33 @@ function SuspenseTimelineInput() {
});
}
const isInitialMount = useRef(true);
// TODO: useEffectEvent here once it's supported in all versions DevTools supports.
// For now we just exclude it from deps since we don't lint those anyway.
function changeTimelineIndex(newIndex: number) {
// Synchronize timeline index with what is resuspended.
if (rootID === null) {
return;
}
const rendererID = store.getRendererIDForElement(rootID);
if (rendererID === null) {
console.error(
`No renderer ID found for root element ${rootID} in suspense timeline.`,
);
return;
}
// We suspend everything after the current selection. The root isn't showing
// anything suspended in the root. The step after that should have one less
// thing suspended. I.e. the first suspense boundary should be unsuspended
// when it's selected. This also lets you show everything in the last step.
const suspendedSet = timeline.slice(timelineIndex + 1);
bridge.send('overrideSuspenseMilestone', {
rendererID,
rootID,
suspendedSet,
});
if (isInitialMount.current) {
// Skip scrolling on initial mount. Only when we're changing the timeline.
isInitialMount.current = false;
} else {
// When we're scrubbing through the timeline, scroll the current boundary
// into view as it was just revealed. This is after we override the milestone
// to reveal it.
const selectedSuspenseID = timeline[timelineIndex];
scrollToHostInstance(selectedSuspenseID);
}
}
useEffect(() => {
changeTimelineIndex(timelineIndex);
}, [timelineIndex]);
useEffect(() => {
if (autoScroll.id > 0) {
const scrollToId = autoScroll.id;
// Consume the scroll ref so that we only trigger this scroll once.
autoScroll.id = 0;
scrollToHostInstance(scrollToId);
}
}, [autoScroll]);
useEffect(() => {
if (!playing) {
return undefined;
@@ -164,20 +144,6 @@ function SuspenseTimelineInput() {
};
}, [playing]);
if (rootID === null) {
return (
<div className={styles.SuspenseTimelineInput}>No root selected.</div>
);
}
if (!store.supportsTogglingSuspense(rootID)) {
return (
<div className={styles.SuspenseTimelineInput}>
Can't step through Suspense in production apps.
</div>
);
}
if (timeline.length === 0) {
return (
<div className={styles.SuspenseTimelineInput}>
@@ -226,10 +192,9 @@ function SuspenseTimelineInput() {
}
export default function SuspenseTimeline(): React$Node {
const {selectedRootID} = useContext(SuspenseTreeStateContext);
return (
<div className={styles.SuspenseTimelineContainer}>
<SuspenseTimelineInput key={selectedRootID} />
<SuspenseTimelineInput />
</div>
);
}

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