+ `); + }); + + it('renders styles', () => { + const style = { + display: 'flex', + flex: 1, + backgroundColor: 'white', + marginInlineStart: 10, + userSelect: 'none', + verticalAlign: 'middle', + }; + + const instance = ReactTestRenderer.create(); + + expect(instance.toJSON()).toMatchInlineSnapshot(` + + `); + }); +}); diff --git a/Libraries/Components/TextInput/__tests__/__snapshots__/InputAccessoryView-test.js.snap b/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/InputAccessoryView-test.js.snap similarity index 100% rename from Libraries/Components/TextInput/__tests__/__snapshots__/InputAccessoryView-test.js.snap rename to packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/InputAccessoryView-test.js.snap diff --git a/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap b/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap similarity index 78% rename from Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap rename to packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap index ea3b4c1f062744..ac93dc2e644c7c 100644 --- a/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap +++ b/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap @@ -2,15 +2,6 @@ exports[`TextInput tests should render as expected: should deep render when mocked (please verify output manually) 1`] = ` `; -exports[`TextInput tests should render as expected: should shallow render as when mocked 1`] = ``; +exports[`TextInput tests should render as expected: should shallow render as when mocked 1`] = ``; -exports[`TextInput tests should render as expected: should shallow render as when not mocked 1`] = ``; +exports[`TextInput tests should render as expected: should shallow render as when not mocked 1`] = ``; diff --git a/Libraries/Components/ToastAndroid/NativeToastAndroid.js b/packages/react-native/Libraries/Components/ToastAndroid/NativeToastAndroid.js similarity index 100% rename from Libraries/Components/ToastAndroid/NativeToastAndroid.js rename to packages/react-native/Libraries/Components/ToastAndroid/NativeToastAndroid.js diff --git a/Libraries/Components/ToastAndroid/ToastAndroid.android.js b/packages/react-native/Libraries/Components/ToastAndroid/ToastAndroid.android.js similarity index 100% rename from Libraries/Components/ToastAndroid/ToastAndroid.android.js rename to packages/react-native/Libraries/Components/ToastAndroid/ToastAndroid.android.js diff --git a/Libraries/Components/ToastAndroid/ToastAndroid.d.ts b/packages/react-native/Libraries/Components/ToastAndroid/ToastAndroid.d.ts similarity index 100% rename from Libraries/Components/ToastAndroid/ToastAndroid.d.ts rename to packages/react-native/Libraries/Components/ToastAndroid/ToastAndroid.d.ts diff --git a/Libraries/Components/ToastAndroid/ToastAndroid.ios.js b/packages/react-native/Libraries/Components/ToastAndroid/ToastAndroid.ios.js similarity index 100% rename from Libraries/Components/ToastAndroid/ToastAndroid.ios.js rename to packages/react-native/Libraries/Components/ToastAndroid/ToastAndroid.ios.js diff --git a/Libraries/Components/Touchable/BoundingDimensions.js b/packages/react-native/Libraries/Components/Touchable/BoundingDimensions.js similarity index 100% rename from Libraries/Components/Touchable/BoundingDimensions.js rename to packages/react-native/Libraries/Components/Touchable/BoundingDimensions.js diff --git a/Libraries/Components/Touchable/PooledClass.js b/packages/react-native/Libraries/Components/Touchable/PooledClass.js similarity index 100% rename from Libraries/Components/Touchable/PooledClass.js rename to packages/react-native/Libraries/Components/Touchable/PooledClass.js diff --git a/Libraries/Components/Touchable/Position.js b/packages/react-native/Libraries/Components/Touchable/Position.js similarity index 100% rename from Libraries/Components/Touchable/Position.js rename to packages/react-native/Libraries/Components/Touchable/Position.js diff --git a/Libraries/Components/Touchable/Touchable.d.ts b/packages/react-native/Libraries/Components/Touchable/Touchable.d.ts similarity index 99% rename from Libraries/Components/Touchable/Touchable.d.ts rename to packages/react-native/Libraries/Components/Touchable/Touchable.d.ts index 6915dd27927d62..3c0065c9e02bbb 100644 --- a/Libraries/Components/Touchable/Touchable.d.ts +++ b/packages/react-native/Libraries/Components/Touchable/Touchable.d.ts @@ -13,7 +13,7 @@ import {GestureResponderEvent} from '../../Types/CoreEventTypes'; /** * //FIXME: need to find documentation on which component is a TTouchable and can implement that interface - * @see React.DOMAtributes + * @see React.DOMAttributes */ export interface Touchable { onTouchStart?: ((event: GestureResponderEvent) => void) | undefined; diff --git a/Libraries/Components/Touchable/Touchable.flow.js b/packages/react-native/Libraries/Components/Touchable/Touchable.flow.js similarity index 89% rename from Libraries/Components/Touchable/Touchable.flow.js rename to packages/react-native/Libraries/Components/Touchable/Touchable.flow.js index 3a583c286dfc74..b93087cbec5d0c 100644 --- a/Libraries/Components/Touchable/Touchable.flow.js +++ b/packages/react-native/Libraries/Components/Touchable/Touchable.flow.js @@ -99,7 +99,30 @@ import * as React from 'react'; * } */ -// Default amount "active" region protrudes beyond box +/** + * Touchable states. + */ + +const States = { + NOT_RESPONDER: 'NOT_RESPONDER', // Not the responder + RESPONDER_INACTIVE_PRESS_IN: 'RESPONDER_INACTIVE_PRESS_IN', // Responder, inactive, in the `PressRect` + RESPONDER_INACTIVE_PRESS_OUT: 'RESPONDER_INACTIVE_PRESS_OUT', // Responder, inactive, out of `PressRect` + RESPONDER_ACTIVE_PRESS_IN: 'RESPONDER_ACTIVE_PRESS_IN', // Responder, active, in the `PressRect` + RESPONDER_ACTIVE_PRESS_OUT: 'RESPONDER_ACTIVE_PRESS_OUT', // Responder, active, out of `PressRect` + RESPONDER_ACTIVE_LONG_PRESS_IN: 'RESPONDER_ACTIVE_LONG_PRESS_IN', // Responder, active, in the `PressRect`, after long press threshold + RESPONDER_ACTIVE_LONG_PRESS_OUT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT', // Responder, active, out of `PressRect`, after long press threshold + ERROR: 'ERROR', +}; + +type State = + | typeof States.NOT_RESPONDER + | typeof States.RESPONDER_INACTIVE_PRESS_IN + | typeof States.RESPONDER_INACTIVE_PRESS_OUT + | typeof States.RESPONDER_ACTIVE_PRESS_IN + | typeof States.RESPONDER_ACTIVE_PRESS_OUT + | typeof States.RESPONDER_ACTIVE_LONG_PRESS_IN + | typeof States.RESPONDER_ACTIVE_LONG_PRESS_OUT + | typeof States.ERROR; /** * By convention, methods prefixed with underscores are meant to be @private, @@ -200,9 +223,12 @@ interface TouchableMixinType { * @return {object} State object to be placed inside of * `this.state.touchable`. */ - touchableGetInitialState: () => $TEMPORARY$object<{| - touchable: $TEMPORARY$object<{|responderID: null, touchState: void|}>, - |}>; + touchableGetInitialState: () => { + touchable: { + touchState: ?State, + responderID: ?PressEvent['currentTarget'], + }, + }; // ==== Hooks to Gesture Responder system ==== /** diff --git a/Libraries/Components/Touchable/Touchable.js b/packages/react-native/Libraries/Components/Touchable/Touchable.js similarity index 99% rename from Libraries/Components/Touchable/Touchable.js rename to packages/react-native/Libraries/Components/Touchable/Touchable.js index 48e15bf1b6d3fb..50c9b8f6a14040 100644 --- a/Libraries/Components/Touchable/Touchable.js +++ b/packages/react-native/Libraries/Components/Touchable/Touchable.js @@ -396,9 +396,12 @@ const TouchableMixin = { * @return {object} State object to be placed inside of * `this.state.touchable`. */ - touchableGetInitialState: function (): $TEMPORARY$object<{| - touchable: $TEMPORARY$object<{|responderID: null, touchState: void|}>, - |}> { + touchableGetInitialState: function (): { + touchable: { + touchState: ?State, + responderID: ?PressEvent['currentTarget'], + }, + } { return { touchable: {touchState: undefined, responderID: null}, }; diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/packages/react-native/Libraries/Components/Touchable/TouchableBounce.js similarity index 100% rename from Libraries/Components/Touchable/TouchableBounce.js rename to packages/react-native/Libraries/Components/Touchable/TouchableBounce.js diff --git a/Libraries/Components/Touchable/TouchableHighlight.d.ts b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.d.ts similarity index 100% rename from Libraries/Components/Touchable/TouchableHighlight.d.ts rename to packages/react-native/Libraries/Components/Touchable/TouchableHighlight.d.ts diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js similarity index 99% rename from Libraries/Components/Touchable/TouchableHighlight.js rename to packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js index 7d648ca83a9938..c937344451ce69 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js @@ -277,7 +277,7 @@ class TouchableHighlight extends React.Component { } render(): React.Node { - const child = React.Children.only(this.props.children); + const child = React.Children.only<$FlowFixMe>(this.props.children); // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before // adopting `Pressability`, so preserve that behavior. diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.d.ts b/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.d.ts similarity index 100% rename from Libraries/Components/Touchable/TouchableNativeFeedback.d.ts rename to packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.d.ts diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.js b/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js similarity index 98% rename from Libraries/Components/Touchable/TouchableNativeFeedback.js rename to packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js index a377694e06c79d..2571ddd0b7ab3a 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js @@ -239,8 +239,8 @@ class TouchableNativeFeedback extends React.Component { } render(): React.Node { - const element = React.Children.only(this.props.children); - const children = [element.props.children]; + const element = React.Children.only<$FlowFixMe>(this.props.children); + const children: Array = [element.props.children]; if (__DEV__) { if (element.type === View) { children.push( diff --git a/Libraries/Components/Touchable/TouchableOpacity.d.ts b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.d.ts similarity index 100% rename from Libraries/Components/Touchable/TouchableOpacity.d.ts rename to packages/react-native/Libraries/Components/Touchable/TouchableOpacity.d.ts diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js similarity index 97% rename from Libraries/Components/Touchable/TouchableOpacity.js rename to packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js index 2aef4e824f10a5..02dc8abd365bfc 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js @@ -206,6 +206,7 @@ class TouchableOpacity extends React.Component { } _getChildStyleOpacityWithDefault(): number { + // $FlowFixMe[underconstrained-implicit-instantiation] const opacity = flattenStyle(this.props.style)?.opacity; return typeof opacity === 'number' ? opacity : 1; } @@ -301,9 +302,10 @@ class TouchableOpacity extends React.Component { this.state.pressability.configure(this._createPressabilityConfig()); if ( this.props.disabled !== prevProps.disabled || - (flattenStyle(prevProps.style)?.opacity !== - flattenStyle(this.props.style)?.opacity) !== - undefined + // $FlowFixMe[underconstrained-implicit-instantiation] + flattenStyle(prevProps.style)?.opacity !== + // $FlowFixMe[underconstrained-implicit-instantiation] + flattenStyle(this.props.style)?.opacity ) { this._opacityInactive(250); } diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.d.ts b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.d.ts similarity index 96% rename from Libraries/Components/Touchable/TouchableWithoutFeedback.d.ts rename to packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.d.ts index a6f3316d36be14..e0146fc31b356c 100644 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.d.ts +++ b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.d.ts @@ -40,7 +40,7 @@ export interface TouchableWithoutFeedbackProps extends TouchableWithoutFeedbackPropsIOS, TouchableWithoutFeedbackPropsAndroid, AccessibilityProps { - children?: React.ReactNode; + children?: React.ReactNode | undefined; /** * Delay in ms, from onPressIn, before onLongPress is called. @@ -69,7 +69,7 @@ export interface TouchableWithoutFeedbackProps * the Z-index of sibling views always takes precedence if a touch hits * two overlapping views. */ - hitSlop?: Insets | undefined; + hitSlop?: null | Insets | number | undefined; /** * Used to reference react managed views from native code. @@ -121,7 +121,7 @@ export interface TouchableWithoutFeedbackProps * while the scroll view is disabled. Ensure you pass in a constant * to reduce memory allocations. */ - pressRetentionOffset?: Insets | undefined; + pressRetentionOffset?: null | Insets | number | undefined; /** * Used to locate this view in end-to-end tests. diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js similarity index 95% rename from Libraries/Components/Touchable/TouchableWithoutFeedback.js rename to packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 70b43370b11f6c..7c029ed9a0fcfe 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -15,7 +15,7 @@ import type { AccessibilityState, AccessibilityValue, } from '../../Components/View/ViewAccessibility'; -import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; +import type {EdgeInsetsOrSizeProp} from '../../StyleSheet/EdgeInsetsPropType'; import type { BlurEvent, FocusEvent, @@ -67,7 +67,7 @@ type Props = $ReadOnly<{| delayPressOut?: ?number, disabled?: ?boolean, focusable?: ?boolean, - hitSlop?: ?EdgeInsetsProp, + hitSlop?: ?EdgeInsetsOrSizeProp, id?: string, importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'), nativeID?: ?string, @@ -79,7 +79,7 @@ type Props = $ReadOnly<{| onPress?: ?(event: PressEvent) => mixed, onPressIn?: ?(event: PressEvent) => mixed, onPressOut?: ?(event: PressEvent) => mixed, - pressRetentionOffset?: ?EdgeInsetsProp, + pressRetentionOffset?: ?EdgeInsetsOrSizeProp, rejectResponderTermination?: ?boolean, testID?: ?string, touchSoundDisabled?: ?boolean, @@ -121,8 +121,8 @@ class TouchableWithoutFeedback extends React.Component { }; render(): React.Node { - const element = React.Children.only(this.props.children); - const children = [element.props.children]; + const element = React.Children.only<$FlowFixMe>(this.props.children); + const children: Array = [element.props.children]; const ariaLive = this.props['aria-live']; if (__DEV__) { diff --git a/Libraries/Components/Touchable/__tests__/TouchableHighlight-test.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableHighlight-test.js similarity index 100% rename from Libraries/Components/Touchable/__tests__/TouchableHighlight-test.js rename to packages/react-native/Libraries/Components/Touchable/__tests__/TouchableHighlight-test.js diff --git a/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-test.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-test.js similarity index 100% rename from Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-test.js rename to packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-test.js diff --git a/Libraries/Components/Touchable/__tests__/TouchableOpacity-test.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-test.js similarity index 100% rename from Libraries/Components/Touchable/__tests__/TouchableOpacity-test.js rename to packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-test.js diff --git a/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-test.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-test.js similarity index 100% rename from Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-test.js rename to packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-test.js diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap similarity index 100% rename from Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap rename to packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap similarity index 100% rename from Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap rename to packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap similarity index 100% rename from Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap rename to packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap similarity index 100% rename from Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap rename to packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap diff --git a/packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js b/packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js new file mode 100644 index 00000000000000..abac38b3bee820 --- /dev/null +++ b/packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js @@ -0,0 +1,189 @@ +/** + * 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 strict-local + * @format + */ + +import type {Overlay} from './TraceUpdateOverlayNativeComponent'; + +import UIManager from '../../ReactNative/UIManager'; +import processColor from '../../StyleSheet/processColor'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Platform from '../../Utilities/Platform'; +import View from '../View/View'; +import TraceUpdateOverlayNativeComponent, { + Commands, +} from './TraceUpdateOverlayNativeComponent'; +import * as React from 'react'; + +type AgentEvents = { + drawTraceUpdates: [Array<{node: TraceNode, color: string}>], + disableTraceUpdates: [], +}; + +interface Agent { + addListener>( + event: Event, + listener: (...AgentEvents[Event]) => void, + ): void; + removeListener(event: $Keys, listener: () => void): void; +} + +type PublicInstance = { + measure?: ( + ( + x: number, + y: number, + width: number, + height: number, + left: number, + top: number, + ) => void, + ) => void, +}; + +type TraceNode = + | PublicInstance + | { + canonical?: + | PublicInstance // TODO: remove this variant when syncing the new version of the renderer from React to React Native. + | { + publicInstance?: PublicInstance, + }, + }; + +type ReactDevToolsGlobalHook = { + on: (eventName: string, (agent: Agent) => void) => void, + off: (eventName: string, (agent: Agent) => void) => void, + reactDevtoolsAgent: Agent, +}; + +const {useEffect, useRef, useState} = React; +const hook: ReactDevToolsGlobalHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; +const isNativeComponentReady = + Platform.OS === 'android' && + UIManager.hasViewManagerConfig('TraceUpdateOverlay'); +let devToolsAgent: ?Agent; + +export default function TraceUpdateOverlay(): React.Node { + const [overlayDisabled, setOverlayDisabled] = useState(false); + // This effect is designed to be explicitly shown here to avoid re-subscribe from the same + // overlay component. + useEffect(() => { + if (!isNativeComponentReady) { + return; + } + + function attachToDevtools(agent: Agent) { + devToolsAgent = agent; + agent.addListener('drawTraceUpdates', onAgentDrawTraceUpdates); + agent.addListener('disableTraceUpdates', onAgentDisableTraceUpdates); + } + + function subscribe() { + hook?.on('react-devtools', attachToDevtools); + if (hook?.reactDevtoolsAgent) { + attachToDevtools(hook.reactDevtoolsAgent); + } + } + + function unsubscribe() { + hook?.off('react-devtools', attachToDevtools); + const agent = devToolsAgent; + if (agent != null) { + agent.removeListener('drawTraceUpdates', onAgentDrawTraceUpdates); + agent.removeListener('disableTraceUpdates', onAgentDisableTraceUpdates); + devToolsAgent = null; + } + } + + function onAgentDrawTraceUpdates( + nodesToDraw: Array<{node: TraceNode, color: string}> = [], + ) { + // If overlay is disabled before, now it's enabled. + setOverlayDisabled(false); + + const newFramesToDraw: Array> = []; + nodesToDraw.forEach(({node, color}) => { + // `canonical.publicInstance` => Fabric + // TODO: remove this check when syncing the new version of the renderer from React to React Native. + // `canonical` => Legacy Fabric + // `node` => Legacy renderer + const component = + (node.canonical && node.canonical.publicInstance) ?? + node.canonical ?? + node; + if (!component || !component.measure) { + return; + } + const frameToDrawPromise = new Promise(resolve => { + // The if statement here is to make flow happy + if (component.measure) { + // TODO(T145522797): We should refactor this to use `getBoundingClientRect` when Paper is no longer supported. + component.measure((x, y, width, height, left, top) => { + resolve({ + rect: {left, top, width, height}, + color: processColor(color), + }); + }); + } + }); + newFramesToDraw.push(frameToDrawPromise); + }); + Promise.all(newFramesToDraw).then( + results => { + if (nativeComponentRef.current != null) { + Commands.draw( + nativeComponentRef.current, + JSON.stringify( + results.filter( + ({rect, color}) => rect.width >= 0 && rect.height >= 0, + ), + ), + ); + } + }, + err => { + console.error(`Failed to measure updated traces. Error: ${err}`); + }, + ); + } + + function onAgentDisableTraceUpdates() { + // When trace updates are disabled from the backend, we won't receive draw events until it's enabled by the next draw. We can safely remove the overlay as it's not needed now. + setOverlayDisabled(true); + } + + subscribe(); + return unsubscribe; + }, []); // Only run once when the overlay initially rendered + + const nativeComponentRef = + useRef>(null); + + return ( + !overlayDisabled && + isNativeComponentReady && ( + + + + ) + ); +} + +const styles = StyleSheet.create({ + overlay: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, +}); diff --git a/packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlayNativeComponent.js b/packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlayNativeComponent.js new file mode 100644 index 00000000000000..837223e3774b2d --- /dev/null +++ b/packages/react-native/Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlayNativeComponent.js @@ -0,0 +1,43 @@ +/** + * 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 strict-local + * @format + */ + +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import type {ProcessedColorValue} from '../../StyleSheet/processColor'; +import type {ViewProps} from '../View/ViewPropTypes'; + +import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; +import codegenNativeComponent from '../../Utilities/codegenNativeComponent'; +import * as React from 'react'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, +|}>; +export type TraceUpdateOverlayNativeComponentType = HostComponent; +export type Overlay = { + rect: {left: number, top: number, width: number, height: number}, + color: ?ProcessedColorValue, +}; + +interface NativeCommands { + +draw: ( + viewRef: React.ElementRef, + // TODO(T144046177): Ideally we can pass array of Overlay, but currently + // Array type is not supported in RN codegen for building native commands. + overlays: string, + ) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['draw'], +}); + +export default (codegenNativeComponent( + 'TraceUpdateOverlay', +): HostComponent); diff --git a/Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js b/packages/react-native/Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js similarity index 100% rename from Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js rename to packages/react-native/Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js diff --git a/Libraries/Components/UnimplementedViews/UnimplementedView.js b/packages/react-native/Libraries/Components/UnimplementedViews/UnimplementedView.js similarity index 100% rename from Libraries/Components/UnimplementedViews/UnimplementedView.js rename to packages/react-native/Libraries/Components/UnimplementedViews/UnimplementedView.js diff --git a/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js similarity index 83% rename from Libraries/Components/View/ReactNativeStyleAttributes.js rename to packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index 926c0c7efa6441..13d8db56a5fe67 100644 --- a/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -46,12 +46,25 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { flexWrap: true, gap: true, height: true, + inset: true, + insetBlock: true, + insetBlockEnd: true, + insetBlockStart: true, + insetInline: true, + insetInlineEnd: true, + insetInlineStart: true, justifyContent: true, left: true, margin: true, + marginBlock: true, + marginBlockEnd: true, + marginBlockStart: true, marginBottom: true, marginEnd: true, marginHorizontal: true, + marginInline: true, + marginInlineEnd: true, + marginInlineStart: true, marginLeft: true, marginRight: true, marginStart: true, @@ -63,9 +76,15 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { minWidth: true, overflow: true, padding: true, + paddingBlock: true, + paddingBlockEnd: true, + paddingBlockStart: true, paddingBottom: true, paddingEnd: true, paddingHorizontal: true, + paddingInline: true, + paddingInlineEnd: true, + paddingInlineStart: true, paddingLeft: true, paddingRight: true, paddingStart: true, @@ -98,6 +117,9 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { */ backfaceVisibility: true, backgroundColor: colorAttributes, + borderBlockColor: colorAttributes, + borderBlockEndColor: colorAttributes, + borderBlockStartColor: colorAttributes, borderBottomColor: colorAttributes, borderBottomEndRadius: true, borderBottomLeftRadius: true, @@ -106,10 +128,14 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { borderColor: colorAttributes, borderCurve: true, borderEndColor: colorAttributes, + borderEndEndRadius: true, + borderEndStartRadius: true, borderLeftColor: colorAttributes, borderRadius: true, borderRightColor: colorAttributes, borderStartColor: colorAttributes, + borderStartEndRadius: true, + borderStartStartRadius: true, borderStyle: true, borderTopColor: colorAttributes, borderTopEndRadius: true, diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js similarity index 99% rename from Libraries/Components/View/ReactNativeViewAttributes.js rename to packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js index f4ea0d2c1f5219..e27df43205f540 100644 --- a/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js @@ -35,6 +35,7 @@ const UIView = { collapsable: true, needsOffscreenAlphaCompositing: true, style: ReactNativeStyleAttributes, + role: true, }; const RCTView = { diff --git a/Libraries/Components/View/View.d.ts b/packages/react-native/Libraries/Components/View/View.d.ts similarity index 100% rename from Libraries/Components/View/View.d.ts rename to packages/react-native/Libraries/Components/View/View.d.ts diff --git a/packages/react-native/Libraries/Components/View/View.js b/packages/react-native/Libraries/Components/View/View.js new file mode 100644 index 00000000000000..a10c06231fbfa7 --- /dev/null +++ b/packages/react-native/Libraries/Components/View/View.js @@ -0,0 +1,148 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type {ViewProps} from './ViewPropTypes'; + +import flattenStyle from '../../StyleSheet/flattenStyle'; +import TextAncestor from '../../Text/TextAncestor'; +import {getAccessibilityRoleFromRole} from '../../Utilities/AcessibilityMapping'; +import ViewNativeComponent from './ViewNativeComponent'; +import * as React from 'react'; + +export type Props = ViewProps; + +/** + * The most fundamental component for building a UI, View is a container that + * supports layout with flexbox, style, some touch handling, and accessibility + * controls. + * + * @see https://reactnative.dev/docs/view + */ +const View: React.AbstractComponent< + ViewProps, + React.ElementRef, +> = React.forwardRef( + ( + { + accessibilityElementsHidden, + accessibilityLabel, + accessibilityLabelledBy, + accessibilityLiveRegion, + accessibilityRole, + accessibilityState, + accessibilityValue, + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-hidden': ariaHidden, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + 'aria-live': ariaLive, + 'aria-selected': ariaSelected, + 'aria-valuemax': ariaValueMax, + 'aria-valuemin': ariaValueMin, + 'aria-valuenow': ariaValueNow, + 'aria-valuetext': ariaValueText, + focusable, + id, + importantForAccessibility, + nativeID, + pointerEvents, + role, + tabIndex, + ...otherProps + }: ViewProps, + forwardedRef, + ) => { + const hasTextAncestor = React.useContext(TextAncestor); + const _accessibilityLabelledBy = + ariaLabelledBy?.split(/\s*,\s*/g) ?? accessibilityLabelledBy; + + let _accessibilityState; + if ( + accessibilityState != null || + ariaBusy != null || + ariaChecked != null || + ariaDisabled != null || + ariaExpanded != null || + ariaSelected != null + ) { + _accessibilityState = { + busy: ariaBusy ?? accessibilityState?.busy, + checked: ariaChecked ?? accessibilityState?.checked, + disabled: ariaDisabled ?? accessibilityState?.disabled, + expanded: ariaExpanded ?? accessibilityState?.expanded, + selected: ariaSelected ?? accessibilityState?.selected, + }; + } + let _accessibilityValue; + if ( + accessibilityValue != null || + ariaValueMax != null || + ariaValueMin != null || + ariaValueNow != null || + ariaValueText != null + ) { + _accessibilityValue = { + max: ariaValueMax ?? accessibilityValue?.max, + min: ariaValueMin ?? accessibilityValue?.min, + now: ariaValueNow ?? accessibilityValue?.now, + text: ariaValueText ?? accessibilityValue?.text, + }; + } + + // $FlowFixMe[underconstrained-implicit-instantiation] + let style = flattenStyle(otherProps.style); + + const newPointerEvents = style?.pointerEvents || pointerEvents; + + const actualView = ( + + ); + + if (hasTextAncestor) { + return ( + + {actualView} + + ); + } + + return actualView; + }, +); + +View.displayName = 'View'; + +module.exports = View; diff --git a/Libraries/Components/View/ViewAccessibility.d.ts b/packages/react-native/Libraries/Components/View/ViewAccessibility.d.ts similarity index 89% rename from Libraries/Components/View/ViewAccessibility.d.ts rename to packages/react-native/Libraries/Components/View/ViewAccessibility.d.ts index c68c758410c889..acb833ef7fbe5e 100644 --- a/Libraries/Components/View/ViewAccessibility.d.ts +++ b/packages/react-native/Libraries/Components/View/ViewAccessibility.d.ts @@ -75,10 +75,10 @@ export interface AccessibilityProps */ accessibilityValue?: AccessibilityValue | undefined; - 'aria-valuemax'?: AccessibilityValue['max']; - 'aria-valuemin'?: AccessibilityValue['min']; - 'aria-valuenow'?: AccessibilityValue['now']; - 'aria-valuetext'?: AccessibilityValue['text']; + 'aria-valuemax'?: AccessibilityValue['max'] | undefined; + 'aria-valuemin'?: AccessibilityValue['min'] | undefined; + 'aria-valuenow'?: AccessibilityValue['now'] | undefined; + 'aria-valuetext'?: AccessibilityValue['text'] | undefined; /** * When `accessible` is true, the system will try to invoke this function when the user performs an accessibility custom action. */ @@ -105,7 +105,7 @@ export interface AccessibilityProps /** * Indicates to accessibility services to treat UI component like a specific role. */ - role?: Role; + role?: Role | undefined; } export type AccessibilityActionInfo = Readonly<{ @@ -251,6 +251,12 @@ export interface AccessibilityPropsAndroid { | 'no' | 'no-hide-descendants' | undefined; + + /** + * A reference to another element `nativeID` used to build complex forms. The value of `accessibilityLabelledBy` should match the `nativeID` of the related element. + * @platform android + */ + accessibilityLabelledBy?: string | string[] | undefined; } export interface AccessibilityPropsIOS { @@ -268,7 +274,7 @@ export interface AccessibilityPropsIOS { accessibilityViewIsModal?: boolean | undefined; /** - * When accessibile is true, the system will invoke this function when the user performs the escape gesture (scrub with two fingers). + * When accessible is true, the system will invoke this function when the user performs the escape gesture (scrub with two fingers). * @platform ios */ onAccessibilityEscape?: (() => void) | undefined; @@ -290,6 +296,13 @@ export interface AccessibilityPropsIOS { * @platform ios */ accessibilityIgnoresInvertColors?: boolean | undefined; + + /** + * By using the accessibilityLanguage property, the screen reader will understand which language to use while reading the element's label, value and hint. The provided string value must follow the BCP 47 specification (https://www.rfc-editor.org/info/bcp47). + * https://reactnative.dev/docs/accessibility#accessibilitylanguage-ios + * @platform ios + */ + accessibilityLanguage?: string | undefined; } export type Role = diff --git a/Libraries/Components/View/ViewAccessibility.js b/packages/react-native/Libraries/Components/View/ViewAccessibility.js similarity index 94% rename from Libraries/Components/View/ViewAccessibility.js rename to packages/react-native/Libraries/Components/View/ViewAccessibility.js index c62be022b046e7..18da75effcb1d7 100644 --- a/Libraries/Components/View/ViewAccessibility.js +++ b/packages/react-native/Libraries/Components/View/ViewAccessibility.js @@ -16,6 +16,7 @@ import type {SyntheticEvent} from '../../Types/CoreEventTypes'; export type AccessibilityRole = | 'none' | 'button' + | 'dropdownlist' | 'togglebutton' | 'link' | 'search' @@ -44,7 +45,15 @@ export type AccessibilityRole = | 'timer' | 'list' | 'toolbar' - | 'grid'; + | 'grid' + | 'pager' + | 'scrollview' + | 'horizontalscrollview' + | 'viewgroup' + | 'webview' + | 'drawerlayout' + | 'slidingdrawer' + | 'iconmenu'; // Role types for web export type Role = diff --git a/packages/react-native/Libraries/Components/View/ViewNativeComponent.js b/packages/react-native/Libraries/Components/View/ViewNativeComponent.js new file mode 100644 index 00000000000000..d8052fb9889b02 --- /dev/null +++ b/packages/react-native/Libraries/Components/View/ViewNativeComponent.js @@ -0,0 +1,128 @@ +/** + * 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 strict-local + * @format + */ + +import type { + HostComponent, + PartialViewConfig, +} from '../../Renderer/shims/ReactNativeTypes'; + +import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry'; +import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; +import Platform from '../../Utilities/Platform'; +import {type ViewProps as Props} from './ViewPropTypes'; +import * as React from 'react'; + +export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = + Platform.OS === 'android' + ? { + uiViewClassName: 'RCTView', + validAttributes: { + // ReactClippingViewManager @ReactProps + removeClippedSubviews: true, + + // ReactViewManager @ReactProps + accessible: true, + hasTVPreferredFocus: true, + nextFocusDown: true, + nextFocusForward: true, + nextFocusLeft: true, + nextFocusRight: true, + nextFocusUp: true, + + borderRadius: true, + borderTopLeftRadius: true, + borderTopRightRadius: true, + borderBottomRightRadius: true, + borderBottomLeftRadius: true, + borderTopStartRadius: true, + borderTopEndRadius: true, + borderBottomStartRadius: true, + borderBottomEndRadius: true, + borderEndEndRadius: true, + borderEndStartRadius: true, + borderStartEndRadius: true, + borderStartStartRadius: true, + borderStyle: true, + hitSlop: true, + pointerEvents: true, + nativeBackgroundAndroid: true, + nativeForegroundAndroid: true, + needsOffscreenAlphaCompositing: true, + + borderWidth: true, + borderLeftWidth: true, + borderRightWidth: true, + borderTopWidth: true, + borderBottomWidth: true, + borderStartWidth: true, + borderEndWidth: true, + + borderColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderLeftColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderRightColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderTopColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderBottomColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderStartColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderEndColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderBlockColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderBlockEndColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderBlockStartColor: { + process: require('../../StyleSheet/processColor').default, + }, + + focusable: true, + overflow: true, + backfaceVisibility: true, + }, + } + : { + uiViewClassName: 'RCTView', + }; + +const ViewNativeComponent: HostComponent = + NativeComponentRegistry.get('RCTView', () => __INTERNAL_VIEW_CONFIG); + +interface NativeCommands { + +hotspotUpdate: ( + viewRef: React.ElementRef>, + x: number, + y: number, + ) => void; + +setPressed: ( + viewRef: React.ElementRef>, + pressed: boolean, + ) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate', 'setPressed'], +}); + +export default ViewNativeComponent; + +export type ViewNativeComponentType = HostComponent; diff --git a/Libraries/Components/View/ViewPropTypes.d.ts b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts similarity index 99% rename from Libraries/Components/View/ViewPropTypes.d.ts rename to packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts index b7da46fe4bc5f2..53f5e82c34149b 100644 --- a/Libraries/Components/View/ViewPropTypes.d.ts +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts @@ -173,7 +173,7 @@ export interface ViewProps Touchable, PointerEvents, AccessibilityProps { - children?: React.ReactNode; + children?: React.ReactNode | undefined; /** * This defines how far a touch event can start away from the view. * Typical interface guidelines recommend touch targets that are at least diff --git a/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js similarity index 98% rename from Libraries/Components/View/ViewPropTypes.js rename to packages/react-native/Libraries/Components/View/ViewPropTypes.js index 2330f45c0051e0..1a37bd3206b286 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -87,6 +87,8 @@ type MouseEventProps = $ReadOnly<{| // Experimental/Work in Progress Pointer Event Callbacks (not yet ready for use) type PointerEventProps = $ReadOnly<{| + onClick?: ?(event: PointerEvent) => void, + onClickCapture?: ?(event: PointerEvent) => void, onPointerEnter?: ?(event: PointerEvent) => void, onPointerEnterCapture?: ?(event: PointerEvent) => void, onPointerLeave?: ?(event: PointerEvent) => void, @@ -543,7 +545,7 @@ export type ViewProps = $ReadOnly<{| 'aria-hidden'?: ?boolean, /** - * It reperesents the nativeID of the associated label text. When the assistive technology focuses on the component with this props, the text is read aloud. + * It represents the nativeID of the associated label text. When the assistive technology focuses on the component with this props, the text is read aloud. * * @platform android */ diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-test.js b/packages/react-native/Libraries/Components/View/__tests__/View-test.js new file mode 100644 index 00000000000000..66fac79f4ab4e2 --- /dev/null +++ b/packages/react-native/Libraries/Components/View/__tests__/View-test.js @@ -0,0 +1,193 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +'use strict'; + +const render = require('../../../../jest/renderer'); +const React = require('../React'); +const View = require('../View'); + +jest.unmock('../View'); +jest.unmock('../ViewNativeComponent'); + +describe('View', () => { + it('default render', () => { + const instance = render.create(); + + expect(instance.toJSON()).toMatchInlineSnapshot(``); + }); + + it('has displayName', () => { + expect(View.displayName).toEqual('View'); + }); +}); + +describe('View compat with web', () => { + it('renders core props', () => { + const props = { + id: 'id', + tabIndex: 0, + testID: 'testID', + }; + + const instance = render.create(); + + expect(instance.toJSON()).toMatchInlineSnapshot(` + + `); + }); + + it('renders "aria-*" props', () => { + const props = { + 'aria-activedescendant': 'activedescendant', + 'aria-atomic': true, + 'aria-autocomplete': 'list', + 'aria-busy': true, + 'aria-checked': true, + 'aria-columncount': 5, + 'aria-columnindex': 3, + 'aria-columnspan': 2, + 'aria-controls': 'controls', + 'aria-current': 'current', + 'aria-describedby': 'describedby', + 'aria-details': 'details', + 'aria-disabled': true, + 'aria-errormessage': 'errormessage', + 'aria-expanded': true, + 'aria-flowto': 'flowto', + 'aria-haspopup': true, + 'aria-hidden': true, + 'aria-invalid': true, + 'aria-keyshortcuts': 'Cmd+S', + 'aria-label': 'label', + 'aria-labelledby': 'labelledby', + 'aria-level': 3, + 'aria-live': 'polite', + 'aria-modal': true, + 'aria-multiline': true, + 'aria-multiselectable': true, + 'aria-orientation': 'portrait', + 'aria-owns': 'owns', + 'aria-placeholder': 'placeholder', + 'aria-posinset': 5, + 'aria-pressed': true, + 'aria-readonly': true, + 'aria-required': true, + role: 'main', + 'aria-roledescription': 'roledescription', + 'aria-rowcount': 5, + 'aria-rowindex': 3, + 'aria-rowspan': 3, + 'aria-selected': true, + 'aria-setsize': 5, + 'aria-sort': 'ascending', + 'aria-valuemax': 5, + 'aria-valuemin': 0, + 'aria-valuenow': 3, + 'aria-valuetext': '3', + }; + + const instance = render.create(); + + expect(instance.toJSON()).toMatchInlineSnapshot(` + + `); + }); + + it('renders styles', () => { + const style = { + display: 'flex', + flex: 1, + backgroundColor: 'white', + marginInlineStart: 10, + pointerEvents: 'none', + }; + + const instance = render.create(); + + expect(instance.toJSON()).toMatchInlineSnapshot(` + + `); + }); +}); diff --git a/Libraries/Components/__tests__/Button-test.js b/packages/react-native/Libraries/Components/__tests__/Button-test.js similarity index 100% rename from Libraries/Components/__tests__/Button-test.js rename to packages/react-native/Libraries/Components/__tests__/Button-test.js diff --git a/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap b/packages/react-native/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap similarity index 100% rename from Libraries/Components/__tests__/__snapshots__/Button-test.js.snap rename to packages/react-native/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap diff --git a/packages/react-native/Libraries/Core/Devtools/__tests__/loadBundleFromServer-test.js b/packages/react-native/Libraries/Core/Devtools/__tests__/loadBundleFromServer-test.js new file mode 100644 index 00000000000000..3f755fbf5e8349 --- /dev/null +++ b/packages/react-native/Libraries/Core/Devtools/__tests__/loadBundleFromServer-test.js @@ -0,0 +1,215 @@ +/** + * 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 strict-local + * @format + * @oncall react_native + */ + +'use strict'; + +jest.mock('react-native/Libraries/Utilities/HMRClient'); + +jest.mock('react-native/Libraries/Core/Devtools/getDevServer', () => + jest.fn(() => ({ + url: 'localhost:8042/', + fullBundleUrl: + 'http://localhost:8042/EntryPoint.bundle?platform=' + + jest.requireActual<$FlowFixMe>('react-native').Platform.OS + + '&dev=true&minify=false&unusedExtraParam=42', + bundleLoadedFromServer: true, + })), +); + +const loadingViewMock = { + showMessage: jest.fn(), + hide: jest.fn(), +}; +jest.mock( + 'react-native/Libraries/Utilities/LoadingView', + () => loadingViewMock, +); + +const sendRequest = jest.fn( + ( + method, + trackingName, + url, + headers, + data, + responseType, + incrementalUpdates, + timeout, + callback, + withCredentials, + ) => { + callback(1); + }, +); + +jest.mock('react-native/Libraries/Network/RCTNetworking', () => ({ + __esModule: true, + default: { + sendRequest, + addListener: jest.fn((name, fn) => { + if (name === 'didReceiveNetworkData') { + setImmediate(() => fn([1, mockDataResponse])); + } else if (name === 'didCompleteNetworkResponse') { + setImmediate(() => fn([1, null])); + } else if (name === 'didReceiveNetworkResponse') { + setImmediate(() => fn([1, null, mockHeaders])); + } + return {remove: () => {}}; + }), + }, +})); + +let loadBundleFromServer: (bundlePathAndQuery: string) => Promise; + +let mockHeaders: {'Content-Type': string}; +let mockDataResponse; + +beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + loadBundleFromServer = require('../loadBundleFromServer'); +}); + +test('loadBundleFromServer will throw for JSON responses', async () => { + mockHeaders = {'Content-Type': 'application/json'}; + mockDataResponse = JSON.stringify({message: 'Error thrown from Metro'}); + + await expect( + loadBundleFromServer('/Fail.bundle?platform=ios'), + ).rejects.toThrow('Error thrown from Metro'); +}); + +test('loadBundleFromServer will request a bundle if import bundles are available', async () => { + mockDataResponse = '"code";'; + mockHeaders = {'Content-Type': 'application/javascript'}; + + await loadBundleFromServer( + '/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + + expect(sendRequest.mock.calls).toEqual([ + [ + 'GET', + expect.anything(), + 'localhost:8042/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + ], + ]); + + sendRequest.mockClear(); + await loadBundleFromServer( + '/Tiny/Apple.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + + expect(sendRequest.mock.calls).toEqual([ + [ + 'GET', + expect.anything(), + 'localhost:8042/Tiny/Apple.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + ], + ]); +}); + +test('shows and hides the loading view around a request', async () => { + mockDataResponse = '"code";'; + mockHeaders = {'Content-Type': 'application/javascript'}; + + const promise = loadBundleFromServer( + '/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + + expect(loadingViewMock.showMessage).toHaveBeenCalledTimes(1); + expect(loadingViewMock.hide).not.toHaveBeenCalled(); + loadingViewMock.showMessage.mockClear(); + loadingViewMock.hide.mockClear(); + + await promise; + + expect(loadingViewMock.showMessage).not.toHaveBeenCalled(); + expect(loadingViewMock.hide).toHaveBeenCalledTimes(1); +}); + +test('shows and hides the loading view around concurrent requests', async () => { + mockDataResponse = '"code";'; + mockHeaders = {'Content-Type': 'application/javascript'}; + + const promise1 = loadBundleFromServer( + '/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + const promise2 = loadBundleFromServer( + '/Apple.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + + expect(loadingViewMock.showMessage).toHaveBeenCalledTimes(2); + expect(loadingViewMock.hide).not.toHaveBeenCalled(); + loadingViewMock.showMessage.mockClear(); + loadingViewMock.hide.mockClear(); + + await promise1; + await promise2; + expect(loadingViewMock.hide).toHaveBeenCalledTimes(1); +}); + +test('loadBundleFromServer does not cache errors', async () => { + mockHeaders = {'Content-Type': 'application/json'}; + mockDataResponse = JSON.stringify({message: 'Error thrown from Metro'}); + + await expect( + loadBundleFromServer('/Fail.bundle?platform=ios'), + ).rejects.toThrow(); + + mockDataResponse = '"code";'; + mockHeaders = {'Content-Type': 'application/javascript'}; + + await expect( + loadBundleFromServer('/Fail.bundle?platform=ios'), + ).resolves.not.toThrow(); +}); + +test('loadBundleFromServer caches successful fetches', async () => { + mockDataResponse = '"code";'; + mockHeaders = {'Content-Type': 'application/javascript'}; + + const promise1 = loadBundleFromServer( + '/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + + // Request again in the same tick = same promise + const promise2 = loadBundleFromServer( + '/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + expect(promise2).toBe(promise1); + + await promise1; + + // Request again once resolved = still the same promise + const promise3 = loadBundleFromServer( + '/Banana.bundle?platform=ios&dev=true&minify=false&unusedExtraParam=42&modulesOnly=true&runModule=false', + ); + expect(promise3).toBe(promise1); + + await promise2; + + expect(sendRequest).toBeCalledTimes(1); +}); diff --git a/Libraries/Core/Devtools/__tests__/parseErrorStack-test.js b/packages/react-native/Libraries/Core/Devtools/__tests__/parseErrorStack-test.js similarity index 100% rename from Libraries/Core/Devtools/__tests__/parseErrorStack-test.js rename to packages/react-native/Libraries/Core/Devtools/__tests__/parseErrorStack-test.js diff --git a/packages/react-native/Libraries/Core/Devtools/__tests__/parseHermesStack-test.js b/packages/react-native/Libraries/Core/Devtools/__tests__/parseHermesStack-test.js new file mode 100644 index 00000000000000..29bd88c3d7f857 --- /dev/null +++ b/packages/react-native/Libraries/Core/Devtools/__tests__/parseHermesStack-test.js @@ -0,0 +1,242 @@ +/** + * 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 strict-local + * @format + * @oncall react_native + */ + +'use strict'; + +const parseHermesStack = require('../parseHermesStack'); + +describe('parseHermesStack', () => { + test('bytecode location', () => { + expect( + parseHermesStack( + [ + 'TypeError: undefined is not a function', + ' at global (address at unknown:1:9)', + ' at foo$bar (address at /js/foo.hbc:10:1234)', + ].join('\n'), + ), + ).toMatchInlineSnapshot(` + Object { + "entries": Array [ + Object { + "functionName": "global", + "location": Object { + "line1Based": 1, + "sourceUrl": "unknown", + "type": "BYTECODE", + "virtualOffset0Based": 9, + }, + "type": "FRAME", + }, + Object { + "functionName": "foo$bar", + "location": Object { + "line1Based": 10, + "sourceUrl": "/js/foo.hbc", + "type": "BYTECODE", + "virtualOffset0Based": 1234, + }, + "type": "FRAME", + }, + ], + "message": "TypeError: undefined is not a function", + } + `); + }); + + test('internal bytecode location', () => { + expect( + parseHermesStack( + [ + 'TypeError: undefined is not a function', + ' at internal (address at InternalBytecode.js:1:9)', + ' at notInternal (address at /js/InternalBytecode.js:10:1234)', + ].join('\n'), + ), + ).toMatchInlineSnapshot(` + Object { + "entries": Array [ + Object { + "functionName": "internal", + "location": Object { + "line1Based": 1, + "sourceUrl": "InternalBytecode.js", + "type": "INTERNAL_BYTECODE", + "virtualOffset0Based": 9, + }, + "type": "FRAME", + }, + Object { + "functionName": "notInternal", + "location": Object { + "line1Based": 10, + "sourceUrl": "/js/InternalBytecode.js", + "type": "BYTECODE", + "virtualOffset0Based": 1234, + }, + "type": "FRAME", + }, + ], + "message": "TypeError: undefined is not a function", + } + `); + }); + + test('source location', () => { + expect( + parseHermesStack( + [ + 'TypeError: undefined is not a function', + ' at global (unknown:1:9)', + ' at foo$bar (/js/foo.js:10:1234)', + ].join('\n'), + ), + ).toMatchInlineSnapshot(` + Object { + "entries": Array [ + Object { + "functionName": "global", + "location": Object { + "column1Based": 9, + "line1Based": 1, + "sourceUrl": "unknown", + "type": "SOURCE", + }, + "type": "FRAME", + }, + Object { + "functionName": "foo$bar", + "location": Object { + "column1Based": 1234, + "line1Based": 10, + "sourceUrl": "/js/foo.js", + "type": "SOURCE", + }, + "type": "FRAME", + }, + ], + "message": "TypeError: undefined is not a function", + } + `); + }); + + test('tolerate empty filename', () => { + expect( + parseHermesStack( + [ + 'TypeError: undefined is not a function', + ' at global (unknown:1:9)', + ' at foo$bar (:10:1234)', + ].join('\n'), + ), + ).toMatchInlineSnapshot(` + Object { + "entries": Array [ + Object { + "functionName": "global", + "location": Object { + "column1Based": 9, + "line1Based": 1, + "sourceUrl": "unknown", + "type": "SOURCE", + }, + "type": "FRAME", + }, + Object { + "functionName": "foo$bar", + "location": Object { + "column1Based": 1234, + "line1Based": 10, + "sourceUrl": "", + "type": "SOURCE", + }, + "type": "FRAME", + }, + ], + "message": "TypeError: undefined is not a function", + } + `); + }); + + test('skipped frames', () => { + expect( + parseHermesStack( + [ + 'TypeError: undefined is not a function', + ' at global (unknown:1:9)', + ' ... skipping 50 frames', + ' at foo$bar (/js/foo.js:10:1234)', + ].join('\n'), + ), + ).toMatchInlineSnapshot(` + Object { + "entries": Array [ + Object { + "functionName": "global", + "location": Object { + "column1Based": 9, + "line1Based": 1, + "sourceUrl": "unknown", + "type": "SOURCE", + }, + "type": "FRAME", + }, + Object { + "count": 50, + "type": "SKIPPED", + }, + Object { + "functionName": "foo$bar", + "location": Object { + "column1Based": 1234, + "line1Based": 10, + "sourceUrl": "/js/foo.js", + "type": "SOURCE", + }, + "type": "FRAME", + }, + ], + "message": "TypeError: undefined is not a function", + } + `); + }); + + test('ignore frames that are part of message', () => { + expect( + parseHermesStack( + [ + 'The next line is not a stack frame', + ' at bogus (filename:1:2)', + ' but the real stack trace follows below.', + ' at foo$bar (/js/foo.js:10:1234)', + ].join('\n'), + ), + ).toMatchInlineSnapshot(` + Object { + "entries": Array [ + Object { + "functionName": "foo$bar", + "location": Object { + "column1Based": 1234, + "line1Based": 10, + "sourceUrl": "/js/foo.js", + "type": "SOURCE", + }, + "type": "FRAME", + }, + ], + "message": "The next line is not a stack frame + at bogus (filename:1:2) + but the real stack trace follows below.", + } + `); + }); +}); diff --git a/Libraries/Core/Devtools/getDevServer.js b/packages/react-native/Libraries/Core/Devtools/getDevServer.js similarity index 100% rename from Libraries/Core/Devtools/getDevServer.js rename to packages/react-native/Libraries/Core/Devtools/getDevServer.js diff --git a/packages/react-native/Libraries/Core/Devtools/loadBundleFromServer.js b/packages/react-native/Libraries/Core/Devtools/loadBundleFromServer.js new file mode 100644 index 00000000000000..222320b22bdf9a --- /dev/null +++ b/packages/react-native/Libraries/Core/Devtools/loadBundleFromServer.js @@ -0,0 +1,139 @@ +/** + * 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 strict-local + * @format + * @oncall react_native + */ + +import Networking from '../../Network/RCTNetworking'; +import HMRClient from '../../Utilities/HMRClient'; +import LoadingView from '../../Utilities/LoadingView'; +import getDevServer from './getDevServer'; + +declare var global: {globalEvalWithSourceUrl?: (string, string) => mixed, ...}; + +let pendingRequests = 0; + +const cachedPromisesByUrl = new Map>(); + +function asyncRequest( + url: string, +): Promise<{body: string, headers: {[string]: string}}> { + let id = null; + let responseText = null; + let headers = null; + let dataListener; + let completeListener; + let responseListener; + return new Promise<{body: string, headers: {[string]: string}}>( + (resolve, reject) => { + dataListener = Networking.addListener( + 'didReceiveNetworkData', + ([requestId, response]) => { + if (requestId === id) { + responseText = response; + } + }, + ); + responseListener = Networking.addListener( + 'didReceiveNetworkResponse', + ([requestId, status, responseHeaders]) => { + if (requestId === id) { + headers = responseHeaders; + } + }, + ); + completeListener = Networking.addListener( + 'didCompleteNetworkResponse', + ([requestId, error]) => { + if (requestId === id) { + if (error) { + reject(error); + } else { + //$FlowFixMe[incompatible-call] + resolve({body: responseText, headers}); + } + } + }, + ); + Networking.sendRequest( + 'GET', + 'asyncRequest', + url, + {}, + '', + 'text', + false, + 0, + requestId => { + id = requestId; + }, + true, + ); + }, + ).finally(() => { + dataListener && dataListener.remove(); + completeListener && completeListener.remove(); + responseListener && responseListener.remove(); + }); +} + +function buildUrlForBundle(bundlePathAndQuery: string) { + const {url: serverUrl} = getDevServer(); + return ( + serverUrl.replace(/\/+$/, '') + '/' + bundlePathAndQuery.replace(/^\/+/, '') + ); +} + +module.exports = function (bundlePathAndQuery: string): Promise { + const requestUrl = buildUrlForBundle(bundlePathAndQuery); + + let loadPromise = cachedPromisesByUrl.get(requestUrl); + + if (loadPromise) { + return loadPromise; + } + LoadingView.showMessage('Downloading...', 'load'); + ++pendingRequests; + + loadPromise = asyncRequest(requestUrl) + .then(({body, headers}) => { + if ( + headers['Content-Type'] != null && + headers['Content-Type'].indexOf('application/json') >= 0 + ) { + // Errors are returned as JSON. + throw new Error( + JSON.parse(body).message || + `Unknown error fetching '${bundlePathAndQuery}'`, + ); + } + + HMRClient.registerBundle(requestUrl); + + // Some engines do not support `sourceURL` as a comment. We expose a + // `globalEvalWithSourceUrl` function to handle updates in that case. + if (global.globalEvalWithSourceUrl) { + global.globalEvalWithSourceUrl(body, requestUrl); + } else { + // eslint-disable-next-line no-eval + eval(body); + } + }) + .catch(e => { + cachedPromisesByUrl.delete(requestUrl); + throw e; + }) + .finally(() => { + if (!--pendingRequests) { + LoadingView.hide(); + } + }); + + cachedPromisesByUrl.set(requestUrl, loadPromise); + return loadPromise; +}; diff --git a/Libraries/Core/Devtools/openFileInEditor.js b/packages/react-native/Libraries/Core/Devtools/openFileInEditor.js similarity index 94% rename from Libraries/Core/Devtools/openFileInEditor.js rename to packages/react-native/Libraries/Core/Devtools/openFileInEditor.js index c9eb343031ef32..4dd8dc08ca5258 100644 --- a/Libraries/Core/Devtools/openFileInEditor.js +++ b/packages/react-native/Libraries/Core/Devtools/openFileInEditor.js @@ -13,6 +13,7 @@ const getDevServer = require('./getDevServer'); function openFileInEditor(file: string, lineNumber: number) { + // $FlowFixMe[unused-promise] fetch(getDevServer().url + 'open-stack-frame', { method: 'POST', headers: { diff --git a/Libraries/Core/Devtools/openURLInBrowser.js b/packages/react-native/Libraries/Core/Devtools/openURLInBrowser.js similarity index 93% rename from Libraries/Core/Devtools/openURLInBrowser.js rename to packages/react-native/Libraries/Core/Devtools/openURLInBrowser.js index 986fa5d1f3f4fb..66f8ccbce5297c 100644 --- a/Libraries/Core/Devtools/openURLInBrowser.js +++ b/packages/react-native/Libraries/Core/Devtools/openURLInBrowser.js @@ -13,6 +13,7 @@ const getDevServer = require('./getDevServer'); function openURLInBrowser(url: string) { + // $FlowFixMe[unused-promise] fetch(getDevServer().url + 'open-url', { method: 'POST', body: JSON.stringify({url}), diff --git a/Libraries/Core/Devtools/parseErrorStack.js b/packages/react-native/Libraries/Core/Devtools/parseErrorStack.js similarity index 90% rename from Libraries/Core/Devtools/parseErrorStack.js rename to packages/react-native/Libraries/Core/Devtools/parseErrorStack.js index c85ddf05f13803..9b84537c3d9863 100644 --- a/Libraries/Core/Devtools/parseErrorStack.js +++ b/packages/react-native/Libraries/Core/Devtools/parseErrorStack.js @@ -22,7 +22,7 @@ function convertHermesStack(stack: HermesParsedStack): Array { continue; } const {location, functionName} = entry; - if (location.type === 'NATIVE') { + if (location.type === 'NATIVE' || location.type === 'INTERNAL_BYTECODE') { continue; } frames.push({ @@ -48,7 +48,7 @@ function parseErrorStack(errorStack?: string): Array { ? errorStack : global.HermesInternal ? convertHermesStack(parseHermesStack(errorStack)) - : stacktraceParser.parse(errorStack).map(frame => ({ + : stacktraceParser.parse(errorStack).map((frame): StackFrame => ({ ...frame, column: frame.column != null ? frame.column - 1 : null, })); diff --git a/packages/react-native/Libraries/Core/Devtools/parseHermesStack.js b/packages/react-native/Libraries/Core/Devtools/parseHermesStack.js new file mode 100644 index 00000000000000..1193afd88f9507 --- /dev/null +++ b/packages/react-native/Libraries/Core/Devtools/parseHermesStack.js @@ -0,0 +1,141 @@ +/** + * 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. + * + * @format + * @flow strict + */ + +'use strict'; + +type HermesStackLocationNative = $ReadOnly<{ + type: 'NATIVE', +}>; + +type HermesStackLocationSource = $ReadOnly<{ + type: 'SOURCE', + sourceUrl: string, + line1Based: number, + column1Based: number, +}>; + +type HermesStackLocationInternalBytecode = $ReadOnly<{ + type: 'INTERNAL_BYTECODE', + sourceUrl: string, + line1Based: number, + virtualOffset0Based: number, +}>; + +type HermesStackLocationBytecode = $ReadOnly<{ + type: 'BYTECODE', + sourceUrl: string, + line1Based: number, + virtualOffset0Based: number, +}>; + +type HermesStackLocation = + | HermesStackLocationNative + | HermesStackLocationSource + | HermesStackLocationInternalBytecode + | HermesStackLocationBytecode; + +type HermesStackEntryFrame = $ReadOnly<{ + type: 'FRAME', + location: HermesStackLocation, + functionName: string, +}>; + +type HermesStackEntrySkipped = $ReadOnly<{ + type: 'SKIPPED', + count: number, +}>; + +type HermesStackEntry = HermesStackEntryFrame | HermesStackEntrySkipped; + +export type HermesParsedStack = $ReadOnly<{ + message: string, + entries: $ReadOnlyArray, +}>; + +// Capturing groups: +// 1. function name +// 2. is this a native stack frame? +// 3. is this a bytecode address or a source location? +// 4. source URL (filename) +// 5. line number (1 based) +// 6. column number (1 based) or virtual offset (0 based) +const RE_FRAME = + /^ {4}at (.+?)(?: \((native)\)?| \((address at )?(.*?):(\d+):(\d+)\))$/; + +// Capturing groups: +// 1. count of skipped frames +const RE_SKIPPED = /^ {4}... skipping (\d+) frames$/; + +function isInternalBytecodeSourceUrl(sourceUrl: string): boolean { + // See https://github.com/facebook/hermes/blob/3332fa020cae0bab751f648db7c94e1d687eeec7/lib/VM/Runtime.cpp#L1100 + return sourceUrl === 'InternalBytecode.js'; +} + +function parseLine(line: string): ?HermesStackEntry { + const asFrame = line.match(RE_FRAME); + if (asFrame) { + return { + type: 'FRAME', + functionName: asFrame[1], + location: + asFrame[2] === 'native' + ? {type: 'NATIVE'} + : asFrame[3] === 'address at ' + ? isInternalBytecodeSourceUrl(asFrame[4]) + ? { + type: 'INTERNAL_BYTECODE', + sourceUrl: asFrame[4], + line1Based: Number.parseInt(asFrame[5], 10), + virtualOffset0Based: Number.parseInt(asFrame[6], 10), + } + : { + type: 'BYTECODE', + sourceUrl: asFrame[4], + line1Based: Number.parseInt(asFrame[5], 10), + virtualOffset0Based: Number.parseInt(asFrame[6], 10), + } + : { + type: 'SOURCE', + sourceUrl: asFrame[4], + line1Based: Number.parseInt(asFrame[5], 10), + column1Based: Number.parseInt(asFrame[6], 10), + }, + }; + } + const asSkipped = line.match(RE_SKIPPED); + if (asSkipped) { + return { + type: 'SKIPPED', + count: Number.parseInt(asSkipped[1], 10), + }; + } +} + +module.exports = function parseHermesStack(stack: string): HermesParsedStack { + const lines = stack.split(/\n/); + let entries: Array = []; + let lastMessageLine = -1; + for (let i = 0; i < lines.length; ++i) { + const line = lines[i]; + if (!line) { + continue; + } + const entry = parseLine(line); + if (entry) { + entries.push(entry); + continue; + } + // No match - we're still in the message + lastMessageLine = i; + entries = []; + } + const message = lines.slice(0, lastMessageLine + 1).join('\n'); + return {message, entries}; +}; diff --git a/Libraries/Core/Devtools/symbolicateStackTrace.js b/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js similarity index 94% rename from Libraries/Core/Devtools/symbolicateStackTrace.js rename to packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js index b45bc368d54f88..bdd93fc49a3f2a 100644 --- a/Libraries/Core/Devtools/symbolicateStackTrace.js +++ b/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js @@ -31,6 +31,7 @@ export type SymbolicatedStackTrace = $ReadOnly<{ async function symbolicateStackTrace( stack: Array, + extraData?: mixed, ): Promise { const devServer = getDevServer(); if (!devServer.bundleLoadedFromServer) { @@ -41,7 +42,7 @@ async function symbolicateStackTrace( const fetch = global.fetch ?? require('../../Network/fetch'); const response = await fetch(devServer.url + 'symbolicate', { method: 'POST', - body: JSON.stringify({stack}), + body: JSON.stringify({stack, extraData}), }); return await response.json(); } diff --git a/Libraries/Core/ExceptionsManager.js b/packages/react-native/Libraries/Core/ExceptionsManager.js similarity index 97% rename from Libraries/Core/ExceptionsManager.js rename to packages/react-native/Libraries/Core/ExceptionsManager.js index 4616569b4b28ab..d1de23cfa023f6 100644 --- a/Libraries/Core/ExceptionsManager.js +++ b/packages/react-native/Libraries/Core/ExceptionsManager.js @@ -103,7 +103,7 @@ function reportException( } if (__DEV__) { - const LogBox = require('../LogBox/LogBox'); + const LogBox = require('../LogBox/LogBox').default; LogBox.addException({ ...data, isComponentError: !!e.isComponentError, @@ -117,7 +117,8 @@ function reportException( } } -declare var console: typeof console & { +declare var console: { + error: typeof console.error, _errorOriginal: typeof console.error, reportErrorsAsExceptions: boolean, ... @@ -184,7 +185,7 @@ function reactConsoleErrorHandler(...args) { // Throwing an uncaught exception: // 1. exception thrown // 2. picked up by handleException - // 3. should be send to console.error (not console._errorOriginal, as DevTools might have patched _later_ and it needs to send it to Metro) + // 3. should be sent to console.error (not console._errorOriginal, as DevTools might have patched _later_ and it needs to send it to Metro) // 4. that _might_ bubble again to the `reactConsoleErrorHandle` defined here // -> should not handle exception _again_, to avoid looping / showing twice (this code branch) // 5. should still bubble up to original console (which might either be console.log, or the DevTools handler in case that one patched _earlier_) diff --git a/Libraries/Core/ExtendedError.js b/packages/react-native/Libraries/Core/ExtendedError.js similarity index 100% rename from Libraries/Core/ExtendedError.js rename to packages/react-native/Libraries/Core/ExtendedError.js diff --git a/Libraries/Core/InitializeCore.js b/packages/react-native/Libraries/Core/InitializeCore.js similarity index 95% rename from Libraries/Core/InitializeCore.js rename to packages/react-native/Libraries/Core/InitializeCore.js index 1379ffd6cbe7d5..25377f6890cb90 100644 --- a/Libraries/Core/InitializeCore.js +++ b/packages/react-native/Libraries/Core/InitializeCore.js @@ -27,6 +27,7 @@ const start = Date.now(); require('./setUpGlobals'); +require('./setUpDOM'); require('./setUpPerformance'); require('./setUpErrorHandling'); require('./polyfillPromise'); @@ -40,7 +41,7 @@ require('./setUpSegmentFetcher'); if (__DEV__) { require('./checkNativeVersion'); require('./setUpDeveloperTools'); - require('../LogBox/LogBox').install(); + require('../LogBox/LogBox').default.install(); } require('../ReactNative/AppRegistry'); diff --git a/Libraries/Core/NativeExceptionsManager.js b/packages/react-native/Libraries/Core/NativeExceptionsManager.js similarity index 100% rename from Libraries/Core/NativeExceptionsManager.js rename to packages/react-native/Libraries/Core/NativeExceptionsManager.js diff --git a/Libraries/Core/RawEventEmitter.js b/packages/react-native/Libraries/Core/RawEventEmitter.js similarity index 100% rename from Libraries/Core/RawEventEmitter.js rename to packages/react-native/Libraries/Core/RawEventEmitter.js diff --git a/Libraries/Core/ReactFiberErrorDialog.js b/packages/react-native/Libraries/Core/ReactFiberErrorDialog.js similarity index 100% rename from Libraries/Core/ReactFiberErrorDialog.js rename to packages/react-native/Libraries/Core/ReactFiberErrorDialog.js diff --git a/Libraries/Core/ReactNativeVersion.js b/packages/react-native/Libraries/Core/ReactNativeVersion.js similarity index 100% rename from Libraries/Core/ReactNativeVersion.js rename to packages/react-native/Libraries/Core/ReactNativeVersion.js diff --git a/Libraries/Core/ReactNativeVersionCheck.js b/packages/react-native/Libraries/Core/ReactNativeVersionCheck.js similarity index 85% rename from Libraries/Core/ReactNativeVersionCheck.js rename to packages/react-native/Libraries/Core/ReactNativeVersionCheck.js index 19f639a16916d8..8b41988696d6b5 100644 --- a/Libraries/Core/ReactNativeVersionCheck.js +++ b/packages/react-native/Libraries/Core/ReactNativeVersionCheck.js @@ -40,15 +40,7 @@ exports.checkVersions = function checkVersions(): void { }; function _formatVersion( - version: - | {major: number, minor: number, patch: number, prerelease: ?number} - | {major: number, minor: number, patch: number, prerelease: ?string} - | $TEMPORARY$object<{ - major: number, - minor: number, - patch: number, - prerelease: null, - }>, + version: (typeof Platform)['constants']['reactNativeVersion'], ): string { return ( `${version.major}.${version.minor}.${version.patch}` + diff --git a/Libraries/Core/SegmentFetcher/NativeSegmentFetcher.js b/packages/react-native/Libraries/Core/SegmentFetcher/NativeSegmentFetcher.js similarity index 100% rename from Libraries/Core/SegmentFetcher/NativeSegmentFetcher.js rename to packages/react-native/Libraries/Core/SegmentFetcher/NativeSegmentFetcher.js diff --git a/Libraries/Core/Timers/JSTimers.js b/packages/react-native/Libraries/Core/Timers/JSTimers.js similarity index 100% rename from Libraries/Core/Timers/JSTimers.js rename to packages/react-native/Libraries/Core/Timers/JSTimers.js diff --git a/Libraries/Core/Timers/NativeTiming.js b/packages/react-native/Libraries/Core/Timers/NativeTiming.js similarity index 100% rename from Libraries/Core/Timers/NativeTiming.js rename to packages/react-native/Libraries/Core/Timers/NativeTiming.js diff --git a/Libraries/Core/Timers/__tests__/JSTimers-test.js b/packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js similarity index 100% rename from Libraries/Core/Timers/__tests__/JSTimers-test.js rename to packages/react-native/Libraries/Core/Timers/__tests__/JSTimers-test.js diff --git a/Libraries/Core/Timers/immediateShim.js b/packages/react-native/Libraries/Core/Timers/immediateShim.js similarity index 97% rename from Libraries/Core/Timers/immediateShim.js rename to packages/react-native/Libraries/Core/Timers/immediateShim.js index 43915d6df16bd1..29af6e568727e2 100644 --- a/Libraries/Core/Timers/immediateShim.js +++ b/packages/react-native/Libraries/Core/Timers/immediateShim.js @@ -40,6 +40,7 @@ function setImmediate(callback: Function, ...args: any): number { clearedImmediates.delete(id); } + // $FlowFixMe[incompatible-call] global.queueMicrotask(() => { if (!clearedImmediates.has(id)) { callback.apply(undefined, args); diff --git a/Libraries/Core/Timers/queueMicrotask.js b/packages/react-native/Libraries/Core/Timers/queueMicrotask.js similarity index 93% rename from Libraries/Core/Timers/queueMicrotask.js rename to packages/react-native/Libraries/Core/Timers/queueMicrotask.js index 7c33d9f30036b9..5cb8529decd43b 100644 --- a/Libraries/Core/Timers/queueMicrotask.js +++ b/packages/react-native/Libraries/Core/Timers/queueMicrotask.js @@ -13,7 +13,7 @@ let resolvedPromise; /** - * Polyfill for the microtask queuening API defined by WHATWG HTMP spec. + * Polyfill for the microtask queueing API defined by WHATWG HTML spec. * https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-queuemicrotask * * The method must queue a microtask to invoke @param {function} callback, and diff --git a/Libraries/Core/__mocks__/ErrorUtils.js b/packages/react-native/Libraries/Core/__mocks__/ErrorUtils.js similarity index 100% rename from Libraries/Core/__mocks__/ErrorUtils.js rename to packages/react-native/Libraries/Core/__mocks__/ErrorUtils.js diff --git a/packages/react-native/Libraries/Core/__mocks__/NativeExceptionsManager.js b/packages/react-native/Libraries/Core/__mocks__/NativeExceptionsManager.js new file mode 100644 index 00000000000000..a43017ce012eee --- /dev/null +++ b/packages/react-native/Libraries/Core/__mocks__/NativeExceptionsManager.js @@ -0,0 +1,20 @@ +/** + * 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 strict + * @format + * @oncall react_native + */ + +import typeof NativeExceptionsManager from '../NativeExceptionsManager'; + +export default ({ + reportFatalException: jest.fn(), + reportSoftException: jest.fn(), + updateExceptionMessage: jest.fn(), + dismissRedbox: jest.fn(), + reportException: jest.fn(), +}: NativeExceptionsManager); diff --git a/Libraries/Core/__tests__/ExceptionsManager-test.js b/packages/react-native/Libraries/Core/__tests__/ExceptionsManager-test.js similarity index 99% rename from Libraries/Core/__tests__/ExceptionsManager-test.js rename to packages/react-native/Libraries/Core/__tests__/ExceptionsManager-test.js index e1dc1f535c1bf6..2dde1759c3df05 100644 --- a/Libraries/Core/__tests__/ExceptionsManager-test.js +++ b/packages/react-native/Libraries/Core/__tests__/ExceptionsManager-test.js @@ -10,7 +10,6 @@ 'use strict'; -const LogBox = require('../../LogBox/LogBox'); const ExceptionsManager = require('../ExceptionsManager'); const NativeExceptionsManager = require('../NativeExceptionsManager').default; const ReactFiberErrorDialog = require('../ReactFiberErrorDialog').default; @@ -57,9 +56,12 @@ function runExceptionsManagerTests() { let logBoxAddException; beforeEach(() => { + logBoxAddException = jest.fn(); jest.resetModules(); jest.mock('../../LogBox/LogBox', () => ({ - addException: jest.fn(), + default: { + addException: logBoxAddException, + }, })); jest.mock('../NativeExceptionsManager', () => { return { @@ -80,7 +82,6 @@ function runExceptionsManagerTests() { ); jest.spyOn(console, 'error').mockReturnValue(undefined); nativeReportException = NativeExceptionsManager.reportException; - logBoxAddException = LogBox.addException; }); afterEach(() => { diff --git a/Libraries/Core/__tests__/ReactNativeVersionCheck-test.js b/packages/react-native/Libraries/Core/__tests__/ReactNativeVersionCheck-test.js similarity index 100% rename from Libraries/Core/__tests__/ReactNativeVersionCheck-test.js rename to packages/react-native/Libraries/Core/__tests__/ReactNativeVersionCheck-test.js diff --git a/Libraries/Core/checkNativeVersion.js b/packages/react-native/Libraries/Core/checkNativeVersion.js similarity index 100% rename from Libraries/Core/checkNativeVersion.js rename to packages/react-native/Libraries/Core/checkNativeVersion.js diff --git a/Libraries/Core/polyfillPromise.js b/packages/react-native/Libraries/Core/polyfillPromise.js similarity index 100% rename from Libraries/Core/polyfillPromise.js rename to packages/react-native/Libraries/Core/polyfillPromise.js diff --git a/Libraries/Core/setUpAlert.js b/packages/react-native/Libraries/Core/setUpAlert.js similarity index 92% rename from Libraries/Core/setUpAlert.js rename to packages/react-native/Libraries/Core/setUpAlert.js index 85bf358c5bc48f..777f7f85fbecbe 100644 --- a/Libraries/Core/setUpAlert.js +++ b/packages/react-native/Libraries/Core/setUpAlert.js @@ -15,7 +15,7 @@ * You can use this module directly, or just require InitializeCore. */ if (!global.alert) { - global.alert = function (text) { + global.alert = function (text: string) { // Require Alert on demand. Requiring it too early can lead to issues // with things like Platform not being fully initialized. require('../Alert/Alert').alert('Alert', '' + text); diff --git a/Libraries/Core/setUpBatchedBridge.js b/packages/react-native/Libraries/Core/setUpBatchedBridge.js similarity index 100% rename from Libraries/Core/setUpBatchedBridge.js rename to packages/react-native/Libraries/Core/setUpBatchedBridge.js diff --git a/packages/react-native/Libraries/Core/setUpDOM.js b/packages/react-native/Libraries/Core/setUpDOM.js new file mode 100644 index 00000000000000..e25aa01371329c --- /dev/null +++ b/packages/react-native/Libraries/Core/setUpDOM.js @@ -0,0 +1,18 @@ +/** + * 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 strict + * @format + */ + +import DOMRect from '../DOM/Geometry/DOMRect'; +import DOMRectReadOnly from '../DOM/Geometry/DOMRectReadOnly'; + +// $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it +global.DOMRect = DOMRect; + +// $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it +global.DOMRectReadOnly = DOMRectReadOnly; diff --git a/Libraries/Core/setUpDeveloperTools.js b/packages/react-native/Libraries/Core/setUpDeveloperTools.js similarity index 91% rename from Libraries/Core/setUpDeveloperTools.js rename to packages/react-native/Libraries/Core/setUpDeveloperTools.js index 06f2079b87869b..67227191a8b71b 100644 --- a/Libraries/Core/setUpDeveloperTools.js +++ b/packages/react-native/Libraries/Core/setUpDeveloperTools.js @@ -10,7 +10,7 @@ import Platform from '../Utilities/Platform'; -declare var console: typeof console & {_isPolyfilled: boolean, ...}; +declare var console: {[string]: $FlowFixMe}; /** * Sets up developer tools for React Native. @@ -56,7 +56,7 @@ if (__DEV__) { 'debug', ].forEach(level => { const originalFunction = console[level]; - console[level] = function (...args) { + console[level] = function (...args: $ReadOnlyArray) { HMRClient.log(level, args); originalFunction.apply(console, args); }; @@ -74,4 +74,8 @@ if (__DEV__) { } require('./setUpReactRefresh'); + + global[ + `${global.__METRO_GLOBAL_PREFIX__ ?? ''}__loadBundleAsync` + ] = require('./Devtools/loadBundleFromServer'); } diff --git a/Libraries/Core/setUpErrorHandling.js b/packages/react-native/Libraries/Core/setUpErrorHandling.js similarity index 100% rename from Libraries/Core/setUpErrorHandling.js rename to packages/react-native/Libraries/Core/setUpErrorHandling.js diff --git a/packages/react-native/Libraries/Core/setUpGlobals.js b/packages/react-native/Libraries/Core/setUpGlobals.js new file mode 100644 index 00000000000000..8ddff03bc66e78 --- /dev/null +++ b/packages/react-native/Libraries/Core/setUpGlobals.js @@ -0,0 +1,35 @@ +/** + * 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 strict + * @format + */ + +'use strict'; + +/** + * Sets up global variables for React Native. + * You can use this module directly, or just require InitializeCore. + */ +if (global.window === undefined) { + // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it. + global.window = global; +} + +if (global.self === undefined) { + // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it. + global.self = global; +} + +// Set up process +// $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it. +global.process = global.process || {}; +// $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it. +global.process.env = global.process.env || {}; +if (!global.process.env.NODE_ENV) { + // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it. + global.process.env.NODE_ENV = __DEV__ ? 'development' : 'production'; +} diff --git a/packages/react-native/Libraries/Core/setUpNavigator.js b/packages/react-native/Libraries/Core/setUpNavigator.js new file mode 100644 index 00000000000000..bcf2cbd597b5fb --- /dev/null +++ b/packages/react-native/Libraries/Core/setUpNavigator.js @@ -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. + * + * @flow strict + * @format + */ + +'use strict'; + +const {polyfillObjectProperty} = require('../Utilities/PolyfillFunctions'); + +const navigator = global.navigator; +if (navigator === undefined) { + // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it. + global.navigator = {product: 'ReactNative'}; +} else { + // see https://github.com/facebook/react-native/issues/10881 + polyfillObjectProperty(navigator, 'product', () => 'ReactNative'); +} diff --git a/packages/react-native/Libraries/Core/setUpPerformance.js b/packages/react-native/Libraries/Core/setUpPerformance.js new file mode 100644 index 00000000000000..c5f25890cdcf72 --- /dev/null +++ b/packages/react-native/Libraries/Core/setUpPerformance.js @@ -0,0 +1,36 @@ +/** + * 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 strict + * @format + */ + +import NativePerformance from '../WebPerformance/NativePerformance'; +import Performance from '../WebPerformance/Performance'; + +// In case if the native implementation of the Performance API is available, use it, +// otherwise fall back to the legacy/default one, which only defines 'Performance.now()' +if (NativePerformance) { + // $FlowExpectedError[cannot-write] + global.performance = new Performance(); +} else { + if (!global.performance) { + // $FlowExpectedError[cannot-write] + global.performance = ({}: {now?: () => number}); + } + + /** + * Returns a double, measured in milliseconds. + * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now + */ + if (typeof global.performance.now !== 'function') { + // $FlowExpectedError[cannot-write] + global.performance.now = function () { + const performanceNow = global.nativePerformanceNow || Date.now; + return performanceNow(); + }; + } +} diff --git a/Libraries/Core/setUpReactDevTools.js b/packages/react-native/Libraries/Core/setUpReactDevTools.js similarity index 95% rename from Libraries/Core/setUpReactDevTools.js rename to packages/react-native/Libraries/Core/setUpReactDevTools.js index be647e33b4d2ca..ecdd14a44a65e4 100644 --- a/Libraries/Core/setUpReactDevTools.js +++ b/packages/react-native/Libraries/Core/setUpReactDevTools.js @@ -62,6 +62,7 @@ if (__DEV__) { }); const ReactNativeStyleAttributes = require('../Components/View/ReactNativeStyleAttributes'); + const devToolsSettingsManager = require('../DevToolsSettings/DevToolsSettingsManager'); reactDevTools.connectToDevTools({ isAppActive, @@ -70,6 +71,7 @@ if (__DEV__) { ReactNativeStyleAttributes, ), websocket: ws, + devToolsSettingsManager, }); } }; diff --git a/Libraries/Core/setUpReactRefresh.js b/packages/react-native/Libraries/Core/setUpReactRefresh.js similarity index 100% rename from Libraries/Core/setUpReactRefresh.js rename to packages/react-native/Libraries/Core/setUpReactRefresh.js diff --git a/Libraries/Core/setUpRegeneratorRuntime.js b/packages/react-native/Libraries/Core/setUpRegeneratorRuntime.js similarity index 100% rename from Libraries/Core/setUpRegeneratorRuntime.js rename to packages/react-native/Libraries/Core/setUpRegeneratorRuntime.js diff --git a/packages/react-native/Libraries/Core/setUpSegmentFetcher.js b/packages/react-native/Libraries/Core/setUpSegmentFetcher.js new file mode 100644 index 00000000000000..2a7115c1d45781 --- /dev/null +++ b/packages/react-native/Libraries/Core/setUpSegmentFetcher.js @@ -0,0 +1,52 @@ +/** + * 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 strict-local + * @format + */ + +'use strict'; + +export type FetchSegmentFunction = typeof __fetchSegment; + +/** + * Set up SegmentFetcher. + * You can use this module directly, or just require InitializeCore. + */ + +function __fetchSegment( + segmentId: number, + options: $ReadOnly<{ + otaBuildNumber: ?string, + requestedModuleName: string, + segmentHash: string, + }>, + callback: (?Error) => void, +) { + const SegmentFetcher = + require('./SegmentFetcher/NativeSegmentFetcher').default; + SegmentFetcher.fetchSegment( + segmentId, + options, + ( + errorObject: ?{ + message: string, + code: string, + ... + }, + ) => { + if (errorObject) { + const error = new Error(errorObject.message); + (error: any).code = errorObject.code; // flowlint-line unclear-type: off + callback(error); + } + + callback(null); + }, + ); +} + +global.__fetchSegment = __fetchSegment; diff --git a/Libraries/Core/setUpTimers.js b/packages/react-native/Libraries/Core/setUpTimers.js similarity index 97% rename from Libraries/Core/setUpTimers.js rename to packages/react-native/Libraries/Core/setUpTimers.js index b2b036fdc9d069..86d73869f5ef9c 100644 --- a/Libraries/Core/setUpTimers.js +++ b/packages/react-native/Libraries/Core/setUpTimers.js @@ -61,7 +61,7 @@ if (global.RN$Bridgeless !== true) { * as the Promise. */ if (hasPromiseQueuedToJSVM) { - // When promise queues to the JSVM microtasks queue, we shim the immedaite + // When promise queues to the JSVM microtasks queue, we shim the immediate // APIs via `queueMicrotask` to maintain the backward compatibility. polyfillGlobal( 'setImmediate', @@ -95,7 +95,7 @@ if (hasHermesPromiseQueuedToJSVM) { // Fast path for Hermes. polyfillGlobal('queueMicrotask', () => global.HermesInternal?.enqueueJob); } else { - // Polyfill it with promise (regardless it's polyfiled or native) otherwise. + // Polyfill it with promise (regardless it's polyfilled or native) otherwise. polyfillGlobal( 'queueMicrotask', () => require('./Timers/queueMicrotask.js').default, diff --git a/Libraries/Core/setUpXHR.js b/packages/react-native/Libraries/Core/setUpXHR.js similarity index 100% rename from Libraries/Core/setUpXHR.js rename to packages/react-native/Libraries/Core/setUpXHR.js diff --git a/packages/react-native/Libraries/DOM/Geometry/DOMRect.js b/packages/react-native/Libraries/DOM/Geometry/DOMRect.js new file mode 100644 index 00000000000000..07af232a399891 --- /dev/null +++ b/packages/react-native/Libraries/DOM/Geometry/DOMRect.js @@ -0,0 +1,82 @@ +/** + * 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 strict + * @format + */ + +/** + * The JSDoc comments in this file have been extracted from [DOMRect](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect). + * Content by [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect/contributors.txt), + * licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/). + */ + +import DOMRectReadOnly, {type DOMRectLike} from './DOMRectReadOnly'; + +// flowlint unsafe-getters-setters:off + +/** + * A `DOMRect` describes the size and position of a rectangle. + * The type of box represented by the `DOMRect` is specified by the method or property that returned it. + * + * This is a (mostly) spec-compliant version of `DOMRect` (https://developer.mozilla.org/en-US/docs/Web/API/DOMRect). + */ +export default class DOMRect extends DOMRectReadOnly { + /** + * The x coordinate of the `DOMRect`'s origin. + */ + get x(): number { + return this.__getInternalX(); + } + + set x(x: ?number) { + this.__setInternalX(x); + } + + /** + * The y coordinate of the `DOMRect`'s origin. + */ + get y(): number { + return this.__getInternalY(); + } + + set y(y: ?number) { + this.__setInternalY(y); + } + + /** + * The width of the `DOMRect`. + */ + get width(): number { + return this.__getInternalWidth(); + } + + set width(width: ?number) { + this.__setInternalWidth(width); + } + + /** + * The height of the `DOMRect`. + */ + get height(): number { + return this.__getInternalHeight(); + } + + set height(height: ?number) { + this.__setInternalHeight(height); + } + + /** + * Creates a new `DOMRect` object with a given location and dimensions. + */ + static fromRect(rect?: ?DOMRectLike): DOMRect { + if (!rect) { + return new DOMRect(); + } + + return new DOMRect(rect.x, rect.y, rect.width, rect.height); + } +} diff --git a/packages/react-native/Libraries/DOM/Geometry/DOMRectReadOnly.js b/packages/react-native/Libraries/DOM/Geometry/DOMRectReadOnly.js new file mode 100644 index 00000000000000..ce5df6767b486a --- /dev/null +++ b/packages/react-native/Libraries/DOM/Geometry/DOMRectReadOnly.js @@ -0,0 +1,188 @@ +/** + * 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 strict + * @format + */ + +/** + * The JSDoc comments in this file have been extracted from [DOMRectReadOnly](https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly). + * Content by [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly/contributors.txt), + * licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/). + */ + +// flowlint sketchy-null:off, unsafe-getters-setters:off + +export interface DOMRectLike { + x?: ?number; + y?: ?number; + width?: ?number; + height?: ?number; +} + +function castToNumber(value: mixed): number { + return value ? Number(value) : 0; +} + +/** + * The `DOMRectReadOnly` interface specifies the standard properties used by `DOMRect` to define a rectangle whose properties are immutable. + * + * This is a (mostly) spec-compliant version of `DOMRectReadOnly` (https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly). + */ +export default class DOMRectReadOnly { + _x: number; + _y: number; + _width: number; + _height: number; + + constructor(x: ?number, y: ?number, width: ?number, height: ?number) { + this.__setInternalX(x); + this.__setInternalY(y); + this.__setInternalWidth(width); + this.__setInternalHeight(height); + } + + /** + * The x coordinate of the `DOMRectReadOnly`'s origin. + */ + get x(): number { + return this._x; + } + + /** + * The y coordinate of the `DOMRectReadOnly`'s origin. + */ + get y(): number { + return this._y; + } + + /** + * The width of the `DOMRectReadOnly`. + */ + get width(): number { + return this._width; + } + + /** + * The height of the `DOMRectReadOnly`. + */ + get height(): number { + return this._height; + } + + /** + * Returns the top coordinate value of the `DOMRect` (has the same value as `y`, or `y + height` if `height` is negative). + */ + get top(): number { + const height = this._height; + const y = this._y; + + if (height < 0) { + return y + height; + } + + return y; + } + + /** + * Returns the right coordinate value of the `DOMRect` (has the same value as ``x + width`, or `x` if `width` is negative). + */ + get right(): number { + const width = this._width; + const x = this._x; + + if (width < 0) { + return x; + } + + return x + width; + } + + /** + * Returns the bottom coordinate value of the `DOMRect` (has the same value as `y + height`, or `y` if `height` is negative). + */ + get bottom(): number { + const height = this._height; + const y = this._y; + + if (height < 0) { + return y; + } + + return y + height; + } + + /** + * Returns the left coordinate value of the `DOMRect` (has the same value as `x`, or `x + width` if `width` is negative). + */ + get left(): number { + const width = this._width; + const x = this._x; + + if (width < 0) { + return x + width; + } + + return x; + } + + toJSON(): { + x: number, + y: number, + width: number, + height: number, + top: number, + left: number, + bottom: number, + right: number, + } { + const {x, y, width, height, top, left, bottom, right} = this; + return {x, y, width, height, top, left, bottom, right}; + } + + /** + * Creates a new `DOMRectReadOnly` object with a given location and dimensions. + */ + static fromRect(rect?: ?DOMRectLike): DOMRectReadOnly { + if (!rect) { + return new DOMRectReadOnly(); + } + + return new DOMRectReadOnly(rect.x, rect.y, rect.width, rect.height); + } + + __getInternalX(): number { + return this._x; + } + + __getInternalY(): number { + return this._y; + } + + __getInternalWidth(): number { + return this._width; + } + + __getInternalHeight(): number { + return this._height; + } + + __setInternalX(x: ?number) { + this._x = castToNumber(x); + } + + __setInternalY(y: ?number) { + this._y = castToNumber(y); + } + + __setInternalWidth(width: ?number) { + this._width = castToNumber(width); + } + + __setInternalHeight(height: ?number) { + this._height = castToNumber(height); + } +} diff --git a/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js b/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js new file mode 100644 index 00000000000000..e9ade0185aac17 --- /dev/null +++ b/packages/react-native/Libraries/DOM/Nodes/ReactNativeElement.js @@ -0,0 +1,185 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +// flowlint unsafe-getters-setters:off + +import type { + HostComponent, + INativeMethods, + InternalInstanceHandle, + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, + MeasureOnSuccessCallback, + ViewConfig, +} from '../../Renderer/shims/ReactNativeTypes'; +import type {ElementRef} from 'react'; + +import TextInputState from '../../Components/TextInput/TextInputState'; +import {getFabricUIManager} from '../../ReactNative/FabricUIManager'; +import {create as createAttributePayload} from '../../ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload'; +import warnForStyleProps from '../../ReactNative/ReactFabricPublicInstance/warnForStyleProps'; +import ReadOnlyElement from './ReadOnlyElement'; +import ReadOnlyNode from './ReadOnlyNode'; +import { + getPublicInstanceFromInternalInstanceHandle, + getShadowNode, +} from './ReadOnlyNode'; +import nullthrows from 'nullthrows'; + +const noop = () => {}; + +export default class ReactNativeElement + extends ReadOnlyElement + implements INativeMethods +{ + // These need to be accessible from `ReactFabricPublicInstanceUtils`. + __nativeTag: number; + __internalInstanceHandle: InternalInstanceHandle; + + _viewConfig: ViewConfig; + + constructor( + tag: number, + viewConfig: ViewConfig, + internalInstanceHandle: InternalInstanceHandle, + ) { + super(internalInstanceHandle); + + this.__nativeTag = tag; + this.__internalInstanceHandle = internalInstanceHandle; + this._viewConfig = viewConfig; + } + + get offsetHeight(): number { + return Math.round(this.getBoundingClientRect().height); + } + + get offsetLeft(): number { + const node = getShadowNode(this); + + if (node != null) { + const offset = nullthrows(getFabricUIManager()).getOffset(node); + if (offset != null) { + return Math.round(offset[2]); + } + } + + return 0; + } + + get offsetParent(): ReadOnlyElement | null { + const node = getShadowNode(this); + + if (node != null) { + const offset = nullthrows(getFabricUIManager()).getOffset(node); + if (offset != null) { + const offsetParentInstanceHandle = offset[0]; + const offsetParent = getPublicInstanceFromInternalInstanceHandle( + offsetParentInstanceHandle, + ); + // $FlowExpectedError[incompatible-type] The value returned by `getOffset` is always an instance handle for `ReadOnlyElement`. + const offsetParentElement: ReadOnlyElement = offsetParent; + return offsetParentElement; + } + } + + return null; + } + + get offsetTop(): number { + const node = getShadowNode(this); + + if (node != null) { + const offset = nullthrows(getFabricUIManager()).getOffset(node); + if (offset != null) { + return Math.round(offset[1]); + } + } + + return 0; + } + + get offsetWidth(): number { + return Math.round(this.getBoundingClientRect().width); + } + + /** + * React Native compatibility methods + */ + + blur(): void { + // $FlowFixMe[incompatible-exact] Migrate all usages of `NativeMethods` to an interface to fix this. + TextInputState.blurTextInput(this); + } + + focus() { + // $FlowFixMe[incompatible-exact] Migrate all usages of `NativeMethods` to an interface to fix this. + TextInputState.focusTextInput(this); + } + + measure(callback: MeasureOnSuccessCallback) { + const node = getShadowNode(this); + if (node != null) { + nullthrows(getFabricUIManager()).measure(node, callback); + } + } + + measureInWindow(callback: MeasureInWindowOnSuccessCallback) { + const node = getShadowNode(this); + if (node != null) { + nullthrows(getFabricUIManager()).measureInWindow(node, callback); + } + } + + measureLayout( + relativeToNativeNode: number | ElementRef>, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail?: () => void /* currently unused */, + ) { + if (!(relativeToNativeNode instanceof ReadOnlyNode)) { + if (__DEV__) { + console.error( + 'Warning: ref.measureLayout must be called with a ref to a native component.', + ); + } + + return; + } + + const toStateNode = getShadowNode(this); + const fromStateNode = getShadowNode(relativeToNativeNode); + + if (toStateNode != null && fromStateNode != null) { + nullthrows(getFabricUIManager()).measureLayout( + toStateNode, + fromStateNode, + onFail != null ? onFail : noop, + onSuccess != null ? onSuccess : noop, + ); + } + } + + setNativeProps(nativeProps: {...}): void { + if (__DEV__) { + warnForStyleProps(nativeProps, this._viewConfig.validAttributes); + } + + const updatePayload = createAttributePayload( + nativeProps, + this._viewConfig.validAttributes, + ); + + const node = getShadowNode(this); + + if (node != null && updatePayload != null) { + nullthrows(getFabricUIManager()).setNativeProps(node, updatePayload); + } + } +} diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyCharacterData.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyCharacterData.js new file mode 100644 index 00000000000000..6c9f87b43200b1 --- /dev/null +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyCharacterData.js @@ -0,0 +1,72 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +// flowlint unsafe-getters-setters:off + +import type ReadOnlyElement from './ReadOnlyElement'; + +import {getFabricUIManager} from '../../ReactNative/FabricUIManager'; +import ReadOnlyNode, {getShadowNode} from './ReadOnlyNode'; +import {getElementSibling} from './Utilities/Traversal'; +import nullthrows from 'nullthrows'; + +export default class ReadOnlyCharacterData extends ReadOnlyNode { + get nextElementSibling(): ReadOnlyElement | null { + return getElementSibling(this, 'next'); + } + + get previousElementSibling(): ReadOnlyElement | null { + return getElementSibling(this, 'previous'); + } + + get data(): string { + const shadowNode = getShadowNode(this); + + if (shadowNode != null) { + return nullthrows(getFabricUIManager()).getTextContent(shadowNode); + } + + return ''; + } + + get length(): number { + return this.data.length; + } + + /** + * @override + */ + get textContent(): string | null { + return this.data; + } + + /** + * @override + */ + get nodeValue(): string { + return this.data; + } + + substringData(offset: number, count: number): string { + const data = this.data; + if (offset < 0) { + throw new TypeError( + `Failed to execute 'substringData' on 'CharacterData': The offset ${offset} is negative.`, + ); + } + if (offset > data.length) { + throw new TypeError( + `Failed to execute 'substringData' on 'CharacterData': The offset ${offset} is greater than the node's length (${data.length}).`, + ); + } + let adjustedCount = count < 0 || count > data.length ? data.length : count; + return data.slice(offset, offset + adjustedCount); + } +} diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js new file mode 100644 index 00000000000000..163deca45fdc58 --- /dev/null +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -0,0 +1,150 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +// flowlint unsafe-getters-setters:off + +import type HTMLCollection from '../OldStyleCollections/HTMLCollection'; + +import {getFabricUIManager} from '../../ReactNative/FabricUIManager'; +import DOMRect from '../Geometry/DOMRect'; +import {createHTMLCollection} from '../OldStyleCollections/HTMLCollection'; +import ReadOnlyNode, {getChildNodes, getShadowNode} from './ReadOnlyNode'; +import {getElementSibling} from './Utilities/Traversal'; +import nullthrows from 'nullthrows'; + +export default class ReadOnlyElement extends ReadOnlyNode { + get childElementCount(): number { + return getChildElements(this).length; + } + + get children(): HTMLCollection { + return createHTMLCollection(getChildElements(this)); + } + + get clientHeight(): number { + throw new TypeError('Unimplemented'); + } + + get clientLeft(): number { + throw new TypeError('Unimplemented'); + } + + get clientTop(): number { + throw new TypeError('Unimplemented'); + } + + get clientWidth(): number { + throw new TypeError('Unimplemented'); + } + + get firstElementChild(): ReadOnlyElement | null { + const childElements = getChildElements(this); + + if (childElements.length === 0) { + return null; + } + + return childElements[0]; + } + + get id(): string { + throw new TypeError('Unimplemented'); + } + + get lastElementChild(): ReadOnlyElement | null { + const childElements = getChildElements(this); + + if (childElements.length === 0) { + return null; + } + + return childElements[childElements.length - 1]; + } + + get nextElementSibling(): ReadOnlyElement | null { + return getElementSibling(this, 'next'); + } + + get nodeName(): string { + return this.tagName; + } + + get nodeType(): number { + return ReadOnlyNode.ELEMENT_NODE; + } + + get nodeValue(): string | null { + return null; + } + + set nodeValue(value: string): void {} + + get previousElementSibling(): ReadOnlyElement | null { + return getElementSibling(this, 'previous'); + } + + get scrollHeight(): number { + throw new TypeError('Unimplemented'); + } + + get scrollLeft(): number { + throw new TypeError('Unimplemented'); + } + + get scrollTop(): number { + throw new TypeError('Unimplemented'); + } + + get scrollWidth(): number { + throw new TypeError('Unimplemented'); + } + + get tagName(): string { + throw new TypeError('Unimplemented'); + } + + get textContent(): string | null { + const shadowNode = getShadowNode(this); + + if (shadowNode != null) { + return nullthrows(getFabricUIManager()).getTextContent(shadowNode); + } + + return ''; + } + + getBoundingClientRect(): DOMRect { + const shadowNode = getShadowNode(this); + + if (shadowNode != null) { + const rect = nullthrows(getFabricUIManager()).getBoundingClientRect( + shadowNode, + ); + + if (rect) { + return new DOMRect(rect[0], rect[1], rect[2], rect[3]); + } + } + + // Empty rect if any of the above failed + return new DOMRect(0, 0, 0, 0); + } + + getClientRects(): DOMRectList { + throw new TypeError('Unimplemented'); + } +} + +function getChildElements(node: ReadOnlyNode): $ReadOnlyArray { + // $FlowIssue[incompatible-call] + return getChildNodes(node).filter( + childNode => childNode instanceof ReadOnlyElement, + ); +} diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js new file mode 100644 index 00000000000000..e92a298be913e8 --- /dev/null +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyNode.js @@ -0,0 +1,356 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +// flowlint unsafe-getters-setters:off + +import type { + InternalInstanceHandle, + Node as ShadowNode, +} from '../../Renderer/shims/ReactNativeTypes'; +import type NodeList from '../OldStyleCollections/NodeList'; +import type ReadOnlyElement from './ReadOnlyElement'; + +import {getFabricUIManager} from '../../ReactNative/FabricUIManager'; +import ReactFabric from '../../Renderer/shims/ReactFabric'; +import {createNodeList} from '../OldStyleCollections/NodeList'; +import nullthrows from 'nullthrows'; + +// We initialize this lazily to avoid a require cycle +// (`ReadOnlyElement` also depends on `ReadOnlyNode`). +let ReadOnlyElementClass: Class; + +export default class ReadOnlyNode { + constructor(internalInstanceHandle: InternalInstanceHandle) { + setInstanceHandle(this, internalInstanceHandle); + } + + get childNodes(): NodeList { + const childNodes = getChildNodes(this); + return createNodeList(childNodes); + } + + get firstChild(): ReadOnlyNode | null { + const childNodes = getChildNodes(this); + + if (childNodes.length === 0) { + return null; + } + + return childNodes[0]; + } + + get isConnected(): boolean { + const shadowNode = getShadowNode(this); + + if (shadowNode == null) { + return false; + } + + return nullthrows(getFabricUIManager()).isConnected(shadowNode); + } + + get lastChild(): ReadOnlyNode | null { + const childNodes = getChildNodes(this); + + if (childNodes.length === 0) { + return null; + } + + return childNodes[childNodes.length - 1]; + } + + get nextSibling(): ReadOnlyNode | null { + const [siblings, position] = getNodeSiblingsAndPosition(this); + + if (position === siblings.length - 1) { + // this node is the last child of its parent, so there is no next sibling. + return null; + } + + return siblings[position + 1]; + } + + /** + * @abstract + */ + get nodeName(): string { + throw new TypeError( + '`nodeName` is abstract and must be implemented in a subclass of `ReadOnlyNode`', + ); + } + + /** + * @abstract + */ + get nodeType(): number { + throw new TypeError( + '`nodeType` is abstract and must be implemented in a subclass of `ReadOnlyNode`', + ); + } + + /** + * @abstract + */ + get nodeValue(): string | null { + throw new TypeError( + '`nodeValue` is abstract and must be implemented in a subclass of `ReadOnlyNode`', + ); + } + + get parentElement(): ReadOnlyElement | null { + const parentNode = this.parentNode; + + if (ReadOnlyElementClass == null) { + // We initialize this lazily to avoid a require cycle. + ReadOnlyElementClass = require('./ReadOnlyElement').default; + } + + if (parentNode instanceof ReadOnlyElementClass) { + return parentNode; + } + + return null; + } + + get parentNode(): ReadOnlyNode | null { + const shadowNode = getShadowNode(this); + + if (shadowNode == null) { + return null; + } + + const parentInstanceHandle = nullthrows(getFabricUIManager()).getParentNode( + shadowNode, + ); + + if (parentInstanceHandle == null) { + return null; + } + + return getPublicInstanceFromInternalInstanceHandle(parentInstanceHandle); + } + + get previousSibling(): ReadOnlyNode | null { + const [siblings, position] = getNodeSiblingsAndPosition(this); + + if (position === 0) { + // this node is the first child of its parent, so there is no previous sibling. + return null; + } + + return siblings[position - 1]; + } + + /** + * @abstract + */ + get textContent(): string | null { + throw new TypeError( + '`textContent` is abstract and must be implemented in a subclass of `ReadOnlyNode`', + ); + } + + compareDocumentPosition(otherNode: ReadOnlyNode): number { + // Quick check to avoid having to call into Fabric if the nodes are the same. + if (otherNode === this) { + return 0; + } + + const shadowNode = getShadowNode(this); + const otherShadowNode = getShadowNode(otherNode); + + if (shadowNode == null || otherShadowNode == null) { + return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED; + } + + return nullthrows(getFabricUIManager()).compareDocumentPosition( + shadowNode, + otherShadowNode, + ); + } + + contains(otherNode: ReadOnlyNode): boolean { + if (otherNode === this) { + return true; + } + + const position = this.compareDocumentPosition(otherNode); + // eslint-disable-next-line no-bitwise + return (position & ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY) !== 0; + } + + getRootNode(): ReadOnlyNode { + // eslint-disable-next-line consistent-this + let lastKnownParent: ReadOnlyNode = this; + let nextPossibleParent: ?ReadOnlyNode = this.parentNode; + + while (nextPossibleParent != null) { + lastKnownParent = nextPossibleParent; + nextPossibleParent = nextPossibleParent.parentNode; + } + + return lastKnownParent; + } + + hasChildNodes(): boolean { + return getChildNodes(this).length > 0; + } + + /* + * Node types, as returned by the `nodeType` property. + */ + + /** + * Type of Element, HTMLElement and ReactNativeElement instances. + */ + static ELEMENT_NODE: number = 1; + /** + * Currently Unused in React Native. + */ + static ATTRIBUTE_NODE: number = 2; + /** + * Text nodes. + */ + static TEXT_NODE: number = 3; + /** + * @deprecated Unused in React Native. + */ + static CDATA_SECTION_NODE: number = 4; + /** + * @deprecated + */ + static ENTITY_REFERENCE_NODE: number = 5; + /** + * @deprecated + */ + static ENTITY_NODE: number = 6; + /** + * @deprecated Unused in React Native. + */ + static PROCESSING_INSTRUCTION_NODE: number = 7; + /** + * @deprecated Unused in React Native. + */ + static COMMENT_NODE: number = 8; + /** + * @deprecated Unused in React Native. + */ + static DOCUMENT_NODE: number = 9; + /** + * @deprecated Unused in React Native. + */ + static DOCUMENT_TYPE_NODE: number = 10; + /** + * @deprecated Unused in React Native. + */ + static DOCUMENT_FRAGMENT_NODE: number = 11; + /** + * @deprecated + */ + static NOTATION_NODE: number = 12; + + /* + * Document position flags. Used to check the return value of + * `compareDocumentPosition()`. + */ + + /** + * Both nodes are in different documents. + */ + static DOCUMENT_POSITION_DISCONNECTED: number = 1; + /** + * `otherNode` precedes the node in either a pre-order depth-first traversal of a tree containing both + * (e.g., as an ancestor or previous sibling or a descendant of a previous sibling or previous sibling of an ancestor) + * or (if they are disconnected) in an arbitrary but consistent ordering. + */ + static DOCUMENT_POSITION_PRECEDING: number = 2; + /** + * `otherNode` follows the node in either a pre-order depth-first traversal of a tree containing both + * (e.g., as a descendant or following sibling or a descendant of a following sibling or following sibling of an ancestor) + * or (if they are disconnected) in an arbitrary but consistent ordering. + */ + static DOCUMENT_POSITION_FOLLOWING: number = 4; + /** + * `otherNode` is an ancestor of the node. + */ + static DOCUMENT_POSITION_CONTAINS: number = 8; + /** + * `otherNode` is a descendant of the node. + */ + static DOCUMENT_POSITION_CONTAINED_BY: number = 16; + /** + * @deprecated Unused in React Native. + */ + static DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: number = 32; +} + +const INSTANCE_HANDLE_KEY = Symbol('internalInstanceHandle'); + +function getInstanceHandle(node: ReadOnlyNode): InternalInstanceHandle { + // $FlowExpectedError[prop-missing] + return node[INSTANCE_HANDLE_KEY]; +} + +function setInstanceHandle( + node: ReadOnlyNode, + instanceHandle: InternalInstanceHandle, +): void { + // $FlowExpectedError[prop-missing] + node[INSTANCE_HANDLE_KEY] = instanceHandle; +} + +export function getShadowNode(node: ReadOnlyNode): ?ShadowNode { + return ReactFabric.getNodeFromInternalInstanceHandle(getInstanceHandle(node)); +} + +export function getChildNodes( + node: ReadOnlyNode, +): $ReadOnlyArray { + const shadowNode = getShadowNode(node); + + if (shadowNode == null) { + return []; + } + + const childNodeInstanceHandles = nullthrows( + getFabricUIManager(), + ).getChildNodes(shadowNode); + return childNodeInstanceHandles.map(instanceHandle => + getPublicInstanceFromInternalInstanceHandle(instanceHandle), + ); +} + +function getNodeSiblingsAndPosition( + node: ReadOnlyNode, +): [$ReadOnlyArray, number] { + const parent = node.parentNode; + if (parent == null) { + // This node is the root or it's disconnected. + return [[node], 0]; + } + + const siblings = getChildNodes(parent); + const position = siblings.indexOf(node); + + if (position === -1) { + throw new TypeError("Missing node in parent's child node list"); + } + + return [siblings, position]; +} + +export function getPublicInstanceFromInternalInstanceHandle( + instanceHandle: InternalInstanceHandle, +): ReadOnlyNode { + const mixedPublicInstance = + ReactFabric.getPublicInstanceFromInternalInstanceHandle(instanceHandle); + // $FlowExpectedError[incompatible-return] React defines public instances as "mixed" because it can't access the definition from React Native. + return mixedPublicInstance; +} diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyText.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyText.js new file mode 100644 index 00000000000000..91131f7298b552 --- /dev/null +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyText.js @@ -0,0 +1,30 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +// flowlint unsafe-getters-setters:off + +import ReadOnlyCharacterData from './ReadOnlyCharacterData'; +import ReadOnlyNode from './ReadOnlyNode'; + +export default class ReadOnlyText extends ReadOnlyCharacterData { + /** + * @override + */ + get nodeName(): string { + return '#text'; + } + + /** + * @override + */ + get nodeType(): number { + return ReadOnlyNode.TEXT_NODE; + } +} diff --git a/packages/react-native/Libraries/DOM/Nodes/Utilities/Traversal.js b/packages/react-native/Libraries/DOM/Nodes/Utilities/Traversal.js new file mode 100644 index 00000000000000..083d6bfafe79f6 --- /dev/null +++ b/packages/react-native/Libraries/DOM/Nodes/Utilities/Traversal.js @@ -0,0 +1,54 @@ +/** + * 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. + * + * @format + * @flow strict-local + */ + +import type ReadOnlyElement from '../ReadOnlyElement'; +import type ReadOnlyNode from '../ReadOnlyNode'; + +import {getChildNodes} from '../ReadOnlyNode'; + +// We initialize this lazily to avoid a require cycle +// (`ReadOnlyElement` also depends on `Traversal`). +let ReadOnlyElementClass: Class; + +export function getElementSibling( + node: ReadOnlyNode, + direction: 'next' | 'previous', +): ReadOnlyElement | null { + const parent = node.parentNode; + if (parent == null) { + // This node is the root or it's disconnected. + return null; + } + + const childNodes = getChildNodes(parent); + + const startPosition = childNodes.indexOf(node); + if (startPosition === -1) { + return null; + } + + const increment = direction === 'next' ? 1 : -1; + + let position = startPosition + increment; + + if (ReadOnlyElementClass == null) { + // We initialize this lazily to avoid a require cycle. + ReadOnlyElementClass = require('../ReadOnlyElement').default; + } + + while ( + childNodes[position] != null && + !(childNodes[position] instanceof ReadOnlyElementClass) + ) { + position = position + increment; + } + + return childNodes[position] ?? null; +} diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/ArrayLikeUtils.js b/packages/react-native/Libraries/DOM/OldStyleCollections/ArrayLikeUtils.js new file mode 100644 index 00000000000000..d906b080df0a06 --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/ArrayLikeUtils.js @@ -0,0 +1,46 @@ +/** + * 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. + * + * @format + * @flow strict + */ + +/** + * This definition is different from the current built-in type `$ArrayLike` + * provided by Flow, in that this is an interface and that one is an object. + * + * The difference is important because, when using objects, Flow thinks + * a `length` property would be copied over when using the spread operator, + * which is incorrect. + */ +export interface ArrayLike extends Iterable { + // This property should've been read-only as well, but Flow doesn't handle + // read-only indexers correctly (thinks reads are writes and fails). + [indexer: number]: T; + +length: number; +} + +export function* createValueIterator(arrayLike: ArrayLike): Iterator { + for (let i = 0; i < arrayLike.length; i++) { + yield arrayLike[i]; + } +} + +export function* createKeyIterator( + arrayLike: ArrayLike, +): Iterator { + for (let i = 0; i < arrayLike.length; i++) { + yield i; + } +} + +export function* createEntriesIterator( + arrayLike: ArrayLike, +): Iterator<[number, T]> { + for (let i = 0; i < arrayLike.length; i++) { + yield [i, arrayLike[i]]; + } +} diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/DOMRectList.js b/packages/react-native/Libraries/DOM/OldStyleCollections/DOMRectList.js new file mode 100644 index 00000000000000..9365a9a5a29f7b --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/DOMRectList.js @@ -0,0 +1,76 @@ +/** + * 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. + * + * @format + * @flow strict + */ + +// flowlint unsafe-getters-setters:off + +import type DOMRectReadOnly from '../Geometry/DOMRectReadOnly'; +import type {ArrayLike} from './ArrayLikeUtils'; + +import {createValueIterator} from './ArrayLikeUtils'; + +// IMPORTANT: The Flow type definition for this module is defined in `DOMRectList.js.flow` +// because Flow only supports indexers in classes in declaration files. + +// $FlowIssue[prop-missing] Flow doesn't understand [Symbol.iterator]() {} and thinks this class doesn't implement the Iterable interface. +export default class DOMRectList implements Iterable { + _length: number; + + /** + * Use `createDOMRectList` to create instances of this class. + * + * @private This is not defined in the declaration file, so users will not see + * the signature of the constructor. + */ + constructor(elements: $ReadOnlyArray) { + for (let i = 0; i < elements.length; i++) { + Object.defineProperty(this, i, { + value: elements[i], + enumerable: true, + configurable: false, + writable: false, + }); + } + + this._length = elements.length; + } + + get length(): number { + return this._length; + } + + item(index: number): DOMRectReadOnly | null { + if (index < 0 || index >= this._length) { + return null; + } + + // assigning to the interface allows us to access the indexer property in a + // type-safe way. + // eslint-disable-next-line consistent-this + const arrayLike: ArrayLike = this; + return arrayLike[index]; + } + + // $FlowIssue[unsupported-syntax] Flow does not support computed properties in classes. + [Symbol.iterator](): Iterator { + return createValueIterator(this); + } +} + +/** + * This is an internal method to create instances of `DOMRectList`, + * which avoids leaking its constructor to end users. + * We can do that because the external definition of `DOMRectList` lives in + * `DOMRectList.js.flow`, not here. + */ +export function createDOMRectList( + elements: $ReadOnlyArray, +): DOMRectList { + return new DOMRectList(elements); +} diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/DOMRectList.js.flow b/packages/react-native/Libraries/DOM/OldStyleCollections/DOMRectList.js.flow new file mode 100644 index 00000000000000..cb2cc2620ae515 --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/DOMRectList.js.flow @@ -0,0 +1,27 @@ +/** + * 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. + * + * @format + * @flow strict + */ + +import type {ArrayLike} from './ArrayLikeUtils'; +import type DOMRectReadOnly from '../Geometry/DOMRectReadOnly'; + +declare export default class DOMRectList + implements Iterable, ArrayLike +{ + // This property should've been read-only as well, but Flow doesn't handle + // read-only indexers correctly (thinks reads are writes and fails). + [index: number]: DOMRectReadOnly; + +length: number; + item(index: number): DOMRectReadOnly | null; + @@iterator(): Iterator; +} + +declare export function createDOMRectList( + domRects: $ReadOnlyArray, +): DOMRectList; diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js b/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js new file mode 100644 index 00000000000000..18cbd0af9e52b9 --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js @@ -0,0 +1,82 @@ +/** + * 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. + * + * @format + * @flow strict + */ + +// flowlint unsafe-getters-setters:off + +import type {ArrayLike} from './ArrayLikeUtils'; + +import {createValueIterator} from './ArrayLikeUtils'; + +// IMPORTANT: The type definition for this module is defined in `HTMLCollection.js.flow` +// because Flow only supports indexers in classes in declaration files. + +// $FlowIssue[prop-missing] Flow doesn't understand [Symbol.iterator]() {} and thinks this class doesn't implement the Iterable interface. +export default class HTMLCollection implements Iterable, ArrayLike { + _length: number; + + /** + * Use `createHTMLCollection` to create instances of this class. + * + * @private This is not defined in the declaration file, so users will not see + * the signature of the constructor. + */ + constructor(elements: $ReadOnlyArray) { + for (let i = 0; i < elements.length; i++) { + Object.defineProperty(this, i, { + value: elements[i], + enumerable: true, + configurable: false, + writable: false, + }); + } + + this._length = elements.length; + } + + get length(): number { + return this._length; + } + + item(index: number): T | null { + if (index < 0 || index >= this._length) { + return null; + } + + // assigning to the interface allows us to access the indexer property in a + // type-safe way. + // eslint-disable-next-line consistent-this + const arrayLike: ArrayLike = this; + return arrayLike[index]; + } + + /** + * @deprecated Unused in React Native. + */ + namedItem(name: string): T | null { + return null; + } + + // $FlowIssue[unsupported-syntax] Flow does not support computed properties in classes. + [Symbol.iterator](): Iterator { + return createValueIterator(this); + } +} + +/** + * This is an internal method to create instances of `HTMLCollection`, + * which avoids leaking its constructor to end users. + * We can do that because the external definition of `HTMLCollection` lives in + * `HTMLCollection.js.flow`, not here. + */ +export function createHTMLCollection( + elements: $ReadOnlyArray, +): HTMLCollection { + return new HTMLCollection(elements); +} diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js.flow b/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js.flow new file mode 100644 index 00000000000000..8f14d1d098c7ec --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js.flow @@ -0,0 +1,27 @@ +/** + * 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. + * + * @format + * @flow strict + */ + +import type {ArrayLike} from './ArrayLikeUtils'; + +declare export default class HTMLCollection<+T> + implements Iterable, ArrayLike +{ + // This property should've been read-only as well, but Flow doesn't handle + // read-only indexers correctly (thinks reads are writes and fails). + [index: number]: T; + +length: number; + item(index: number): T | null; + namedItem(name: string): T | null; + @@iterator(): Iterator; +} + +declare export function createHTMLCollection( + elements: $ReadOnlyArray, +): HTMLCollection; diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/NodeList.js b/packages/react-native/Libraries/DOM/OldStyleCollections/NodeList.js new file mode 100644 index 00000000000000..592919e4680611 --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/NodeList.js @@ -0,0 +1,104 @@ +/** + * 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. + * + * @format + * @flow strict + */ + +// flowlint unsafe-getters-setters:off + +import type {ArrayLike} from './ArrayLikeUtils'; + +import { + createEntriesIterator, + createKeyIterator, + createValueIterator, +} from './ArrayLikeUtils'; + +// IMPORTANT: The Flow type definition for this module is defined in `NodeList.js.flow` +// because Flow only supports indexers in classes in declaration files. + +// $FlowIssue[prop-missing] Flow doesn't understand [Symbol.iterator]() {} and thinks this class doesn't implement the Iterable interface. +export default class NodeList implements Iterable, ArrayLike { + _length: number; + + /** + * Use `createNodeList` to create instances of this class. + * + * @private This is not defined in the declaration file, so users will not see + * the signature of the constructor. + */ + constructor(elements: $ReadOnlyArray) { + for (let i = 0; i < elements.length; i++) { + Object.defineProperty(this, i, { + value: elements[i], + writable: false, + }); + } + this._length = elements.length; + } + + get length(): number { + return this._length; + } + + item(index: number): T | null { + if (index < 0 || index >= this._length) { + return null; + } + + // assigning to the interface allows us to access the indexer property in a + // type-safe way. + // eslint-disable-next-line consistent-this + const arrayLike: ArrayLike = this; + return arrayLike[index]; + } + + entries(): Iterator<[number, T]> { + return createEntriesIterator(this); + } + + forEach( + callbackFn: (value: T, index: number, array: NodeList) => mixed, + thisArg?: ThisType, + ): void { + // assigning to the interface allows us to access the indexer property in a + // type-safe way. + // eslint-disable-next-line consistent-this + const arrayLike: ArrayLike = this; + + for (let index = 0; index < this._length; index++) { + if (thisArg == null) { + callbackFn(arrayLike[index], index, this); + } else { + callbackFn.call(thisArg, arrayLike[index], index, this); + } + } + } + + keys(): Iterator { + return createKeyIterator(this); + } + + values(): Iterator { + return createValueIterator(this); + } + + // $FlowIssue[unsupported-syntax] Flow does not support computed properties in classes. + [Symbol.iterator](): Iterator { + return createValueIterator(this); + } +} + +/** + * This is an internal method to create instances of `NodeList`, + * which avoids leaking its constructor to end users. + * We can do that because the external definition of `NodeList` lives in + * `NodeList.js.flow`, not here. + */ +export function createNodeList(elements: $ReadOnlyArray): NodeList { + return new NodeList(elements); +} diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/NodeList.js.flow b/packages/react-native/Libraries/DOM/OldStyleCollections/NodeList.js.flow new file mode 100644 index 00000000000000..89e02eef540f91 --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/NodeList.js.flow @@ -0,0 +1,31 @@ +/** + * 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. + * + * @format + * @flow strict + */ + +import type {ArrayLike} from './ArrayLikeUtils'; + +declare export default class NodeList<+T> implements Iterable, ArrayLike { + // This property should've been read-only as well, but Flow doesn't handle + // read-only indexers correctly (thinks reads are writes and fails). + [index: number]: T; + +length: number; + item(index: number): T | null; + entries(): Iterator<[number, T]>; + forEach( + callbackFn: (value: T, index: number, array: NodeList) => mixed, + thisArg?: ThisType, + ): void; + keys(): Iterator; + values(): Iterator; + @@iterator(): Iterator; +} + +declare export function createNodeList( + elements: $ReadOnlyArray, +): NodeList; diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/DOMRectList-test.js b/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/DOMRectList-test.js new file mode 100644 index 00000000000000..c91abc26947367 --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/DOMRectList-test.js @@ -0,0 +1,85 @@ +/** + * 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 strict-local + * @format + * @oncall react_native + */ + +import DOMRectReadOnly from '../../Geometry/DOMRectReadOnly'; +import {createDOMRectList} from '../DOMRectList'; + +const domRectA = new DOMRectReadOnly(); +const domRectB = new DOMRectReadOnly(); +const domRectC = new DOMRectReadOnly(); + +describe('DOMRectList', () => { + it('provides an array-like interface', () => { + const collection = createDOMRectList([domRectA, domRectB, domRectC]); + + expect(collection[0]).toBe(domRectA); + expect(collection[1]).toBe(domRectB); + expect(collection[2]).toBe(domRectC); + expect(collection[3]).toBe(undefined); + expect(collection.length).toBe(3); + }); + + it('is immutable (loose mode)', () => { + const collection = createDOMRectList([domRectA, domRectB, domRectC]); + + collection[0] = new DOMRectReadOnly(); + expect(collection[0]).toBe(domRectA); + + // $FlowExpectedError[cannot-write] + collection.length = 100; + expect(collection.length).toBe(3); + }); + + it('is immutable (strict mode)', () => { + 'use strict'; + + const collection = createDOMRectList([domRectA, domRectB, domRectC]); + + expect(() => { + collection[0] = new DOMRectReadOnly(); + }).toThrow(TypeError); + expect(collection[0]).toBe(domRectA); + + expect(() => { + // $FlowExpectedError[cannot-write] + collection.length = 100; + }).toThrow(TypeError); + expect(collection.length).toBe(3); + }); + + it('can be converted to an array through common methods', () => { + const collection = createDOMRectList([domRectA, domRectB, domRectC]); + + expect(Array.from(collection)).toEqual([domRectA, domRectB, domRectC]); + expect([...collection]).toEqual([domRectA, domRectB, domRectC]); + }); + + it('can be traversed with for-of', () => { + const collection = createDOMRectList([domRectA, domRectB, domRectC]); + + let i = 0; + for (const value of collection) { + expect(value).toBe(collection[i]); + i++; + } + }); + + describe('item()', () => { + it('returns elements at the specified position, or null', () => { + const collection = createDOMRectList([domRectA, domRectB, domRectC]); + + expect(collection.item(0)).toBe(domRectA); + expect(collection.item(1)).toBe(domRectB); + expect(collection.item(2)).toBe(domRectC); + expect(collection.item(3)).toBe(null); + }); + }); +}); diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/HTMLCollection-test.js b/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/HTMLCollection-test.js new file mode 100644 index 00000000000000..8000e1551bfd6d --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/HTMLCollection-test.js @@ -0,0 +1,80 @@ +/** + * 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 strict-local + * @format + * @oncall react_native + */ + +import {createHTMLCollection} from '../HTMLCollection'; + +describe('HTMLCollection', () => { + it('provides an array-like interface', () => { + const collection = createHTMLCollection(['a', 'b', 'c']); + + expect(collection[0]).toBe('a'); + expect(collection[1]).toBe('b'); + expect(collection[2]).toBe('c'); + expect(collection[3]).toBe(undefined); + expect(collection.length).toBe(3); + }); + + it('is immutable (loose mode)', () => { + const collection = createHTMLCollection(['a', 'b', 'c']); + + collection[0] = 'replacement'; + expect(collection[0]).toBe('a'); + + // $FlowExpectedError[cannot-write] + collection.length = 100; + expect(collection.length).toBe(3); + }); + + it('is immutable (strict mode)', () => { + 'use strict'; + + const collection = createHTMLCollection(['a', 'b', 'c']); + + expect(() => { + collection[0] = 'replacement'; + }).toThrow(TypeError); + expect(collection[0]).toBe('a'); + + expect(() => { + // $FlowExpectedError[cannot-write] + collection.length = 100; + }).toThrow(TypeError); + expect(collection.length).toBe(3); + }); + + it('can be converted to an array through common methods', () => { + const collection = createHTMLCollection(['a', 'b', 'c']); + + expect(Array.from(collection)).toEqual(['a', 'b', 'c']); + expect([...collection]).toEqual(['a', 'b', 'c']); + }); + + it('can be traversed with for-of', () => { + const collection = createHTMLCollection(['a', 'b', 'c']); + + let i = 0; + for (const value of collection) { + expect(value).toBe(collection[i]); + i++; + } + }); + + describe('item()', () => { + it('returns elements at the specified position, or null', () => { + const collection = createHTMLCollection(['a', 'b', 'c']); + + expect(collection.item(0)).toBe('a'); + expect(collection.item(1)).toBe('b'); + expect(collection.item(2)).toBe('c'); + expect(collection.item(3)).toBe(null); + }); + }); +}); diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/NodeList-test.js b/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/NodeList-test.js new file mode 100644 index 00000000000000..8f42e30bb55ab4 --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/NodeList-test.js @@ -0,0 +1,161 @@ +/** + * 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 strict-local + * @format + * @oncall react_native + */ + +import {createNodeList} from '../NodeList'; + +describe('NodeList', () => { + it('provides an array-like interface', () => { + const collection = createNodeList(['a', 'b', 'c']); + + expect(collection[0]).toBe('a'); + expect(collection[1]).toBe('b'); + expect(collection[2]).toBe('c'); + expect(collection[3]).toBe(undefined); + expect(collection.length).toBe(3); + }); + + it('provides indexed access through the item method', () => { + const collection = createNodeList(['a', 'b', 'c']); + + expect(collection.item(0)).toBe('a'); + expect(collection.item(1)).toBe('b'); + expect(collection.item(2)).toBe('c'); + expect(collection.item(3)).toBe(null); + }); + + it('is immutable (loose mode)', () => { + const collection = createNodeList(['a', 'b', 'c']); + + collection[0] = 'replacement'; + expect(collection[0]).toBe('a'); + + // $FlowExpectedError[cannot-write] + collection.length = 100; + expect(collection.length).toBe(3); + }); + + it('is immutable (strict mode)', () => { + 'use strict'; + + const collection = createNodeList(['a', 'b', 'c']); + + expect(() => { + collection[0] = 'replacement'; + }).toThrow(TypeError); + expect(collection[0]).toBe('a'); + + expect(() => { + // $FlowExpectedError[cannot-write] + collection.length = 100; + }).toThrow(TypeError); + expect(collection.length).toBe(3); + }); + + it('can be converted to an array through common methods', () => { + const collection = createNodeList(['a', 'b', 'c']); + + expect(Array.from(collection)).toEqual(['a', 'b', 'c']); + expect([...collection]).toEqual(['a', 'b', 'c']); + }); + + it('can be traversed with for-of', () => { + const collection = createNodeList(['a', 'b', 'c']); + + let i = 0; + for (const value of collection) { + expect(value).toBe(collection[i]); + i++; + } + }); + + describe('keys()', () => { + it('returns an iterator for keys', () => { + const collection = createNodeList(['a', 'b', 'c']); + + const keys = collection.keys(); + expect(keys.next()).toEqual({value: 0, done: false}); + expect(keys.next()).toEqual({value: 1, done: false}); + expect(keys.next()).toEqual({value: 2, done: false}); + expect(keys.next()).toEqual({done: true}); + + let i = 0; + for (const key of collection.keys()) { + expect(key).toBe(i); + i++; + } + }); + }); + + describe('values()', () => { + it('returns an iterator for values', () => { + const collection = createNodeList(['a', 'b', 'c']); + + const values = collection.values(); + expect(values.next()).toEqual({value: 'a', done: false}); + expect(values.next()).toEqual({value: 'b', done: false}); + expect(values.next()).toEqual({value: 'c', done: false}); + expect(values.next()).toEqual({done: true}); + + let i = 0; + for (const value of collection.values()) { + expect(value).toBe(collection[i]); + i++; + } + }); + }); + + describe('entries()', () => { + it('returns an iterator for entries', () => { + const collection = createNodeList(['a', 'b', 'c']); + + const entries = collection.entries(); + expect(entries.next()).toEqual({value: [0, 'a'], done: false}); + expect(entries.next()).toEqual({value: [1, 'b'], done: false}); + expect(entries.next()).toEqual({value: [2, 'c'], done: false}); + expect(entries.next()).toEqual({done: true}); + + let i = 0; + for (const entry of collection.entries()) { + expect(entry).toEqual([i, collection[i]]); + i++; + } + }); + }); + + describe('forEach()', () => { + it('iterates over the elements like array.forEach (implicit `this`)', () => { + const collection = createNodeList(['a', 'b', 'c']); + + let i = 0; + collection.forEach(function (this: mixed, value, index, list) { + expect(value).toBe(collection[i]); + expect(index).toBe(i); + expect(list).toBe(collection); + expect(this).toBe(window); + i++; + }); + }); + + it('iterates over the elements like array.forEach (explicit `this`)', () => { + const collection = createNodeList(['a', 'b', 'c']); + + let i = 0; + const explicitThis = {id: 'foo'}; + collection.forEach(function (this: mixed, value, index, list) { + expect(value).toBe(collection[i]); + expect(index).toBe(i); + expect(list).toBe(collection); + expect(this).toBe(explicitThis); + i++; + }, explicitThis); + }); + }); +}); diff --git a/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.android.js b/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.android.js new file mode 100644 index 00000000000000..f5399c48e19574 --- /dev/null +++ b/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.android.js @@ -0,0 +1,35 @@ +/** + * 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 strict-local + * @format + */ + +import DevSettings from '../Utilities/DevSettings'; +import NativeDevToolsSettingsManager from './NativeDevToolsSettingsManager'; + +module.exports = { + setConsolePatchSettings(newSettings: string) { + NativeDevToolsSettingsManager?.setConsolePatchSettings(newSettings); + }, + getConsolePatchSettings(): ?string { + return NativeDevToolsSettingsManager?.getConsolePatchSettings(); + }, + setProfilingSettings(newSettings: string) { + if (NativeDevToolsSettingsManager?.setProfilingSettings != null) { + NativeDevToolsSettingsManager.setProfilingSettings(newSettings); + } + }, + getProfilingSettings(): ?string { + if (NativeDevToolsSettingsManager?.getProfilingSettings != null) { + return NativeDevToolsSettingsManager.getProfilingSettings(); + } + return null; + }, + reload(): void { + DevSettings?.reload(); + }, +}; diff --git a/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.d.ts b/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.d.ts new file mode 100644 index 00000000000000..cbb59711fa5b2b --- /dev/null +++ b/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.d.ts @@ -0,0 +1,20 @@ +/** + * 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 strict + * @format + */ + +export interface DevToolsSettingsManagerStatic { + reload(): void; + setConsolePatchSettings(newSettings: string): void; + getConsolePatchSettings(): string | null; + setProfilingSettings(newSettings: string): void; + getProfilingSettings(): string | null; +} + +export const DevToolsSettingsManager: DevToolsSettingsManagerStatic; +export type DevToolsSettingsManager = DevToolsSettingsManagerStatic; diff --git a/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.ios.js b/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.ios.js new file mode 100644 index 00000000000000..534152bff758d2 --- /dev/null +++ b/packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.ios.js @@ -0,0 +1,49 @@ +/** + * 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 strict-local + * @format + */ + +import Settings from '../Settings/Settings'; +import DevSettings from '../Utilities/DevSettings'; + +const CONSOLE_PATCH_SETTINGS_KEY = 'ReactDevTools::ConsolePatchSettings'; +const PROFILING_SETTINGS_KEY = 'ReactDevTools::ProfilingSettings'; + +const DevToolsSettingsManager = { + setConsolePatchSettings(newConsolePatchSettings: string): void { + Settings.set({ + [CONSOLE_PATCH_SETTINGS_KEY]: newConsolePatchSettings, + }); + }, + getConsolePatchSettings(): ?string { + const value = Settings.get(CONSOLE_PATCH_SETTINGS_KEY); + if (typeof value === 'string') { + return value; + } + return null; + }, + + setProfilingSettings(newProfilingSettings: string): void { + Settings.set({ + [PROFILING_SETTINGS_KEY]: newProfilingSettings, + }); + }, + getProfilingSettings(): ?string { + const value = Settings.get(PROFILING_SETTINGS_KEY); + if (typeof value === 'string') { + return value; + } + return null; + }, + + reload(): void { + DevSettings?.reload(); + }, +}; + +module.exports = DevToolsSettingsManager; diff --git a/packages/react-native/Libraries/DevToolsSettings/NativeDevToolsSettingsManager.js b/packages/react-native/Libraries/DevToolsSettings/NativeDevToolsSettingsManager.js new file mode 100644 index 00000000000000..8370f8f19740e6 --- /dev/null +++ b/packages/react-native/Libraries/DevToolsSettings/NativeDevToolsSettingsManager.js @@ -0,0 +1,24 @@ +/** + * 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 strict + * @format + */ + +import type {TurboModule} from '../TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +setConsolePatchSettings: (newConsolePatchSettings: string) => void; + +getConsolePatchSettings: () => ?string; + +setProfilingSettings?: (newProfilingSettings: string) => void; + +getProfilingSettings?: () => ?string; +} + +export default (TurboModuleRegistry.get( + 'DevToolsSettingsManager', +): ?Spec); diff --git a/Libraries/EventEmitter/NativeEventEmitter.d.ts b/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.d.ts similarity index 98% rename from Libraries/EventEmitter/NativeEventEmitter.d.ts rename to packages/react-native/Libraries/EventEmitter/NativeEventEmitter.d.ts index c9f30d0cceb41e..911d0a7ca8a7f8 100644 --- a/Libraries/EventEmitter/NativeEventEmitter.d.ts +++ b/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.d.ts @@ -7,9 +7,8 @@ * @format */ -import { +import EventEmitter, { EmitterSubscription, - EventEmitter, } from '../vendor/emitter/EventEmitter'; /** diff --git a/Libraries/EventEmitter/NativeEventEmitter.js b/packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js similarity index 100% rename from Libraries/EventEmitter/NativeEventEmitter.js rename to packages/react-native/Libraries/EventEmitter/NativeEventEmitter.js diff --git a/Libraries/EventEmitter/RCTDeviceEventEmitter.d.ts b/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.d.ts similarity index 96% rename from Libraries/EventEmitter/RCTDeviceEventEmitter.d.ts rename to packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.d.ts index 1e92935b9b5b12..7c5563bac35a85 100644 --- a/Libraries/EventEmitter/RCTDeviceEventEmitter.d.ts +++ b/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.d.ts @@ -7,9 +7,8 @@ * @format */ -import { +import EventEmitter, { EmitterSubscription, - EventEmitter, EventSubscriptionVendor, } from '../vendor/emitter/EventEmitter'; diff --git a/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js b/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js new file mode 100644 index 00000000000000..256983291888be --- /dev/null +++ b/packages/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.js @@ -0,0 +1,32 @@ +/** + * 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 strict + * @format + */ + +import type {IEventEmitter} from '../vendor/emitter/EventEmitter'; + +import EventEmitter from '../vendor/emitter/EventEmitter'; + +// FIXME: use typed events +type RCTDeviceEventDefinitions = $FlowFixMe; + +/** + * Global EventEmitter used by the native platform to emit events to JavaScript. + * Events are identified by globally unique event names. + * + * NativeModules that emit events should instead subclass `NativeEventEmitter`. + */ +const RCTDeviceEventEmitter: IEventEmitter = + new EventEmitter(); + +Object.defineProperty(global, '__rctDeviceEventEmitter', { + configurable: true, + value: RCTDeviceEventEmitter, +}); + +export default (RCTDeviceEventEmitter: IEventEmitter); diff --git a/Libraries/EventEmitter/RCTEventEmitter.js b/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js similarity index 100% rename from Libraries/EventEmitter/RCTEventEmitter.js rename to packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js diff --git a/Libraries/EventEmitter/RCTNativeAppEventEmitter.d.ts b/packages/react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter.d.ts similarity index 100% rename from Libraries/EventEmitter/RCTNativeAppEventEmitter.d.ts rename to packages/react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter.d.ts diff --git a/Libraries/EventEmitter/RCTNativeAppEventEmitter.js b/packages/react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter.js similarity index 100% rename from Libraries/EventEmitter/RCTNativeAppEventEmitter.js rename to packages/react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter.js diff --git a/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js b/packages/react-native/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js similarity index 100% rename from Libraries/EventEmitter/__mocks__/NativeEventEmitter.js rename to packages/react-native/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js diff --git a/Libraries/Events/CustomEvent.js b/packages/react-native/Libraries/Events/CustomEvent.js similarity index 100% rename from Libraries/Events/CustomEvent.js rename to packages/react-native/Libraries/Events/CustomEvent.js diff --git a/Libraries/Events/EventPolyfill.js b/packages/react-native/Libraries/Events/EventPolyfill.js similarity index 99% rename from Libraries/Events/EventPolyfill.js rename to packages/react-native/Libraries/Events/EventPolyfill.js index e6e0e7b12805b8..dda91a3f4e5739 100644 --- a/Libraries/Events/EventPolyfill.js +++ b/packages/react-native/Libraries/Events/EventPolyfill.js @@ -159,7 +159,7 @@ class EventPolyfill implements IEvent { // data with the other in sync. _syntheticEvent: mixed; - constructor(type: string, eventInitDict?: Event$Init): void { + constructor(type: string, eventInitDict?: Event$Init) { this.type = type; this.bubbles = !!(eventInitDict?.bubbles || false); this.cancelable = !!(eventInitDict?.cancelable || false); diff --git a/Libraries/FBLazyVector/FBLazyVector.podspec b/packages/react-native/Libraries/FBLazyVector/FBLazyVector.podspec similarity index 88% rename from Libraries/FBLazyVector/FBLazyVector.podspec rename to packages/react-native/Libraries/FBLazyVector/FBLazyVector.podspec index 41a0f756f0fdb0..a67c495bfb207b 100644 --- a/Libraries/FBLazyVector/FBLazyVector.podspec +++ b/packages/react-native/Libraries/FBLazyVector/FBLazyVector.podspec @@ -22,8 +22,8 @@ Pod::Spec.new do |s| s.summary = "-" # TODO s.homepage = "https://reactnative.dev/" s.license = package["license"] - s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "12.4" } + s.author = "Meta Platforms, Inc. and its affiliates" + s.platforms = { :ios => min_ios_version_supported } s.source = source s.source_files = "**/*.{c,h,m,mm,cpp}" s.header_dir = "FBLazyVector" diff --git a/Libraries/FBLazyVector/FBLazyVector/FBLazyIterator.h b/packages/react-native/Libraries/FBLazyVector/FBLazyVector/FBLazyIterator.h similarity index 100% rename from Libraries/FBLazyVector/FBLazyVector/FBLazyIterator.h rename to packages/react-native/Libraries/FBLazyVector/FBLazyVector/FBLazyIterator.h diff --git a/Libraries/FBLazyVector/FBLazyVector/FBLazyVector.h b/packages/react-native/Libraries/FBLazyVector/FBLazyVector/FBLazyVector.h similarity index 100% rename from Libraries/FBLazyVector/FBLazyVector/FBLazyVector.h rename to packages/react-native/Libraries/FBLazyVector/FBLazyVector/FBLazyVector.h diff --git a/Libraries/HeapCapture/HeapCapture.js b/packages/react-native/Libraries/HeapCapture/HeapCapture.js similarity index 100% rename from Libraries/HeapCapture/HeapCapture.js rename to packages/react-native/Libraries/HeapCapture/HeapCapture.js diff --git a/Libraries/HeapCapture/NativeJSCHeapCapture.js b/packages/react-native/Libraries/HeapCapture/NativeJSCHeapCapture.js similarity index 100% rename from Libraries/HeapCapture/NativeJSCHeapCapture.js rename to packages/react-native/Libraries/HeapCapture/NativeJSCHeapCapture.js diff --git a/Libraries/Image/AssetRegistry.js b/packages/react-native/Libraries/Image/AssetRegistry.js similarity index 77% rename from Libraries/Image/AssetRegistry.js rename to packages/react-native/Libraries/Image/AssetRegistry.js index 78613fbe5be08f..098d663ccbd395 100644 --- a/Libraries/Image/AssetRegistry.js +++ b/packages/react-native/Libraries/Image/AssetRegistry.js @@ -10,4 +10,4 @@ 'use strict'; -module.exports = require('@react-native/assets/registry'); +module.exports = require('@react-native/assets-registry/registry'); diff --git a/Libraries/Image/AssetSourceResolver.js b/packages/react-native/Libraries/Image/AssetSourceResolver.js similarity index 95% rename from Libraries/Image/AssetSourceResolver.js rename to packages/react-native/Libraries/Image/AssetSourceResolver.js index 5e7f2c2b738ef4..d7ae700437b732 100644 --- a/Libraries/Image/AssetSourceResolver.js +++ b/packages/react-native/Libraries/Image/AssetSourceResolver.js @@ -18,16 +18,16 @@ export type ResolvedAssetSource = {| +scale: number, |}; -import type {PackagerAsset} from '@react-native/assets/registry'; +import type {PackagerAsset} from '@react-native/assets-registry/registry'; -const PixelRatio = require('../Utilities/PixelRatio'); +const PixelRatio = require('../Utilities/PixelRatio').default; const Platform = require('../Utilities/Platform'); const {pickScale} = require('./AssetUtils'); const { getAndroidResourceFolderName, getAndroidResourceIdentifier, getBasePath, -} = require('@react-native/assets/path-support'); +} = require('@react-native/assets-registry/path-support'); const invariant = require('invariant'); /** diff --git a/Libraries/Image/AssetUtils.js b/packages/react-native/Libraries/Image/AssetUtils.js similarity index 100% rename from Libraries/Image/AssetUtils.js rename to packages/react-native/Libraries/Image/AssetUtils.js diff --git a/Libraries/Image/Image.android.js b/packages/react-native/Libraries/Image/Image.android.js similarity index 98% rename from Libraries/Image/Image.android.js rename to packages/react-native/Libraries/Image/Image.android.js index f874c6abc93f02..18a1c3fa9e848d 100644 --- a/Libraries/Image/Image.android.js +++ b/packages/react-native/Libraries/Image/Image.android.js @@ -152,19 +152,22 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => { let style; let sources; if (Array.isArray(source)) { + // $FlowFixMe[underconstrained-implicit-instantiation] style = flattenStyle([styles.base, props.style]); sources = source; } else { + // $FlowFixMe[incompatible-type] const {width = props.width, height = props.height, uri} = source; + // $FlowFixMe[underconstrained-implicit-instantiation] style = flattenStyle([{width, height}, styles.base, props.style]); sources = [source]; - if (uri === '') { console.warn('source.uri should not be an empty string'); } } const {height, width, ...restProps} = props; + const {onLoadStart, onLoad, onLoadEnd, onError} = props; const nativeProps = { ...restProps, diff --git a/Libraries/Image/Image.d.ts b/packages/react-native/Libraries/Image/Image.d.ts similarity index 86% rename from Libraries/Image/Image.d.ts rename to packages/react-native/Libraries/Image/Image.d.ts index dd2c089e75a4be..20b3808533f850 100644 --- a/Libraries/Image/Image.d.ts +++ b/packages/react-native/Libraries/Image/Image.d.ts @@ -12,7 +12,7 @@ import {Constructor} from '../../types/private/Utilities'; import {AccessibilityProps} from '../Components/View/ViewAccessibility'; import {Insets} from '../../types/public/Insets'; import {NativeMethods} from '../../types/public/ReactNativeTypes'; -import {StyleProp} from '../StyleSheet/StyleSheet'; +import {ColorValue, StyleProp} from '../StyleSheet/StyleSheet'; import {ImageStyle, ViewStyle} from '../StyleSheet/StyleSheetTypes'; import {LayoutChangeEvent, NativeSyntheticEvent} from '../Types/CoreEventTypes'; import {ImageResizeMode} from './ImageResizeMode'; @@ -225,6 +225,21 @@ export interface ImagePropsBase */ source: ImageSourcePropType; + /** + * A string representing the resource identifier for the image. Similar to + * src from HTML. + * + * See https://reactnative.dev/docs/image#src + */ + src?: string | undefined; + + /** + * Similar to srcset from HTML. + * + * See https://reactnative.dev/docs/image#srcset + */ + srcSet?: string | undefined; + /** * similarly to `source`, this property represents the resource used to render * the loading indicator for the image, displayed until image is ready to be @@ -254,6 +269,52 @@ export interface ImagePropsBase * See https://reactnative.dev/docs/image#alt */ alt?: string | undefined; + + /** + * Height of the image component. + * + * See https://reactnative.dev/docs/image#height + */ + height?: number | undefined; + + /** + * Width of the image component. + * + * See https://reactnative.dev/docs/image#width + */ + width?: number | undefined; + + /** + * Adds the CORS related header to the request. + * Similar to crossorigin from HTML. + * + * See https://reactnative.dev/docs/image#crossorigin + */ + crossOrigin?: 'anonymous' | 'use-credentials' | undefined; + + /** + * Changes the color of all the non-transparent pixels to the tintColor. + * + * See https://reactnative.dev/docs/image#tintcolor + */ + tintColor?: ColorValue | undefined; + + /** + * A string indicating which referrer to use when fetching the resource. + * Similar to referrerpolicy from HTML. + * + * See https://reactnative.dev/docs/image#referrerpolicy + */ + referrerPolicy?: + | 'no-referrer' + | 'no-referrer-when-downgrade' + | 'origin' + | 'origin-when-cross-origin' + | 'same-origin' + | 'strict-origin' + | 'strict-origin-when-cross-origin' + | 'unsafe-url' + | undefined; } export interface ImageProps extends ImagePropsBase { @@ -298,7 +359,7 @@ export class Image extends ImageBase { } export interface ImageBackgroundProps extends ImagePropsBase { - children?: React.ReactNode; + children?: React.ReactNode | undefined; imageStyle?: StyleProp | undefined; style?: StyleProp | undefined; imageRef?(image: Image): void; diff --git a/Libraries/Image/Image.flow.js b/packages/react-native/Libraries/Image/Image.flow.js similarity index 100% rename from Libraries/Image/Image.flow.js rename to packages/react-native/Libraries/Image/Image.flow.js diff --git a/Libraries/Image/Image.ios.js b/packages/react-native/Libraries/Image/Image.ios.js similarity index 98% rename from Libraries/Image/Image.ios.js rename to packages/react-native/Libraries/Image/Image.ios.js index 6f1d72fcb5a8c3..ab1f443c633773 100644 --- a/Libraries/Image/Image.ios.js +++ b/packages/react-native/Libraries/Image/Image.ios.js @@ -114,10 +114,13 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => { let sources; let style: ImageStyleProp; if (Array.isArray(source)) { + // $FlowFixMe[underconstrained-implicit-instantiation] style = flattenStyle([styles.base, props.style]) || {}; sources = source; } else { + // $FlowFixMe[incompatible-type] const {width = props.width, height = props.height, uri} = source; + // $FlowFixMe[underconstrained-implicit-instantiation] style = flattenStyle([{width, height}, styles.base, props.style]) || {}; sources = [source]; diff --git a/Libraries/Image/ImageAnalyticsTagContext.js b/packages/react-native/Libraries/Image/ImageAnalyticsTagContext.js similarity index 100% rename from Libraries/Image/ImageAnalyticsTagContext.js rename to packages/react-native/Libraries/Image/ImageAnalyticsTagContext.js diff --git a/Libraries/Image/ImageBackground.js b/packages/react-native/Libraries/Image/ImageBackground.js similarity index 98% rename from Libraries/Image/ImageBackground.js rename to packages/react-native/Libraries/Image/ImageBackground.js index 7beb733e7032ab..7a51378ce4491a 100644 --- a/Libraries/Image/ImageBackground.js +++ b/packages/react-native/Libraries/Image/ImageBackground.js @@ -76,6 +76,7 @@ class ImageBackground extends React.Component { ...props } = this.props; + // $FlowFixMe[underconstrained-implicit-instantiation] const flattenedStyle = flattenStyle(style); return ( RCT_EXTERN void RCTSetImageCacheLimits( - NSUInteger maxCachableDecodedImageSizeInBytes, + NSUInteger maxCacheableDecodedImageSizeInBytes, NSUInteger imageCacheTotalCostLimit); @end diff --git a/Libraries/Image/RCTImageCache.m b/packages/react-native/Libraries/Image/RCTImageCache.m similarity index 94% rename from Libraries/Image/RCTImageCache.m rename to packages/react-native/Libraries/Image/RCTImageCache.m index 5a061516cf967f..5fa3e14ea5a817 100644 --- a/Libraries/Image/RCTImageCache.m +++ b/packages/react-native/Libraries/Image/RCTImageCache.m @@ -18,12 +18,12 @@ #import -static NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 2 * 1024 * 1024; +static NSUInteger RCTMaxCacheableDecodedImageSizeInBytes = 2 * 1024 * 1024; static NSUInteger RCTImageCacheTotalCostLimit = 20 * 1024 * 1024; -void RCTSetImageCacheLimits(NSUInteger maxCachableDecodedImageSizeInBytes, NSUInteger imageCacheTotalCostLimit) +void RCTSetImageCacheLimits(NSUInteger maxCacheableDecodedImageSizeInBytes, NSUInteger imageCacheTotalCostLimit) { - RCTMaxCachableDecodedImageSizeInBytes = maxCachableDecodedImageSizeInBytes; + RCTMaxCacheableDecodedImageSizeInBytes = maxCacheableDecodedImageSizeInBytes; RCTImageCacheTotalCostLimit = imageCacheTotalCostLimit; } @@ -73,7 +73,7 @@ - (void)addImageToCache:(UIImage *)image forKey:(NSString *)cacheKey return; } NSInteger bytes = image.reactDecodedImageBytes; - if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) { + if (bytes <= RCTMaxCacheableDecodedImageSizeInBytes) { [self->_decodedImageCache setObject:image forKey:cacheKey cost:bytes]; } } diff --git a/Libraries/Image/RCTImageDataDecoder.h b/packages/react-native/Libraries/Image/RCTImageDataDecoder.h similarity index 100% rename from Libraries/Image/RCTImageDataDecoder.h rename to packages/react-native/Libraries/Image/RCTImageDataDecoder.h diff --git a/Libraries/Image/RCTImageEditingManager.h b/packages/react-native/Libraries/Image/RCTImageEditingManager.h similarity index 100% rename from Libraries/Image/RCTImageEditingManager.h rename to packages/react-native/Libraries/Image/RCTImageEditingManager.h diff --git a/Libraries/Image/RCTImageEditingManager.mm b/packages/react-native/Libraries/Image/RCTImageEditingManager.mm similarity index 100% rename from Libraries/Image/RCTImageEditingManager.mm rename to packages/react-native/Libraries/Image/RCTImageEditingManager.mm diff --git a/Libraries/Image/RCTImageLoader.h b/packages/react-native/Libraries/Image/RCTImageLoader.h similarity index 100% rename from Libraries/Image/RCTImageLoader.h rename to packages/react-native/Libraries/Image/RCTImageLoader.h diff --git a/Libraries/Image/RCTImageLoader.mm b/packages/react-native/Libraries/Image/RCTImageLoader.mm similarity index 100% rename from Libraries/Image/RCTImageLoader.mm rename to packages/react-native/Libraries/Image/RCTImageLoader.mm diff --git a/Libraries/Image/RCTImageLoaderLoggable.h b/packages/react-native/Libraries/Image/RCTImageLoaderLoggable.h similarity index 100% rename from Libraries/Image/RCTImageLoaderLoggable.h rename to packages/react-native/Libraries/Image/RCTImageLoaderLoggable.h diff --git a/Libraries/Image/RCTImageLoaderProtocol.h b/packages/react-native/Libraries/Image/RCTImageLoaderProtocol.h similarity index 100% rename from Libraries/Image/RCTImageLoaderProtocol.h rename to packages/react-native/Libraries/Image/RCTImageLoaderProtocol.h diff --git a/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h b/packages/react-native/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h similarity index 100% rename from Libraries/Image/RCTImageLoaderWithAttributionProtocol.h rename to packages/react-native/Libraries/Image/RCTImageLoaderWithAttributionProtocol.h diff --git a/Libraries/Image/RCTImagePlugins.h b/packages/react-native/Libraries/Image/RCTImagePlugins.h similarity index 100% rename from Libraries/Image/RCTImagePlugins.h rename to packages/react-native/Libraries/Image/RCTImagePlugins.h diff --git a/Libraries/Image/RCTImagePlugins.mm b/packages/react-native/Libraries/Image/RCTImagePlugins.mm similarity index 100% rename from Libraries/Image/RCTImagePlugins.mm rename to packages/react-native/Libraries/Image/RCTImagePlugins.mm diff --git a/Libraries/Image/RCTImageShadowView.h b/packages/react-native/Libraries/Image/RCTImageShadowView.h similarity index 100% rename from Libraries/Image/RCTImageShadowView.h rename to packages/react-native/Libraries/Image/RCTImageShadowView.h diff --git a/Libraries/Image/RCTImageShadowView.m b/packages/react-native/Libraries/Image/RCTImageShadowView.m similarity index 100% rename from Libraries/Image/RCTImageShadowView.m rename to packages/react-native/Libraries/Image/RCTImageShadowView.m diff --git a/Libraries/Image/RCTImageStoreManager.h b/packages/react-native/Libraries/Image/RCTImageStoreManager.h similarity index 100% rename from Libraries/Image/RCTImageStoreManager.h rename to packages/react-native/Libraries/Image/RCTImageStoreManager.h diff --git a/Libraries/Image/RCTImageStoreManager.mm b/packages/react-native/Libraries/Image/RCTImageStoreManager.mm similarity index 100% rename from Libraries/Image/RCTImageStoreManager.mm rename to packages/react-native/Libraries/Image/RCTImageStoreManager.mm diff --git a/Libraries/Image/RCTImageURLLoader.h b/packages/react-native/Libraries/Image/RCTImageURLLoader.h similarity index 100% rename from Libraries/Image/RCTImageURLLoader.h rename to packages/react-native/Libraries/Image/RCTImageURLLoader.h diff --git a/Libraries/Image/RCTImageURLLoaderWithAttribution.h b/packages/react-native/Libraries/Image/RCTImageURLLoaderWithAttribution.h similarity index 97% rename from Libraries/Image/RCTImageURLLoaderWithAttribution.h rename to packages/react-native/Libraries/Image/RCTImageURLLoaderWithAttribution.h index c512736e106501..87b59f0843c6cf 100644 --- a/Libraries/Image/RCTImageURLLoaderWithAttribution.h +++ b/packages/react-native/Libraries/Image/RCTImageURLLoaderWithAttribution.h @@ -11,8 +11,7 @@ // TODO (T61325135): Remove C++ checks #ifdef __cplusplus -namespace facebook { -namespace react { +namespace facebook::react { struct ImageURLLoaderAttribution { int32_t nativeViewTag = 0; @@ -21,8 +20,7 @@ struct ImageURLLoaderAttribution { NSString *analyticTag; }; -} // namespace react -} // namespace facebook +} // namespace facebook::react #endif @interface RCTImageURLLoaderRequest : NSObject diff --git a/Libraries/Image/RCTImageURLLoaderWithAttribution.mm b/packages/react-native/Libraries/Image/RCTImageURLLoaderWithAttribution.mm similarity index 100% rename from Libraries/Image/RCTImageURLLoaderWithAttribution.mm rename to packages/react-native/Libraries/Image/RCTImageURLLoaderWithAttribution.mm diff --git a/Libraries/Image/RCTImageUtils.h b/packages/react-native/Libraries/Image/RCTImageUtils.h similarity index 100% rename from Libraries/Image/RCTImageUtils.h rename to packages/react-native/Libraries/Image/RCTImageUtils.h diff --git a/Libraries/Image/RCTImageUtils.m b/packages/react-native/Libraries/Image/RCTImageUtils.m similarity index 100% rename from Libraries/Image/RCTImageUtils.m rename to packages/react-native/Libraries/Image/RCTImageUtils.m diff --git a/Libraries/Image/RCTImageView.h b/packages/react-native/Libraries/Image/RCTImageView.h similarity index 100% rename from Libraries/Image/RCTImageView.h rename to packages/react-native/Libraries/Image/RCTImageView.h diff --git a/Libraries/Image/RCTImageView.mm b/packages/react-native/Libraries/Image/RCTImageView.mm similarity index 97% rename from Libraries/Image/RCTImageView.mm rename to packages/react-native/Libraries/Image/RCTImageView.mm index 050a44769bd62d..3b3878fccb6495 100644 --- a/Libraries/Image/RCTImageView.mm +++ b/packages/react-native/Libraries/Image/RCTImageView.mm @@ -99,15 +99,11 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge selector:@selector(clearImageIfDetached) name:UIApplicationDidEnterBackgroundNotification object:nil]; -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 - if (@available(iOS 13.0, *)) { - [center addObserver:self - selector:@selector(clearImageIfDetached) + [center addObserver:self + selector:@selector(clearImageIfDetached) - name:UISceneDidEnterBackgroundNotification - object:nil]; - } -#endif + name:UISceneDidEnterBackgroundNotification + object:nil]; } return self; } @@ -137,7 +133,7 @@ - (void)updateWithImage:(UIImage *)image image = [image resizableImageWithCapInsets:_capInsets resizingMode:UIImageResizingModeStretch]; } - // Apply trilinear filtering to smooth out mis-sized images + // Apply trilinear filtering to smooth out missized images _imageView.layer.minificationFilter = kCAFilterTrilinear; _imageView.layer.magnificationFilter = kCAFilterTrilinear; diff --git a/Libraries/Image/RCTImageViewManager.h b/packages/react-native/Libraries/Image/RCTImageViewManager.h similarity index 100% rename from Libraries/Image/RCTImageViewManager.h rename to packages/react-native/Libraries/Image/RCTImageViewManager.h diff --git a/Libraries/Image/RCTImageViewManager.mm b/packages/react-native/Libraries/Image/RCTImageViewManager.mm similarity index 100% rename from Libraries/Image/RCTImageViewManager.mm rename to packages/react-native/Libraries/Image/RCTImageViewManager.mm diff --git a/Libraries/Image/RCTLocalAssetImageLoader.h b/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.h similarity index 100% rename from Libraries/Image/RCTLocalAssetImageLoader.h rename to packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.h diff --git a/Libraries/Image/RCTLocalAssetImageLoader.mm b/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.mm similarity index 100% rename from Libraries/Image/RCTLocalAssetImageLoader.mm rename to packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.mm diff --git a/Libraries/Image/RCTResizeMode.h b/packages/react-native/Libraries/Image/RCTResizeMode.h similarity index 100% rename from Libraries/Image/RCTResizeMode.h rename to packages/react-native/Libraries/Image/RCTResizeMode.h diff --git a/Libraries/Image/RCTResizeMode.m b/packages/react-native/Libraries/Image/RCTResizeMode.m similarity index 100% rename from Libraries/Image/RCTResizeMode.m rename to packages/react-native/Libraries/Image/RCTResizeMode.m diff --git a/Libraries/Image/RCTUIImageViewAnimated.h b/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.h similarity index 100% rename from Libraries/Image/RCTUIImageViewAnimated.h rename to packages/react-native/Libraries/Image/RCTUIImageViewAnimated.h diff --git a/Libraries/Image/RCTUIImageViewAnimated.m b/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.m similarity index 99% rename from Libraries/Image/RCTUIImageViewAnimated.m rename to packages/react-native/Libraries/Image/RCTUIImageViewAnimated.m index f532ce51a63d18..aa0abfe6489417 100644 --- a/Libraries/Image/RCTUIImageViewAnimated.m +++ b/packages/react-native/Libraries/Image/RCTUIImageViewAnimated.m @@ -11,12 +11,12 @@ #import #import -static NSUInteger RCTDeviceTotalMemory() +static NSUInteger RCTDeviceTotalMemory(void) { return (NSUInteger)[[NSProcessInfo processInfo] physicalMemory]; } -static NSUInteger RCTDeviceFreeMemory() +static NSUInteger RCTDeviceFreeMemory(void) { mach_port_t host_port = mach_host_self(); mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t); diff --git a/packages/react-native/Libraries/Image/React-RCTImage.podspec b/packages/react-native/Libraries/Image/React-RCTImage.podspec new file mode 100644 index 00000000000000..f6a91baaf99c27 --- /dev/null +++ b/packages/react-native/Libraries/Image/React-RCTImage.podspec @@ -0,0 +1,62 @@ +# 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. + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json"))) +version = package['version'] + +source = { :git => 'https://github.com/facebook/react-native.git' } +if version == '1000.0.0' + # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") +else + source[:tag] = "v#{version}" +end + +folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' +folly_version = '2021.07.22.00' + +header_search_paths = [ + "\"$(PODS_ROOT)/RCT-Folly\"", + "\"${PODS_ROOT}/Headers/Public/React-Codegen/react/renderer/components\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-Codegen/React_Codegen.framework/Headers\"" +] + +if ENV["USE_FRAMEWORKS"] + header_search_paths = header_search_paths.concat([ + "\"$(PODS_CONFIGURATION_BUILD_DIR)/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core\"", + "\"$(PODS_CONFIGURATION_BUILD_DIR)/React-NativeModulesApple/React_NativeModulesApple.framework/Headers\"" + ]) +end + +Pod::Spec.new do |s| + s.name = "React-RCTImage" + s.version = version + s.summary = "A React component for displaying different types of images." + s.homepage = "https://reactnative.dev/" + s.documentation_url = "https://reactnative.dev/docs/image" + s.license = package["license"] + s.author = "Meta Platforms, Inc. and its affiliates" + s.platforms = { :ios => min_ios_version_supported } + s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' + s.source = source + s.source_files = "*.{m,mm}" + s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" + s.header_dir = "RCTImage" + s.pod_target_xcconfig = { + "USE_HEADERMAP" => "YES", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", + "HEADER_SEARCH_PATHS" => header_search_paths.join(' ') + } + + s.dependency "RCT-Folly", folly_version + s.dependency "React-Codegen", version + s.dependency "RCTTypeSafety", version + s.dependency "ReactCommon/turbomodule/core", version + s.dependency "React-jsi", version + s.dependency "React-Core/RCTImageHeaders", version + s.dependency "React-RCTNetwork", version +end diff --git a/Libraries/Image/RelativeImageStub.js b/packages/react-native/Libraries/Image/RelativeImageStub.js similarity index 89% rename from Libraries/Image/RelativeImageStub.js rename to packages/react-native/Libraries/Image/RelativeImageStub.js index c8cf0f4da4418c..fbb125a89b1e70 100644 --- a/Libraries/Image/RelativeImageStub.js +++ b/packages/react-native/Libraries/Image/RelativeImageStub.js @@ -13,7 +13,7 @@ // This is a stub for flow to make it understand require('./icon.png') // See metro/src/Bundler/index.js -const AssetRegistry = require('@react-native/assets/registry'); +const AssetRegistry = require('@react-native/assets-registry/registry'); module.exports = (AssetRegistry.registerAsset({ __packager_asset: true, diff --git a/Libraries/Image/TextInlineImageNativeComponent.js b/packages/react-native/Libraries/Image/TextInlineImageNativeComponent.js similarity index 95% rename from Libraries/Image/TextInlineImageNativeComponent.js rename to packages/react-native/Libraries/Image/TextInlineImageNativeComponent.js index 1ce6818b113c36..ff0bdb28b9a229 100644 --- a/Libraries/Image/TextInlineImageNativeComponent.js +++ b/packages/react-native/Libraries/Image/TextInlineImageNativeComponent.js @@ -36,7 +36,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { resizeMode: true, src: true, tintColor: { - process: require('../StyleSheet/processColor'), + process: require('../StyleSheet/processColor').default, }, headers: true, }, diff --git a/Libraries/Image/__tests__/AssetUtils-test.js b/packages/react-native/Libraries/Image/__tests__/AssetUtils-test.js similarity index 100% rename from Libraries/Image/__tests__/AssetUtils-test.js rename to packages/react-native/Libraries/Image/__tests__/AssetUtils-test.js diff --git a/Libraries/Image/__tests__/Image-test.js b/packages/react-native/Libraries/Image/__tests__/Image-test.js similarity index 100% rename from Libraries/Image/__tests__/Image-test.js rename to packages/react-native/Libraries/Image/__tests__/Image-test.js diff --git a/Libraries/Image/__tests__/ImageBackground-test.js b/packages/react-native/Libraries/Image/__tests__/ImageBackground-test.js similarity index 100% rename from Libraries/Image/__tests__/ImageBackground-test.js rename to packages/react-native/Libraries/Image/__tests__/ImageBackground-test.js diff --git a/Libraries/Image/__tests__/ImageSourceUtils-test.js b/packages/react-native/Libraries/Image/__tests__/ImageSourceUtils-test.js similarity index 98% rename from Libraries/Image/__tests__/ImageSourceUtils-test.js rename to packages/react-native/Libraries/Image/__tests__/ImageSourceUtils-test.js index dc3f599bf72d95..a1bc0500184cc8 100644 --- a/Libraries/Image/__tests__/ImageSourceUtils-test.js +++ b/packages/react-native/Libraries/Image/__tests__/ImageSourceUtils-test.js @@ -94,7 +94,7 @@ describe('ImageSourceUtils', () => { }); it('should warn when an unsupported scale is provided in srcSet', () => { - const mockWarn = jest.spyOn(console, 'warn'); + const mockWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}); let uri1 = 'uri1'; let scale1 = '300w'; diff --git a/Libraries/Image/__tests__/__snapshots__/Image-test.js.snap b/packages/react-native/Libraries/Image/__tests__/__snapshots__/Image-test.js.snap similarity index 100% rename from Libraries/Image/__tests__/__snapshots__/Image-test.js.snap rename to packages/react-native/Libraries/Image/__tests__/__snapshots__/Image-test.js.snap diff --git a/Libraries/Image/__tests__/__snapshots__/ImageBackground-test.js.snap b/packages/react-native/Libraries/Image/__tests__/__snapshots__/ImageBackground-test.js.snap similarity index 100% rename from Libraries/Image/__tests__/__snapshots__/ImageBackground-test.js.snap rename to packages/react-native/Libraries/Image/__tests__/__snapshots__/ImageBackground-test.js.snap diff --git a/Libraries/Image/__tests__/__snapshots__/assetRelativePathInSnapshot-test.js.snap b/packages/react-native/Libraries/Image/__tests__/__snapshots__/assetRelativePathInSnapshot-test.js.snap similarity index 100% rename from Libraries/Image/__tests__/__snapshots__/assetRelativePathInSnapshot-test.js.snap rename to packages/react-native/Libraries/Image/__tests__/__snapshots__/assetRelativePathInSnapshot-test.js.snap diff --git a/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js b/packages/react-native/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js similarity index 100% rename from Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js rename to packages/react-native/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js diff --git a/Libraries/Image/__tests__/img/img1.png b/packages/react-native/Libraries/Image/__tests__/img/img1.png similarity index 100% rename from Libraries/Image/__tests__/img/img1.png rename to packages/react-native/Libraries/Image/__tests__/img/img1.png diff --git a/Libraries/Image/__tests__/img/img2.png b/packages/react-native/Libraries/Image/__tests__/img/img2.png similarity index 100% rename from Libraries/Image/__tests__/img/img2.png rename to packages/react-native/Libraries/Image/__tests__/img/img2.png diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js similarity index 99% rename from Libraries/Image/__tests__/resolveAssetSource-test.js rename to packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js index 475e623741991f..d05f0b7aba3c97 100644 --- a/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -19,7 +19,7 @@ describe('resolveAssetSource', () => { beforeEach(() => { jest.resetModules(); - AssetRegistry = require('@react-native/assets/registry'); + AssetRegistry = require('@react-native/assets-registry/registry'); resolveAssetSource = require('../resolveAssetSource'); NativeSourceCode = require('../../NativeModules/specs/NativeSourceCode').default; diff --git a/Libraries/Image/nativeImageSource.js b/packages/react-native/Libraries/Image/nativeImageSource.js similarity index 100% rename from Libraries/Image/nativeImageSource.js rename to packages/react-native/Libraries/Image/nativeImageSource.js diff --git a/Libraries/Image/resolveAssetSource.js b/packages/react-native/Libraries/Image/resolveAssetSource.js similarity index 97% rename from Libraries/Image/resolveAssetSource.js rename to packages/react-native/Libraries/Image/resolveAssetSource.js index 8848a0f18c10e4..2ff2bcfd0194da 100644 --- a/Libraries/Image/resolveAssetSource.js +++ b/packages/react-native/Libraries/Image/resolveAssetSource.js @@ -16,7 +16,7 @@ import type {ResolvedAssetSource} from './AssetSourceResolver'; const AssetSourceResolver = require('./AssetSourceResolver'); const {pickScale} = require('./AssetUtils'); -const AssetRegistry = require('@react-native/assets/registry'); +const AssetRegistry = require('@react-native/assets-registry/registry'); let _customSourceTransformer, _serverURL, _scriptURL; diff --git a/Libraries/Inspector/BorderBox.js b/packages/react-native/Libraries/Inspector/BorderBox.js similarity index 100% rename from Libraries/Inspector/BorderBox.js rename to packages/react-native/Libraries/Inspector/BorderBox.js diff --git a/Libraries/Inspector/BoxInspector.js b/packages/react-native/Libraries/Inspector/BoxInspector.js similarity index 100% rename from Libraries/Inspector/BoxInspector.js rename to packages/react-native/Libraries/Inspector/BoxInspector.js diff --git a/Libraries/Inspector/DevtoolsOverlay.js b/packages/react-native/Libraries/Inspector/DevtoolsOverlay.js similarity index 82% rename from Libraries/Inspector/DevtoolsOverlay.js rename to packages/react-native/Libraries/Inspector/DevtoolsOverlay.js index a4711672f6c5f6..f1861f2e0b8cf4 100644 --- a/Libraries/Inspector/DevtoolsOverlay.js +++ b/packages/react-native/Libraries/Inspector/DevtoolsOverlay.js @@ -8,6 +8,7 @@ * @flow */ +import type {PointerEvent} from '../Types/CoreEventTypes'; import type {PressEvent} from '../Types/CoreEventTypes'; import type {HostRef} from './getInspectorDataForViewAtPoint'; @@ -50,8 +51,15 @@ export default function DevtoolsOverlay({ function onAgentShowNativeHighlight(node: any) { clearTimeout(hideTimeoutId); - // Shape of `node` is different in Fabric. - const component = node.canonical ?? node; + + // `canonical.publicInstance` => Fabric + // `canonical` => Legacy Fabric + // `node` => Legacy renderer + const component = + (node.canonical && node.canonical.publicInstance) ?? + // TODO: remove this check when syncing the new version of the renderer from React to React Native. + node.canonical ?? + node; if (!component || !component.measure) { return; } @@ -125,11 +133,11 @@ export default function DevtoolsOverlay({ getInspectorDataForViewAtPoint(inspectedView, x, y, viewData => { const {touchedViewTag, closestInstance, frame} = viewData; if (closestInstance != null || touchedViewTag != null) { + // We call `selectNode` for both non-fabric(viewTag) and fabric(instance), + // this makes sure it works for both architectures. + agent.selectNode(findNodeHandle(touchedViewTag)); if (closestInstance != null) { - // Fabric agent.selectNode(closestInstance); - } else { - agent.selectNode(findNodeHandle(touchedViewTag)); } setInspected({ frame, @@ -153,14 +161,14 @@ export default function DevtoolsOverlay({ }, []); const onPointerMove = useCallback( - e => { + (e: PointerEvent) => { findViewForLocation(e.nativeEvent.x, e.nativeEvent.y); }, [findViewForLocation], ); const onResponderMove = useCallback( - e => { + (e: PressEvent) => { findViewForLocation( e.nativeEvent.touches[0].locationX, e.nativeEvent.touches[0].locationY, @@ -169,7 +177,7 @@ export default function DevtoolsOverlay({ [findViewForLocation], ); - const shouldSetResponser = useCallback( + const shouldSetResponder = useCallback( (e: PressEvent): boolean => { onResponderMove(e); return true; @@ -179,17 +187,19 @@ export default function DevtoolsOverlay({ let highlight = inspected ? : null; if (isInspecting) { - const events = ReactNativeFeatureFlags.shouldEmitW3CPointerEvents - ? { - onPointerMove, - onPointerDown: onPointerMove, - onPointerUp: stopInspecting, - } - : { - onStartShouldSetResponder: shouldSetResponser, - onResponderMove: onResponderMove, - onResponderRelease: stopInspecting, - }; + const events = + // Pointer events only work on fabric + ReactNativeFeatureFlags.shouldEmitW3CPointerEvents() + ? { + onPointerMove, + onPointerDown: onPointerMove, + onPointerUp: stopInspecting, + } + : { + onStartShouldSetResponder: shouldSetResponder, + onResponderMove: onResponderMove, + onResponderRelease: stopInspecting, + }; return ( { render(): React.Node { + // $FlowFixMe[underconstrained-implicit-instantiation] const style = flattenStyle(this.props.style) || {}; let margin = resolveBoxStyle('margin', style); let padding = resolveBoxStyle('padding', style); diff --git a/Libraries/Inspector/ElementProperties.js b/packages/react-native/Libraries/Inspector/ElementProperties.js similarity index 100% rename from Libraries/Inspector/ElementProperties.js rename to packages/react-native/Libraries/Inspector/ElementProperties.js diff --git a/Libraries/Inspector/Inspector.js b/packages/react-native/Libraries/Inspector/Inspector.js similarity index 96% rename from Libraries/Inspector/Inspector.js rename to packages/react-native/Libraries/Inspector/Inspector.js index d660528e938a26..bc76e794870dbb 100644 --- a/Libraries/Inspector/Inspector.js +++ b/packages/react-native/Libraries/Inspector/Inspector.js @@ -18,7 +18,7 @@ const View = require('../Components/View/View'); const PressabilityDebug = require('../Pressability/PressabilityDebug'); const {findNodeHandle} = require('../ReactNative/RendererProxy'); const StyleSheet = require('../StyleSheet/StyleSheet'); -const Dimensions = require('../Utilities/Dimensions'); +const Dimensions = require('../Utilities/Dimensions').default; const Platform = require('../Utilities/Platform'); const getInspectorDataForViewAtPoint = require('./getInspectorDataForViewAtPoint'); const InspectorOverlay = require('./InspectorOverlay'); @@ -142,12 +142,11 @@ class Inspector extends React.Component< // Sync the touched view with React DevTools. // Note: This is Paper only. To support Fabric, // DevTools needs to be updated to not rely on view tags. - if (this.state.devtoolsAgent) { + const agent = this.state.devtoolsAgent; + if (agent) { + agent.selectNode(findNodeHandle(touchedViewTag)); if (closestInstance != null) { - // Fabric - this.state.devtoolsAgent.selectNode(closestInstance); - } else if (touchedViewTag != null) { - this.state.devtoolsAgent.selectNode(findNodeHandle(touchedViewTag)); + agent.selectNode(closestInstance); } } diff --git a/Libraries/Inspector/InspectorOverlay.js b/packages/react-native/Libraries/Inspector/InspectorOverlay.js similarity index 90% rename from Libraries/Inspector/InspectorOverlay.js rename to packages/react-native/Libraries/Inspector/InspectorOverlay.js index f6bb0b7b098e40..c8973baea394bf 100644 --- a/Libraries/Inspector/InspectorOverlay.js +++ b/packages/react-native/Libraries/Inspector/InspectorOverlay.js @@ -15,7 +15,7 @@ import type {PressEvent} from '../Types/CoreEventTypes'; const View = require('../Components/View/View'); const StyleSheet = require('../StyleSheet/StyleSheet'); -const Dimensions = require('../Utilities/Dimensions'); +const Dimensions = require('../Utilities/Dimensions').default; const ElementBox = require('./ElementBox'); const React = require('react'); @@ -36,7 +36,7 @@ class InspectorOverlay extends React.Component { this.props.onTouchPoint(locationX, locationY); }; - shouldSetResponser: (e: PressEvent) => boolean = (e: PressEvent): boolean => { + shouldSetResponder: (e: PressEvent) => boolean = (e: PressEvent): boolean => { this.findViewForTouchEvent(e); return true; }; @@ -54,7 +54,7 @@ class InspectorOverlay extends React.Component { return ( diff --git a/Libraries/Inspector/InspectorPanel.js b/packages/react-native/Libraries/Inspector/InspectorPanel.js similarity index 100% rename from Libraries/Inspector/InspectorPanel.js rename to packages/react-native/Libraries/Inspector/InspectorPanel.js diff --git a/Libraries/Inspector/NetworkOverlay.js b/packages/react-native/Libraries/Inspector/NetworkOverlay.js similarity index 98% rename from Libraries/Inspector/NetworkOverlay.js rename to packages/react-native/Libraries/Inspector/NetworkOverlay.js index 169318cea27f28..1e4d63f27fd9d2 100644 --- a/Libraries/Inspector/NetworkOverlay.js +++ b/packages/react-native/Libraries/Inspector/NetworkOverlay.js @@ -10,7 +10,7 @@ 'use strict'; -import type {RenderItemProps} from '../Lists/VirtualizedList'; +import type {RenderItemProps} from '@react-native/virtualized-lists'; const ScrollView = require('../Components/ScrollView/ScrollView'); const TouchableHighlight = require('../Components/Touchable/TouchableHighlight'); @@ -64,7 +64,7 @@ function getStringByValue(value: any): string { } if (typeof value === 'string' && value.length > 500) { return String(value) - .substr(0, 500) + .slice(0, 500) .concat('\n***TRUNCATED TO 500 CHARACTERS***'); } return value; @@ -88,7 +88,7 @@ function keyExtractor(request: NetworkRequestInfo): string { * Show all the intercepted network requests over the InspectorPanel. */ class NetworkOverlay extends React.Component { - _requestsListView: ?React.ElementRef; + _requestsListView: ?React.ElementRef>>; _detailScrollView: ?React.ElementRef; // Metrics are used to decide when if the request list should be sticky, and diff --git a/Libraries/Inspector/PerformanceOverlay.js b/packages/react-native/Libraries/Inspector/PerformanceOverlay.js similarity index 100% rename from Libraries/Inspector/PerformanceOverlay.js rename to packages/react-native/Libraries/Inspector/PerformanceOverlay.js diff --git a/Libraries/Inspector/StyleInspector.js b/packages/react-native/Libraries/Inspector/StyleInspector.js similarity index 100% rename from Libraries/Inspector/StyleInspector.js rename to packages/react-native/Libraries/Inspector/StyleInspector.js diff --git a/Libraries/Inspector/getInspectorDataForViewAtPoint.js b/packages/react-native/Libraries/Inspector/getInspectorDataForViewAtPoint.js similarity index 100% rename from Libraries/Inspector/getInspectorDataForViewAtPoint.js rename to packages/react-native/Libraries/Inspector/getInspectorDataForViewAtPoint.js diff --git a/Libraries/Inspector/resolveBoxStyle.js b/packages/react-native/Libraries/Inspector/resolveBoxStyle.js similarity index 100% rename from Libraries/Inspector/resolveBoxStyle.js rename to packages/react-native/Libraries/Inspector/resolveBoxStyle.js diff --git a/Libraries/Interaction/FrameRateLogger.js b/packages/react-native/Libraries/Interaction/FrameRateLogger.js similarity index 100% rename from Libraries/Interaction/FrameRateLogger.js rename to packages/react-native/Libraries/Interaction/FrameRateLogger.js diff --git a/Libraries/Interaction/InteractionManager.d.ts b/packages/react-native/Libraries/Interaction/InteractionManager.d.ts similarity index 100% rename from Libraries/Interaction/InteractionManager.d.ts rename to packages/react-native/Libraries/Interaction/InteractionManager.d.ts diff --git a/Libraries/Interaction/InteractionManager.js b/packages/react-native/Libraries/Interaction/InteractionManager.js similarity index 100% rename from Libraries/Interaction/InteractionManager.js rename to packages/react-native/Libraries/Interaction/InteractionManager.js diff --git a/Libraries/Interaction/JSEventLoopWatchdog.js b/packages/react-native/Libraries/Interaction/JSEventLoopWatchdog.js similarity index 96% rename from Libraries/Interaction/JSEventLoopWatchdog.js rename to packages/react-native/Libraries/Interaction/JSEventLoopWatchdog.js index 3eae297b74184b..113d073ee7000c 100644 --- a/Libraries/Interaction/JSEventLoopWatchdog.js +++ b/packages/react-native/Libraries/Interaction/JSEventLoopWatchdog.js @@ -27,7 +27,7 @@ type Handler = { * other events from being processed in a timely manner. * * The "stall" time is defined as the amount of time in access of the acceptable - * threshold, which is typically around 100-200ms. So if the treshold is set to + * threshold, which is typically around 100-200ms. So if the threshold is set to * 100 and a timer fires 150 ms later than it was scheduled because the event * loop was tied up, that would be considered a 50ms stall. * diff --git a/Libraries/Interaction/NativeFrameRateLogger.js b/packages/react-native/Libraries/Interaction/NativeFrameRateLogger.js similarity index 100% rename from Libraries/Interaction/NativeFrameRateLogger.js rename to packages/react-native/Libraries/Interaction/NativeFrameRateLogger.js diff --git a/Libraries/Interaction/PanResponder.d.ts b/packages/react-native/Libraries/Interaction/PanResponder.d.ts similarity index 100% rename from Libraries/Interaction/PanResponder.d.ts rename to packages/react-native/Libraries/Interaction/PanResponder.d.ts diff --git a/Libraries/Interaction/PanResponder.js b/packages/react-native/Libraries/Interaction/PanResponder.js similarity index 99% rename from Libraries/Interaction/PanResponder.js rename to packages/react-native/Libraries/Interaction/PanResponder.js index 02658c1cf0c793..e64011e7873a6b 100644 --- a/Libraries/Interaction/PanResponder.js +++ b/packages/react-native/Libraries/Interaction/PanResponder.js @@ -11,7 +11,6 @@ 'use strict'; import type {PressEvent} from '../Types/CoreEventTypes'; -import type {PanResponderType} from './PanResponder.flow.js'; const InteractionManager = require('./InteractionManager'); const TouchHistoryMath = require('./TouchHistoryMath'); @@ -191,7 +190,7 @@ type ActiveCallback = ( type PassiveCallback = (event: PressEvent, gestureState: GestureState) => mixed; -type PanHandlers = {| +export type PanHandlers = {| onMoveShouldSetResponder: (event: PressEvent) => boolean, onMoveShouldSetResponderCapture: (event: PressEvent) => boolean, onResponderEnd: (event: PressEvent) => void, @@ -227,7 +226,7 @@ type PanResponderConfig = $ReadOnly<{| onShouldBlockNativeResponder?: ?ActiveCallback, |}>; -const PanResponder: PanResponderType = { +const PanResponder = { /** * * A graphical explanation of the touch data flow: @@ -399,10 +398,10 @@ const PanResponder: PanResponderType = { * accordingly. (numberActiveTouches) may not be totally accurate unless you * are the responder. */ - create(config: PanResponderConfig): $TEMPORARY$object<{| + create(config: PanResponderConfig): { getInteractionHandle: () => ?number, panHandlers: PanHandlers, - |}> { + } { const interactionState = { handle: (null: ?number), }; @@ -580,4 +579,4 @@ export type PanResponderInstance = $Call< PanResponderConfig, >; -module.exports = PanResponder; +export default PanResponder; diff --git a/Libraries/Interaction/TaskQueue.js b/packages/react-native/Libraries/Interaction/TaskQueue.js similarity index 100% rename from Libraries/Interaction/TaskQueue.js rename to packages/react-native/Libraries/Interaction/TaskQueue.js diff --git a/Libraries/Interaction/TouchHistoryMath.js b/packages/react-native/Libraries/Interaction/TouchHistoryMath.js similarity index 100% rename from Libraries/Interaction/TouchHistoryMath.js rename to packages/react-native/Libraries/Interaction/TouchHistoryMath.js diff --git a/Libraries/Interaction/__tests__/InteractionManager-test.js b/packages/react-native/Libraries/Interaction/__tests__/InteractionManager-test.js similarity index 94% rename from Libraries/Interaction/__tests__/InteractionManager-test.js rename to packages/react-native/Libraries/Interaction/__tests__/InteractionManager-test.js index b0757356d8c1f9..c21f4c18b61d97 100644 --- a/Libraries/Interaction/__tests__/InteractionManager-test.js +++ b/packages/react-native/Libraries/Interaction/__tests__/InteractionManager-test.js @@ -10,16 +10,15 @@ 'use strict'; -jest - .mock('../../vendor/core/ErrorUtils') - .mock('../../BatchedBridge/BatchedBridge'); +const BatchedBridge = require('../../BatchedBridge/BatchedBridge'); + +jest.mock('../../vendor/core/ErrorUtils'); +jest.mock('../../BatchedBridge/BatchedBridge'); const isWindows = process.platform === 'win32'; +const itif = condition => (condition ? it : it.skip); + function expectToBeCalledOnce(fn) { - // todo fix this test case on windows - if (isWindows) { - return; - } expect(fn.mock.calls.length).toBe(1); } @@ -159,7 +158,6 @@ describe('InteractionManager', () => { describe('promise tasks', () => { let InteractionManager; - let BatchedBridge; let sequenceId; function createSequenceTask(expectedSequenceId) { return jest.fn(() => { @@ -168,12 +166,15 @@ describe('promise tasks', () => { } beforeEach(() => { jest.resetModules(); - jest.useFakeTimers('legacy'); + jest.useFakeTimers({legacyFakeTimers: true}); InteractionManager = require('../InteractionManager'); - BatchedBridge = require('../../BatchedBridge/BatchedBridge'); sequenceId = 0; }); + afterEach(() => { + jest.useRealTimers(); + }); + it('should run a basic promise task', () => { const task1 = jest.fn(() => { expect(++sequenceId).toBe(1); @@ -304,11 +305,14 @@ describe('promise tasks', () => { }, 100); }; - it('resolves async tasks recursively before other queued tasks', () => { - return new Promise(bigAsyncTest); - }); + itif(!isWindows)( + 'resolves async tasks recursively before other queued tasks', + () => { + return new Promise(bigAsyncTest); + }, + ); - it('should also work with a deadline', () => { + itif(!isWindows)('should also work with a deadline', () => { InteractionManager.setDeadline(100); BatchedBridge.getEventLoopRunningTime.mockReturnValue(200); return new Promise(bigAsyncTest); diff --git a/Libraries/Interaction/__tests__/TaskQueue-test.js b/packages/react-native/Libraries/Interaction/__tests__/TaskQueue-test.js similarity index 100% rename from Libraries/Interaction/__tests__/TaskQueue-test.js rename to packages/react-native/Libraries/Interaction/__tests__/TaskQueue-test.js diff --git a/Libraries/JSInspector/InspectorAgent.js b/packages/react-native/Libraries/JSInspector/InspectorAgent.js similarity index 100% rename from Libraries/JSInspector/InspectorAgent.js rename to packages/react-native/Libraries/JSInspector/InspectorAgent.js diff --git a/Libraries/JSInspector/JSInspector.js b/packages/react-native/Libraries/JSInspector/JSInspector.js similarity index 100% rename from Libraries/JSInspector/JSInspector.js rename to packages/react-native/Libraries/JSInspector/JSInspector.js diff --git a/Libraries/JSInspector/NetworkAgent.js b/packages/react-native/Libraries/JSInspector/NetworkAgent.js similarity index 100% rename from Libraries/JSInspector/NetworkAgent.js rename to packages/react-native/Libraries/JSInspector/NetworkAgent.js diff --git a/Libraries/LayoutAnimation/LayoutAnimation.d.ts b/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.d.ts similarity index 100% rename from Libraries/LayoutAnimation/LayoutAnimation.d.ts rename to packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.d.ts diff --git a/Libraries/LayoutAnimation/LayoutAnimation.js b/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js similarity index 98% rename from Libraries/LayoutAnimation/LayoutAnimation.js rename to packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js index a8c20e3960c776..e4489d0543bf60 100644 --- a/Libraries/LayoutAnimation/LayoutAnimation.js +++ b/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js @@ -17,6 +17,7 @@ import type { LayoutAnimationType, } from '../Renderer/shims/ReactNativeTypes'; +import {getFabricUIManager} from '../ReactNative/FabricUIManager'; import ReactNativeFeatureFlags from '../ReactNative/ReactNativeFeatureFlags'; import Platform from '../Utilities/Platform'; @@ -77,7 +78,7 @@ function configureNext( // In Fabric, LayoutAnimations are unconditionally enabled for Android, and // conditionally enabled on iOS (pending fully shipping; this is a temporary state). - const FabricUIManager: FabricUIManagerSpec = global?.nativeFabricUIManager; + const FabricUIManager = getFabricUIManager(); if (FabricUIManager?.configureNextLayoutAnimation) { global?.nativeFabricUIManager?.configureNextLayoutAnimation( config, diff --git a/Libraries/Linking/Linking.d.ts b/packages/react-native/Libraries/Linking/Linking.d.ts similarity index 100% rename from Libraries/Linking/Linking.d.ts rename to packages/react-native/Libraries/Linking/Linking.d.ts diff --git a/Libraries/Linking/Linking.js b/packages/react-native/Libraries/Linking/Linking.js similarity index 94% rename from Libraries/Linking/Linking.js rename to packages/react-native/Libraries/Linking/Linking.js index a59fa72973a3a2..76d187988ff6e2 100644 --- a/Libraries/Linking/Linking.js +++ b/packages/react-native/Libraries/Linking/Linking.js @@ -11,7 +11,6 @@ import type {EventSubscription} from '../vendor/emitter/EventEmitter'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; -import InteractionManager from '../Interaction/InteractionManager'; import Platform from '../Utilities/Platform'; import NativeIntentAndroid from './NativeIntentAndroid'; import NativeLinkingManager from './NativeLinkingManager'; @@ -96,9 +95,7 @@ class Linking extends NativeEventEmitter { */ getInitialURL(): Promise { return Platform.OS === 'android' - ? InteractionManager.runAfterInteractions().then(() => - nullthrows(NativeIntentAndroid).getInitialURL(), - ) + ? nullthrows(NativeIntentAndroid).getInitialURL() : nullthrows(NativeLinkingManager).getInitialURL(); } diff --git a/Libraries/Linking/NativeIntentAndroid.js b/packages/react-native/Libraries/Linking/NativeIntentAndroid.js similarity index 100% rename from Libraries/Linking/NativeIntentAndroid.js rename to packages/react-native/Libraries/Linking/NativeIntentAndroid.js diff --git a/Libraries/Linking/NativeLinkingManager.js b/packages/react-native/Libraries/Linking/NativeLinkingManager.js similarity index 100% rename from Libraries/Linking/NativeLinkingManager.js rename to packages/react-native/Libraries/Linking/NativeLinkingManager.js diff --git a/Libraries/LinkingIOS/RCTLinkingManager.h b/packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.h similarity index 100% rename from Libraries/LinkingIOS/RCTLinkingManager.h rename to packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.h diff --git a/Libraries/LinkingIOS/RCTLinkingManager.mm b/packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.mm similarity index 100% rename from Libraries/LinkingIOS/RCTLinkingManager.mm rename to packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.mm diff --git a/Libraries/LinkingIOS/RCTLinkingPlugins.h b/packages/react-native/Libraries/LinkingIOS/RCTLinkingPlugins.h similarity index 100% rename from Libraries/LinkingIOS/RCTLinkingPlugins.h rename to packages/react-native/Libraries/LinkingIOS/RCTLinkingPlugins.h diff --git a/Libraries/LinkingIOS/RCTLinkingPlugins.mm b/packages/react-native/Libraries/LinkingIOS/RCTLinkingPlugins.mm similarity index 100% rename from Libraries/LinkingIOS/RCTLinkingPlugins.mm rename to packages/react-native/Libraries/LinkingIOS/RCTLinkingPlugins.mm diff --git a/packages/react-native/Libraries/LinkingIOS/React-RCTLinking.podspec b/packages/react-native/Libraries/LinkingIOS/React-RCTLinking.podspec new file mode 100644 index 00000000000000..5bf0a98802dc70 --- /dev/null +++ b/packages/react-native/Libraries/LinkingIOS/React-RCTLinking.podspec @@ -0,0 +1,59 @@ +# 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. + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json"))) +version = package['version'] + +source = { :git => 'https://github.com/facebook/react-native.git' } +if version == '1000.0.0' + # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") +else + source[:tag] = "v#{version}" +end + +folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' +folly_version = '2021.07.22.00' + +header_search_paths = [ + "\"$(PODS_ROOT)/RCT-Folly\"", + "\"${PODS_ROOT}/Headers/Public/React-Codegen/react/renderer/components\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/React-Codegen/React_Codegen.framework/Headers\"" +] + +if ENV["USE_FRAMEWORKS"] + header_search_paths = header_search_paths.concat([ + "\"$(PODS_CONFIGURATION_BUILD_DIR)/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core\"", + "\"$(PODS_CONFIGURATION_BUILD_DIR)/React-NativeModulesApple/React_NativeModulesApple.framework/Headers\"" + ]) +end + +Pod::Spec.new do |s| + s.name = "React-RCTLinking" + s.version = version + s.summary = "A general interface to interact with both incoming and outgoing app links." + s.homepage = "https://reactnative.dev/" + s.documentation_url = "https://reactnative.dev/docs/linking" + s.license = package["license"] + s.author = "Meta Platforms, Inc. and its affiliates" + s.platforms = { :ios => min_ios_version_supported } + s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' + s.source = source + s.source_files = "*.{m,mm}" + s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" + s.header_dir = "RCTLinking" + s.pod_target_xcconfig = { + "USE_HEADERMAP" => "YES", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", + "HEADER_SEARCH_PATHS" => header_search_paths.join(' ') + } + + s.dependency "React-Codegen", version + s.dependency "React-Core/RCTLinkingHeaders", version + s.dependency "ReactCommon/turbomodule/core", version + s.dependency "React-jsi", version +end diff --git a/packages/react-native/Libraries/Lists/FillRateHelper.js b/packages/react-native/Libraries/Lists/FillRateHelper.js new file mode 100644 index 00000000000000..141fe98eb067bb --- /dev/null +++ b/packages/react-native/Libraries/Lists/FillRateHelper.js @@ -0,0 +1,19 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import {typeof FillRateHelper as FillRateHelperType} from '@react-native/virtualized-lists'; + +const FillRateHelper: FillRateHelperType = + require('@react-native/virtualized-lists').FillRateHelper; + +export type {FillRateInfo} from '@react-native/virtualized-lists'; +module.exports = FillRateHelper; diff --git a/Libraries/Lists/FlatList.d.ts b/packages/react-native/Libraries/Lists/FlatList.d.ts similarity index 77% rename from Libraries/Lists/FlatList.d.ts rename to packages/react-native/Libraries/Lists/FlatList.d.ts index 0cb4302f1ee080..5bbe2fa3b80c7a 100644 --- a/Libraries/Lists/FlatList.d.ts +++ b/packages/react-native/Libraries/Lists/FlatList.d.ts @@ -12,55 +12,14 @@ import type { ListRenderItem, ViewToken, VirtualizedListProps, -} from './VirtualizedList'; + ViewabilityConfig, +} from '@react-native/virtualized-lists'; import type {ScrollViewComponent} from '../Components/ScrollView/ScrollView'; -import {StyleProp} from '../StyleSheet/StyleSheet'; -import {ViewStyle} from '../StyleSheet/StyleSheetTypes'; -import {View} from '../Components/View/View'; +import type {StyleProp} from '../StyleSheet/StyleSheet'; +import type {ViewStyle} from '../StyleSheet/StyleSheetTypes'; +import type {View} from '../Components/View/View'; export interface FlatListProps extends VirtualizedListProps { - /** - * Rendered in between each item, but not at the top or bottom - */ - ItemSeparatorComponent?: React.ComponentType | null | undefined; - - /** - * Rendered when the list is empty. - */ - ListEmptyComponent?: - | React.ComponentType - | React.ReactElement - | null - | undefined; - - /** - * Rendered at the very end of the list. - */ - ListFooterComponent?: - | React.ComponentType - | React.ReactElement - | null - | undefined; - - /** - * Styling for internal View for ListFooterComponent - */ - ListFooterComponentStyle?: StyleProp | undefined; - - /** - * Rendered at the very beginning of the list. - */ - ListHeaderComponent?: - | React.ComponentType - | React.ReactElement - | null - | undefined; - - /** - * Styling for internal View for ListHeaderComponent - */ - ListHeaderComponentStyle?: StyleProp | undefined; - /** * Optional custom style for multi-item rows generated when numColumns > 1 */ @@ -82,17 +41,17 @@ export interface FlatListProps extends VirtualizedListProps { | undefined; /** - * For simplicity, data is just a plain array. If you want to use something else, - * like an immutable list, use the underlying VirtualizedList directly. + * An array (or array-like list) of items to render. Other data types can be + * used by targeting VirtualizedList directly. */ - data: ReadonlyArray | null | undefined; + data: ArrayLike | null | undefined; /** * A marker property for telling the list to re-render (since it implements PureComponent). * If any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the `data` prop, * stick it here and treat it immutably. */ - extraData?: any; + extraData?: any | undefined; /** * `getItemLayout` is an optional optimization that lets us skip measurement of dynamic @@ -108,7 +67,7 @@ export interface FlatListProps extends VirtualizedListProps { */ getItemLayout?: | (( - data: Array | null | undefined, + data: ArrayLike | null | undefined, index: number, ) => {length: number; offset: number; index: number}) | undefined; @@ -146,19 +105,6 @@ export interface FlatListProps extends VirtualizedListProps { */ numColumns?: number | undefined; - /** - * Called once when the scroll position gets within onEndReachedThreshold of the rendered content. - */ - onEndReached?: ((info: {distanceFromEnd: number}) => void) | null | undefined; - - /** - * How far from the end (in units of visible length of the list) the bottom edge of the - * list must be from the end of the content to trigger the `onEndReached` callback. - * Thus a value of 0.5 will trigger `onEndReached` when the end of the content is - * within half the visible length of the list. - */ - onEndReachedThreshold?: number | null | undefined; - /** * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. * Make sure to also set the refreshing prop correctly. @@ -187,7 +133,7 @@ export interface FlatListProps extends VirtualizedListProps { * _renderItem = ({item}) => ( * this._onPress(item)}> * {item.title} - * + * * ); * ... * @@ -199,7 +145,7 @@ export interface FlatListProps extends VirtualizedListProps { /** * See `ViewabilityHelper` for flow type and further documentation. */ - viewabilityConfig?: any; + viewabilityConfig?: ViewabilityConfig | undefined; /** * Note: may have bugs (missing content) in some circumstances - use at your own risk. @@ -221,9 +167,10 @@ export interface FlatListProps extends VirtualizedListProps { fadingEdgeLength?: number | undefined; } -export class FlatList extends React.Component< - FlatListProps -> { +export abstract class FlatListComponent< + ItemT, + Props, +> extends React.Component { /** * Scrolls to the end of the content. May be janky without `getItemLayout` prop. */ @@ -248,6 +195,7 @@ export class FlatList extends React.Component< scrollToItem: (params: { animated?: boolean | null | undefined; item: ItemT; + viewOffset?: number | undefined; viewPosition?: number | undefined; }) => void; @@ -290,3 +238,8 @@ export class FlatList extends React.Component< // TODO: use `unknown` instead of `any` for Typescript >= 3.0 setNativeProps: (props: {[key: string]: any}) => void; } + +export class FlatList extends FlatListComponent< + ItemT, + FlatListProps +> {} diff --git a/Libraries/Lists/FlatList.js b/packages/react-native/Libraries/Lists/FlatList.js similarity index 94% rename from Libraries/Lists/FlatList.js rename to packages/react-native/Libraries/Lists/FlatList.js index 3da7714a670b29..63e2de283821b1 100644 --- a/Libraries/Lists/FlatList.js +++ b/packages/react-native/Libraries/Lists/FlatList.js @@ -11,14 +11,17 @@ import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; import type { + RenderItemProps, + RenderItemType, ViewabilityConfigCallbackPair, ViewToken, -} from './ViewabilityHelper'; -import type {RenderItemProps, RenderItemType} from './VirtualizedList'; +} from '@react-native/virtualized-lists'; import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import VirtualizedList from './VirtualizedList'; -import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; +import { + VirtualizedList, + keyExtractor as defaultKeyExtractor, +} from '@react-native/virtualized-lists'; import memoizeOne from 'memoize-one'; const View = require('../Components/View/View'); @@ -30,10 +33,10 @@ const React = require('react'); type RequiredProps = {| /** - * For simplicity, data is just a plain array. If you want to use something else, like an - * immutable list, use the underlying `VirtualizedList` directly. + * An array (or array-like list) of items to render. Other data types can be + * used by targeting VirtualizedList directly. */ - data: ?$ReadOnlyArray, + data: ?$ReadOnly<$ArrayLike>, |}; type OptionalProps = {| /** @@ -88,7 +91,7 @@ type OptionalProps = {| * specify `ItemSeparatorComponent`. */ getItemLayout?: ( - data: ?Array, + data: ?$ReadOnly<$ArrayLike>, index: number, ) => { length: number, @@ -163,6 +166,11 @@ function numColumnsOrDefault(numColumns: ?number) { return numColumns ?? 1; } +function isArrayLike(data: mixed): boolean { + // $FlowExpectedError[incompatible-use] + return typeof Object(data).length === 'number'; +} + type FlatListProps = {| ...RequiredProps, ...OptionalProps, @@ -334,6 +342,7 @@ class FlatList extends React.PureComponent, void> { scrollToItem(params: { animated?: ?boolean, item: ItemT, + viewOffset?: number, viewPosition?: number, ... }) { @@ -496,8 +505,10 @@ class FlatList extends React.PureComponent, void> { ); } - // $FlowFixMe[missing-local-annot] - _getItem = (data: Array, index: number) => { + _getItem = ( + data: $ArrayLike, + index: number, + ): ?(ItemT | $ReadOnlyArray) => { const numColumns = numColumnsOrDefault(this.props.numColumns); if (numColumns > 1) { const ret = []; @@ -514,8 +525,14 @@ class FlatList extends React.PureComponent, void> { } }; - _getItemCount = (data: ?Array): number => { - if (Array.isArray(data)) { + _getItemCount = (data: ?$ArrayLike): number => { + // Legacy behavior of FlatList was to forward "undefined" length if invalid + // data like a non-arraylike object is passed. VirtualizedList would then + // coerce this, and the math would work out to no-op. For compatibility, if + // invalid data is passed, we tell VirtualizedList there are zero items + // available to prevent it from trying to read from the invalid data + // (without propagating invalidly typed data). + if (data != null && isArrayLike(data)) { const numColumns = numColumnsOrDefault(this.props.numColumns); return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length; } else { diff --git a/Libraries/Lists/SectionList.d.ts b/packages/react-native/Libraries/Lists/SectionList.d.ts similarity index 88% rename from Libraries/Lists/SectionList.d.ts rename to packages/react-native/Libraries/Lists/SectionList.d.ts index 48c24b1a60f9d2..396092286b05d1 100644 --- a/Libraries/Lists/SectionList.d.ts +++ b/packages/react-native/Libraries/Lists/SectionList.d.ts @@ -11,7 +11,7 @@ import type * as React from 'react'; import type { ListRenderItemInfo, VirtualizedListWithoutRenderItemProps, -} from './VirtualizedList'; +} from '@react-native/virtualized-lists'; import type { ScrollView, ScrollViewProps, @@ -61,48 +61,6 @@ export type SectionListRenderItem = ( export interface SectionListProps extends VirtualizedListWithoutRenderItemProps { - /** - * Rendered in between adjacent Items within each section. - */ - ItemSeparatorComponent?: React.ComponentType | null | undefined; - - /** - * Rendered when the list is empty. - */ - ListEmptyComponent?: - | React.ComponentType - | React.ReactElement - | null - | undefined; - - /** - * Rendered at the very end of the list. - */ - ListFooterComponent?: - | React.ComponentType - | React.ReactElement - | null - | undefined; - - /** - * Styling for internal View for ListFooterComponent - */ - ListFooterComponentStyle?: StyleProp | undefined | null; - - /** - * Rendered at the very beginning of the list. - */ - ListHeaderComponent?: - | React.ComponentType - | React.ReactElement - | null - | undefined; - - /** - * Styling for internal View for ListHeaderComponent - */ - ListHeaderComponentStyle?: StyleProp | undefined | null; - /** * Rendered in between each section. */ @@ -117,7 +75,7 @@ export interface SectionListProps * If any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the `data` prop, * stick it here and treat it immutably. */ - extraData?: any; + extraData?: any | undefined; /** * `getItemLayout` is an optional optimization that lets us skip measurement of dynamic @@ -252,10 +210,9 @@ export interface SectionListScrollParams { viewPosition?: number | undefined; } -export class SectionList< - ItemT = any, - SectionT = DefaultSectionT, -> extends React.Component> { +export abstract class SectionListComponent< + Props, +> extends React.Component { /** * Scrolls to the item at the specified sectionIndex and itemIndex (within the section) * positioned in the viewable area such that viewPosition 0 places it at the top @@ -288,6 +245,11 @@ export class SectionList< getScrollableNode(): NodeHandle | undefined; } +export class SectionList< + ItemT = any, + SectionT = DefaultSectionT, +> extends SectionListComponent> {} + /* This definition is deprecated because it extends the wrong base type */ export interface SectionListStatic extends React.ComponentClass> { diff --git a/Libraries/Lists/SectionList.js b/packages/react-native/Libraries/Lists/SectionList.js similarity index 95% rename from Libraries/Lists/SectionList.js rename to packages/react-native/Libraries/Lists/SectionList.js index 3520b31eca9829..6177b07d58dec3 100644 --- a/Libraries/Lists/SectionList.js +++ b/packages/react-native/Libraries/Lists/SectionList.js @@ -12,13 +12,13 @@ import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type { - Props as VirtualizedSectionListProps, ScrollToLocationParamsType, SectionBase as _SectionBase, -} from './VirtualizedSectionList'; + VirtualizedSectionListProps, +} from '@react-native/virtualized-lists'; import Platform from '../Utilities/Platform'; -import VirtualizedSectionList from './VirtualizedSectionList'; +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; import * as React from 'react'; type Item = any; @@ -244,11 +244,17 @@ export default class SectionList< const stickySectionHeadersEnabled = _stickySectionHeadersEnabled ?? Platform.OS === 'ios'; return ( + /* $FlowFixMe[incompatible-type] Error revealed after improved builtin + * React utility types */ + /* $FlowFixMe[incompatible-type] Error revealed after improved builtin + * React utility types */ items.length} + // $FlowFixMe[missing-local-annot] getItem={(items, index) => items[index]} /> ); diff --git a/Libraries/Lists/SectionListModern.js b/packages/react-native/Libraries/Lists/SectionListModern.js similarity index 98% rename from Libraries/Lists/SectionListModern.js rename to packages/react-native/Libraries/Lists/SectionListModern.js index c7856e13d9a666..d9676f106f8cfc 100644 --- a/Libraries/Lists/SectionListModern.js +++ b/packages/react-native/Libraries/Lists/SectionListModern.js @@ -12,14 +12,14 @@ import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type { - Props as VirtualizedSectionListProps, ScrollToLocationParamsType, SectionBase as _SectionBase, -} from './VirtualizedSectionList'; + VirtualizedSectionListProps, +} from '@react-native/virtualized-lists'; import type {AbstractComponent, Element, ElementRef} from 'react'; import Platform from '../Utilities/Platform'; -import VirtualizedSectionList from './VirtualizedSectionList'; +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; import React, {forwardRef, useImperativeHandle, useRef} from 'react'; type Item = any; diff --git a/packages/react-native/Libraries/Lists/ViewabilityHelper.js b/packages/react-native/Libraries/Lists/ViewabilityHelper.js new file mode 100644 index 00000000000000..c7dedfdf496b93 --- /dev/null +++ b/packages/react-native/Libraries/Lists/ViewabilityHelper.js @@ -0,0 +1,24 @@ +/** + * 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 + * @format + */ + +'use strict'; + +export type { + ViewToken, + ViewabilityConfig, + ViewabilityConfigCallbackPair, +} from '@react-native/virtualized-lists'; + +import {typeof ViewabilityHelper as ViewabilityHelperType} from '@react-native/virtualized-lists'; + +const ViewabilityHelper: ViewabilityHelperType = + require('@react-native/virtualized-lists').ViewabilityHelper; + +module.exports = ViewabilityHelper; diff --git a/packages/react-native/Libraries/Lists/VirtualizeUtils.js b/packages/react-native/Libraries/Lists/VirtualizeUtils.js new file mode 100644 index 00000000000000..535d25b3abc538 --- /dev/null +++ b/packages/react-native/Libraries/Lists/VirtualizeUtils.js @@ -0,0 +1,18 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import {typeof keyExtractor as KeyExtractorType} from '@react-native/virtualized-lists'; + +const keyExtractor: KeyExtractorType = + require('@react-native/virtualized-lists').keyExtractor; + +module.exports = {keyExtractor}; diff --git a/packages/react-native/Libraries/Lists/VirtualizedList.js b/packages/react-native/Libraries/Lists/VirtualizedList.js new file mode 100644 index 00000000000000..2488b1e5e37f57 --- /dev/null +++ b/packages/react-native/Libraries/Lists/VirtualizedList.js @@ -0,0 +1,23 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import {typeof VirtualizedList as VirtualizedListType} from '@react-native/virtualized-lists'; + +const VirtualizedList: VirtualizedListType = + require('@react-native/virtualized-lists').VirtualizedList; + +export type { + RenderItemProps, + RenderItemType, + Separators, +} from '@react-native/virtualized-lists'; +module.exports = VirtualizedList; diff --git a/packages/react-native/Libraries/Lists/VirtualizedListContext.js b/packages/react-native/Libraries/Lists/VirtualizedListContext.js new file mode 100644 index 00000000000000..5686ccf372286c --- /dev/null +++ b/packages/react-native/Libraries/Lists/VirtualizedListContext.js @@ -0,0 +1,18 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterType} from '@react-native/virtualized-lists'; + +const VirtualizedListContextResetter: VirtualizedListContextResetterType = + require('@react-native/virtualized-lists').VirtualizedListContextResetter; + +module.exports = {VirtualizedListContextResetter}; diff --git a/packages/react-native/Libraries/Lists/VirtualizedSectionList.js b/packages/react-native/Libraries/Lists/VirtualizedSectionList.js new file mode 100644 index 00000000000000..242dfe34c6b231 --- /dev/null +++ b/packages/react-native/Libraries/Lists/VirtualizedSectionList.js @@ -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. + * + * @flow + * @format + */ + +'use strict'; + +import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@react-native/virtualized-lists'; + +const VirtualizedSectionList: VirtualizedSectionListType = + require('@react-native/virtualized-lists').VirtualizedSectionList; + +export type { + SectionBase, + ScrollToLocationParamsType, +} from '@react-native/virtualized-lists'; +module.exports = VirtualizedSectionList; diff --git a/Libraries/Lists/__flowtests__/FlatList-flowtest.js b/packages/react-native/Libraries/Lists/__flowtests__/FlatList-flowtest.js similarity index 97% rename from Libraries/Lists/__flowtests__/FlatList-flowtest.js rename to packages/react-native/Libraries/Lists/__flowtests__/FlatList-flowtest.js index fb62a18ca4f430..7b425087e3c0d2 100644 --- a/Libraries/Lists/__flowtests__/FlatList-flowtest.js +++ b/packages/react-native/Libraries/Lists/__flowtests__/FlatList-flowtest.js @@ -93,6 +93,7 @@ module.exports = { } + // $FlowExpectedError - bad title type number, should be string data={data} />, // EverythingIsFine diff --git a/Libraries/Lists/__flowtests__/SectionList-flowtest.js b/packages/react-native/Libraries/Lists/__flowtests__/SectionList-flowtest.js similarity index 98% rename from Libraries/Lists/__flowtests__/SectionList-flowtest.js rename to packages/react-native/Libraries/Lists/__flowtests__/SectionList-flowtest.js index 4112d87a6fde4a..9f878d89b0607d 100644 --- a/Libraries/Lists/__flowtests__/SectionList-flowtest.js +++ b/packages/react-native/Libraries/Lists/__flowtests__/SectionList-flowtest.js @@ -76,7 +76,7 @@ module.exports = { }, testBadInheritedDefaultProp(): React.MixedElement { - const sections = []; + const sections: $FlowFixMe = []; return ( { expect(renderItemInThreeColumns).toHaveBeenCalledTimes(7); }); + it('renders array-like data', () => { + const arrayLike = { + length: 3, + 0: {key: 'i1'}, + 1: {key: 'i2'}, + 2: {key: 'i3'}, + }; + + const component = ReactTestRenderer.create( + } + />, + ); + expect(component).toMatchSnapshot(); + }); + it('ignores invalid data', () => { + const component = ReactTestRenderer.create( + } + />, + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/Libraries/Lists/__tests__/SectionList-test.js b/packages/react-native/Libraries/Lists/__tests__/SectionList-test.js similarity index 100% rename from Libraries/Lists/__tests__/SectionList-test.js rename to packages/react-native/Libraries/Lists/__tests__/SectionList-test.js diff --git a/packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap b/packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap new file mode 100644 index 00000000000000..86901053a00d9f --- /dev/null +++ b/packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap @@ -0,0 +1,598 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FlatList ignores invalid data 1`] = ` + + + +`; + +exports[`FlatList renders all the bells and whistles 1`] = ` + + } + refreshing={false} + removeClippedSubviews={false} + renderItem={[Function]} + scrollEventThrottle={50} + stickyHeaderIndices={Array []} + viewabilityConfigCallbackPairs={Array []} +> + + + +
+ + + + + + + + + + + + + + + + + + + + + +