Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d357eb9be | ||
|
|
a757cb7667 | ||
|
|
d74f061b69 | ||
|
|
f7254efc5c | ||
|
|
79ca5ae855 | ||
|
|
ae74234eae | ||
|
|
861811347b | ||
|
|
7f9d99749c | ||
|
|
aef8b1b562 | ||
|
|
67e24bc527 | ||
|
|
bbc2d596fa | ||
|
|
1bd1f01f2a | ||
|
|
548235db10 | ||
|
|
1f460f31ee | ||
|
|
2f0649a0b2 | ||
|
|
7bccdbd765 | ||
|
|
5667a41fe4 | ||
|
|
cf884083e0 | ||
|
|
57b16e3788 | ||
|
|
2a04bae651 | ||
|
|
92cfdc3a4e | ||
|
|
a55e98f738 | ||
|
|
063394cf82 | ||
|
|
d8a15c49a4 | ||
|
|
0d8ff4d8c7 | ||
|
|
554a373d7e | ||
|
|
5dd163b49e | ||
|
|
ef8894452b | ||
|
|
e6f2a8a376 | ||
|
|
ba2214e571 | ||
|
|
ecb2ce6c5f | ||
|
|
3580584ba2 | ||
|
|
319a7867d0 | ||
|
|
d15d7fd79e | ||
|
|
8674c3ba28 | ||
|
|
24e260d35b | ||
|
|
2bbb7be0e1 | ||
|
|
dce1f6cd5d | ||
|
|
7c0fff6f2b | ||
|
|
e2d19bf6a9 | ||
|
|
a7d8dddaf3 | ||
|
|
8309724cb4 | ||
|
|
09d3cd8fb5 | ||
|
|
f78b2343cc | ||
|
|
e08f53b182 | ||
|
|
2622487a74 | ||
|
|
8a24ef3e75 | ||
|
|
c552618a82 | ||
|
|
df38ac9a3b | ||
|
|
8bb7241f4c | ||
|
|
8d557a638e | ||
|
|
6a51a9fea6 | ||
|
|
1fd291d3c5 | ||
|
|
047715c4ba | ||
|
|
250f1b20e0 | ||
|
|
b0c1dc01ec | ||
|
|
6eb5d67e9c | ||
|
|
ac2c1a5a58 | ||
|
|
c44fbf43b1 | ||
|
|
8ad773b1f3 | ||
|
|
58d17912e8 | ||
|
|
2c6d92fd80 | ||
|
|
e233218359 | ||
|
|
05b61f812a | ||
|
|
e0c421ab71 | ||
|
|
2ee6147510 | ||
|
|
e02c173fa5 | ||
|
|
24a2ba03fb | ||
|
|
012b371cde | ||
|
|
83c88ad470 | ||
|
|
cad813ac1e |
@@ -517,6 +517,14 @@ module.exports = {
|
||||
__IS_INTERNAL_VERSION__: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/react-devtools-*/**/*.js'],
|
||||
excludedFiles: '**/__tests__/**/*.js',
|
||||
plugins: ['eslint-plugin-react-hooks-published'],
|
||||
rules: {
|
||||
'react-hooks-published/rules-of-hooks': ERROR,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
|
||||
extends: ['plugin:@typescript-eslint/recommended'],
|
||||
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
@@ -1,3 +1,76 @@
|
||||
## 19.2.0 (October 1st, 2025)
|
||||
|
||||
Below is a list of all new features, APIs, and bug fixes.
|
||||
|
||||
Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2) for more information.
|
||||
|
||||
### New React Features
|
||||
|
||||
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
|
||||
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
|
||||
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
|
||||
- [React Performance tracks](https://react.dev/reference/developer-tooling/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools
|
||||
|
||||
### New React DOM Features
|
||||
|
||||
- Added resume APIs for partial pre-rendering with Web Streams:
|
||||
- [`resume`](https://react.dev/reference/react-dom/server/resume): to resume a prerender to a stream.
|
||||
- [`resumeAndPrerender`](https://react.dev/reference/react-dom/static/resumeAndPrerender): to resume a prerender to HTML.
|
||||
- Added resume APIs for partial pre-rendering with Node Streams:
|
||||
- [`resumeToPipeableStream`](https://react.dev/reference/react-dom/server/resumeToPipeableStream): to resume a prerender to a stream.
|
||||
- [`resumeAndPrerenderToNodeStream`](https://react.dev/reference/react-dom/static/resumeAndPrerenderToNodeStream): to resume a prerender to HTML.
|
||||
- Updated [`prerender`](https://react.dev/reference/react-dom/static/prerender) APIs to return a `postponed` state that can be passed to the `resume` APIs.
|
||||
|
||||
### Notable changes
|
||||
|
||||
- React DOM now batches suspense boundary reveals, matching the behavior of client side rendering. This change is especially noticeable when animating the reveal of Suspense boundaries e.g. with the upcoming `<ViewTransition>` Component. React will batch as much reveals as possible before the first paint while trying to hit popular first-contentful paint metrics.
|
||||
- Add Node Web Streams (`prerender`, `renderToReadableStream`) to server-side-rendering APIs for Node.js
|
||||
- Use underscore instead of `:` IDs generated by useId
|
||||
|
||||
### All Changes
|
||||
|
||||
#### React
|
||||
|
||||
- `<Activity />` was developed over many years, starting before `ClassComponent.setState` (@acdlite @sebmarkbage and many others)
|
||||
- Stringify context as "SomeContext" instead of "SomeContext.Provider" (@kassens [#33507](https://github.com/facebook/react/pull/33507))
|
||||
- Include stack of cause of React instrumentation errors with `%o` placeholder (@eps1lon [#34198](https://github.com/facebook/react/pull/34198))
|
||||
- Fix infinite `useDeferredValue` loop in popstate event (@acdlite [#32821](https://github.com/facebook/react/pull/32821))
|
||||
- Fix a bug when an initial value was passed to `useDeferredValue` (@acdlite [#34376](https://github.com/facebook/react/pull/34376))
|
||||
- Fix a crash when submitting forms with Client Actions (@sebmarkbage [#33055](https://github.com/facebook/react/pull/33055))
|
||||
- Hide/unhide the content of dehydrated suspense boundaries if they resuspend (@sebmarkbage [#32900](https://github.com/facebook/react/pull/32900))
|
||||
- Avoid stack overflow on wide trees during Hot Reload (@sophiebits [#34145](https://github.com/facebook/react/pull/34145))
|
||||
- Improve Owner and Component stacks in various places (@sebmarkbage, @eps1lon: [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723))
|
||||
- Add `cacheSignal` (@sebmarkbage [#33557](https://github.com/facebook/react/pull/33557))
|
||||
|
||||
#### React DOM
|
||||
|
||||
- Block on Suspensey Fonts during reveal of server-side-rendered content (@sebmarkbage [#33342](https://github.com/facebook/react/pull/33342))
|
||||
- Use underscore instead of `:` for IDs generated by `useId` (@sebmarkbage, @eps1lon: [#32001](https://github.com/facebook/react/pull/32001), [https://github.com/facebook/react/pull/33342](https://github.com/facebook/react/pull/33342)[#33099](https://github.com/facebook/react/pull/33099), [#33422](https://github.com/facebook/react/pull/33422))
|
||||
- Stop warning when ARIA 1.3 attributes are used (@Abdul-Omira [#34264](https://github.com/facebook/react/pull/34264))
|
||||
- Allow `nonce` to be used on hoistable styles (@Andarist [#32461](https://github.com/facebook/react/pull/32461))
|
||||
- Warn for using a React owned node as a Container if it also has text content (@sebmarkbage [#32774](https://github.com/facebook/react/pull/32774))
|
||||
- s/HTML/text for for error messages if text hydration mismatches (@rickhanlonii [#32763](https://github.com/facebook/react/pull/32763))
|
||||
- Fix a bug with `React.use` inside `React.lazy`\-ed Component (@hi-ogawa [#33941](https://github.com/facebook/react/pull/33941))
|
||||
- Enable the `progressiveChunkSize` option for server-side-rendering APIs (@sebmarkbage [#33027](https://github.com/facebook/react/pull/33027))
|
||||
- Fix a bug with deeply nested Suspense inside Suspense fallback when server-side-rendering (@gnoff [#33467](https://github.com/facebook/react/pull/33467))
|
||||
- Avoid hanging when suspending after aborting while rendering (@gnoff [#34192](https://github.com/facebook/react/pull/34192))
|
||||
- Add Node Web Streams to server-side-rendering APIs for Node.js (@sebmarkbage [#33475](https://github.com/facebook/react/pull/33475))
|
||||
|
||||
#### React Server Components
|
||||
|
||||
- Preload `<img>` and `<link>` using hints before they're rendered (@sebmarkbage [#34604](https://github.com/facebook/react/pull/34604))
|
||||
- Log error if production elements are rendered during development (@eps1lon [#34189](https://github.com/facebook/react/pull/34189))
|
||||
- Fix a bug when returning a Temporary reference (e.g. a Client Reference) from Server Functions (@sebmarkbage [#34084](https://github.com/facebook/react/pull/34084), @denk0403 [#33761](https://github.com/facebook/react/pull/33761))
|
||||
- Pass line/column to `filterStackFrame` (@eps1lon [#33707](https://github.com/facebook/react/pull/33707))
|
||||
- Support Async Modules in Turbopack Server References (@lubieowoce [#34531](https://github.com/facebook/react/pull/34531))
|
||||
- Add support for .mjs file extension in Webpack (@jennyscript [#33028](https://github.com/facebook/react/pull/33028))
|
||||
- Fix a wrong missing key warning (@unstubbable [#34350](https://github.com/facebook/react/pull/34350))
|
||||
- Make console log resolve in predictable order (@sebmarkbage [#33665](https://github.com/facebook/react/pull/33665))
|
||||
|
||||
#### React Reconciler
|
||||
|
||||
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
|
||||
|
||||
## 19.1.1 (July 28, 2025)
|
||||
|
||||
### React
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
//
|
||||
// The @latest channel uses the version as-is, e.g.:
|
||||
//
|
||||
// 19.1.0
|
||||
// 19.3.0
|
||||
//
|
||||
// The @canary channel appends additional information, with the scheme
|
||||
// <version>-<label>-<commit_sha>, e.g.:
|
||||
//
|
||||
// 19.1.0-canary-a1c2d3e4
|
||||
// 19.3.0-canary-a1c2d3e4
|
||||
//
|
||||
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
|
||||
//
|
||||
// 0.0.0-experimental-241c4467e-20200129
|
||||
|
||||
const ReactVersion = '19.2.0';
|
||||
const ReactVersion = '19.3.0';
|
||||
|
||||
// The label used by the @canary channel. Represents the upcoming release's
|
||||
// stability. Most of the time, this will be "canary", but we may temporarily
|
||||
@@ -33,8 +33,8 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '6.1.0',
|
||||
'jest-react': '0.17.0',
|
||||
'eslint-plugin-react-hooks': '6.2.0',
|
||||
'jest-react': '0.18.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
'react-dom': ReactVersion,
|
||||
@@ -42,12 +42,12 @@ const stablePackages = {
|
||||
'react-server-dom-turbopack': ReactVersion,
|
||||
'react-server-dom-parcel': ReactVersion,
|
||||
'react-is': ReactVersion,
|
||||
'react-reconciler': '0.33.0',
|
||||
'react-refresh': '0.18.0',
|
||||
'react-reconciler': '0.34.0',
|
||||
'react-refresh': '0.19.0',
|
||||
'react-test-renderer': ReactVersion,
|
||||
'use-subscription': '1.12.0',
|
||||
'use-sync-external-store': '1.6.0',
|
||||
scheduler: '0.27.0',
|
||||
'use-subscription': '1.13.0',
|
||||
'use-sync-external-store': '1.7.0',
|
||||
scheduler: '0.28.0',
|
||||
};
|
||||
|
||||
// These packages do not exist in the @canary or @latest channel, only
|
||||
|
||||
@@ -2,4 +2,4 @@ import type { PluginOptions } from
|
||||
'babel-plugin-react-compiler/dist';
|
||||
({
|
||||
//compilationMode: "all"
|
||||
} satisfies Partial<PluginOptions>);
|
||||
} satisfies PluginOptions);
|
||||
@@ -23,7 +23,8 @@ function formatPrint(data: Array<string>): Promise<string> {
|
||||
|
||||
async function expandConfigs(page: Page): Promise<void> {
|
||||
const expandButton = page.locator('[title="Expand config editor"]');
|
||||
expandButton.click();
|
||||
await expandButton.click();
|
||||
await page.waitForSelector('.monaco-editor-config', {state: 'visible'});
|
||||
}
|
||||
|
||||
const TEST_SOURCE = `export default function TestComponent({ x }) {
|
||||
@@ -263,7 +264,7 @@ test('error is displayed when config has validation error', async ({page}) => {
|
||||
|
||||
({
|
||||
compilationMode: "123"
|
||||
} satisfies Partial<PluginOptions>);`,
|
||||
} satisfies PluginOptions);`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
@@ -293,7 +294,7 @@ test('disableMemoizationForDebugging flag works as expected', async ({
|
||||
environment: {
|
||||
disableMemoizationForDebugging: true
|
||||
}
|
||||
} satisfies Partial<PluginOptions>);`,
|
||||
} satisfies PluginOptions);`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
|
||||
@@ -6,7 +6,14 @@
|
||||
*/
|
||||
|
||||
import {Resizable} from 're-resizable';
|
||||
import React, {useCallback} from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useId,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import {EXPAND_ACCORDION_TRANSITION} from '../lib/transitionTypes';
|
||||
|
||||
type TabsRecord = Map<string, React.ReactNode>;
|
||||
|
||||
@@ -16,21 +23,25 @@ export default function AccordionWindow(props: {
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
changedPasses: Set<string>;
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className="flex flex-row h-full">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<AccordionWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<div className="flex flex-row h-full">
|
||||
{Array.from(props.tabs.keys()).map(name => {
|
||||
return (
|
||||
<AccordionWindowItem
|
||||
name={name}
|
||||
key={name}
|
||||
tabs={props.tabs}
|
||||
tabsOpen={props.tabsOpen}
|
||||
setTabsOpen={props.setTabsOpen}
|
||||
hasChanged={props.changedPasses.has(name)}
|
||||
isFailure={props.isFailure}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -41,24 +52,32 @@ function AccordionWindowItem({
|
||||
tabsOpen,
|
||||
setTabsOpen,
|
||||
hasChanged,
|
||||
isFailure,
|
||||
}: {
|
||||
name: string;
|
||||
tabs: TabsRecord;
|
||||
tabsOpen: Set<string>;
|
||||
setTabsOpen: (newTab: Set<string>) => void;
|
||||
hasChanged: boolean;
|
||||
isFailure: boolean;
|
||||
}): React.ReactElement {
|
||||
const isShow = tabsOpen.has(name);
|
||||
const id = useId();
|
||||
const isShow = isFailure ? name === 'Output' : tabsOpen.has(name);
|
||||
|
||||
const toggleTabs = useCallback(() => {
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
}, [tabsOpen, name, setTabsOpen]);
|
||||
const transitionName = `accordion-window-item-${id}`;
|
||||
|
||||
const toggleTabs = () => {
|
||||
startTransition(() => {
|
||||
addTransitionType(EXPAND_ACCORDION_TRANSITION);
|
||||
const nextState = new Set(tabsOpen);
|
||||
if (nextState.has(name)) {
|
||||
nextState.delete(name);
|
||||
} else {
|
||||
nextState.add(name);
|
||||
}
|
||||
setTabsOpen(nextState);
|
||||
});
|
||||
};
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
@@ -66,31 +85,45 @@ function AccordionWindowItem({
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
<ViewTransition
|
||||
name={transitionName}
|
||||
update={{
|
||||
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
|
||||
default: 'none',
|
||||
}}>
|
||||
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
|
||||
<h2
|
||||
title="Minimize tab"
|
||||
aria-label="Minimize tab"
|
||||
onClick={toggleTabs}
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
</ViewTransition>
|
||||
) : (
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
<ViewTransition
|
||||
name={transitionName}
|
||||
update={{
|
||||
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
|
||||
default: 'none',
|
||||
}}>
|
||||
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
|
||||
<button
|
||||
title={`Expand compiler tab: ${name}`}
|
||||
aria-label={`Expand compiler tab: ${name}`}
|
||||
style={{transform: 'rotate(90deg) translate(-50%)'}}
|
||||
onClick={toggleTabs}
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
</ViewTransition>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,12 +9,19 @@ 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, {useState, useRef} from 'react';
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import {Resizable} from 're-resizable';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} 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
|
||||
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
@@ -36,7 +43,12 @@ export default function ConfigEditor({
|
||||
display: isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<ExpandedEditor
|
||||
onToggle={setIsExpanded}
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
addTransitionType(CONFIG_PANEL_TRANSITION);
|
||||
setIsExpanded(false);
|
||||
});
|
||||
}}
|
||||
appliedOptions={appliedOptions}
|
||||
/>
|
||||
</div>
|
||||
@@ -44,7 +56,14 @@ export default function ConfigEditor({
|
||||
style={{
|
||||
display: !isExpanded ? 'block' : 'none',
|
||||
}}>
|
||||
<CollapsedEditor onToggle={setIsExpanded} />
|
||||
<CollapsedEditor
|
||||
onToggle={() => {
|
||||
startTransition(() => {
|
||||
addTransitionType(CONFIG_PANEL_TRANSITION);
|
||||
setIsExpanded(true);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -54,7 +73,7 @@ function ExpandedEditor({
|
||||
onToggle,
|
||||
appliedOptions,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
onToggle: () => void;
|
||||
appliedOptions: PluginOptions | null;
|
||||
}): React.ReactElement {
|
||||
const store = useStore();
|
||||
@@ -111,90 +130,93 @@ function ExpandedEditor({
|
||||
: 'Invalid configs';
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350}}
|
||||
enable={{right: true, bottom: false}}>
|
||||
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
|
||||
<div
|
||||
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
|
||||
title="Minimize config editor"
|
||||
onClick={() => onToggle(false)}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
right: '-32px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="left" className="text-blue-50" />
|
||||
</div>
|
||||
<ViewTransition
|
||||
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
|
||||
<Resizable
|
||||
minWidth={300}
|
||||
maxWidth={600}
|
||||
defaultSize={{width: 350}}
|
||||
enable={{right: true, bottom: false}}>
|
||||
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
|
||||
<div
|
||||
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
|
||||
title="Minimize config editor"
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
right: '-32px',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}>
|
||||
<IconChevron displayDirection="left" className="text-blue-50" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col m-2 mb-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Config Overrides
|
||||
</h2>
|
||||
<div className="flex-1 flex flex-col m-2 mb-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Config Overrides
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
loading={''}
|
||||
className="monaco-editor-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
loading={''}
|
||||
className="monaco-editor-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1 flex flex-col m-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Applied Configs
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedOptions}
|
||||
loading={''}
|
||||
className="monaco-editor-applied-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
readOnly: true,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col m-2">
|
||||
<div className="pb-2">
|
||||
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
|
||||
Applied Configs
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg overflow-hidden border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'applied-config.js'}
|
||||
language={'javascript'}
|
||||
value={formattedAppliedOptions}
|
||||
loading={''}
|
||||
className="monaco-editor-applied-config"
|
||||
options={{
|
||||
...monacoOptions,
|
||||
lineNumbers: 'off',
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
fontSize: 12,
|
||||
scrollBeyondLastLine: false,
|
||||
readOnly: true,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Resizable>
|
||||
</Resizable>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsedEditor({
|
||||
onToggle,
|
||||
}: {
|
||||
onToggle: (expanded: boolean) => void;
|
||||
onToggle: () => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
@@ -203,7 +225,7 @@ function CollapsedEditor({
|
||||
<div
|
||||
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
|
||||
title="Expand config editor"
|
||||
onClick={() => onToggle(true)}
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
top: '50%',
|
||||
marginTop: '-32px',
|
||||
|
||||
@@ -24,7 +24,11 @@ import BabelPluginReactCompiler, {
|
||||
printFunctionWithOutlined,
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {useDeferredValue, useMemo} from 'react';
|
||||
import {
|
||||
useDeferredValue,
|
||||
useMemo,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
} from 'react';
|
||||
import {useStore} from '../StoreContext';
|
||||
import ConfigEditor from './ConfigEditor';
|
||||
import Input from './Input';
|
||||
@@ -343,12 +347,8 @@ export default function Editor(): JSX.Element {
|
||||
<ConfigEditor appliedOptions={appliedOptions} />
|
||||
</div>
|
||||
<div className="flex flex-1 min-w-0">
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Input language={language} errors={errors} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
<Input language={language} errors={errors} />
|
||||
<Output store={deferredStore} compilerOutput={mergedOutput} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -13,11 +13,17 @@ import {
|
||||
import invariant from 'invariant';
|
||||
import type {editor} from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
} from 'react';
|
||||
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
|
||||
|
||||
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
|
||||
import React$Types from '../../node_modules/@types/react/index.d.ts';
|
||||
|
||||
@@ -155,9 +161,13 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState('Input');
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col flex-none border-r border-gray-200">
|
||||
<div className="!h-[calc(100vh_-_3.5rem)]">
|
||||
<div className="flex flex-col h-full">
|
||||
<ViewTransition
|
||||
update={{
|
||||
[CONFIG_PANEL_TRANSITION]: 'container',
|
||||
default: 'none',
|
||||
}}>
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<div className="flex flex-col h-full !h-[calc(100vh_-_3.5rem)] border-r border-gray-200">
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
@@ -165,6 +175,6 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,12 +19,24 @@ import {
|
||||
import parserBabel from 'prettier/plugins/babel';
|
||||
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {memo, ReactNode, useEffect, useState} from 'react';
|
||||
import {type Store} from '../../lib/stores';
|
||||
import {
|
||||
memo,
|
||||
ReactNode,
|
||||
use,
|
||||
useState,
|
||||
Suspense,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
} from 'react';
|
||||
import AccordionWindow from '../AccordionWindow';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
import {
|
||||
CONFIG_PANEL_TRANSITION,
|
||||
TOGGLE_INTERNALS_TRANSITION,
|
||||
} from '../../lib/transitionTypes';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
const MemoizedOutput = memo(Output);
|
||||
|
||||
@@ -32,6 +44,10 @@ export default MemoizedOutput;
|
||||
|
||||
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
|
||||
|
||||
const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
|
||||
max: 5,
|
||||
});
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'hir';
|
||||
@@ -200,6 +216,25 @@ ${code}
|
||||
return reorderedTabs;
|
||||
}
|
||||
|
||||
function tabifyCached(
|
||||
store: Store,
|
||||
compilerOutput: CompilerOutput,
|
||||
): Promise<Map<string, ReactNode>> {
|
||||
const cached = tabifyCache.get(store);
|
||||
if (cached) return cached;
|
||||
const result = tabify(store.source, compilerOutput, store.showInternals);
|
||||
tabifyCache.set(store, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function Fallback(): JSX.Element {
|
||||
return (
|
||||
<div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function utf16ToUTF8(s: string): string {
|
||||
return unescape(encodeURIComponent(s));
|
||||
}
|
||||
@@ -213,33 +248,25 @@ function getSourceMapUrl(code: string, map: string): string | null {
|
||||
}
|
||||
|
||||
function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={<Fallback />}>
|
||||
<OutputContent store={store} compilerOutput={compilerOutput} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function OutputContent({store, compilerOutput}: Props): JSX.Element {
|
||||
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
|
||||
() => new Set(['Output']),
|
||||
);
|
||||
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<string>('Output');
|
||||
|
||||
/*
|
||||
* 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,
|
||||
);
|
||||
if (compilerOutput.kind !== previousOutputKind) {
|
||||
setPreviousOutputKind(compilerOutput.kind);
|
||||
setTabsOpen(new Set(['Output']));
|
||||
setActiveTab('Output');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
tabify(store.source, compilerOutput, store.showInternals).then(tabs => {
|
||||
setTabs(tabs);
|
||||
});
|
||||
}, [store.source, compilerOutput, store.showInternals]);
|
||||
|
||||
const isFailure = compilerOutput.kind !== 'ok';
|
||||
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) {
|
||||
@@ -254,25 +281,43 @@ function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
lastResult = currResult;
|
||||
}
|
||||
}
|
||||
const tabs = use(tabifyCached(store, compilerOutput));
|
||||
|
||||
if (!store.showInternals) {
|
||||
return (
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
<ViewTransition
|
||||
update={{
|
||||
[CONFIG_PANEL_TRANSITION]: 'container',
|
||||
[TOGGLE_INTERNALS_TRANSITION]: '',
|
||||
default: 'none',
|
||||
}}>
|
||||
<TabbedWindow
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
// Display the Output tab on compilation failure
|
||||
activeTabOverride={isFailure ? 'Output' : undefined}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccordionWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
/>
|
||||
<ViewTransition
|
||||
update={{
|
||||
[CONFIG_PANEL_TRANSITION]: 'accordion-container',
|
||||
[TOGGLE_INTERNALS_TRANSITION]: '',
|
||||
default: 'none',
|
||||
}}>
|
||||
<AccordionWindow
|
||||
defaultTab={store.showInternals ? 'HIR' : 'Output'}
|
||||
setTabsOpen={setTabsOpen}
|
||||
tabsOpen={tabsOpen}
|
||||
tabs={tabs}
|
||||
changedPasses={changedPasses}
|
||||
isFailure={isFailure}
|
||||
/>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,16 @@ import {CheckIcon} from '@heroicons/react/solid';
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import {useSnackbar} from 'notistack';
|
||||
import {useState} from 'react';
|
||||
import {
|
||||
useState,
|
||||
startTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
} from 'react';
|
||||
import {defaultStore} from '../lib/defaultStore';
|
||||
import {IconGitHub} from './Icons/IconGitHub';
|
||||
import Logo from './Logo';
|
||||
import {useStore, useStoreDispatch} from './StoreContext';
|
||||
import {TOGGLE_INTERNALS_TRANSITION} from '../lib/transitionTypes';
|
||||
|
||||
export default function Header(): JSX.Element {
|
||||
const [showCheck, setShowCheck] = useState(false);
|
||||
@@ -62,7 +67,12 @@ export default function Header(): JSX.Element {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={store.showInternals}
|
||||
onChange={() => dispatchStore({type: 'toggleInternals'})}
|
||||
onChange={() =>
|
||||
startTransition(() => {
|
||||
addTransitionType(TOGGLE_INTERNALS_TRANSITION);
|
||||
dispatchStore({type: 'toggleInternals'});
|
||||
})
|
||||
}
|
||||
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -4,39 +4,81 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, {
|
||||
startTransition,
|
||||
useId,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {TOGGLE_TAB_TRANSITION} from '../lib/transitionTypes';
|
||||
|
||||
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}`;
|
||||
|
||||
const handleTabChange = (tab: string): void => {
|
||||
startTransition(() => {
|
||||
addTransitionType(TOGGLE_TAB_TRANSITION);
|
||||
onTabChange(tab);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<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 = activeTab === tab;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => onTabChange(tab)}
|
||||
className={clsx(
|
||||
'active:scale-95 transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm',
|
||||
!isActive && 'hover:bg-primary/5',
|
||||
isActive && 'bg-highlight text-link',
|
||||
)}>
|
||||
{tab}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(activeTab)}
|
||||
<div className="flex-1 min-w-[550px] sm:min-w-0">
|
||||
<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;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => handleTabChange(tab)}
|
||||
className={clsx(
|
||||
'transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm relative',
|
||||
isActive ? 'text-link' : 'hover:bg-primary/5',
|
||||
)}>
|
||||
{isActive && (
|
||||
<ViewTransition
|
||||
name={transitionName}
|
||||
enter={{default: 'none'}}
|
||||
exit={{default: 'none'}}
|
||||
share={{
|
||||
[TOGGLE_TAB_TRANSITION]: 'tab-highlight',
|
||||
default: 'none',
|
||||
}}
|
||||
update={{default: 'none'}}>
|
||||
<div className="absolute inset-0 bg-highlight rounded-full" />
|
||||
</ViewTransition>
|
||||
)}
|
||||
<ViewTransition
|
||||
enter={{default: 'none'}}
|
||||
exit={{default: 'none'}}
|
||||
update={{
|
||||
[TOGGLE_TAB_TRANSITION]: 'tab-text',
|
||||
default: 'none',
|
||||
}}>
|
||||
<span className="relative z-1">{tab}</span>
|
||||
</ViewTransition>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden w-full h-full">
|
||||
{tabs.get(currentActiveTab)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
11
compiler/apps/playground/lib/transitionTypes.ts
Normal file
11
compiler/apps/playground/lib/transitionTypes.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export const CONFIG_PANEL_TRANSITION = 'config-panel';
|
||||
export const TOGGLE_TAB_TRANSITION = 'toggle-tab';
|
||||
export const TOGGLE_INTERNALS_TRANSITION = 'toggle-internals';
|
||||
export const EXPAND_ACCORDION_TRANSITION = 'open-accordion';
|
||||
@@ -11,6 +11,7 @@ const path = require('path');
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
reactCompiler: true,
|
||||
viewTransition: true,
|
||||
},
|
||||
reactStrictMode: true,
|
||||
webpack: (config, options) => {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"hermes-eslint": "^0.25.0",
|
||||
"hermes-parser": "^0.25.0",
|
||||
"invariant": "^2.2.4",
|
||||
"lru-cache": "^11.2.2",
|
||||
"lz-string": "^1.5.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"next": "15.6.0-canary.7",
|
||||
|
||||
@@ -69,3 +69,58 @@
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(.slide-in) {
|
||||
animation-name: slideOutLeft;
|
||||
}
|
||||
::view-transition-new(.slide-in) {
|
||||
animation-name: slideInLeft;
|
||||
}
|
||||
::view-transition-group(.slide-in) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes slideOutLeft {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(.container),
|
||||
::view-transition-new(.container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::view-transition-old(.accordion-container),
|
||||
::view-transition-new(.accordion-container) {
|
||||
height: 100%;
|
||||
object-fit: none;
|
||||
object-position: left;
|
||||
}
|
||||
|
||||
::view-transition-old(.tab-highlight),
|
||||
::view-transition-new(.tab-highlight) {
|
||||
height: 100%;
|
||||
}
|
||||
::view-transition-group(.tab-text) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::view-transition-old(.expand-accordion),
|
||||
::view-transition-new(.expand-accordion) {
|
||||
width: auto;
|
||||
}
|
||||
::view-transition-group(.expand-accordion) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
@@ -3104,6 +3104,11 @@ lru-cache@^10.2.0:
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||
|
||||
lru-cache@^11.2.2:
|
||||
version "11.2.2"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24"
|
||||
integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==
|
||||
|
||||
lru-cache@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||
|
||||
@@ -240,7 +240,7 @@ export function addImportsToProgram(
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const existingImports = getExistingImports(path);
|
||||
const stmts: Array<t.ImportDeclaration> = [];
|
||||
const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
|
||||
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
@@ -303,9 +303,29 @@ export function addImportsToProgram(
|
||||
if (maybeExistingImports != null) {
|
||||
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
|
||||
} else {
|
||||
stmts.push(
|
||||
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
|
||||
);
|
||||
if (path.node.sourceType === 'module') {
|
||||
stmts.push(
|
||||
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
|
||||
);
|
||||
} else {
|
||||
stmts.push(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.objectPattern(
|
||||
sortedImport.map(specifier => {
|
||||
return t.objectProperty(
|
||||
t.identifier(specifier.imported),
|
||||
t.identifier(specifier.name),
|
||||
);
|
||||
}),
|
||||
),
|
||||
t.callExpression(t.identifier('require'), [
|
||||
t.stringLiteral(moduleName),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
path.unshiftContainer('body', stmts);
|
||||
|
||||
@@ -454,6 +454,32 @@ function collectNonNullsInBlocks(
|
||||
assumedNonNullObjects.add(entry);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
fn.env.config.enablePreserveExistingMemoizationGuarantees &&
|
||||
instr.value.kind === 'StartMemoize' &&
|
||||
instr.value.deps != null
|
||||
) {
|
||||
for (const dep of instr.value.deps) {
|
||||
if (dep.root.kind === 'NamedLocal') {
|
||||
if (
|
||||
!isImmutableAtInstr(dep.root.value.identifier, instr.id, context)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < dep.path.length; i++) {
|
||||
const pathEntry = dep.path[i]!;
|
||||
if (pathEntry.optional) {
|
||||
break;
|
||||
}
|
||||
const depNode = context.registry.getOrCreateProperty({
|
||||
identifier: dep.root.value.identifier,
|
||||
path: dep.path.slice(0, i),
|
||||
reactive: dep.root.value.reactive,
|
||||
});
|
||||
assumedNonNullObjects.add(depNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export function nameAnonymousFunctions(fn: HIRFunction): void {
|
||||
const parentName = fn.id;
|
||||
const functions = nameAnonymousFunctionsImpl(fn);
|
||||
function visit(node: Node, prefix: string): void {
|
||||
if (node.generatedName != null) {
|
||||
if (node.generatedName != null && node.fn.nameHint == null) {
|
||||
/**
|
||||
* Note that we don't generate a name for functions that already had one,
|
||||
* so we'll only add the prefix to anonymous functions regardless of
|
||||
@@ -70,6 +70,10 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
if (name != null && name.kind === 'named') {
|
||||
names.set(lvalue.identifier.id, name.value);
|
||||
}
|
||||
const func = functions.get(value.place.identifier.id);
|
||||
if (func != null) {
|
||||
functions.set(lvalue.identifier.id, func);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
@@ -106,6 +110,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
const variableName = value.lvalue.place.identifier.name;
|
||||
if (
|
||||
node != null &&
|
||||
node.generatedName == null &&
|
||||
variableName != null &&
|
||||
variableName.kind === 'named'
|
||||
) {
|
||||
@@ -137,7 +142,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(arg.identifier.id);
|
||||
if (node != null) {
|
||||
if (node != null && node.generatedName == null) {
|
||||
const generatedName =
|
||||
fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
|
||||
node.generatedName = generatedName;
|
||||
@@ -152,7 +157,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
|
||||
continue;
|
||||
}
|
||||
const node = functions.get(attr.place.identifier.id);
|
||||
if (node != null) {
|
||||
if (node != null && node.generatedName == null) {
|
||||
const elementName =
|
||||
value.tag.kind === 'BuiltinTag'
|
||||
? value.tag.name
|
||||
|
||||
@@ -5,33 +5,21 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, Effect} from '..';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
Place,
|
||||
CallExpression,
|
||||
Instruction,
|
||||
isUseStateType,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {printInstruction} from '../HIR/PrintHIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sourcesIds: Set<IdentifierId>;
|
||||
};
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
@@ -57,227 +45,102 @@ type DerivationMetadata = {
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
|
||||
const derivationCache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
derivationCache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
derivationCache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let sourcesIds: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const operandMetadata = derivationCache.get(operand.identifier.id);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
sourcesIds.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
addDerivationEntry(phi.place, sourcesIds, typeOfValue, derivationCache);
|
||||
}
|
||||
}
|
||||
for (const i of block.instructions) {
|
||||
function recordInstructiorDerivations(instr: Instruction): void {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructiorDerivations(instr);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
candidateDependencies.set(lvalue.identifier.id, value);
|
||||
} else if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
const deps = candidateDependencies.get(value.args[1].identifier.id);
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
effectFunction != null &&
|
||||
deps != null &&
|
||||
deps.elements.length !== 0 &&
|
||||
deps.elements.every(element => element.kind === 'Identifier')
|
||||
) {
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
if (effectFunction != null) {
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
errors,
|
||||
derivationCache,
|
||||
);
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier)) {
|
||||
const stateValueSource = value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.add(stateValueSource.identifier.id);
|
||||
}
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const operandMetadata = derivationCache.get(operand.identifier.id);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
for (const id of operandMetadata.sourcesIds) {
|
||||
sources.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
addDerivationEntry(lvalue, sources, typeOfValue, derivationCache);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
switch (operand.effect) {
|
||||
case Effect.Capture:
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
if (isMutable(instr, operand)) {
|
||||
addDerivationEntry(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
derivationCache,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
|
||||
CompilerError.invariant(dep.kind === 'Identifier', {
|
||||
reason: `Dependency is checked as a place above`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
loc: value.loc,
|
||||
message: 'this is checked as a place above',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
return locals.get(dep.identifier.id) ?? dep.identifier.id;
|
||||
});
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
recordInstructiorDerivations(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function joinValue(
|
||||
lvalueType: TypeOfValue,
|
||||
valueType: TypeOfValue,
|
||||
): TypeOfValue {
|
||||
if (lvalueType === 'ignored') return valueType;
|
||||
if (valueType === 'ignored') return lvalueType;
|
||||
if (lvalueType === valueType) return lvalueType;
|
||||
return 'fromPropsAndState';
|
||||
}
|
||||
|
||||
function addDerivationEntry(
|
||||
derivedVar: Place,
|
||||
sourcesIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: derivedVar,
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sourcesIds !== undefined) {
|
||||
for (const id of sourcesIds) {
|
||||
const sourcePlace = derivationCache.get(id)?.place;
|
||||
|
||||
if (sourcePlace === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
if (
|
||||
sourcePlace.identifier.name === null ||
|
||||
sourcePlace.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
} else {
|
||||
newValue.sourcesIds.add(sourcePlace.identifier.id);
|
||||
}
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const dep of effectDeps) {
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
) {
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
derivationCache.set(derivedVar.identifier.id, newValue);
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
errors: CompilerError,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
|
||||
const effectDerivedSetStateCalls: Array<{
|
||||
value: CallExpression;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
}> = [];
|
||||
|
||||
const globals: Set<IdentifierId> = new Set();
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -285,64 +148,90 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
// Early return if any instruction is deriving a value from a ref
|
||||
if (isUseRefType(instr.lvalue.identifier)) {
|
||||
return;
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const argMetadata = derivationCache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: instr.value,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
});
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
case 'LoadGlobal': {
|
||||
break;
|
||||
}
|
||||
} else if (instr.value.kind === 'CallExpression') {
|
||||
const calleeMetadata = derivationCache.get(
|
||||
instr.value.callee.identifier.id,
|
||||
);
|
||||
case 'LoadLocal': {
|
||||
const deps = values.get(instr.value.place.identifier.id);
|
||||
if (deps != null) {
|
||||
values.set(instr.lvalue.identifier.id, deps);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad':
|
||||
case 'BinaryExpression':
|
||||
case 'TemplateLiteral':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
|
||||
if (
|
||||
calleeMetadata !== undefined &&
|
||||
(calleeMetadata.typeOfValue === 'fromProps' ||
|
||||
calleeMetadata.typeOfValue === 'fromPropsAndState')
|
||||
) {
|
||||
// If the callee is a prop we can't confidently say that it should be derived in render
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (globals.has(instr.value.callee.identifier.id)) {
|
||||
// If the callee is a global we can't confidently say that it should be derived in render
|
||||
return;
|
||||
}
|
||||
} else if (instr.value.kind === 'LoadGlobal') {
|
||||
globals.add(instr.lvalue.identifier.id);
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
globals.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
loc: derivedSetStateCall.value.callee.loc,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(myRef.current + test);
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState("");
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
setLocal(myRef.current + test);
|
||||
};
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: "testString" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) nulltestString
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(myRef.current + test);
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
onChange();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test', onChange: () => {}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { propValue, onChange } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
if ($[0] !== onChange || $[1] !== propValue) {
|
||||
t1 = () => {
|
||||
setValue(propValue);
|
||||
onChange();
|
||||
};
|
||||
$[0] = onChange;
|
||||
$[1] = propValue;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== propValue) {
|
||||
t2 = [propValue];
|
||||
$[3] = propValue;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== value) {
|
||||
t3 = <div>{value}</div>;
|
||||
$[5] = value;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test", onChange: () => {} }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,17 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
onChange();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test', onChange: () => {}}],
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { propValue } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== propValue) {
|
||||
t1 = () => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
};
|
||||
t2 = [propValue];
|
||||
$[0] = propValue;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== value) {
|
||||
t3 = <div>{value}</div>;
|
||||
$[3] = value;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) globalCall is not defined
|
||||
@@ -1,17 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
globalCall();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.derived-state-conditionally-in-effect.ts:9:6
|
||||
7 | useEffect(() => {
|
||||
8 | if (enabled) {
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | } else {
|
||||
11 | setLocalValue('disabled');
|
||||
12 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.derived-state-from-default-props.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setCurrInput(input + localConst);
|
||||
| ^^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [input, localConst]);
|
||||
11 |
|
||||
12 | return <div>{currInput}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.derived-state-from-prop-local-state-and-component-scope.ts:11:4
|
||||
9 |
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
12 | }, [firstName, middleName, lastName]);
|
||||
13 |
|
||||
14 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.derived-state-from-prop-setter-call-outside-effect-no-error.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setName(initialName);
|
||||
| ^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
9 | }, [initialName]);
|
||||
10 |
|
||||
11 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
|
||||
}
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
return <MockComponent onSet={setValue} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.derived-state-from-prop-setter-used-outside-effect-no-error.ts:11:4
|
||||
9 | const [value, setValue] = useState(null);
|
||||
10 | useEffect(() => {
|
||||
> 11 | setValue(propValue);
|
||||
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
12 | }, [propValue]);
|
||||
13 |
|
||||
14 | return <MockComponent onSet={setValue} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
|
||||
}
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
return <MockComponent onSet={setValue} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.derived-state-from-prop-with-side-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
9 | document.title = `Value: ${value}`;
|
||||
10 | }, [value]);
|
||||
11 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.effect-contains-local-function-call.ts:12:4
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
> 12 | setValue(propValue);
|
||||
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
13 | localFunction();
|
||||
14 | }, [propValue]);
|
||||
15 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-state-from-computed-props.ts:9:4
|
||||
7 | useEffect(() => {
|
||||
8 | const computed = props.prefix + props.value + props.suffix;
|
||||
> 9 | setDisplayValue(computed);
|
||||
| ^^^^^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [props.prefix, props.value, props.suffix]);
|
||||
11 |
|
||||
12 | return <div>{displayValue}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-state-from-destructured-props.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(props.firstName + ' ' + props.lastName);
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
11 | }, [props.firstName, props.lastName]);
|
||||
12 |
|
||||
13 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState(0);
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (myRef.current) {
|
||||
setLocal(test);
|
||||
} else {
|
||||
setLocal(test + test);
|
||||
}
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 4}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState(0);
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
if (myRef.current) {
|
||||
setLocal(test);
|
||||
} else {
|
||||
setLocal(test + test);
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: 4 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 8
|
||||
@@ -1,23 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState(0);
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (myRef.current) {
|
||||
setLocal(test);
|
||||
} else {
|
||||
setLocal(test + test);
|
||||
}
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 4}],
|
||||
};
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function BadExample() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
@@ -12,7 +10,7 @@ function BadExample() {
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
setFullName(capitalize(firstName + ' ' + lastName));
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
@@ -28,14 +26,14 @@ Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
error.invalid-derived-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function BadExample() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
@@ -8,7 +6,7 @@ function BadExample() {
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
setFullName(capitalize(firstName + ' ' + lastName));
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
const object = useMemo(() => {
|
||||
return identity({
|
||||
callback: () => {
|
||||
// This is a bug in our dependency inference: we stop capturing dependencies
|
||||
// after x.a.b?.c. But what this dependency is telling us is that if `x.a.b`
|
||||
// was non-nullish, then we can access `.c.d?.e`. Thus we should take the
|
||||
// full property chain, exactly as-is with optionals/non-optionals, as a
|
||||
// dependency
|
||||
return identity(x.a.b?.c.d?.e);
|
||||
},
|
||||
});
|
||||
}, [x.a.b?.c.d?.e]);
|
||||
const result = useMemo(() => {
|
||||
return [object.callback()];
|
||||
}, [object]);
|
||||
return <Inner x={x} result={result} />;
|
||||
}
|
||||
|
||||
function Inner({x, result}) {
|
||||
'use no memo';
|
||||
return <ValidateMemoization inputs={[x.y.z]} output={result} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: {y: {z: 42}}}],
|
||||
sequentialRenders: [
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `x.a.b?.c`, but the source dependencies were [x.a.b?.c.d?.e]. Inferred less specific property than source.
|
||||
|
||||
error.todo-preserve-memo-deps-mixed-optional-nonoptional-property-chain.ts:7:25
|
||||
5 |
|
||||
6 | function Component({x}) {
|
||||
> 7 | const object = useMemo(() => {
|
||||
| ^^^^^^^
|
||||
> 8 | return identity({
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | callback: () => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 10 | // This is a bug in our dependency inference: we stop capturing dependencies
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 11 | // after x.a.b?.c. But what this dependency is telling us is that if `x.a.b`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 12 | // was non-nullish, then we can access `.c.d?.e`. Thus we should take the
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 13 | // full property chain, exactly as-is with optionals/non-optionals, as a
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 14 | // dependency
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 15 | return identity(x.a.b?.c.d?.e);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 16 | },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 17 | });
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 18 | }, [x.a.b?.c.d?.e]);
|
||||
| ^^^^ Could not preserve existing manual memoization
|
||||
19 | const result = useMemo(() => {
|
||||
20 | return [object.callback()];
|
||||
21 | }, [object]);
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
const object = useMemo(() => {
|
||||
return identity({
|
||||
callback: () => {
|
||||
// This is a bug in our dependency inference: we stop capturing dependencies
|
||||
// after x.a.b?.c. But what this dependency is telling us is that if `x.a.b`
|
||||
// was non-nullish, then we can access `.c.d?.e`. Thus we should take the
|
||||
// full property chain, exactly as-is with optionals/non-optionals, as a
|
||||
// dependency
|
||||
return identity(x.a.b?.c.d?.e);
|
||||
},
|
||||
});
|
||||
}, [x.a.b?.c.d?.e]);
|
||||
const result = useMemo(() => {
|
||||
return [object.callback()];
|
||||
}, [object]);
|
||||
return <Inner x={x} result={result} />;
|
||||
}
|
||||
|
||||
function Inner({x, result}) {
|
||||
'use no memo';
|
||||
return <ValidateMemoization inputs={[x.y.z]} output={result} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: {y: {z: 42}}}],
|
||||
sequentialRenders: [
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
],
|
||||
};
|
||||
@@ -4,15 +4,19 @@
|
||||
```javascript
|
||||
// @enableNameAnonymousFunctions
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
import * as SharedRuntime from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
function named() {
|
||||
const inner = () => props.named;
|
||||
return inner();
|
||||
const innerIdentity = identity(() => props.named);
|
||||
return inner(innerIdentity());
|
||||
}
|
||||
const callback = useCallback(() => {
|
||||
return 'ok';
|
||||
}, []);
|
||||
const namedVariable = function () {
|
||||
return props.namedVariable;
|
||||
};
|
||||
@@ -30,6 +34,7 @@ function Component(props) {
|
||||
return (
|
||||
<>
|
||||
{named()}
|
||||
{callback()}
|
||||
{namedVariable()}
|
||||
{methodCall()}
|
||||
{call()}
|
||||
@@ -63,7 +68,7 @@ export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { identity, Stringify, useIdentity } from "shared-runtime";
|
||||
import * as SharedRuntime from "shared-runtime";
|
||||
|
||||
@@ -75,7 +80,12 @@ function Component(props) {
|
||||
const inner = { "Component[named > inner]": () => props.named }[
|
||||
"Component[named > inner]"
|
||||
];
|
||||
return inner();
|
||||
const innerIdentity = identity(
|
||||
{ "Component[named > identity()]": () => props.named }[
|
||||
"Component[named > identity()]"
|
||||
],
|
||||
);
|
||||
return inner(innerIdentity());
|
||||
};
|
||||
$[0] = props.named;
|
||||
$[1] = t0;
|
||||
@@ -83,6 +93,8 @@ function Component(props) {
|
||||
t0 = $[1];
|
||||
}
|
||||
const named = t0;
|
||||
|
||||
const callback = _ComponentCallback;
|
||||
let t1;
|
||||
if ($[2] !== props.namedVariable) {
|
||||
t1 = {
|
||||
@@ -197,57 +209,62 @@ function Component(props) {
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
const t10 = callback();
|
||||
let t11;
|
||||
if ($[19] !== namedVariable) {
|
||||
t10 = namedVariable();
|
||||
t11 = namedVariable();
|
||||
$[19] = namedVariable;
|
||||
$[20] = t10;
|
||||
$[20] = t11;
|
||||
} else {
|
||||
t10 = $[20];
|
||||
}
|
||||
const t11 = methodCall();
|
||||
const t12 = call();
|
||||
let t13;
|
||||
if ($[21] !== hookArgument) {
|
||||
t13 = hookArgument();
|
||||
$[21] = hookArgument;
|
||||
$[22] = t13;
|
||||
} else {
|
||||
t13 = $[22];
|
||||
t11 = $[20];
|
||||
}
|
||||
const t12 = methodCall();
|
||||
const t13 = call();
|
||||
let t14;
|
||||
if ($[21] !== hookArgument) {
|
||||
t14 = hookArgument();
|
||||
$[21] = hookArgument;
|
||||
$[22] = t14;
|
||||
} else {
|
||||
t14 = $[22];
|
||||
}
|
||||
let t15;
|
||||
if (
|
||||
$[23] !== builtinElementAttr ||
|
||||
$[24] !== namedElementAttr ||
|
||||
$[25] !== t10 ||
|
||||
$[26] !== t11 ||
|
||||
$[27] !== t12 ||
|
||||
$[28] !== t13 ||
|
||||
$[25] !== t11 ||
|
||||
$[26] !== t12 ||
|
||||
$[27] !== t13 ||
|
||||
$[28] !== t14 ||
|
||||
$[29] !== t9
|
||||
) {
|
||||
t14 = (
|
||||
t15 = (
|
||||
<>
|
||||
{t9}
|
||||
{t10}
|
||||
{t11}
|
||||
{t12}
|
||||
{t13}
|
||||
{builtinElementAttr}
|
||||
{namedElementAttr}
|
||||
{t13}
|
||||
{t14}
|
||||
</>
|
||||
);
|
||||
$[23] = builtinElementAttr;
|
||||
$[24] = namedElementAttr;
|
||||
$[25] = t10;
|
||||
$[26] = t11;
|
||||
$[27] = t12;
|
||||
$[28] = t13;
|
||||
$[25] = t11;
|
||||
$[26] = t12;
|
||||
$[27] = t13;
|
||||
$[28] = t14;
|
||||
$[29] = t9;
|
||||
$[30] = t14;
|
||||
$[30] = t15;
|
||||
} else {
|
||||
t14 = $[30];
|
||||
t15 = $[30];
|
||||
}
|
||||
return t14;
|
||||
return t15;
|
||||
}
|
||||
function _ComponentCallback() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
export const TODO_FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// @enableNameAnonymousFunctions
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
import {identity, Stringify, useIdentity} from 'shared-runtime';
|
||||
import * as SharedRuntime from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
function named() {
|
||||
const inner = () => props.named;
|
||||
return inner();
|
||||
const innerIdentity = identity(() => props.named);
|
||||
return inner(innerIdentity());
|
||||
}
|
||||
const callback = useCallback(() => {
|
||||
return 'ok';
|
||||
}, []);
|
||||
const namedVariable = function () {
|
||||
return props.namedVariable;
|
||||
};
|
||||
@@ -26,6 +30,7 @@ function Component(props) {
|
||||
return (
|
||||
<>
|
||||
{named()}
|
||||
{callback()}
|
||||
{namedVariable()}
|
||||
{methodCall()}
|
||||
{call()}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
const object = useMemo(() => {
|
||||
return identity({
|
||||
callback: () => {
|
||||
return identity(x.y.z); // accesses more levels of properties than the manual memo
|
||||
},
|
||||
});
|
||||
// x.y as a manual dep only tells us that x is non-nullable, not that x.y is non-nullable
|
||||
// we can only take a dep on x.y, not x.y.z
|
||||
}, [x.y]);
|
||||
const result = useMemo(() => {
|
||||
return [object.callback()];
|
||||
}, [object]);
|
||||
return <ValidateMemoization inputs={[x.y]} output={result} />;
|
||||
}
|
||||
|
||||
const input1 = {x: {y: {z: 42}}};
|
||||
const input1b = {x: {y: {z: 42}}};
|
||||
const input2 = {x: {y: {z: 3.14}}};
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [input1],
|
||||
sequentialRenders: [
|
||||
input1,
|
||||
input1,
|
||||
input1b, // should reset even though .z didn't change
|
||||
input1,
|
||||
input2,
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { identity, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(11);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x.y) {
|
||||
t1 = identity({ callback: () => identity(x.y.z) });
|
||||
$[0] = x.y;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const object = t1;
|
||||
let t2;
|
||||
if ($[2] !== object) {
|
||||
t2 = object.callback();
|
||||
$[2] = object;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== t2) {
|
||||
t3 = [t2];
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
const result = t3;
|
||||
let t4;
|
||||
if ($[6] !== x.y) {
|
||||
t4 = [x.y];
|
||||
$[6] = x.y;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== result || $[9] !== t4) {
|
||||
t5 = <ValidateMemoization inputs={t4} output={result} />;
|
||||
$[8] = result;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
|
||||
const input1 = { x: { y: { z: 42 } } };
|
||||
const input1b = { x: { y: { z: 42 } } };
|
||||
const input2 = { x: { y: { z: 3.14 } } };
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [input1],
|
||||
sequentialRenders: [
|
||||
input1,
|
||||
input1,
|
||||
input1b, // should reset even though .z didn't change
|
||||
input1,
|
||||
input2,
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[{"z":42}],"output":[42]}</div>
|
||||
<div>{"inputs":[{"z":42}],"output":[42]}</div>
|
||||
<div>{"inputs":[{"z":42}],"output":[42]}</div>
|
||||
<div>{"inputs":[{"z":42}],"output":[42]}</div>
|
||||
<div>{"inputs":[{"z":3.14}],"output":[3.14]}</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
const object = useMemo(() => {
|
||||
return identity({
|
||||
callback: () => {
|
||||
return identity(x.y.z); // accesses more levels of properties than the manual memo
|
||||
},
|
||||
});
|
||||
// x.y as a manual dep only tells us that x is non-nullable, not that x.y is non-nullable
|
||||
// we can only take a dep on x.y, not x.y.z
|
||||
}, [x.y]);
|
||||
const result = useMemo(() => {
|
||||
return [object.callback()];
|
||||
}, [object]);
|
||||
return <ValidateMemoization inputs={[x.y]} output={result} />;
|
||||
}
|
||||
|
||||
const input1 = {x: {y: {z: 42}}};
|
||||
const input1b = {x: {y: {z: 42}}};
|
||||
const input2 = {x: {y: {z: 3.14}}};
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [input1],
|
||||
sequentialRenders: [
|
||||
input1,
|
||||
input1,
|
||||
input1b, // should reset even though .z didn't change
|
||||
input1,
|
||||
input2,
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
const object = useMemo(() => {
|
||||
return identity({
|
||||
callback: () => {
|
||||
return identity(x.y.z);
|
||||
},
|
||||
});
|
||||
}, [x.y.z]);
|
||||
const result = useMemo(() => {
|
||||
return [object.callback()];
|
||||
}, [object]);
|
||||
return <ValidateMemoization inputs={[x.y.z]} output={result} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: {y: {z: 42}}}],
|
||||
sequentialRenders: [
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { identity, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(11);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x.y.z) {
|
||||
t1 = identity({ callback: () => identity(x.y.z) });
|
||||
$[0] = x.y.z;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const object = t1;
|
||||
let t2;
|
||||
if ($[2] !== object) {
|
||||
t2 = object.callback();
|
||||
$[2] = object;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== t2) {
|
||||
t3 = [t2];
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
const result = t3;
|
||||
let t4;
|
||||
if ($[6] !== x.y.z) {
|
||||
t4 = [x.y.z];
|
||||
$[6] = x.y.z;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== result || $[9] !== t4) {
|
||||
t5 = <ValidateMemoization inputs={t4} output={result} />;
|
||||
$[8] = result;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: { y: { z: 42 } } }],
|
||||
sequentialRenders: [
|
||||
{ x: { y: { z: 42 } } },
|
||||
{ x: { y: { z: 42 } } },
|
||||
{ x: { y: { z: 3.14 } } },
|
||||
{ x: { y: { z: 42 } } },
|
||||
{ x: { y: { z: 3.14 } } },
|
||||
{ x: { y: { z: 42 } } },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[42],"output":[42]}</div>
|
||||
<div>{"inputs":[42],"output":[42]}</div>
|
||||
<div>{"inputs":[3.14],"output":[3.14]}</div>
|
||||
<div>{"inputs":[42],"output":[42]}</div>
|
||||
<div>{"inputs":[3.14],"output":[3.14]}</div>
|
||||
<div>{"inputs":[42],"output":[42]}</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x}) {
|
||||
const object = useMemo(() => {
|
||||
return identity({
|
||||
callback: () => {
|
||||
return identity(x.y.z);
|
||||
},
|
||||
});
|
||||
}, [x.y.z]);
|
||||
const result = useMemo(() => {
|
||||
return [object.callback()];
|
||||
}, [object]);
|
||||
return <ValidateMemoization inputs={[x.y.z]} output={result} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: {y: {z: 42}}}],
|
||||
sequentialRenders: [
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const object = useMemo(() => {
|
||||
return identity({
|
||||
callback: () => {
|
||||
return identity(x?.y?.z, y.a?.b, z.a.b?.c);
|
||||
},
|
||||
});
|
||||
}, [x?.y?.z, y.a?.b, z.a.b?.c]);
|
||||
const result = useMemo(() => {
|
||||
return [object.callback()];
|
||||
}, [object]);
|
||||
return <Inner x={x} result={result} />;
|
||||
}
|
||||
|
||||
function Inner({x, result}) {
|
||||
'use no memo';
|
||||
return <ValidateMemoization inputs={[x.y.z]} output={result} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: {y: {z: 42}}}],
|
||||
sequentialRenders: [
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { identity, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(11);
|
||||
const { x, y, z } = t0;
|
||||
|
||||
x?.y?.z;
|
||||
y.a?.b;
|
||||
z.a.b?.c;
|
||||
let t1;
|
||||
if ($[0] !== x?.y?.z || $[1] !== y.a?.b || $[2] !== z.a.b?.c) {
|
||||
t1 = identity({ callback: () => identity(x?.y?.z, y.a?.b, z.a.b?.c) });
|
||||
$[0] = x?.y?.z;
|
||||
$[1] = y.a?.b;
|
||||
$[2] = z.a.b?.c;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const object = t1;
|
||||
let t2;
|
||||
if ($[4] !== object) {
|
||||
t2 = object.callback();
|
||||
$[4] = object;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== t2) {
|
||||
t3 = [t2];
|
||||
$[6] = t2;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
const result = t3;
|
||||
let t4;
|
||||
if ($[8] !== result || $[9] !== x) {
|
||||
t4 = <Inner x={x} result={result} />;
|
||||
$[8] = result;
|
||||
$[9] = x;
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
function Inner({ x, result }) {
|
||||
"use no memo";
|
||||
return <ValidateMemoization inputs={[x.y.z]} output={result} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: { y: { z: 42 } } }],
|
||||
sequentialRenders: [
|
||||
{ x: { y: { z: 42 } } },
|
||||
{ x: { y: { z: 42 } } },
|
||||
{ x: { y: { z: 3.14 } } },
|
||||
{ x: { y: { z: 42 } } },
|
||||
{ x: { y: { z: 3.14 } } },
|
||||
{ x: { y: { z: 42 } } },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]]
|
||||
[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]]
|
||||
[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]]
|
||||
[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]]
|
||||
[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]]
|
||||
[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'a') ]]
|
||||
@@ -0,0 +1,36 @@
|
||||
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {identity, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
const object = useMemo(() => {
|
||||
return identity({
|
||||
callback: () => {
|
||||
return identity(x?.y?.z, y.a?.b, z.a.b?.c);
|
||||
},
|
||||
});
|
||||
}, [x?.y?.z, y.a?.b, z.a.b?.c]);
|
||||
const result = useMemo(() => {
|
||||
return [object.callback()];
|
||||
}, [object]);
|
||||
return <Inner x={x} result={result} />;
|
||||
}
|
||||
|
||||
function Inner({x, result}) {
|
||||
'use no memo';
|
||||
return <ValidateMemoization inputs={[x.y.z]} output={result} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: {y: {z: 42}}}],
|
||||
sequentialRenders: [
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
{x: {y: {z: 3.14}}},
|
||||
{x: {y: {z: 42}}},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @script
|
||||
const React = require('react');
|
||||
|
||||
function Component(props) {
|
||||
return <div>{props.name}</div>;
|
||||
}
|
||||
|
||||
// To work with snap evaluator
|
||||
exports = {
|
||||
FIXTURE_ENTRYPOINT: {
|
||||
fn: Component,
|
||||
params: [{name: 'React Compiler'}],
|
||||
},
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
const { c: _c } = require("react/compiler-runtime"); // @script
|
||||
const React = require("react");
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.name) {
|
||||
t0 = <div>{props.name}</div>;
|
||||
$[0] = props.name;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
// To work with snap evaluator
|
||||
exports = {
|
||||
FIXTURE_ENTRYPOINT: {
|
||||
fn: Component,
|
||||
params: [{ name: "React Compiler" }],
|
||||
},
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>React Compiler</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
// @script
|
||||
const React = require('react');
|
||||
|
||||
function Component(props) {
|
||||
return <div>{props.name}</div>;
|
||||
}
|
||||
|
||||
// To work with snap evaluator
|
||||
exports = {
|
||||
FIXTURE_ENTRYPOINT: {
|
||||
fn: Component,
|
||||
params: [{name: 'React Compiler'}],
|
||||
},
|
||||
};
|
||||
@@ -31,10 +31,15 @@ import prettier from 'prettier';
|
||||
import SproutTodoFilter from './SproutTodoFilter';
|
||||
import {isExpectError} from './fixture-utils';
|
||||
import {makeSharedRuntimeTypeProvider} from './sprout/shared-runtime-type-provider';
|
||||
|
||||
export function parseLanguage(source: string): 'flow' | 'typescript' {
|
||||
return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript';
|
||||
}
|
||||
|
||||
export function parseSourceType(source: string): 'script' | 'module' {
|
||||
return source.indexOf('@script') !== -1 ? 'script' : 'module';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse react compiler plugin + environment options from test fixture. Note
|
||||
* that although this primarily uses `Environment:parseConfigPragma`, it also
|
||||
@@ -98,6 +103,7 @@ export function parseInput(
|
||||
input: string,
|
||||
filename: string,
|
||||
language: 'flow' | 'typescript',
|
||||
sourceType: 'module' | 'script',
|
||||
): BabelCore.types.File {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
@@ -105,14 +111,14 @@ export function parseInput(
|
||||
babel: true,
|
||||
flow: 'all',
|
||||
sourceFilename: filename,
|
||||
sourceType: 'module',
|
||||
sourceType,
|
||||
enableExperimentalComponentSyntax: true,
|
||||
});
|
||||
} else {
|
||||
return BabelParser.parse(input, {
|
||||
sourceFilename: filename,
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
sourceType,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -221,11 +227,12 @@ export async function transformFixtureInput(
|
||||
const firstLine = input.substring(0, input.indexOf('\n'));
|
||||
|
||||
const language = parseLanguage(firstLine);
|
||||
const sourceType = parseSourceType(firstLine);
|
||||
// Preserve file extension as it determines typescript's babel transform
|
||||
// mode (e.g. stripping types, parsing rules for brackets)
|
||||
const filename =
|
||||
path.basename(fixturePath) + (language === 'typescript' ? '.ts' : '');
|
||||
const inputAst = parseInput(input, filename, language);
|
||||
const inputAst = parseInput(input, filename, language, sourceType);
|
||||
// Give babel transforms an absolute path as relative paths get prefixed
|
||||
// with `cwd`, which is different across machines
|
||||
const virtualFilepath = '/' + filename;
|
||||
|
||||
@@ -298,7 +298,10 @@ export function doEval(source: string): EvaluatorResult {
|
||||
return {
|
||||
kind: 'UnexpectedError',
|
||||
value:
|
||||
'Unexpected error during eval, possible syntax error?\n' + e.message,
|
||||
'Unexpected error during eval, possible syntax error?\n' +
|
||||
e.message +
|
||||
'\n\nsource:\n' +
|
||||
source,
|
||||
logs,
|
||||
};
|
||||
} finally {
|
||||
|
||||
@@ -133,7 +133,7 @@ async function renderApp(res, returnValue, formState, noCache, debugChannel) {
|
||||
}
|
||||
|
||||
async function prerenderApp(res, returnValue, formState, noCache) {
|
||||
const {unstable_prerenderToNodeStream: prerenderToNodeStream} = await import(
|
||||
const {prerenderToNodeStream} = await import(
|
||||
'react-server-dom-webpack/static'
|
||||
);
|
||||
// const m = require('../src/App.js');
|
||||
|
||||
@@ -238,8 +238,8 @@ export default function Page({url, navigate}) {
|
||||
<Suspend />
|
||||
</div>
|
||||
</ViewTransition>
|
||||
{show ? <Component /> : null}
|
||||
</Suspense>
|
||||
{show ? <Component /> : null}
|
||||
</div>
|
||||
</ViewTransition>
|
||||
</SwipeRecognizer>
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"eslint-plugin-no-for-of-loops": "^1.0.0",
|
||||
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
|
||||
"eslint-plugin-react": "^6.7.1",
|
||||
"eslint-plugin-react-hooks-published": "npm:eslint-plugin-react-hooks@^5.2.0",
|
||||
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
|
||||
"fbjs-scripts": "^3.0.1",
|
||||
"filesize": "^6.0.1",
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
## 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.
|
||||
|
||||
- **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))
|
||||
- **New Violations:** Disallow calling `use` within try/catch blocks. ([@poteto](https://github.com/poteto) in [#34040](https://github.com/facebook/react/pull/34040))
|
||||
- **New Violations:** Disallow calling `useEffectEvent` functions in arbitrary closures. ([@jbrown215](https://github.com/jbrown215) in [#33544](https://github.com/facebook/react/pull/33544))
|
||||
- Handle `React.useEffect` in addition to `useEffect` in rules-of-hooks. ([@Ayc0](https://github.com/Ayc0) in [#34076](https://github.com/facebook/react/pull/34076))
|
||||
- Added `react-hooks` settings config option that to accept `additionalEffectHooks` that are used across exhaustive-deps and rules-of-hooks rules. ([@jbrown215](https://github.com/jbrown215)) in [#34497](https://github.com/facebook/react/pull/34497)
|
||||
|
||||
## 6.0.0
|
||||
|
||||
Accidentally released. See 6.1.0 for the actual changes.
|
||||
|
||||
## 5.2.0
|
||||
|
||||
- Support flat config ([@michaelfaith](https://github.com/michaelfaith) in [#30774](https://github.com/facebook/react/pull/30774))
|
||||
|
||||
@@ -1485,6 +1485,85 @@ const tests = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
// Test settings-based additionalHooks - should work with settings
|
||||
code: normalizeIndent`
|
||||
function MyComponent(props) {
|
||||
useCustomEffect(() => {
|
||||
console.log(props.foo);
|
||||
});
|
||||
}
|
||||
`,
|
||||
settings: {
|
||||
'react-hooks': {
|
||||
additionalEffectHooks: 'useCustomEffect',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test settings-based additionalHooks - should work with dependencies
|
||||
code: normalizeIndent`
|
||||
function MyComponent(props) {
|
||||
useCustomEffect(() => {
|
||||
console.log(props.foo);
|
||||
}, [props.foo]);
|
||||
}
|
||||
`,
|
||||
settings: {
|
||||
'react-hooks': {
|
||||
additionalEffectHooks: 'useCustomEffect',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test that rule-level additionalHooks takes precedence over settings
|
||||
code: normalizeIndent`
|
||||
function MyComponent(props) {
|
||||
useCustomEffect(() => {
|
||||
console.log(props.foo);
|
||||
}, []);
|
||||
}
|
||||
`,
|
||||
options: [{additionalHooks: 'useAnotherEffect'}],
|
||||
settings: {
|
||||
'react-hooks': {
|
||||
additionalEffectHooks: 'useCustomEffect',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test settings with multiple hooks pattern
|
||||
code: normalizeIndent`
|
||||
function MyComponent(props) {
|
||||
useCustomEffect(() => {
|
||||
console.log(props.foo);
|
||||
}, [props.foo]);
|
||||
useAnotherEffect(() => {
|
||||
console.log(props.bar);
|
||||
}, [props.bar]);
|
||||
}
|
||||
`,
|
||||
settings: {
|
||||
'react-hooks': {
|
||||
additionalEffectHooks: '(useCustomEffect|useAnotherEffect)',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
const onStuff = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useEffect(() => {
|
||||
onStuff();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
onStuff();
|
||||
}, []);
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
@@ -3714,6 +3793,40 @@ const tests = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Test settings-based additionalHooks - should detect missing dependency
|
||||
code: normalizeIndent`
|
||||
function MyComponent(props) {
|
||||
useCustomEffect(() => {
|
||||
console.log(props.foo);
|
||||
}, []);
|
||||
}
|
||||
`,
|
||||
settings: {
|
||||
'react-hooks': {
|
||||
additionalEffectHooks: 'useCustomEffect',
|
||||
},
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
"React Hook useCustomEffect has a missing dependency: 'props.foo'. " +
|
||||
'Either include it or remove the dependency array.',
|
||||
suggestions: [
|
||||
{
|
||||
desc: 'Update the dependencies array to be: [props.foo]',
|
||||
output: normalizeIndent`
|
||||
function MyComponent(props) {
|
||||
useCustomEffect(() => {
|
||||
console.log(props.foo);
|
||||
}, [props.foo]);
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent() {
|
||||
@@ -7721,31 +7834,6 @@ const tests = {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (__EXPERIMENTAL__) {
|
||||
tests.valid = [
|
||||
...tests.valid,
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
const onStuff = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useEffect(() => {
|
||||
onStuff();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
onStuff();
|
||||
}, []);
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
tests.invalid = [
|
||||
...tests.invalid,
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
@@ -7809,8 +7897,8 @@ if (__EXPERIMENTAL__) {
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
// Tests that are only valid/invalid across parsers supporting Flow
|
||||
const testsFlow = {
|
||||
|
||||
@@ -581,6 +581,164 @@ const allTests = {
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useMyEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
useServerEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
settings: {
|
||||
'react-hooks': {
|
||||
additionalEffectHooks: '(useMyEffect|useServerEffect)',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be called in a useEffect.
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
React.useEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
|
||||
// and useEffectEvent.
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
const onClick2 = useEffectEvent(() => {
|
||||
debounce(onClick);
|
||||
debounce(() => onClick());
|
||||
debounce(() => { onClick() });
|
||||
deboucne(() => debounce(onClick));
|
||||
});
|
||||
useEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
useEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
// Can receive arguments
|
||||
const onEvent = useEffectEvent((text) => {
|
||||
console.log(text);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onEvent('Hello world');
|
||||
});
|
||||
React.useEffect(() => {
|
||||
onEvent('Hello world');
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useInsertionEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
React.useInsertionEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
|
||||
// and useInsertionEffect.
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
const onClick2 = useEffectEvent(() => {
|
||||
debounce(onClick);
|
||||
debounce(() => onClick());
|
||||
debounce(() => { onClick() });
|
||||
deboucne(() => debounce(onClick));
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
React.useLayoutEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
useInsertionEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
React.useInsertionEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
@@ -1353,86 +1511,39 @@ const allTests = {
|
||||
`,
|
||||
errors: [tryCatchUseError('use')],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (__EXPERIMENTAL__) {
|
||||
allTests.valid = [
|
||||
...allTests.valid,
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be called in a useEffect.
|
||||
// Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
React.useEffect(() => {
|
||||
useCustomHook(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
errors: [useEffectEventError('onClick', true)],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
|
||||
// and useEffectEvent.
|
||||
// Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
const onClick2 = useEffectEvent(() => {
|
||||
debounce(onClick);
|
||||
debounce(() => onClick());
|
||||
debounce(() => { onClick() });
|
||||
deboucne(() => debounce(onClick));
|
||||
});
|
||||
useEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
useEffect(() => {
|
||||
useWrongHook(() => {
|
||||
onClick();
|
||||
});
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
}
|
||||
`,
|
||||
settings: {
|
||||
'react-hooks': {
|
||||
additionalEffectHooks: 'useMyEffect',
|
||||
},
|
||||
},
|
||||
errors: [useEffectEventError('onClick', true)],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
// Can receive arguments
|
||||
const onEvent = useEffectEvent((text) => {
|
||||
console.log(text);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onEvent('Hello world');
|
||||
});
|
||||
React.useEffect(() => {
|
||||
onEvent('Hello world');
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
allTests.invalid = [
|
||||
...allTests.invalid,
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
@@ -1526,21 +1637,36 @@ if (__EXPERIMENTAL__) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
// error message 1
|
||||
const onClick2 = () => { onClick() };
|
||||
// error message 2
|
||||
const onClick3 = useCallback(() => onClick(), []);
|
||||
// error message 3
|
||||
const onClick4 = onClick;
|
||||
return <>
|
||||
{/** error message 4 */}
|
||||
<Child onClick={onClick}></Child>
|
||||
<Child onClick={onClick2}></Child>
|
||||
<Child onClick={onClick3}></Child>
|
||||
</>;
|
||||
}
|
||||
`,
|
||||
// Explicitly test error messages here for various cases
|
||||
errors: [
|
||||
useEffectEventError('onClick', true),
|
||||
useEffectEventError('onClick', true),
|
||||
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
'Effects and Effect Events in the same component.',
|
||||
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
'Effects and Effect Events in the same component.',
|
||||
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
`Effects and Effect Events in the same component. ` +
|
||||
`It cannot be assigned to a variable or passed down.`,
|
||||
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
`Effects and Effect Events in the same component. ` +
|
||||
`It cannot be assigned to a variable or passed down.`,
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
function conditionalError(hook, hasPreviousFinalizer = false) {
|
||||
return {
|
||||
@@ -1603,7 +1729,8 @@ function useEffectEventError(fn, called) {
|
||||
return {
|
||||
message:
|
||||
`\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
`the same component.${called ? '' : ' They cannot be assigned to variables or passed down.'}`,
|
||||
'Effects and Effect Events in the same component.' +
|
||||
(called ? '' : ' It cannot be assigned to a variable or passed down.'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
import type {Linter, Rule} from 'eslint';
|
||||
|
||||
import ExhaustiveDeps from './rules/ExhaustiveDeps';
|
||||
import {
|
||||
allRules,
|
||||
mapErrorSeverityToESlint,
|
||||
recommendedRules,
|
||||
} from './shared/ReactCompiler';
|
||||
import {allRules} from './shared/ReactCompiler';
|
||||
import RulesOfHooks from './rules/RulesOfHooks';
|
||||
|
||||
// All rules
|
||||
@@ -19,7 +15,7 @@ const rules = {
|
||||
'exhaustive-deps': ExhaustiveDeps,
|
||||
'rules-of-hooks': RulesOfHooks,
|
||||
...Object.fromEntries(
|
||||
Object.entries(allRules).map(([name, config]) => [name, config.rule])
|
||||
Object.entries(allRules).map(([name, config]) => [name, config.rule]),
|
||||
),
|
||||
} satisfies Record<string, Rule.RuleModule>;
|
||||
|
||||
@@ -27,15 +23,6 @@ const rules = {
|
||||
const ruleConfigs = {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
// Compiler rules
|
||||
...Object.fromEntries(
|
||||
Object.entries(recommendedRules).map(([name, ruleConfig]) => {
|
||||
return [
|
||||
'react-hooks/' + name,
|
||||
mapErrorSeverityToESlint(ruleConfig.severity),
|
||||
];
|
||||
}),
|
||||
),
|
||||
} satisfies Linter.RulesRecord;
|
||||
|
||||
const plugin = {
|
||||
|
||||
@@ -21,6 +21,8 @@ import type {
|
||||
VariableDeclarator,
|
||||
} from 'estree';
|
||||
|
||||
import { getAdditionalEffectHooksFromSettings } from '../shared/Utils';
|
||||
|
||||
type DeclaredDependency = {
|
||||
key: string;
|
||||
node: Node;
|
||||
@@ -69,19 +71,22 @@ const rule = {
|
||||
},
|
||||
requireExplicitEffectDeps: {
|
||||
type: 'boolean',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context: Rule.RuleContext) {
|
||||
const rawOptions = context.options && context.options[0];
|
||||
const settings = context.settings || {};
|
||||
|
||||
|
||||
// Parse the `additionalHooks` regex.
|
||||
// Use rule-level additionalHooks if provided, otherwise fall back to settings
|
||||
const additionalHooks =
|
||||
rawOptions && rawOptions.additionalHooks
|
||||
? new RegExp(rawOptions.additionalHooks)
|
||||
: undefined;
|
||||
: getAdditionalEffectHooksFromSettings(settings);
|
||||
|
||||
const enableDangerousAutofixThisMayCauseInfiniteLoops: boolean =
|
||||
(rawOptions &&
|
||||
@@ -93,7 +98,8 @@ const rule = {
|
||||
? rawOptions.experimental_autoDependenciesHooks
|
||||
: [];
|
||||
|
||||
const requireExplicitEffectDeps: boolean = rawOptions && rawOptions.requireExplicitEffectDeps || false;
|
||||
const requireExplicitEffectDeps: boolean =
|
||||
(rawOptions && rawOptions.requireExplicitEffectDeps) || false;
|
||||
|
||||
const options = {
|
||||
additionalHooks,
|
||||
@@ -1351,7 +1357,7 @@ const rule = {
|
||||
node: reactiveHook,
|
||||
message:
|
||||
`React Hook ${reactiveHookName} always requires dependencies. ` +
|
||||
`Please add a dependency array or an explicit \`undefined\``
|
||||
`Please add a dependency array or an explicit \`undefined\``,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2116,10 +2122,7 @@ function isAncestorNodeOf(a: Node, b: Node): boolean {
|
||||
}
|
||||
|
||||
function isUseEffectEventIdentifier(node: Node): boolean {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||
}
|
||||
return false;
|
||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||
}
|
||||
|
||||
function getUnknownDependenciesMessage(reactiveHookName: string): string {
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
|
||||
// @ts-expect-error untyped module
|
||||
import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer';
|
||||
import {getAdditionalEffectHooksFromSettings} from '../shared/Utils';
|
||||
|
||||
/**
|
||||
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
||||
@@ -147,16 +148,37 @@ function getNodeWithoutReactNamespace(
|
||||
return node;
|
||||
}
|
||||
|
||||
function isUseEffectIdentifier(node: Node): boolean {
|
||||
return node.type === 'Identifier' && node.name === 'useEffect';
|
||||
}
|
||||
function isUseEffectEventIdentifier(node: Node): boolean {
|
||||
if (__EXPERIMENTAL__) {
|
||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||
function isEffectIdentifier(node: Node, additionalHooks?: RegExp): boolean {
|
||||
const isBuiltInEffect =
|
||||
node.type === 'Identifier' &&
|
||||
(node.name === 'useEffect' ||
|
||||
node.name === 'useLayoutEffect' ||
|
||||
node.name === 'useInsertionEffect');
|
||||
|
||||
if (isBuiltInEffect) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if this matches additional hooks configured by the user
|
||||
if (additionalHooks && node.type === 'Identifier') {
|
||||
return additionalHooks.test(node.name);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUseEffectEventIdentifier(node: Node): boolean {
|
||||
return node.type === 'Identifier' && node.name === 'useEffectEvent';
|
||||
}
|
||||
|
||||
function useEffectEventError(fn: string, called: boolean): string {
|
||||
return (
|
||||
`\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
'Effects and Effect Events in the same component.' +
|
||||
(called ? '' : ' It cannot be assigned to a variable or passed down.')
|
||||
);
|
||||
}
|
||||
|
||||
function isUseIdentifier(node: Node): boolean {
|
||||
return isReactFunction(node, 'use');
|
||||
}
|
||||
@@ -169,8 +191,24 @@ const rule = {
|
||||
recommended: true,
|
||||
url: 'https://react.dev/reference/rules/rules-of-hooks',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
additionalHooks: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context: Rule.RuleContext) {
|
||||
const settings = context.settings || {};
|
||||
|
||||
const additionalEffectHooks =
|
||||
getAdditionalEffectHooksFromSettings(settings);
|
||||
|
||||
let lastEffect: CallExpression | null = null;
|
||||
const codePathReactHooksMapStack: Array<
|
||||
Map<Rule.CodePathSegment, Array<Node>>
|
||||
@@ -726,7 +764,7 @@ const rule = {
|
||||
// Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`
|
||||
const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
|
||||
if (
|
||||
(isUseEffectIdentifier(nodeWithoutNamespace) ||
|
||||
(isEffectIdentifier(nodeWithoutNamespace, additionalEffectHooks) ||
|
||||
isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
|
||||
node.arguments.length > 0
|
||||
) {
|
||||
@@ -740,14 +778,11 @@ const rule = {
|
||||
// This identifier resolves to a useEffectEvent function, but isn't being referenced in an
|
||||
// effect or another event function. It isn't being called either.
|
||||
if (lastEffect == null && useEffectEventFunctions.has(node)) {
|
||||
const message =
|
||||
`\`${getSourceCode().getText(
|
||||
node,
|
||||
)}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
'the same component.' +
|
||||
(node.parent.type === 'CallExpression'
|
||||
? ''
|
||||
: ' They cannot be assigned to variables or passed down.');
|
||||
const message = useEffectEventError(
|
||||
getSourceCode().getText(node),
|
||||
node.parent.type === 'CallExpression',
|
||||
);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message,
|
||||
|
||||
22
packages/eslint-plugin-react-hooks/src/shared/Utils.ts
Normal file
22
packages/eslint-plugin-react-hooks/src/shared/Utils.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { Rule } from 'eslint';
|
||||
|
||||
const SETTINGS_KEY = 'react-hooks';
|
||||
const SETTINGS_ADDITIONAL_EFFECT_HOOKS_KEY = 'additionalEffectHooks';
|
||||
|
||||
export function getAdditionalEffectHooksFromSettings(
|
||||
settings: Rule.RuleContext['settings'],
|
||||
): RegExp | undefined {
|
||||
const additionalHooks = settings[SETTINGS_KEY]?.[SETTINGS_ADDITIONAL_EFFECT_HOOKS_KEY];
|
||||
if (additionalHooks != null && typeof additionalHooks === 'string') {
|
||||
return new RegExp(additionalHooks);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "react-art",
|
||||
"description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).",
|
||||
"version": "19.1.0",
|
||||
"version": "19.3.0",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,10 +24,10 @@
|
||||
"dependencies": {
|
||||
"art": "^0.10.1",
|
||||
"create-react-class": "^15.6.2",
|
||||
"scheduler": "^0.26.0"
|
||||
"scheduler": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0"
|
||||
"react": "^19.3.0"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
|
||||
212
packages/react-client/src/ReactFlightClient.js
vendored
212
packages/react-client/src/ReactFlightClient.js
vendored
@@ -499,10 +499,44 @@ function createErrorChunk<T>(
|
||||
return new ReactPromise(ERRORED, null, error);
|
||||
}
|
||||
|
||||
function moveDebugInfoFromChunkToInnerValue<T>(
|
||||
chunk: InitializedChunk<T>,
|
||||
value: T,
|
||||
): void {
|
||||
// Remove the debug info from the initialized chunk, and add it to the inner
|
||||
// value instead. This can be a React element, an array, or an uninitialized
|
||||
// Lazy.
|
||||
const resolvedValue = resolveLazy(value);
|
||||
if (
|
||||
typeof resolvedValue === 'object' &&
|
||||
resolvedValue !== null &&
|
||||
(isArray(resolvedValue) ||
|
||||
typeof resolvedValue[ASYNC_ITERATOR] === 'function' ||
|
||||
resolvedValue.$$typeof === REACT_ELEMENT_TYPE ||
|
||||
resolvedValue.$$typeof === REACT_LAZY_TYPE)
|
||||
) {
|
||||
const debugInfo = chunk._debugInfo.splice(0);
|
||||
if (isArray(resolvedValue._debugInfo)) {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
resolvedValue._debugInfo.unshift.apply(
|
||||
resolvedValue._debugInfo,
|
||||
debugInfo,
|
||||
);
|
||||
} else {
|
||||
Object.defineProperty((resolvedValue: any), '_debugInfo', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: debugInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wakeChunk<T>(
|
||||
listeners: Array<InitializationReference | (T => mixed)>,
|
||||
value: T,
|
||||
chunk: SomeChunk<T>,
|
||||
chunk: InitializedChunk<T>,
|
||||
): void {
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
const listener = listeners[i];
|
||||
@@ -512,6 +546,10 @@ function wakeChunk<T>(
|
||||
fulfillReference(listener, value, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(chunk, value);
|
||||
}
|
||||
}
|
||||
|
||||
function rejectChunk(
|
||||
@@ -649,7 +687,6 @@ function triggerErrorOnChunk<T>(
|
||||
}
|
||||
try {
|
||||
initializeDebugChunk(response, chunk);
|
||||
chunk._debugChunk = null;
|
||||
if (initializingHandler !== null) {
|
||||
if (initializingHandler.errored) {
|
||||
// Ignore error parsing debug info, we'll report the original error instead.
|
||||
@@ -932,9 +969,9 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
// Lazily initialize any debug info and block the initializing chunk on any unresolved entries.
|
||||
// Initialize any debug info and block the initializing chunk on any
|
||||
// unresolved entries.
|
||||
initializeDebugChunk(response, chunk);
|
||||
chunk._debugChunk = null;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -946,7 +983,14 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
if (resolveListeners !== null) {
|
||||
cyclicChunk.value = null;
|
||||
cyclicChunk.reason = null;
|
||||
wakeChunk(resolveListeners, value, cyclicChunk);
|
||||
for (let i = 0; i < resolveListeners.length; i++) {
|
||||
const listener = resolveListeners[i];
|
||||
if (typeof listener === 'function') {
|
||||
listener(value);
|
||||
} else {
|
||||
fulfillReference(listener, value, cyclicChunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (initializingHandler !== null) {
|
||||
if (initializingHandler.errored) {
|
||||
@@ -963,6 +1007,10 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
||||
initializedChunk.status = INITIALIZED;
|
||||
initializedChunk.value = value;
|
||||
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(initializedChunk, value);
|
||||
}
|
||||
} catch (error) {
|
||||
const erroredChunk: ErroredChunk<T> = (chunk: any);
|
||||
erroredChunk.status = ERRORED;
|
||||
@@ -1079,7 +1127,7 @@ function getTaskName(type: mixed): string {
|
||||
function initializeElement(
|
||||
response: Response,
|
||||
element: any,
|
||||
lazyType: null | LazyComponent<
|
||||
lazyNode: null | LazyComponent<
|
||||
React$Element<any>,
|
||||
SomeChunk<React$Element<any>>,
|
||||
>,
|
||||
@@ -1151,15 +1199,33 @@ function initializeElement(
|
||||
initializeFakeStack(response, owner);
|
||||
}
|
||||
|
||||
// In case the JSX runtime has validated the lazy type as a static child, we
|
||||
// need to transfer this information to the element.
|
||||
if (
|
||||
lazyType &&
|
||||
lazyType._store &&
|
||||
lazyType._store.validated &&
|
||||
!element._store.validated
|
||||
) {
|
||||
element._store.validated = lazyType._store.validated;
|
||||
if (lazyNode !== null) {
|
||||
// In case the JSX runtime has validated the lazy type as a static child, we
|
||||
// need to transfer this information to the element.
|
||||
if (
|
||||
lazyNode._store &&
|
||||
lazyNode._store.validated &&
|
||||
!element._store.validated
|
||||
) {
|
||||
element._store.validated = lazyNode._store.validated;
|
||||
}
|
||||
|
||||
// If the lazy node is initialized, we move its debug info to the inner
|
||||
// value.
|
||||
if (lazyNode._payload.status === INITIALIZED && lazyNode._debugInfo) {
|
||||
const debugInfo = lazyNode._debugInfo.splice(0);
|
||||
if (element._debugInfo) {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
element._debugInfo.unshift.apply(element._debugInfo, debugInfo);
|
||||
} else {
|
||||
Object.defineProperty(element, '_debugInfo', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: debugInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We should be freezing the element but currently, we might write into
|
||||
@@ -1279,13 +1345,13 @@ function createElement(
|
||||
createBlockedChunk(response);
|
||||
handler.value = element;
|
||||
handler.chunk = blockedChunk;
|
||||
const lazyType = createLazyChunkWrapper(blockedChunk, validated);
|
||||
const lazyNode = createLazyChunkWrapper(blockedChunk, validated);
|
||||
if (__DEV__) {
|
||||
// After we have initialized any blocked references, initialize stack etc.
|
||||
const init = initializeElement.bind(null, response, element, lazyType);
|
||||
const init = initializeElement.bind(null, response, element, lazyNode);
|
||||
blockedChunk.then(init, init);
|
||||
}
|
||||
return lazyType;
|
||||
return lazyNode;
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
@@ -1466,7 +1532,7 @@ function fulfillReference(
|
||||
const element: any = handler.value;
|
||||
switch (key) {
|
||||
case '3':
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
|
||||
element.props = mappedValue;
|
||||
break;
|
||||
case '4':
|
||||
@@ -1482,11 +1548,11 @@ function fulfillReference(
|
||||
}
|
||||
break;
|
||||
default:
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
|
||||
break;
|
||||
}
|
||||
} else if (__DEV__ && !reference.isDebug) {
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
|
||||
}
|
||||
|
||||
handler.deps--;
|
||||
@@ -1808,47 +1874,34 @@ function loadServerReference<A: Iterable<any>, T>(
|
||||
return (null: any);
|
||||
}
|
||||
|
||||
function resolveLazy(value: any): mixed {
|
||||
while (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
value.$$typeof === REACT_LAZY_TYPE
|
||||
) {
|
||||
const payload: SomeChunk<any> = value._payload;
|
||||
if (payload.status === INITIALIZED) {
|
||||
value = payload.value;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function transferReferencedDebugInfo(
|
||||
parentChunk: null | SomeChunk<any>,
|
||||
referencedChunk: SomeChunk<any>,
|
||||
referencedValue: mixed,
|
||||
): void {
|
||||
if (__DEV__) {
|
||||
const referencedDebugInfo = referencedChunk._debugInfo;
|
||||
// If we have a direct reference to an object that was rendered by a synchronous
|
||||
// server component, it might have some debug info about how it was rendered.
|
||||
// We forward this to the underlying object. This might be a React Element or
|
||||
// an Array fragment.
|
||||
// If this was a string / number return value we lose the debug info. We choose
|
||||
// that tradeoff to allow sync server components to return plain values and not
|
||||
// use them as React Nodes necessarily. We could otherwise wrap them in a Lazy.
|
||||
if (
|
||||
typeof referencedValue === 'object' &&
|
||||
referencedValue !== null &&
|
||||
(isArray(referencedValue) ||
|
||||
typeof referencedValue[ASYNC_ITERATOR] === 'function' ||
|
||||
referencedValue.$$typeof === REACT_ELEMENT_TYPE)
|
||||
) {
|
||||
// We should maybe use a unique symbol for arrays but this is a React owned array.
|
||||
// $FlowFixMe[prop-missing]: This should be added to elements.
|
||||
const existingDebugInfo: ?ReactDebugInfo =
|
||||
(referencedValue._debugInfo: any);
|
||||
if (existingDebugInfo == null) {
|
||||
Object.defineProperty((referencedValue: any), '_debugInfo', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: referencedDebugInfo.slice(0), // Clone so that pushing later isn't going into the original
|
||||
});
|
||||
} else {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
existingDebugInfo.push.apply(existingDebugInfo, referencedDebugInfo);
|
||||
}
|
||||
}
|
||||
// We also add the debug info to the initializing chunk since the resolution of that promise is
|
||||
// also blocked by the referenced debug info. By adding it to both we can track it even if the array/element
|
||||
// is extracted, or if the root is rendered as is.
|
||||
// We add the debug info to the initializing chunk since the resolution of
|
||||
// that promise is also blocked by the referenced debug info. By adding it
|
||||
// to both we can track it even if the array/element/lazy is extracted, or
|
||||
// if the root is rendered as is.
|
||||
if (parentChunk !== null) {
|
||||
const referencedDebugInfo = referencedChunk._debugInfo;
|
||||
const parentDebugInfo = parentChunk._debugInfo;
|
||||
for (let i = 0; i < referencedDebugInfo.length; ++i) {
|
||||
const debugInfoEntry = referencedDebugInfo[i];
|
||||
@@ -1999,7 +2052,7 @@ function getOutlinedModel<T>(
|
||||
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
|
||||
// transferReferencedDebugInfo because this reference is to a debug chunk.
|
||||
} else {
|
||||
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
|
||||
transferReferencedDebugInfo(initializingChunk, chunk);
|
||||
}
|
||||
return chunkValue;
|
||||
case PENDING:
|
||||
@@ -2709,14 +2762,47 @@ function incrementChunkDebugInfo(
|
||||
}
|
||||
}
|
||||
|
||||
function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
|
||||
const value = resolveLazy(chunk.value);
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
(isArray(value) ||
|
||||
typeof value[ASYNC_ITERATOR] === 'function' ||
|
||||
value.$$typeof === REACT_ELEMENT_TYPE ||
|
||||
value.$$typeof === REACT_LAZY_TYPE)
|
||||
) {
|
||||
if (isArray(value._debugInfo)) {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
value._debugInfo.push.apply(value._debugInfo, debugInfo);
|
||||
} else {
|
||||
Object.defineProperty((value: any), '_debugInfo', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: debugInfo,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
chunk._debugInfo.push.apply(chunk._debugInfo, debugInfo);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChunkDebugInfo(
|
||||
streamState: StreamState,
|
||||
chunk: SomeChunk<any>,
|
||||
): void {
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
// Push the currently resolving chunk's debug info representing the stream on the Promise
|
||||
// that was waiting on the stream.
|
||||
chunk._debugInfo.push({awaited: streamState._debugInfo});
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2909,7 +2995,8 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
|
||||
const resolveListeners = chunk.value;
|
||||
|
||||
if (__DEV__) {
|
||||
// Lazily initialize any debug info and block the initializing chunk on any unresolved entries.
|
||||
// Initialize any debug info and block the initializing chunk on any
|
||||
// unresolved entries.
|
||||
if (chunk._debugChunk != null) {
|
||||
const prevHandler = initializingHandler;
|
||||
const prevChunk = initializingChunk;
|
||||
@@ -2923,7 +3010,6 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
|
||||
}
|
||||
try {
|
||||
initializeDebugChunk(response, chunk);
|
||||
chunk._debugChunk = null;
|
||||
if (initializingHandler !== null) {
|
||||
if (initializingHandler.errored) {
|
||||
// Ignore error parsing debug info, we'll report the original error instead.
|
||||
@@ -2947,7 +3033,7 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
|
||||
resolvedChunk.value = stream;
|
||||
resolvedChunk.reason = controller;
|
||||
if (resolveListeners !== null) {
|
||||
wakeChunk(resolveListeners, chunk.value, chunk);
|
||||
wakeChunk(resolveListeners, chunk.value, (chunk: any));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -327,8 +327,8 @@ describe('ReactFlight', () => {
|
||||
const transport = ReactNoopFlightServer.render(root);
|
||||
|
||||
await act(async () => {
|
||||
const promise = ReactNoopFlightClient.read(transport);
|
||||
expect(getDebugInfo(promise)).toEqual(
|
||||
const result = await ReactNoopFlightClient.read(transport);
|
||||
expect(getDebugInfo(result)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 12},
|
||||
@@ -346,7 +346,7 @@ describe('ReactFlight', () => {
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
ReactNoop.render(await promise);
|
||||
ReactNoop.render(result);
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
|
||||
@@ -1378,9 +1378,7 @@ describe('ReactFlight', () => {
|
||||
environmentName: 'Server',
|
||||
},
|
||||
],
|
||||
findSourceMapURLCalls: [
|
||||
[__filename, 'Server'],
|
||||
[__filename, 'Server'],
|
||||
findSourceMapURLCalls: expect.arrayContaining([
|
||||
// TODO: What should we request here? The outer (<anonymous>) or the inner (inspected-page.html)?
|
||||
['inspected-page.html:29:11), <anonymous>', 'Server'],
|
||||
[
|
||||
@@ -1389,8 +1387,7 @@ describe('ReactFlight', () => {
|
||||
],
|
||||
['file:///testing.js', 'Server'],
|
||||
['', 'Server'],
|
||||
[__filename, 'Server'],
|
||||
],
|
||||
]),
|
||||
});
|
||||
} else {
|
||||
expect(errors.map(getErrorForJestMatcher)).toEqual([
|
||||
@@ -2785,8 +2782,8 @@ describe('ReactFlight', () => {
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const promise = ReactNoopFlightClient.read(transport);
|
||||
expect(getDebugInfo(promise)).toEqual(
|
||||
const result = await ReactNoopFlightClient.read(transport);
|
||||
expect(getDebugInfo(result)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 22 : 20},
|
||||
@@ -2803,11 +2800,10 @@ describe('ReactFlight', () => {
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
const result = await promise;
|
||||
|
||||
const thirdPartyChildren = await result.props.children[1];
|
||||
// We expect the debug info to be transferred from the inner stream to the outer.
|
||||
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
|
||||
expect(getDebugInfo(await thirdPartyChildren[0])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
|
||||
@@ -2910,8 +2906,8 @@ describe('ReactFlight', () => {
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const promise = ReactNoopFlightClient.read(transport);
|
||||
expect(getDebugInfo(promise)).toEqual(
|
||||
const result = await ReactNoopFlightClient.read(transport);
|
||||
expect(getDebugInfo(result)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 16},
|
||||
@@ -2924,17 +2920,10 @@ describe('ReactFlight', () => {
|
||||
transport: expect.arrayContaining([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
{time: 31},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
const result = await promise;
|
||||
const thirdPartyFragment = await result.props.children;
|
||||
expect(getDebugInfo(thirdPartyFragment)).toEqual(
|
||||
__DEV__
|
||||
@@ -2949,15 +2938,7 @@ describe('ReactFlight', () => {
|
||||
children: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
time: 33,
|
||||
},
|
||||
{
|
||||
time: 33,
|
||||
},
|
||||
{
|
||||
time: 33,
|
||||
},
|
||||
{time: 33},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3013,8 +2994,8 @@ describe('ReactFlight', () => {
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const promise = ReactNoopFlightClient.read(transport);
|
||||
expect(getDebugInfo(promise)).toEqual(
|
||||
const result = await ReactNoopFlightClient.read(transport);
|
||||
expect(getDebugInfo(result)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 16},
|
||||
@@ -3040,7 +3021,6 @@ describe('ReactFlight', () => {
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
const result = await promise;
|
||||
ReactNoop.render(result);
|
||||
});
|
||||
|
||||
@@ -3891,15 +3871,6 @@ describe('ReactFlight', () => {
|
||||
{
|
||||
time: 13,
|
||||
},
|
||||
{
|
||||
time: 14,
|
||||
},
|
||||
{
|
||||
time: 15,
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
expect(root._debugInfo).toBe(undefined);
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('React hooks DevTools integration', () => {
|
||||
let act;
|
||||
let overrideHookState;
|
||||
let scheduleUpdate;
|
||||
let scheduleRetry;
|
||||
let setSuspenseHandler;
|
||||
let waitForAll;
|
||||
|
||||
@@ -27,6 +28,7 @@ describe('React hooks DevTools integration', () => {
|
||||
inject: injected => {
|
||||
overrideHookState = injected.overrideHookState;
|
||||
scheduleUpdate = injected.scheduleUpdate;
|
||||
scheduleRetry = injected.scheduleRetry;
|
||||
setSuspenseHandler = injected.setSuspenseHandler;
|
||||
},
|
||||
supportsFiber: true,
|
||||
@@ -312,5 +314,17 @@ describe('React hooks DevTools integration', () => {
|
||||
} else {
|
||||
expect(renderer.toJSON().children).toEqual(['Done']);
|
||||
}
|
||||
|
||||
if (scheduleRetry) {
|
||||
// Lock again, synchronously
|
||||
setSuspenseHandler(() => true);
|
||||
await act(() => scheduleUpdate(fiber)); // Re-render
|
||||
expect(renderer.toJSON().children).toEqual(['Loading']);
|
||||
|
||||
// Release the lock again but this time using retry lane
|
||||
setSuspenseHandler(() => false);
|
||||
await act(() => scheduleRetry(fiber)); // Re-render
|
||||
expect(renderer.toJSON().children).toEqual(['Done']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.10.4",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@jridgewell/sourcemap-codec": "1.5.5",
|
||||
"acorn-jsx": "^5.2.0",
|
||||
"archiver": "^3.0.0",
|
||||
"babel-core": "^7.0.0-bridge",
|
||||
@@ -60,7 +61,6 @@
|
||||
"raw-loader": "^3.1.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"source-map-js": "^0.6.2",
|
||||
"sourcemap-codec": "^1.4.8",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^5.82.1",
|
||||
"webpack-cli": "^5.1.1",
|
||||
|
||||
@@ -23,7 +23,7 @@ function fetchResource(url) {
|
||||
});
|
||||
};
|
||||
|
||||
fetch(url, {cache: 'force-cache'}).then(
|
||||
fetch(url, {cache: 'force-cache', signal: AbortSignal.timeout(60000)}).then(
|
||||
response => {
|
||||
if (response.ok) {
|
||||
response
|
||||
|
||||
@@ -78,6 +78,18 @@ const fetchFromNetworkCache = (url, resolve, reject) => {
|
||||
});
|
||||
};
|
||||
|
||||
const pendingFetchRequests = new Set();
|
||||
function pendingFetchRequestsCleanup({payload, source}) {
|
||||
if (source === 'react-devtools-background') {
|
||||
switch (payload?.type) {
|
||||
case 'fetch-file-with-cache-complete':
|
||||
case 'fetch-file-with-cache-error':
|
||||
pendingFetchRequests.delete(payload.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
chrome.runtime.onMessage.addListener(pendingFetchRequestsCleanup);
|
||||
|
||||
const fetchFromPage = async (url, resolve, reject) => {
|
||||
debugLog('[main] fetchFromPage()', url);
|
||||
|
||||
@@ -97,7 +109,11 @@ const fetchFromPage = async (url, resolve, reject) => {
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(onPortMessage);
|
||||
if (pendingFetchRequests.has(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingFetchRequests.add(url);
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'devtools-page',
|
||||
payload: {
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
"test:e2e": "playwright test --config=playwright.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map-js": "^0.6.2",
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
"@jridgewell/sourcemap-codec": "1.5.5",
|
||||
"source-map-js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.1",
|
||||
|
||||
@@ -130,24 +130,28 @@ describe('Timeline profiler', () => {
|
||||
// @reactVersion <= 18.2
|
||||
// @reactVersion >= 18.0
|
||||
it('should mark sync render without suspends or state updates', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
legacyRender(<div />);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(registeredMarks).toMatchInlineSnapshot(`
|
||||
[
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
[
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
// TODO(hoxyq): investigate why running this test with React 18 fails
|
||||
@@ -260,46 +264,50 @@ describe('Timeline profiler', () => {
|
||||
throw Error('Expected error');
|
||||
}
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
legacyRender(
|
||||
<ErrorBoundary>
|
||||
<ExampleThatThrows />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(registeredMarks).toMatchInlineSnapshot(`
|
||||
[
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-stop",
|
||||
"--error-ExampleThatThrows-mount-Expected error",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--schedule-state-update-1-ErrorBoundary",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
[
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-stop",
|
||||
"--error-ExampleThatThrows-mount-Expected error",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--schedule-state-update-1-ErrorBoundary",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1095,24 +1103,28 @@ describe('Timeline profiler', () => {
|
||||
// @reactVersion <= 18.2
|
||||
// @reactVersion >= 18.0
|
||||
it('regression test SyncLane', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
legacyRender(<div />);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(registeredMarks).toMatchInlineSnapshot(`
|
||||
[
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
[
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-<filtered-version>",
|
||||
"--profiler-version-1",
|
||||
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
|
||||
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
|
||||
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1432,19 +1444,19 @@ describe('Timeline profiler', () => {
|
||||
expect(timelineData.suspenseEvents).toHaveLength(1);
|
||||
const suspenseEvent = timelineData.suspenseEvents[0];
|
||||
expect(suspenseEvent).toMatchInlineSnapshot(`
|
||||
{
|
||||
"componentName": "Example",
|
||||
"depth": 0,
|
||||
"duration": 10,
|
||||
"id": "0",
|
||||
"phase": "mount",
|
||||
"promiseName": "",
|
||||
"resolution": "resolved",
|
||||
"timestamp": 10,
|
||||
"type": "suspense",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
{
|
||||
"componentName": "Example",
|
||||
"depth": 0,
|
||||
"duration": 0,
|
||||
"id": "0",
|
||||
"phase": "mount",
|
||||
"promiseName": "",
|
||||
"resolution": "unresolved",
|
||||
"timestamp": 10,
|
||||
"type": "suspense",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
|
||||
// There should be two batches of renders: Suspeneded and resolved.
|
||||
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
|
||||
@@ -1490,19 +1502,19 @@ describe('Timeline profiler', () => {
|
||||
expect(timelineData.suspenseEvents).toHaveLength(1);
|
||||
const suspenseEvent = timelineData.suspenseEvents[0];
|
||||
expect(suspenseEvent).toMatchInlineSnapshot(`
|
||||
{
|
||||
"componentName": "Example",
|
||||
"depth": 0,
|
||||
"duration": 10,
|
||||
"id": "0",
|
||||
"phase": "mount",
|
||||
"promiseName": "",
|
||||
"resolution": "rejected",
|
||||
"timestamp": 10,
|
||||
"type": "suspense",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
{
|
||||
"componentName": "Example",
|
||||
"depth": 0,
|
||||
"duration": 0,
|
||||
"id": "0",
|
||||
"phase": "mount",
|
||||
"promiseName": "",
|
||||
"resolution": "unresolved",
|
||||
"timestamp": 10,
|
||||
"type": "suspense",
|
||||
"warning": null,
|
||||
}
|
||||
`);
|
||||
|
||||
// There should be two batches of renders: Suspeneded and resolved.
|
||||
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
|
||||
|
||||
@@ -24,21 +24,8 @@ let utils;
|
||||
let assertLog;
|
||||
let waitFor;
|
||||
|
||||
// This flag is on experimental which disables timeline profiler.
|
||||
const enableComponentPerformanceTrack =
|
||||
React.version.startsWith('19') && React.version.includes('experimental');
|
||||
|
||||
describe('Timeline profiler', () => {
|
||||
if (enableComponentPerformanceTrack) {
|
||||
test('no tests', () => {});
|
||||
// Ignore all tests.
|
||||
return;
|
||||
}
|
||||
|
||||
describe('User Timing API', () => {
|
||||
if (enableComponentPerformanceTrack) {
|
||||
return;
|
||||
}
|
||||
let currentlyNotClearedMarks;
|
||||
let registeredMarks;
|
||||
let featureDetectionMarkName = null;
|
||||
@@ -111,9 +98,31 @@ describe('Timeline profiler', () => {
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
Scheduler = require('scheduler');
|
||||
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
waitFor = InternalTestUtils.waitFor;
|
||||
if (typeof Scheduler.log !== 'function') {
|
||||
// backwards compat for older scheduler versions
|
||||
Scheduler.log = Scheduler.unstable_yieldValue;
|
||||
Scheduler.unstable_clearLog = Scheduler.unstable_clearYields;
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
|
||||
// polyfill waitFor as Scheduler.toFlushAndYieldThrough
|
||||
waitFor = expectedYields => {
|
||||
let actualYields = Scheduler.unstable_clearYields();
|
||||
if (actualYields.length !== 0) {
|
||||
throw new Error(
|
||||
'Log of yielded values is not empty. ' +
|
||||
'Call expect(Scheduler).toHaveYielded(...) first.',
|
||||
);
|
||||
}
|
||||
Scheduler.unstable_flushNumberOfYields(expectedYields.length);
|
||||
actualYields = Scheduler.unstable_clearYields();
|
||||
expect(actualYields).toEqual(expectedYields);
|
||||
};
|
||||
} else {
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
waitFor = InternalTestUtils.waitFor;
|
||||
}
|
||||
|
||||
setPerformanceMock =
|
||||
require('react-devtools-shared/src/backend/profilingHooks').setPerformanceMock_ONLY_FOR_TESTING;
|
||||
@@ -146,6 +155,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should return array of lane numbers from bitmask string', () => {
|
||||
expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]);
|
||||
expect(getLanesFromTransportDecimalBitmask('512')).toEqual([9]);
|
||||
@@ -162,6 +172,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should return empty array if laneBitmaskString is not a bitmask', () => {
|
||||
expect(getLanesFromTransportDecimalBitmask('')).toEqual([]);
|
||||
expect(getLanesFromTransportDecimalBitmask('hello')).toEqual([]);
|
||||
@@ -170,6 +181,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should ignore lanes outside REACT_TOTAL_NUM_LANES', () => {
|
||||
const REACT_TOTAL_NUM_LANES =
|
||||
require('react-devtools-timeline/src/constants').REACT_TOTAL_NUM_LANES;
|
||||
@@ -295,11 +307,13 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should throw given an empty timeline', async () => {
|
||||
await expect(async () => preprocessData([])).rejects.toThrow();
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should throw given a timeline with no Profile event', async () => {
|
||||
const randomSample = createUserTimingEntry({
|
||||
dur: 100,
|
||||
@@ -316,6 +330,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should throw given a timeline without an explicit profiler version mark nor any other React marks', async () => {
|
||||
const cpuProfilerSample = creactCpuProfilerSample();
|
||||
|
||||
@@ -327,6 +342,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should throw given a timeline with React scheduling marks, but without an explicit profiler version mark', async () => {
|
||||
const cpuProfilerSample = creactCpuProfilerSample();
|
||||
const scheduleRenderSample = createUserTimingEntry({
|
||||
@@ -341,6 +357,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should return empty data given a timeline with no React scheduling profiling marks', async () => {
|
||||
const cpuProfilerSample = creactCpuProfilerSample();
|
||||
const randomSample = createUserTimingEntry({
|
||||
@@ -445,6 +462,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should process legacy data format (before lane labels were added)', async () => {
|
||||
const cpuProfilerSample = creactCpuProfilerSample();
|
||||
|
||||
@@ -832,6 +850,7 @@ describe('Timeline profiler', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion < 19.2
|
||||
it('should process a sample createRoot render sequence', async () => {
|
||||
function App() {
|
||||
const [didMount, setDidMount] = React.useState(false);
|
||||
@@ -1168,6 +1187,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should populate other user timing marks', async () => {
|
||||
const userTimingData = createUserTimingData([]);
|
||||
userTimingData.push(
|
||||
@@ -1218,6 +1238,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('should include a suspended resource "displayName" if one is set', async () => {
|
||||
let promise = null;
|
||||
let resolvedValue = null;
|
||||
@@ -1359,6 +1380,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.2
|
||||
// @reactVersion < 19.2
|
||||
it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', async () => {
|
||||
function Yield({id, value}) {
|
||||
Scheduler.log(`${id}:${value}`);
|
||||
@@ -1421,6 +1443,7 @@ describe('Timeline profiler', () => {
|
||||
|
||||
describe('nested updates', () => {
|
||||
// @reactVersion >= 18.2
|
||||
// @reactVersion < 19.2
|
||||
it('should not warn about short nested (state) updates during layout effects', async () => {
|
||||
function Component() {
|
||||
const [didMount, setDidMount] = React.useState(false);
|
||||
@@ -1452,6 +1475,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.2
|
||||
// @reactVersion < 19.2
|
||||
it('should not warn about short (forced) updates during layout effects', async () => {
|
||||
class Component extends React.Component {
|
||||
_didMount: boolean = false;
|
||||
@@ -1607,6 +1631,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.2
|
||||
// @reactVersion < 19.2
|
||||
it('should not warn about transition updates scheduled during commit phase', async () => {
|
||||
function Component() {
|
||||
const [value, setValue] = React.useState(0);
|
||||
@@ -1748,6 +1773,7 @@ describe('Timeline profiler', () => {
|
||||
|
||||
describe('errors thrown while rendering', () => {
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion < 19.2
|
||||
it('shoult parse Errors thrown during render', async () => {
|
||||
jest.spyOn(console, 'error');
|
||||
|
||||
@@ -1796,6 +1822,7 @@ describe('Timeline profiler', () => {
|
||||
// This also tests an edge case where a component suspends while profiling
|
||||
// before the first commit is logged (so the lane-to-labels map will not yet exist).
|
||||
// @reactVersion >= 18.2
|
||||
// @reactVersion < 19.2
|
||||
it('should warn about suspending during an update', async () => {
|
||||
let promise = null;
|
||||
let resolvedValue = null;
|
||||
@@ -1862,6 +1889,7 @@ describe('Timeline profiler', () => {
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.2
|
||||
// @reactVersion < 19.2
|
||||
it('should not warn about suspending during an transition', async () => {
|
||||
let promise = null;
|
||||
let resolvedValue = null;
|
||||
@@ -2130,6 +2158,7 @@ describe('Timeline profiler', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion < 19.2
|
||||
it('should process a sample createRoot render sequence', async () => {
|
||||
function App() {
|
||||
const [didMount, setDidMount] = React.useState(false);
|
||||
|
||||
@@ -725,14 +725,14 @@ describe('ProfilingCache', () => {
|
||||
const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
|
||||
expect(commitData).toHaveLength(2);
|
||||
|
||||
const isLegacySuspense = React.version.startsWith('17');
|
||||
if (isLegacySuspense) {
|
||||
if (React.version.startsWith('17')) {
|
||||
// React 17 will mount all children until it suspends in a LegacyHidden
|
||||
// The ID gap is from the Fiber for <Async> that's in the disconnected tree.
|
||||
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
1 => 15,
|
||||
2 => 15,
|
||||
3 => 5,
|
||||
4 => 3,
|
||||
5 => 2,
|
||||
}
|
||||
`);
|
||||
@@ -741,7 +741,6 @@ describe('ProfilingCache', () => {
|
||||
1 => 0,
|
||||
2 => 10,
|
||||
3 => 3,
|
||||
4 => 3,
|
||||
5 => 2,
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -19,8 +19,6 @@ describe('commit tree', () => {
|
||||
let Scheduler;
|
||||
let store: Store;
|
||||
let utils;
|
||||
const isLegacySuspense =
|
||||
React.version.startsWith('16') || React.version.startsWith('17');
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
@@ -186,24 +184,13 @@ describe('commit tree', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
await Promise.resolve();
|
||||
if (isLegacySuspense) {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<Lazy>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
} else {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
}
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
@@ -231,13 +218,7 @@ describe('commit tree', () => {
|
||||
);
|
||||
}
|
||||
|
||||
expect(commitTrees[0].nodes.size).toBe(
|
||||
isLegacySuspense
|
||||
? // <Root> + <App> + <Suspense> + <Lazy>
|
||||
4
|
||||
: // <Root> + <App> + <Suspense>
|
||||
3,
|
||||
);
|
||||
expect(commitTrees[0].nodes.size).toBe(3);
|
||||
expect(commitTrees[1].nodes.size).toBe(4); // <Root> + <App> + <Suspense> + <LazyInnerComponent>
|
||||
expect(commitTrees[2].nodes.size).toBe(2); // <Root> + <App>
|
||||
});
|
||||
@@ -291,24 +272,13 @@ describe('commit tree', () => {
|
||||
it('should support Lazy components that are unmounted before resolving (legacy render)', async () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => legacyRender(<App renderChildren={true} />));
|
||||
if (isLegacySuspense) {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<Lazy>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
} else {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
}
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Suspense>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="App" rects={null}>
|
||||
`);
|
||||
utils.act(() => legacyRender(<App renderChildren={false} />));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
@@ -327,13 +297,7 @@ describe('commit tree', () => {
|
||||
);
|
||||
}
|
||||
|
||||
expect(commitTrees[0].nodes.size).toBe(
|
||||
isLegacySuspense
|
||||
? // <Root> + <App> + <Suspense> + <Lazy>
|
||||
4
|
||||
: // <Root> + <App> + <Suspense>
|
||||
3,
|
||||
);
|
||||
expect(commitTrees[0].nodes.size).toBe(3);
|
||||
expect(commitTrees[1].nodes.size).toBe(2); // <Root> + <App>
|
||||
});
|
||||
|
||||
|
||||
@@ -838,7 +838,7 @@ describe('Store', () => {
|
||||
<Suspense name="two" rects={null}>
|
||||
<Suspense name="three" rects={null}>
|
||||
`);
|
||||
await act(() =>
|
||||
await actAsync(() =>
|
||||
agent.overrideSuspense({
|
||||
id: store.getElementIDAtIndex(2),
|
||||
rendererID,
|
||||
@@ -2828,7 +2828,7 @@ describe('Store', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion >= 17.0
|
||||
it('can reconcile Suspense in fallback positions', async () => {
|
||||
let resolveFallback;
|
||||
const fallbackPromise = new Promise(resolve => {
|
||||
@@ -2907,7 +2907,7 @@ describe('Store', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion >= 17.0
|
||||
it('can reconcile resuspended Suspense with Suspense in fallback positions', async () => {
|
||||
let resolveHeadFallback;
|
||||
let resolveHeadContent;
|
||||
@@ -3107,4 +3107,45 @@ describe('Store', () => {
|
||||
await actAsync(() => render(<span />));
|
||||
expect(store).toMatchInlineSnapshot(`[root]`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19.0
|
||||
it('should reconcile promise-as-a-child', async () => {
|
||||
function Component({children}) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
await actAsync(() =>
|
||||
render(
|
||||
<React.Suspense>
|
||||
{Promise.resolve(<Component key="A">A</Component>)}
|
||||
</React.Suspense>,
|
||||
),
|
||||
);
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense>
|
||||
<Component key="A">
|
||||
[suspense-root] rects={[{x:1,y:2,width:1,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:1,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() =>
|
||||
render(
|
||||
<React.Suspense>
|
||||
{Promise.resolve(<Component key="not-A">not A</Component>)}
|
||||
</React.Suspense>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense>
|
||||
<Component key="not-A">
|
||||
[suspense-root] rects={[{x:1,y:2,width:5,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => render(null));
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ import type {
|
||||
} from './types';
|
||||
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {GroupItem} from './views/TraceUpdates/canvas';
|
||||
import {isReactNativeEnvironment} from './utils';
|
||||
import {gte, isReactNativeEnvironment} from './utils';
|
||||
import {
|
||||
sessionStorageGetItem,
|
||||
sessionStorageRemoveItem,
|
||||
@@ -739,7 +739,7 @@ export default class Agent extends EventEmitter<{
|
||||
if (renderer !== null) {
|
||||
const devRenderer = renderer.bundleType === 1;
|
||||
const enableSuspenseTab =
|
||||
devRenderer && renderer.version.includes('-experimental-');
|
||||
devRenderer && gte(renderer.version, '19.2.0-canary');
|
||||
if (enableSuspenseTab) {
|
||||
this._bridge.send('enableSuspenseTab');
|
||||
}
|
||||
|
||||
@@ -299,6 +299,7 @@ type SuspenseNode = {
|
||||
nextSibling: null | SuspenseNode,
|
||||
rects: null | Array<Rect>, // The bounding rects of content children.
|
||||
suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance>>, // Tracks which data we're suspended by and the children that suspend it.
|
||||
environments: Map<string, number>, // Tracks the Flight environment names that suspended this. I.e. if the server blocked this.
|
||||
// Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
|
||||
// also in the parent sets. This determine whether this could contribute in the loading sequence.
|
||||
hasUniqueSuspenders: boolean,
|
||||
@@ -327,6 +328,7 @@ function createSuspenseNode(
|
||||
nextSibling: null,
|
||||
rects: null,
|
||||
suspendedBy: new Map(),
|
||||
environments: new Map(),
|
||||
hasUniqueSuspenders: false,
|
||||
hasUnknownSuspenders: false,
|
||||
});
|
||||
@@ -460,10 +462,10 @@ export function getInternalReactConstants(version: string): {
|
||||
IncompleteFunctionComponent: 28,
|
||||
IndeterminateComponent: 2, // removed in 19.0.0
|
||||
LazyComponent: 16,
|
||||
LegacyHiddenComponent: 23,
|
||||
LegacyHiddenComponent: 23, // Does not exist in 18+ OSS but exists in fb builds
|
||||
MemoComponent: 14,
|
||||
Mode: 8,
|
||||
OffscreenComponent: 22, // Experimental
|
||||
OffscreenComponent: 22, // Experimental in 17. Stable in 18+
|
||||
Profiler: 12,
|
||||
ScopeComponent: 21, // Experimental
|
||||
SimpleMemoComponent: 15,
|
||||
@@ -1063,6 +1065,7 @@ export function attach(
|
||||
setErrorHandler,
|
||||
setSuspenseHandler,
|
||||
scheduleUpdate,
|
||||
scheduleRetry,
|
||||
getCurrentFiber,
|
||||
} = renderer;
|
||||
const supportsTogglingError =
|
||||
@@ -2220,6 +2223,10 @@ export function attach(
|
||||
}
|
||||
operations[i++] = fiberIdWithChanges;
|
||||
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
|
||||
operations[i++] = suspense.environments.size;
|
||||
suspense.environments.forEach((count, env) => {
|
||||
operations[i++] = getStringID(env);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2244,8 +2251,23 @@ export function attach(
|
||||
}
|
||||
if (typeof instance.getClientRects === 'function') {
|
||||
// DOM
|
||||
const result: Array<Rect> = [];
|
||||
const doc = instance.ownerDocument;
|
||||
if (instance === doc.documentElement) {
|
||||
// This is the document element. The size of this element is not actually
|
||||
// what determines the whole scrollable area of the screen. Because any
|
||||
// thing that overflows the document will also contribute to the scrollable.
|
||||
// This is unlike overflow: scroll which clips those.
|
||||
// Therefore, we use the scrollable size for this rect instead.
|
||||
return [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: instance.scrollWidth,
|
||||
height: instance.scrollHeight,
|
||||
},
|
||||
];
|
||||
}
|
||||
const result: Array<Rect> = [];
|
||||
const win = doc && doc.defaultView;
|
||||
const scrollX = win ? win.scrollX : 0;
|
||||
const scrollY = win ? win.scrollY : 0;
|
||||
@@ -2725,6 +2747,13 @@ export function attach(
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Just enqueue the operations here instead of stashing by id.
|
||||
|
||||
// Ensure each environment gets recorded in the string table since it is emitted
|
||||
// before we loop it over again later during flush.
|
||||
suspenseNode.environments.forEach((count, env) => {
|
||||
getStringID(env);
|
||||
});
|
||||
pendingSuspenderChanges.add(fiberInstance.id);
|
||||
}
|
||||
|
||||
@@ -2807,7 +2836,20 @@ export function attach(
|
||||
let suspendedBySet = suspenseNodeSuspendedBy.get(ioInfo);
|
||||
if (suspendedBySet === undefined) {
|
||||
suspendedBySet = new Set();
|
||||
suspenseNodeSuspendedBy.set(asyncInfo.awaited, suspendedBySet);
|
||||
suspenseNodeSuspendedBy.set(ioInfo, suspendedBySet);
|
||||
// We've added a dependency. We must increment the ref count of the environment.
|
||||
const env = ioInfo.env;
|
||||
if (env != null) {
|
||||
const environmentCounts = parentSuspenseNode.environments;
|
||||
const count = environmentCounts.get(env);
|
||||
if (count === undefined || count === 0) {
|
||||
environmentCounts.set(env, 1);
|
||||
// We've discovered a new environment for this SuspenseNode. We'll to update the node.
|
||||
recordSuspenseSuspenders(parentSuspenseNode);
|
||||
} else {
|
||||
environmentCounts.set(env, count + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The child of the Suspense boundary that was suspended on this, or null if suspended at the root.
|
||||
// This is used to keep track of how many dependents are still alive and also to get information
|
||||
@@ -2886,10 +2928,18 @@ export function attach(
|
||||
previousSuspendedBy: null | Array<ReactAsyncInfo>,
|
||||
parentSuspenseNode: null | SuspenseNode,
|
||||
): void {
|
||||
// Remove any async info from the parent, if they were in the previous set but
|
||||
// Remove any async info if they were in the previous set but
|
||||
// is no longer in the new set.
|
||||
if (previousSuspendedBy !== null && parentSuspenseNode !== null) {
|
||||
// If we just reconciled a SuspenseNode, we need to remove from that node instead of the parent.
|
||||
// This is different from inserting because inserting is done during reconiliation
|
||||
// whereas removal is done after we're done reconciling.
|
||||
const suspenseNode =
|
||||
instance.suspenseNode === null
|
||||
? parentSuspenseNode
|
||||
: instance.suspenseNode;
|
||||
if (previousSuspendedBy !== null && suspenseNode !== null) {
|
||||
const nextSuspendedBy = instance.suspendedBy;
|
||||
let changedEnvironment = false;
|
||||
for (let i = 0; i < previousSuspendedBy.length; i++) {
|
||||
const asyncInfo = previousSuspendedBy[i];
|
||||
if (
|
||||
@@ -2901,7 +2951,7 @@ export function attach(
|
||||
// This IO entry is no longer blocking the current tree.
|
||||
// Let's remove it from the parent SuspenseNode.
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
const suspendedBySet = parentSuspenseNode.suspendedBy.get(ioInfo);
|
||||
const suspendedBySet = suspenseNode.suspendedBy.get(ioInfo);
|
||||
|
||||
if (
|
||||
suspendedBySet === undefined ||
|
||||
@@ -2928,19 +2978,41 @@ export function attach(
|
||||
}
|
||||
}
|
||||
if (suspendedBySet !== undefined && suspendedBySet.size === 0) {
|
||||
parentSuspenseNode.suspendedBy.delete(asyncInfo.awaited);
|
||||
suspenseNode.suspendedBy.delete(ioInfo);
|
||||
// Successfully removed all dependencies. We can decrement the ref count of the environment.
|
||||
const env = ioInfo.env;
|
||||
if (env != null) {
|
||||
const environmentCounts = suspenseNode.environments;
|
||||
const count = environmentCounts.get(env);
|
||||
if (count === undefined || count === 0) {
|
||||
throw new Error(
|
||||
'We are removing an environment but it was not in the set. ' +
|
||||
'This is a bug in React.',
|
||||
);
|
||||
}
|
||||
if (count === 1) {
|
||||
environmentCounts.delete(env);
|
||||
// Last one. We've now change the set of environments. We'll need to update the node.
|
||||
changedEnvironment = true;
|
||||
} else {
|
||||
environmentCounts.set(env, count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
parentSuspenseNode.hasUniqueSuspenders &&
|
||||
!ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo)
|
||||
suspenseNode.hasUniqueSuspenders &&
|
||||
!ioExistsInSuspenseAncestor(suspenseNode, ioInfo)
|
||||
) {
|
||||
// This entry wasn't in any ancestor and is no longer in this suspense boundary.
|
||||
// This means that a child might now be the unique suspender for this IO.
|
||||
// Search the child boundaries to see if we can reveal any of them.
|
||||
unblockSuspendedBy(parentSuspenseNode, ioInfo);
|
||||
unblockSuspendedBy(suspenseNode, ioInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changedEnvironment) {
|
||||
recordSuspenseSuspenders(suspenseNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3057,13 +3129,23 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function isHiddenOffscreen(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;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function unmountRemainingChildren() {
|
||||
if (
|
||||
reconcilingParent !== null &&
|
||||
(reconcilingParent.kind === FIBER_INSTANCE ||
|
||||
reconcilingParent.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
reconcilingParent.data.tag === OffscreenComponent &&
|
||||
reconcilingParent.data.memoizedState !== null &&
|
||||
isHiddenOffscreen(reconcilingParent.data) &&
|
||||
!isInDisconnectedSubtree
|
||||
) {
|
||||
// This is a hidden offscreen, we need to execute this in the context of a disconnected subtree.
|
||||
@@ -3170,8 +3252,7 @@ export function attach(
|
||||
if (
|
||||
(parent.kind === FIBER_INSTANCE ||
|
||||
parent.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
parent.data.tag === OffscreenComponent &&
|
||||
parent.data.memoizedState !== null
|
||||
isHiddenOffscreen(parent.data)
|
||||
) {
|
||||
// We're inside a hidden offscreen Fiber. We're in a disconnected tree.
|
||||
return;
|
||||
@@ -3819,7 +3900,9 @@ export function attach(
|
||||
(reconcilingParent !== null &&
|
||||
reconcilingParent.kind === VIRTUAL_INSTANCE) ||
|
||||
fiber.tag === SuspenseComponent ||
|
||||
fiber.tag === OffscreenComponent // Use to keep resuspended instances alive inside a SuspenseComponent.
|
||||
// Use to keep resuspended instances alive inside a SuspenseComponent.
|
||||
fiber.tag === OffscreenComponent ||
|
||||
fiber.tag === LegacyHiddenComponent
|
||||
) {
|
||||
// If the parent is a Virtual Instance and we filtered this Fiber we include a
|
||||
// hidden node. We also include this if it's a Suspense boundary so we can track those
|
||||
@@ -3939,7 +4022,7 @@ export function attach(
|
||||
trackDebugInfoFromHostComponent(nearestInstance, fiber);
|
||||
}
|
||||
|
||||
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
|
||||
if (isHiddenOffscreen(fiber)) {
|
||||
// If an Offscreen component is hidden, mount its children as disconnected.
|
||||
const stashedDisconnected = isInDisconnectedSubtree;
|
||||
isInDisconnectedSubtree = true;
|
||||
@@ -4261,7 +4344,7 @@ export function attach(
|
||||
while (child !== null) {
|
||||
if (child.kind === FILTERED_FIBER_INSTANCE) {
|
||||
const fiber = child.data;
|
||||
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
|
||||
if (isHiddenOffscreen(fiber)) {
|
||||
// The children of this Offscreen are hidden so they don't get added.
|
||||
} else {
|
||||
addUnfilteredChildrenIDs(child, nextChildren);
|
||||
@@ -4888,9 +4971,8 @@ export function attach(
|
||||
const nextDidTimeOut =
|
||||
isLegacySuspense && nextFiber.memoizedState !== null;
|
||||
|
||||
const isOffscreen = nextFiber.tag === OffscreenComponent;
|
||||
const prevWasHidden = isOffscreen && prevFiber.memoizedState !== null;
|
||||
const nextIsHidden = isOffscreen && nextFiber.memoizedState !== null;
|
||||
const prevWasHidden = isHiddenOffscreen(prevFiber);
|
||||
const nextIsHidden = isHiddenOffscreen(nextFiber);
|
||||
|
||||
if (isLegacySuspense) {
|
||||
if (
|
||||
@@ -5245,8 +5327,7 @@ export function attach(
|
||||
if (
|
||||
(child.kind === FIBER_INSTANCE ||
|
||||
child.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
child.data.tag === OffscreenComponent &&
|
||||
child.data.memoizedState !== null
|
||||
isHiddenOffscreen(child.data)
|
||||
) {
|
||||
// This instance's children are already disconnected.
|
||||
} else {
|
||||
@@ -5275,8 +5356,7 @@ export function attach(
|
||||
if (
|
||||
(child.kind === FIBER_INSTANCE ||
|
||||
child.kind === FILTERED_FIBER_INSTANCE) &&
|
||||
child.data.tag === OffscreenComponent &&
|
||||
child.data.memoizedState !== null
|
||||
isHiddenOffscreen(child.data)
|
||||
) {
|
||||
// This instance's children should remain disconnected.
|
||||
} else {
|
||||
@@ -5586,6 +5666,23 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function findLastKnownRectsForID(id: number): null | Array<Rect> {
|
||||
try {
|
||||
const devtoolsInstance = idToDevToolsInstanceMap.get(id);
|
||||
if (devtoolsInstance === undefined) {
|
||||
console.warn(`Could not find DevToolsInstance with id "${id}"`);
|
||||
return null;
|
||||
}
|
||||
if (devtoolsInstance.suspenseNode === null) {
|
||||
return null;
|
||||
}
|
||||
return devtoolsInstance.suspenseNode.rects;
|
||||
} catch (err) {
|
||||
// The fiber might have unmounted by now.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayNameForElementID(id: number): null | string {
|
||||
const devtoolsInstance = idToDevToolsInstanceMap.get(id);
|
||||
if (devtoolsInstance === undefined) {
|
||||
@@ -7690,7 +7787,13 @@ export function attach(
|
||||
// First override is added. Switch React to slower path.
|
||||
setErrorHandler(shouldErrorFiberAccordingToMap);
|
||||
}
|
||||
scheduleUpdate(fiber);
|
||||
if (!forceError && typeof scheduleRetry === 'function') {
|
||||
// If we're dismissing an error and the renderer supports it, use a Retry instead of Sync
|
||||
// This would allow View Transitions to proceed as if the error was dismissed using a Transition.
|
||||
scheduleRetry(fiber);
|
||||
} else {
|
||||
scheduleUpdate(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSuspendFiberAlwaysFalse() {
|
||||
@@ -7748,7 +7851,13 @@ export function attach(
|
||||
setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
|
||||
}
|
||||
}
|
||||
scheduleUpdate(fiber);
|
||||
if (!forceFallback && typeof scheduleRetry === 'function') {
|
||||
// If we're unsuspending and the renderer supports it, use a Retry instead of Sync
|
||||
// to allow for things like View Transitions to proceed the way they would for real.
|
||||
scheduleRetry(fiber);
|
||||
} else {
|
||||
scheduleUpdate(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7770,11 +7879,10 @@ export function attach(
|
||||
}
|
||||
|
||||
// TODO: Allow overriding the timeline for the specified root.
|
||||
forceFallbackForFibers.forEach(fiber => {
|
||||
scheduleUpdate(fiber);
|
||||
});
|
||||
forceFallbackForFibers.clear();
|
||||
|
||||
const unsuspendedSet: Set<Fiber> = new Set(forceFallbackForFibers);
|
||||
|
||||
let resuspended = false;
|
||||
for (let i = 0; i < suspendedSet.length; ++i) {
|
||||
const instance = idToDevToolsInstanceMap.get(suspendedSet[i]);
|
||||
if (instance === undefined) {
|
||||
@@ -7786,15 +7894,41 @@ export function attach(
|
||||
|
||||
if (instance.kind === FIBER_INSTANCE) {
|
||||
const fiber = instance.data;
|
||||
forceFallbackForFibers.add(fiber);
|
||||
// We could find a minimal set that covers all the Fibers in this suspended set.
|
||||
// For now we rely on React's batching of updates.
|
||||
scheduleUpdate(fiber);
|
||||
if (
|
||||
forceFallbackForFibers.has(fiber) ||
|
||||
(fiber.alternate !== null &&
|
||||
forceFallbackForFibers.has(fiber.alternate))
|
||||
) {
|
||||
// We're already forcing fallback for this fiber. Mark it as not unsuspended.
|
||||
unsuspendedSet.delete(fiber);
|
||||
if (fiber.alternate !== null) {
|
||||
unsuspendedSet.delete(fiber.alternate);
|
||||
}
|
||||
} else {
|
||||
forceFallbackForFibers.add(fiber);
|
||||
// We could find a minimal set that covers all the Fibers in this suspended set.
|
||||
// For now we rely on React's batching of updates.
|
||||
scheduleUpdate(fiber);
|
||||
resuspended = true;
|
||||
}
|
||||
} else {
|
||||
console.warn(`Cannot not suspend ID '${suspendedSet[i]}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Unsuspend any existing forced fallbacks if they're not in the new set.
|
||||
unsuspendedSet.forEach(fiber => {
|
||||
forceFallbackForFibers.delete(fiber);
|
||||
if (!resuspended && typeof scheduleRetry === 'function') {
|
||||
// If nothing new resuspended we don't need this to be sync. If we're only
|
||||
// unsuspending then we can schedule this as a Retry if the renderer supports it.
|
||||
// That way we can trigger animations.
|
||||
scheduleRetry(fiber);
|
||||
} else {
|
||||
scheduleUpdate(fiber);
|
||||
}
|
||||
});
|
||||
|
||||
if (forceFallbackForFibers.size > 0) {
|
||||
// First override is added. Switch React to slower path.
|
||||
// TODO: Semantics for suspending a timeline are different. We want a suspended
|
||||
@@ -8285,6 +8419,7 @@ export function attach(
|
||||
getSerializedElementValueByPath,
|
||||
deletePath,
|
||||
findHostInstancesForElementID,
|
||||
findLastKnownRectsForID,
|
||||
flushInitialOperations,
|
||||
getBestMatchForTrackedPath,
|
||||
getDisplayNameForElementID,
|
||||
|
||||
@@ -152,6 +152,9 @@ export function attach(
|
||||
findHostInstancesForElementID() {
|
||||
return null;
|
||||
},
|
||||
findLastKnownRectsForID() {
|
||||
return null;
|
||||
},
|
||||
flushInitialOperations() {},
|
||||
getBestMatchForTrackedPath() {
|
||||
return null;
|
||||
|
||||
@@ -1168,6 +1168,9 @@ export function attach(
|
||||
const hostInstance = findHostInstanceForInternalID(id);
|
||||
return hostInstance == null ? null : [hostInstance];
|
||||
},
|
||||
findLastKnownRectsForID() {
|
||||
return null;
|
||||
},
|
||||
getOwnersList,
|
||||
getPathForElement,
|
||||
getProfilingData,
|
||||
|
||||
@@ -101,6 +101,16 @@ export type FindHostInstancesForElementID = (
|
||||
id: number,
|
||||
) => null | $ReadOnlyArray<HostInstance>;
|
||||
|
||||
type Rect = {
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
...
|
||||
};
|
||||
export type FindLastKnownRectsForID = (
|
||||
id: number,
|
||||
) => null | $ReadOnlyArray<Rect>;
|
||||
export type ReactProviderType<T> = {
|
||||
$$typeof: symbol | number,
|
||||
_context: ReactContext<T>,
|
||||
@@ -155,6 +165,8 @@ export type ReactRenderer = {
|
||||
) => void,
|
||||
// 16.9+
|
||||
scheduleUpdate?: ?(fiber: Object) => void,
|
||||
// 19.2+
|
||||
scheduleRetry?: ?(fiber: Object) => void,
|
||||
setSuspenseHandler?: ?(shouldSuspend: (fiber: Object) => boolean) => void,
|
||||
// Only injected by React v16.8+ in order to support hooks inspection.
|
||||
currentDispatcherRef?: LegacyDispatcherRef | CurrentDispatcherRef,
|
||||
@@ -409,6 +421,7 @@ export type RendererInterface = {
|
||||
path: Array<string | number>,
|
||||
) => void,
|
||||
findHostInstancesForElementID: FindHostInstancesForElementID,
|
||||
findLastKnownRectsForID: FindLastKnownRectsForID,
|
||||
flushInitialOperations: () => void,
|
||||
getBestMatchForTrackedPath: () => PathMatch | null,
|
||||
getComponentStack?: GetComponentStack,
|
||||
|
||||
@@ -11,6 +11,7 @@ import Agent from 'react-devtools-shared/src/backend/agent';
|
||||
import {hideOverlay, showOverlay} from './Highlighter';
|
||||
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {RendererInterface} from '../../types';
|
||||
|
||||
// This plug-in provides in-page highlighting of the selected element.
|
||||
// It is used by the browser extension and the standalone DevTools shell (when connected to a browser).
|
||||
@@ -25,6 +26,7 @@ export default function setupHighlighter(
|
||||
): void {
|
||||
bridge.addListener('clearHostInstanceHighlight', clearHostInstanceHighlight);
|
||||
bridge.addListener('highlightHostInstance', highlightHostInstance);
|
||||
bridge.addListener('scrollToHostInstance', scrollToHostInstance);
|
||||
bridge.addListener('shutdown', stopInspectingHost);
|
||||
bridge.addListener('startInspectingHost', startInspectingHost);
|
||||
bridge.addListener('stopInspectingHost', stopInspectingHost);
|
||||
@@ -111,24 +113,162 @@ export default function setupHighlighter(
|
||||
}
|
||||
|
||||
const nodes = renderer.findHostInstancesForElementID(id);
|
||||
if (nodes != null) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (node === null) {
|
||||
continue;
|
||||
}
|
||||
const nodeRects =
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof node.getClientRects === 'function'
|
||||
? node.getClientRects()
|
||||
: [];
|
||||
// If this is currently display: none, then try another node.
|
||||
// This can happen when one of the host instances is a hoistable.
|
||||
if (
|
||||
nodeRects.length > 0 &&
|
||||
(nodeRects.length > 2 ||
|
||||
nodeRects[0].width > 0 ||
|
||||
nodeRects[0].height > 0)
|
||||
) {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (scrollIntoView && typeof node.scrollIntoView === 'function') {
|
||||
if (scrollDelayTimer) {
|
||||
clearTimeout(scrollDelayTimer);
|
||||
scrollDelayTimer = null;
|
||||
}
|
||||
// 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'});
|
||||
}
|
||||
|
||||
if (nodes != null && nodes[0] != null) {
|
||||
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);
|
||||
|
||||
if (openBuiltinElementsPanel) {
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = node;
|
||||
bridge.send('syncSelectionToBuiltinElementsPanel');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showOverlay(nodes, displayName, agent, hideAfterTimeout);
|
||||
hideOverlay(agent);
|
||||
}
|
||||
|
||||
if (openBuiltinElementsPanel) {
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = node;
|
||||
bridge.send('syncSelectionToBuiltinElementsPanel');
|
||||
function attemptScrollToHostInstance(
|
||||
renderer: RendererInterface,
|
||||
id: number,
|
||||
) {
|
||||
const nodes = renderer.findHostInstancesForElementID(id);
|
||||
if (nodes != null) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (node === null) {
|
||||
continue;
|
||||
}
|
||||
const nodeRects =
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof node.getClientRects === 'function'
|
||||
? node.getClientRects()
|
||||
: [];
|
||||
// If this is currently display: none, then try another node.
|
||||
// This can happen when one of the host instances is a hoistable.
|
||||
if (
|
||||
nodeRects.length > 0 &&
|
||||
(nodeRects.length > 2 ||
|
||||
nodeRects[0].width > 0 ||
|
||||
nodeRects[0].height > 0)
|
||||
) {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (typeof node.scrollIntoView === 'function') {
|
||||
node.scrollIntoView({
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hideOverlay(agent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let scrollDelayTimer = null;
|
||||
function scrollToHostInstance({
|
||||
id,
|
||||
rendererID,
|
||||
}: {
|
||||
id: number,
|
||||
rendererID: number,
|
||||
}) {
|
||||
// Always hide the existing overlay so it doesn't obscure the element.
|
||||
// If you wanted to show the overlay, highlightHostInstance should be used instead
|
||||
// with the scrollIntoView option.
|
||||
hideOverlay(agent);
|
||||
|
||||
if (scrollDelayTimer) {
|
||||
clearTimeout(scrollDelayTimer);
|
||||
scrollDelayTimer = null;
|
||||
}
|
||||
|
||||
const renderer = agent.rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
// In some cases fiber may already be unmounted
|
||||
if (!renderer.hasElementWithId(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attemptScrollToHostInstance(renderer, id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's possible that the current state of a Suspense boundary doesn't have a position
|
||||
// in the tree. E.g. because it's not yet mounted in the state we're moving to.
|
||||
// Such as if it's in a null tree or inside another boundary's hidden state.
|
||||
// In this case we use the last known position and try to scroll to that.
|
||||
const rects = renderer.findLastKnownRectsForID(id);
|
||||
if (rects !== null && rects.length > 0) {
|
||||
let x = Infinity;
|
||||
let y = Infinity;
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
const rect = rects[i];
|
||||
if (rect.x < x) {
|
||||
x = rect.x;
|
||||
}
|
||||
if (rect.y < y) {
|
||||
y = rect.y;
|
||||
}
|
||||
}
|
||||
const element = document.documentElement;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
// Check if the target corner is already in the viewport.
|
||||
if (
|
||||
x < window.scrollX ||
|
||||
y < window.scrollY ||
|
||||
x > window.scrollX + element.clientWidth ||
|
||||
y > window.scrollY + element.clientHeight
|
||||
) {
|
||||
window.scrollTo({
|
||||
top: y,
|
||||
left: x,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
// It's possible that after mount, we're able to scroll deeper once the new nodes
|
||||
// have mounted. Let's try again after mount. Ideally we'd know which commit this
|
||||
// is going to be but for now we just try after 100ms.
|
||||
scrollDelayTimer = setTimeout(() => {
|
||||
attemptScrollToHostInstance(renderer, id);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
packages/react-devtools-shared/src/bridge.js
vendored
5
packages/react-devtools-shared/src/bridge.js
vendored
@@ -93,6 +93,10 @@ type HighlightHostInstance = {
|
||||
scrollIntoView: boolean,
|
||||
};
|
||||
|
||||
type ScrollToHostInstance = {
|
||||
...ElementAndRendererID,
|
||||
};
|
||||
|
||||
type OverrideValue = {
|
||||
...ElementAndRendererID,
|
||||
path: Array<string | number>,
|
||||
@@ -254,6 +258,7 @@ type FrontendEvents = {
|
||||
startInspectingHost: [],
|
||||
startProfiling: [StartProfilingParams],
|
||||
stopInspectingHost: [boolean],
|
||||
scrollToHostInstance: [ScrollToHostInstance],
|
||||
stopProfiling: [],
|
||||
storeAsGlobal: [StoreAsGlobalParams],
|
||||
updateComponentFilters: [Array<ComponentFilter>],
|
||||
|
||||
@@ -42,6 +42,7 @@ export type Resource<Input, Key, Value> = {
|
||||
let readContext;
|
||||
if (typeof React.use === 'function') {
|
||||
readContext = function (Context: ReactContext<null>) {
|
||||
// eslint-disable-next-line react-hooks-published/rules-of-hooks
|
||||
return React.use(Context);
|
||||
};
|
||||
} else if (
|
||||
@@ -141,6 +142,7 @@ export function createResource<Input, Key, Value>(
|
||||
const key = hashInput(input);
|
||||
const result: Thenable<Value> = accessResult(resource, fetch, input, key);
|
||||
if (typeof React.use === 'function') {
|
||||
// eslint-disable-next-line react-hooks-published/rules-of-hooks
|
||||
return React.use(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -1759,12 +1759,22 @@ export default class Store extends EventEmitter<{
|
||||
break;
|
||||
}
|
||||
case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
|
||||
const changeLength = operations[i + 1];
|
||||
i += 2;
|
||||
i++;
|
||||
const changeLength = operations[i++];
|
||||
|
||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||
const id = operations[i];
|
||||
const hasUniqueSuspenders = operations[i + 1] === 1;
|
||||
const id = operations[i++];
|
||||
const hasUniqueSuspenders = operations[i++] === 1;
|
||||
const environmentNamesLength = operations[i++];
|
||||
const environmentNames = [];
|
||||
for (
|
||||
let envIndex = 0;
|
||||
envIndex < environmentNamesLength;
|
||||
envIndex++
|
||||
) {
|
||||
const environmentNameStringID = operations[i++];
|
||||
environmentNames.push(stringTable[environmentNameStringID]);
|
||||
}
|
||||
const suspense = this._idToSuspense.get(id);
|
||||
|
||||
if (suspense === undefined) {
|
||||
@@ -1777,8 +1787,6 @@ export default class Store extends EventEmitter<{
|
||||
break;
|
||||
}
|
||||
|
||||
i += 2;
|
||||
|
||||
if (__DEBUG__) {
|
||||
const previousHasUniqueSuspenders = suspense.hasUniqueSuspenders;
|
||||
debug(
|
||||
@@ -1788,6 +1796,7 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
|
||||
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
|
||||
// TODO: Recompute the environment names.
|
||||
}
|
||||
|
||||
hasSuspenseTreeChanged = true;
|
||||
|
||||
@@ -40,6 +40,12 @@ export type IconType =
|
||||
| 'panel-right-open'
|
||||
| 'panel-bottom-open'
|
||||
| 'panel-bottom-close'
|
||||
| 'filter-on'
|
||||
| 'filter-off'
|
||||
| 'play'
|
||||
| 'pause'
|
||||
| 'skip-previous'
|
||||
| 'skip-next'
|
||||
| 'error'
|
||||
| 'suspend'
|
||||
| 'undo'
|
||||
@@ -52,7 +58,7 @@ type Props = {
|
||||
type: IconType,
|
||||
};
|
||||
|
||||
const panelIcons = '0 -960 960 820';
|
||||
const panelIcons = '96 -864 768 768';
|
||||
export default function ButtonIcon({className = '', type}: Props): React.Node {
|
||||
let pathData = null;
|
||||
let viewBox = '0 0 24 24';
|
||||
@@ -153,6 +159,30 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
|
||||
pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'filter-on':
|
||||
pathData = PATH_MATERIAL_FILTER_ALT;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'filter-off':
|
||||
pathData = PATH_MATERIAL_FILTER_ALT_OFF;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'play':
|
||||
pathData = PATH_MATERIAL_PLAY_ARROW;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'pause':
|
||||
pathData = PATH_MATERIAL_PAUSE;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'skip-previous':
|
||||
pathData = PATH_MATERIAL_SKIP_PREVIOUS_ARROW;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'skip-next':
|
||||
pathData = PATH_MATERIAL_SKIP_NEXT_ARROW;
|
||||
viewBox = panelIcons;
|
||||
break;
|
||||
case 'suspend':
|
||||
pathData = PATH_SUSPEND;
|
||||
break;
|
||||
@@ -338,3 +368,33 @@ const PATH_MATERIAL_PANEL_BOTTOM_OPEN = `
|
||||
const PATH_MATERIAL_PANEL_BOTTOM_CLOSE = `
|
||||
m506-508 102-110q8-8.82 3.5-19.41T595-648H365q-12.25 0-16.62 10.5Q344-627 352-618l102 110q11.18 11 26.09 11T506-508Zm243-308q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538ZM216-336v120h528v-120H216Zm528-72v-336H216v336h528Zm-528 72v120-120Z
|
||||
`;
|
||||
|
||||
// Source: Material Design Icons filter_alt
|
||||
const PATH_MATERIAL_FILTER_ALT = `
|
||||
M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z
|
||||
`;
|
||||
|
||||
// Source: Material Design Icons filter_alt_off
|
||||
const PATH_MATERIAL_FILTER_ALT_OFF = `
|
||||
m592-481-57-57 143-182H353l-80-80h487q25 0 36 22t-4 42L592-481ZM791-56 560-287v87q0 17-11.5 28.5T520-160h-80q-17 0-28.5-11.5T400-200v-247L56-791l56-57 736 736-57 56ZM535-538Z
|
||||
`;
|
||||
|
||||
// Source: Material Design Icons play_arrow
|
||||
const PATH_MATERIAL_PLAY_ARROW = `
|
||||
M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z
|
||||
`;
|
||||
|
||||
// Source: Material Design Icons pause
|
||||
const PATH_MATERIAL_PAUSE = `
|
||||
M520-200v-560h240v560H520Zm-320 0v-560h240v560H200Zm400-80h80v-400h-80v400Zm-320 0h80v-400h-80v400Zm0-400v400-400Zm320 0v400-400Z
|
||||
`;
|
||||
|
||||
// Source: Material Design Icons skip_previous
|
||||
const PATH_MATERIAL_SKIP_PREVIOUS_ARROW = `
|
||||
M220-240v-480h80v480h-80Zm520 0L380-480l360-240v480Zm-80-240Zm0 90v-180l-136 90 136 90Z
|
||||
`;
|
||||
|
||||
// Source: Material Design Icons skip_next
|
||||
const PATH_MATERIAL_SKIP_NEXT_ARROW = `
|
||||
M660-240v-480h80v480h-80Zm-440 0v-480l360 240-360 240Zm80-240Zm0 90 136-90-136-90v180Z
|
||||
`;
|
||||
|
||||
@@ -17,7 +17,10 @@ import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Icon from '../Icon';
|
||||
import Toggle from '../Toggle';
|
||||
import {ElementTypeSuspense} from 'react-devtools-shared/src/frontend/types';
|
||||
import {
|
||||
ElementTypeSuspense,
|
||||
ElementTypeRoot,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import InspectedElementView from './InspectedElementView';
|
||||
import {InspectedElementContext} from './InspectedElementContext';
|
||||
import {getAlwaysOpenInEditor} from '../../../utils';
|
||||
@@ -205,6 +208,16 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
);
|
||||
}
|
||||
|
||||
let fullName = element.displayName || '';
|
||||
if (element.nameProp !== null) {
|
||||
fullName += ' "' + element.nameProp + '"';
|
||||
}
|
||||
if (element.type === ElementTypeRoot) {
|
||||
// The root only has "suspended by" and it represents the things that block
|
||||
// Initial Paint.
|
||||
fullName = 'Initial Paint';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.InspectedElement}
|
||||
@@ -228,8 +241,8 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
? `${styles.ComponentName} ${styles.StrictModeNonCompliantComponentName}`
|
||||
: styles.ComponentName
|
||||
}
|
||||
title={element.displayName}>
|
||||
{element.displayName}
|
||||
title={fullName}>
|
||||
{fullName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.CollapsableHeader[data-pending="true"] {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.CollapsableHeaderIcon {
|
||||
flex: 0 0 1rem;
|
||||
margin-left: -0.25rem;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user