diff --git a/.eslintrc.js b/.eslintrc.js index e8ace6311d8dc..18a3112e7382b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -583,8 +583,11 @@ module.exports = { mixin$Animatable: 'readonly', MouseEventHandler: 'readonly', NavigateEvent: 'readonly', + PerformanceMeasureOptions: 'readonly', PropagationPhases: 'readonly', PropertyDescriptor: 'readonly', + PropertyDescriptorMap: 'readonly', + Proxy$traps: 'readonly', React$Component: 'readonly', React$Config: 'readonly', React$Context: 'readonly', diff --git a/package.json b/package.json index f5b3052010c30..3439ed756a346 100644 --- a/package.json +++ b/package.json @@ -74,15 +74,15 @@ "eslint-plugin-react-internal": "link:./scripts/eslint-rules", "fbjs-scripts": "^3.0.1", "filesize": "^6.0.1", - "flow-bin": "^0.274", - "flow-remove-types": "^2.274", + "flow-bin": "^0.279.0", + "flow-remove-types": "^2.279.0", "flow-typed": "^4.1.1", "glob": "^7.1.6", "glob-stream": "^6.1.0", "google-closure-compiler": "^20230206.0.0", "gzip-size": "^5.1.1", - "hermes-eslint": "^0.25.1", - "hermes-parser": "^0.25.1", + "hermes-eslint": "^0.32.0", + "hermes-parser": "^0.32.0", "jest": "^29.4.2", "jest-cli": "^29.4.2", "jest-diff": "^29.4.2", diff --git a/packages/eslint-plugin-react-hooks/package.json b/packages/eslint-plugin-react-hooks/package.json index 25215d71e530f..8f7cfc361d1ff 100644 --- a/packages/eslint-plugin-react-hooks/package.json +++ b/packages/eslint-plugin-react-hooks/package.json @@ -41,7 +41,7 @@ "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", - "@babel/plugin-transform-private-methods": "^7.24.4", + "@babel/plugin-proposal-private-methods": "^7.18.6", "hermes-parser": "^0.25.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index 717d536dc94a1..81474767363fd 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -153,7 +153,7 @@ export function logComponentAborted( const entryName = isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; if (__DEV__) { - const properties = [ + const properties: Array<[string, string]> = [ [ 'Aborted', 'The stream was aborted before this Component finished rendering.', @@ -215,7 +215,7 @@ export function logComponentErrored( String(error.message) : // eslint-disable-next-line react-internal/safe-string-coercion String(error); - const properties = [['Error', message]]; + const properties: Array<[string, string]> = [['Error', message]]; if (componentInfo.key != null) { addValueToProperties('key', componentInfo.key, properties, 0, ''); } diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 5a49b2e073c0f..db9495a97dd4d 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -789,7 +789,7 @@ const Dispatcher: DispatcherType = { // create a proxy to throw a custom error // in case future versions of React adds more hooks -const DispatcherProxyHandler = { +const DispatcherProxyHandler: Proxy$traps = { get(target: DispatcherType, prop: string) { if (target.hasOwnProperty(prop)) { // $FlowFixMe[invalid-computed-prop] diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 88a9b70093fd1..d2f5c801aab7a 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -2217,7 +2217,7 @@ export function attach( } if (typeof instance.getClientRects === 'function') { // DOM - const result = []; + const result: Array = []; const doc = instance.ownerDocument; const win = doc && doc.defaultView; const scrollX = win ? win.scrollX : 0; @@ -7455,6 +7455,13 @@ export function attach( } function overrideSuspense(id: number, forceFallback: boolean) { + if (!supportsTogglingSuspense) { + // TODO:: Add getter to decide if overrideSuspense is available. + // Currently only available on inspectElement. + // Probably need a different affordance to batch since the timeline + // fallback is not the same as resuspending. + return; + } if ( typeof setSuspenseHandler !== 'function' || typeof scheduleUpdate !== 'function' diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js index 2739325749786..80c9f2f92d0b1 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js @@ -223,7 +223,10 @@ function HookView({ const hookDisplayName = hookName ? ( <> {name} - {!!hookName && ({hookName})} + { + // $FlowFixMe[constant-condition] + !!hookName && ({hookName}) + } ) : ( name diff --git a/packages/react-devtools-shared/src/devtools/views/ModalDialog.js b/packages/react-devtools-shared/src/devtools/views/ModalDialog.js index 542961b4c932b..a584d9a9e3d14 100644 --- a/packages/react-devtools-shared/src/devtools/views/ModalDialog.js +++ b/packages/react-devtools-shared/src/devtools/views/ModalDialog.js @@ -75,7 +75,7 @@ function dialogReducer(state: State, action: Action) { content: action.content, id: action.id, title: action.title || null, - }, + } as Dialog, ], }; default: diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.js index 6b92ef26c791c..124be4286d1a8 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.js @@ -64,7 +64,7 @@ function getTooltipPosition( mouseY: number, width: number, }, -) { +): {left: string, top: string} { const {height, mouseX, mouseY, width} = mousePosition; let top: number | string = 0; let left: number | string = 0; @@ -108,7 +108,13 @@ function getMousePosition( } } - const {height, left, top, width} = targetContainer.getBoundingClientRect(); + const {height, left, top, width} = + targetContainer.getBoundingClientRect() as { + height: number, + left: number, + top: number, + width: number, + }; const mouseX = mouseEvent.clientX - left; const mouseY = mouseEvent.clientY - top; diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css index 5a153545fecf6..3a7bb0735012b 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css @@ -110,6 +110,7 @@ padding: 0.25rem; display: flex; flex-direction: row; + align-items: flex-start; } .Timeline { diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js index 8a53365df5cef..9df107feab354 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js @@ -20,6 +20,7 @@ import InspectedElement from '../Components/InspectedElement'; import portaledContent from '../portaledContent'; import styles from './SuspenseTab.css'; import SuspenseRects from './SuspenseRects'; +import SuspenseTimeline from './SuspenseTimeline'; import SuspenseTreeList from './SuspenseTreeList'; import Button from '../Button'; import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; @@ -46,10 +47,6 @@ type LayoutState = { }; type LayoutDispatch = (action: LayoutAction) => void; -function SuspenseTimeline() { - return
timeline
; -} - function ToggleTreeList({ dispatch, state, @@ -309,7 +306,9 @@ function SuspenseTab(_: {}) {
- +
+ +
* { + flex: 1 1 0; + overflow: visible; + visibility: hidden; + width: 0 +} + +.SuspenseTimelineActiveMarker { + visibility: visible; +} diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js new file mode 100644 index 0000000000000..413760d0786fe --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -0,0 +1,216 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Element, SuspenseNode} from '../../../frontend/types'; +import type Store from '../../store'; + +import * as React from 'react'; +import { + useContext, + useId, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; +import {BridgeContext, StoreContext} from '../context'; +import {TreeDispatcherContext} from '../Components/TreeContext'; +import {useHighlightHostInstance} from '../hooks'; +import {SuspenseTreeStateContext} from './SuspenseTreeContext'; +import styles from './SuspenseTimeline.css'; +import typeof { + SyntheticEvent, + SyntheticPointerEvent, +} from 'react-dom-bindings/src/events/SyntheticEvent'; + +// TODO: This returns the roots which would mean we attempt to suspend the shell. +// Suspending the shell is currently not supported and we don't have a good view +// for inspecting the root. But we probably should? +function getDocumentOrderSuspense( + store: Store, + roots: $ReadOnlyArray, +): Array { + const suspenseTreeList: SuspenseNode[] = []; + for (let i = 0; i < roots.length; i++) { + const root = store.getElementByID(roots[i]); + if (root === null) { + continue; + } + const suspense = store.getSuspenseByID(root.id); + if (suspense !== null) { + const stack = [suspense]; + while (stack.length > 0) { + const current = stack.pop(); + if (current === undefined) { + continue; + } + suspenseTreeList.push(current); + // Add children in reverse order to maintain document order + for (let j = current.children.length - 1; j >= 0; j--) { + const childSuspense = store.getSuspenseByID(current.children[j]); + if (childSuspense !== null) { + stack.push(childSuspense); + } + } + } + } + } + + return suspenseTreeList; +} + +export default function SuspenseTimeline(): React$Node { + const bridge = useContext(BridgeContext); + const store = useContext(StoreContext); + const dispatch = useContext(TreeDispatcherContext); + const {shells} = useContext(SuspenseTreeStateContext); + + const timeline = useMemo(() => { + return getDocumentOrderSuspense(store, shells); + }, [store, shells]); + + const {highlightHostInstance, clearHighlightHostInstance} = + useHighlightHostInstance(); + + const inputRef = useRef(null); + const inputBBox = useRef(null); + useLayoutEffect(() => { + const input = inputRef.current; + if (input === null) { + throw new Error('Expected an input HTML element to be present.'); + } + + inputBBox.current = input.getBoundingClientRect(); + const observer = new ResizeObserver(entries => { + inputBBox.current = input.getBoundingClientRect(); + }); + observer.observe(input); + return () => { + inputBBox.current = null; + observer.disconnect(); + }; + }, []); + + const min = 0; + const max = timeline.length > 0 ? timeline.length - 1 : 0; + + const [value, setValue] = useState(max); + if (value > max) { + // TODO: Handle timeline changes + setValue(max); + } + + const markersID = useId(); + const markers: React.Node[] = useMemo(() => { + return timeline.map((suspense, index) => { + const takesUpSpace = + suspense.rects !== null && + suspense.rects.some(rect => { + return rect.width > 0 && rect.height > 0; + }); + + return takesUpSpace ? ( + + ) : ( +