Commit Graph
21787 Commits
Author SHA1 Message Date
Joseph Savona db85de83d5 Merge 69fbdea303 into sapling-pr-archive-josephsavona 2025-10-03 09:44:40 -07:00
kolvian 69fbdea303 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). 2025-10-03 09:44:31 -07:00
Joe Savona 01ff8b3011 merge commit for archive created by Sapling 2025-10-03 09:42:50 -07:00
kolvian 45c23756d2 feat(eslint-plugin): add Zod v4 compatibility
Update Zod function schemas to use v4 API syntax:
- Replace z.function().args(z.string()) with z.function(z.tuple([z.string()]), z.any())
- Update moduleTypeProvider and flowTypeProvider in Environment.ts
- Allow both Zod v3 and v4 in package dependencies (^3.22.4 || ^4.0.0)

This fixes the crash when users have Zod v4 installed, where the
.args() method no longer exists. The new API is compatible with
both Zod v3.23+ and v4.x.

Fixes TypeError: zod.z.function(...).args is not a function
2025-10-03 09:42:39 -07:00
Joseph Savona e5c4445107 Merge 69f9e147a2 into sapling-pr-archive-josephsavona 2025-10-03 09:18:09 -07:00
kolvian 69f9e147a2 feat(eslint-plugin): add Zod v4 compatibility
Update Zod function schemas to use v4 API syntax:
- Replace z.function().args(z.string()) with z.function(z.tuple([z.string()]), z.any())
- Update moduleTypeProvider and flowTypeProvider in Environment.ts
- Allow both Zod v3 and v4 in package dependencies (^3.22.4 || ^4.0.0)

This fixes the crash when users have Zod v4 installed, where the
.args() method no longer exists. The new API is compatible with
both Zod v3.23+ and v4.x.

Fixes TypeError: zod.z.function(...).args is not a function
2025-10-03 09:18:02 -07:00
Joseph Savona 43288f5d44 Merge 1f8165c9d1 into sapling-pr-archive-josephsavona 2025-10-03 09:12:35 -07:00
kolvian 1f8165c9d1 feat(eslint-plugin): add Zod v4 compatibility
Update Zod function schemas to use v4 API syntax:
- Replace z.function().args(z.string()) with z.function(z.tuple([z.string()]), z.any())
- Update moduleTypeProvider and flowTypeProvider in Environment.ts
- Allow both Zod v3 and v4 in package dependencies (^3.22.4 || ^4.0.0)

This fixes the crash when users have Zod v4 installed, where the
.args() method no longer exists. The new API is compatible with
both Zod v3.23+ and v4.x.

Fixes TypeError: zod.z.function(...).args is not a function
2025-10-03 09:11:53 -07:00
Joseph Savona a9ec19ed7d Merge 9f572cbcd8 into sapling-pr-archive-josephsavona 2025-10-03 08:56:41 -07:00
Arnaud Barré 9f572cbcd8 [compiler] Remove @babel/plugin-proposal-private-methods 2025-10-03 08:56:00 -07:00
Joseph Savona eb705fe6d7 Merge ac87ec4dbd into sapling-pr-archive-josephsavona 2025-10-03 08:54:13 -07:00
Arnaud Barré ac87ec4dbd [compiler] Remove @babel/plugin-proposal-private-methods 2025-10-03 08:54:02 -07:00
Joseph Savona c45346b419 Merge 749fe33363 into sapling-pr-archive-josephsavona 2025-10-03 08:44:25 -07:00
Joe Savona 749fe33363 Update readme for eprh 2025-10-03 08:44:20 -07:00
Joseph Savona cc21616c8c Merge 67c687ccda into sapling-pr-archive-josephsavona 2025-10-03 08:36:29 -07:00
Joe Savona 67c687ccda Update readme for eprh 2025-10-03 08:36:21 -07: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
Joseph Savona 1a61287d0e Merge ae3444675f into sapling-pr-archive-josephsavona 2025-10-02 09:54:07 -07:00
Joe Savona ae3444675f [compiler] @enablePreserveExistingMemoizationGuarantees on by default
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 09:53:51 -07:00
Joseph Savona 57d5a59748 [compiler] enablePreserveMemo treats manual deps as non-nullable (#34503)
The `@enablePreserveExistingMemoizationGuarantees` mode can still fail
to preserve manual memoization due to mismtached dependencies.
Specifically, where the user's dependencies are more precise than the
compiler infers bc the compiler is being conservative about what might
be nullable. In this mode though we're intentionally using information
from the manual memoization and can also rely on the deps as a signal
for what's non-nullable.

The idea of the PR is that we treat manual memo deps just like other
inferred-as-non-nullable objects during PropagateScopeDeps. We're
careful to not treat the full path as non-nullable, only up to the last
property index. So `x.y.z` as a manual dep treats `x` and `x.y` as
non-nullable, allowing us to preserve a conditional dependency on
`x.y.z`.

Optionals within manual dependencies are a bit trickier and aren't
handled yet, but hopefully that's less common and something we can
improve in a follow-up. Not handling them just means that developers may
hit false positives on validating existing memoization if they use
optional chains in manual dependencies.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34503).
* #34689
* __->__ #34503
2025-10-02 09:48:52 -07:00
Joseph Savona abdaf09644 Merge 26111cadff into sapling-pr-archive-josephsavona 2025-10-02 09:47:35 -07:00
Joe Savona 26111cadff [compiler] @enablePreserveExistingMemoizationGuarantees on by default
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 09:47:24 -07:00
Sebastian "Sebbie" Silbermann bc828bf6e3 [DevTools] Recommend React Performance tracks if supported when Timeline profiler is not supported (#34684) 2025-10-02 18:33:50 +02:00
Joseph Savona 1e383b1929 Merge 8cca076ad9 into sapling-pr-archive-josephsavona 2025-10-02 09:19:36 -07:00
Joe Savona 8cca076ad9 [compiler] ValidateExhaustiveDeps disallows unnecessary non-reactive deps
Just to be consistent, we disallow unnecessary deps even if they're known to be non-reactive.
2025-10-02 09:19:24 -07:00
Joe Savona 3c0ea0c7a6 [compiler] Auto-fix for non-exhaustive deps
Records more information in DropManualMemoization so that we know the full span of the manual dependencies array (if present). This allows ValidateExhaustiveDeps to include a suggestion with the correct deps.
2025-10-02 09:19:22 -07:00
Joe Savona 5809d69de3 [compiler] Implement exhaustive dependency checking for manual memoization
The compiler currently drops manual memoization and rewrites it using its own inference. If the existing manual memo dependencies has missing or extra dependencies, compilation can change behavior by running the computation more often (if deps were missing) or less often (if there were extra deps). We currently address this by relying on the developer to use the ESLint plugin and have `eslint-disable-next-line react-hooks/exhaustive-deps` suppressions in their code. If a suppression exists, we skip compilation.

But not everyone is using the linter! Relying on the linter is also imprecise since it forces us to bail out on exhaustive-deps checks that only effect (ahem) effects — and while it isn't good to have incorrect deps on effects, it isn't a problem for compilation.

So this PR is a rough sketch of validating manual memoization dependencies in the compiler. Long-term we could use this to also check effect deps and replace the ExhaustiveDeps lint rule, but for now I'm focused specifically on manual memoization use-cases. If this works, we can stop bailing out on ESLint suppressions, since the compiler will implement all the appropriate checks (we already check rules of hooks).
2025-10-02 09:19:21 -07:00
Joe Savona 2d357eb9be [compiler] enablePreserveMemo treats manual deps as non-nullable
The `@enablePreserveExistingMemoizationGuarantees` mode can still fail to preserve manual memoization due to mismtached dependencies. Specifically, where the user's dependencies are more precise than the compiler infers bc the compiler is being conservative about what might be nullable. In this mode though we're intentionally using information from the manual memoization and can also rely on the deps as a signal for what's non-nullable.

The idea of the PR is that we treat manual memo deps just like other inferred-as-non-nullable objects during PropagateScopeDeps. We're careful to not treat the full path as non-nullable, only up to the last property index. So `x.y.z` as a manual dep treats `x` and `x.y` as non-nullable, allowing us to preserve a conditional dependency on `x.y.z`.

Optionals within manual dependencies are a bit trickier and aren't handled yet, but hopefully that's less common and something we can improve in a follow-up. Not handling them just means that developers may hit false positives on validating existing memoization if they use optional chains in manual dependencies.
2025-10-02 09:19:19 -07:00
Joe Savona a56d634dce merge commit for archive created by Sapling 2025-10-02 09:14:35 -07:00
Joe Savona b4b3863b8f [compiler] ValidateExhaustiveDeps disallows unnecessary non-reactive deps
Just to be consistent, we disallow unnecessary deps even if they're known to be non-reactive.
2025-10-02 09:14:09 -07:00
Joe Savona 767ae4970a [compiler] Auto-fix for non-exhaustive deps
Records more information in DropManualMemoization so that we know the full span of the manual dependencies array (if present). This allows ValidateExhaustiveDeps to include a suggestion with the correct deps.
2025-10-02 09:14:09 -07:00
Joe Savona 93696d8d6c [compiler] Implement exhaustive dependency checking for manual memoization
The compiler currently drops manual memoization and rewrites it using its own inference. If the existing manual memo dependencies has missing or extra dependencies, compilation can change behavior by running the computation more often (if deps were missing) or less often (if there were extra deps). We currently address this by relying on the developer to use the ESLint plugin and have `eslint-disable-next-line react-hooks/exhaustive-deps` suppressions in their code. If a suppression exists, we skip compilation.

But not everyone is using the linter! Relying on the linter is also imprecise since it forces us to bail out on exhaustive-deps checks that only effect (ahem) effects — and while it isn't good to have incorrect deps on effects, it isn't a problem for compilation.

So this PR is a rough sketch of validating manual memoization dependencies in the compiler. Long-term we could use this to also check effect deps and replace the ExhaustiveDeps lint rule, but for now I'm focused specifically on manual memoization use-cases. If this works, we can stop bailing out on ESLint suppressions, since the compiler will implement all the appropriate checks (we already check rules of hooks).
2025-10-02 09:14:09 -07:00