From ee8088b6157837c239db47ac5bd3a8603ceefc3c Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Thu, 30 Jan 2025 01:58:27 -0800 Subject: [PATCH 01/17] fix(dev-middleware): add missing `invariant` dependency (#49047) Summary: `dev-middleware` uses `invariant` but does not declare it as a dependency. Under certain hoisting scenarios, or when using pnpm, this will cause `dev-middleware` to fail while being loaded. ## Changelog: [GENERAL] [FIXED] - add missing `invariant` dependency Pull Request resolved: https://github.com/facebook/react-native/pull/49047 Test Plan: n/a Reviewed By: cortinico Differential Revision: D68835789 Pulled By: huntie fbshipit-source-id: 13718f4970ed55e6e062b7c2bd719be977abdd0c --- packages/dev-middleware/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dev-middleware/package.json b/packages/dev-middleware/package.json index e48febb599e8c8..2305c3d353d592 100644 --- a/packages/dev-middleware/package.json +++ b/packages/dev-middleware/package.json @@ -28,6 +28,7 @@ "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^2.2.0", + "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "selfsigned": "^2.4.1", From 252294bc769fb5ebec839bab38032e188e90b869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 30 Jan 2025 03:24:39 -0800 Subject: [PATCH 02/17] Improve naming for benchmark suite options (#49014) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49014 Changelog: [internal] Use better names for the Fantom benchmarking API than the ones defined in `tinybench`: * `time` => `minDuration` * `iterations` = `minIterations` * `warmupTime` => `minWarmupDuration` * `warmupIterations` => `minWarmupIterations` Reviewed By: rshest Differential Revision: D68710952 fbshipit-source-id: 05dc1145a72a50ea73de7ccbb08bb28d7975245f --- packages/react-native-fantom/src/Benchmark.js | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/react-native-fantom/src/Benchmark.js b/packages/react-native-fantom/src/Benchmark.js index ed3d451cd77e55..2549d174d5f5a3 100644 --- a/packages/react-native-fantom/src/Benchmark.js +++ b/packages/react-native-fantom/src/Benchmark.js @@ -19,10 +19,11 @@ import { } from 'tinybench'; type SuiteOptions = $ReadOnly<{ - ...Pick< - BenchOptions, - 'iterations' | 'time' | 'warmup' | 'warmupIterations' | 'warmupTime', - >, + minIterations?: number, + minDuration?: number, + warmup?: boolean, + minWarmupDuration?: number, + minWarmupIterations?: number, disableOptimizedBuildCheck?: boolean, }>; @@ -35,7 +36,7 @@ interface SuiteAPI { export function suite( suiteName: string, - suiteOptions?: ?SuiteOptions, + suiteOptions?: SuiteOptions = {}, ): SuiteAPI { const tasks: Array<{ name: string, @@ -57,7 +58,7 @@ export function suite( // logic in the benchmark doesn't break. const isTestOnly = isRunningFromCI && verifyFns.length === 0; - const overriddenOptions: BenchOptions = isTestOnly + const benchOptions: BenchOptions = isTestOnly ? { warmupIterations: 1, warmupTime: 0, @@ -66,15 +67,31 @@ export function suite( } : {}; - const {disableOptimizedBuildCheck, ...benchOptions} = suiteOptions ?? {}; + benchOptions.name = suiteName; + benchOptions.throws = true; + benchOptions.now = () => NativeCPUTime.getCPUTimeNanos() / 1000000; - const bench = new Bench({ - ...benchOptions, - ...overriddenOptions, - name: suiteName, - throws: true, - now: () => NativeCPUTime.getCPUTimeNanos() / 1000000, - }); + if (suiteOptions.minIterations != null) { + benchOptions.iterations = suiteOptions.minIterations; + } + + if (suiteOptions.minDuration != null) { + benchOptions.time = suiteOptions.minDuration; + } + + if (suiteOptions.warmup != null) { + benchOptions.warmup = suiteOptions.warmup; + } + + if (suiteOptions.minWarmupDuration != null) { + benchOptions.warmupTime = suiteOptions.minWarmupDuration; + } + + if (suiteOptions.minWarmupIterations != null) { + benchOptions.warmupIterations = suiteOptions.minWarmupIterations; + } + + const bench = new Bench(benchOptions); for (const task of tasks) { bench.add(task.name, task.fn, task.options); @@ -96,7 +113,7 @@ export function suite( ); } - if (__DEV__ && disableOptimizedBuildCheck !== true) { + if (__DEV__ && suiteOptions.disableOptimizedBuildCheck !== true) { throw new Error('Benchmarks should not be run in development mode'); } }); From 45a2d9c5a80f93de907a73ee2d61518c4b528d9a Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 30 Jan 2025 06:49:13 -0800 Subject: [PATCH 03/17] Delete deprecated YellowBox API (#49061) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49061 Changelog: [General][Breaking] - Remove deprecated `YellowBox` and `console.ignoredYellowBox` APIs. Use `LogBox`. Reviewed By: cortinico, hezi Differential Revision: D68893550 fbshipit-source-id: 5030a20a2d1a60ca37eaf928339b8dd5d5abaa27 --- packages/metro-config/src/index.flow.js | 1 - .../YellowBox/YellowBoxDeprecated.d.ts | 19 ----- .../YellowBox/YellowBoxDeprecated.js | 76 ------------------- .../__tests__/YellowBoxDeprecated-test.js | 62 --------------- .../__snapshots__/public-api-test.js.snap | 12 --- packages/react-native/index.js | 4 - .../types/__typetests__/index.tsx | 4 - packages/react-native/types/index.d.ts | 5 -- 8 files changed, 183 deletions(-) delete mode 100644 packages/react-native/Libraries/YellowBox/YellowBoxDeprecated.d.ts delete mode 100644 packages/react-native/Libraries/YellowBox/YellowBoxDeprecated.js delete mode 100644 packages/react-native/Libraries/YellowBox/__tests__/YellowBoxDeprecated-test.js diff --git a/packages/metro-config/src/index.flow.js b/packages/metro-config/src/index.flow.js index 452f6b7e02aab6..1c68e400739b56 100644 --- a/packages/metro-config/src/index.flow.js +++ b/packages/metro-config/src/index.flow.js @@ -26,7 +26,6 @@ const INTERNAL_CALLSITES_REGEX = new RegExp( '/Libraries/Utilities/.+\\.js$', '/Libraries/vendor/.+\\.js$', '/Libraries/WebSocket/.+\\.js$', - '/Libraries/YellowBox/.+\\.js$', '/src/private/renderer/errorhandling/.+\\.js$', '/metro-runtime/.+\\.js$', '/node_modules/@babel/runtime/.+\\.js$', diff --git a/packages/react-native/Libraries/YellowBox/YellowBoxDeprecated.d.ts b/packages/react-native/Libraries/YellowBox/YellowBoxDeprecated.d.ts deleted file mode 100644 index 4c4e0c24c6e73c..00000000000000 --- a/packages/react-native/Libraries/YellowBox/YellowBoxDeprecated.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - */ - -import type * as React from 'react'; - -/** - * YellowBox has been replaced with LogBox. - * @see LogBox - * @deprecated - */ -export const YellowBox: React.ComponentClass & { - ignoreWarnings: (warnings: string[]) => void; -}; diff --git a/packages/react-native/Libraries/YellowBox/YellowBoxDeprecated.js b/packages/react-native/Libraries/YellowBox/YellowBoxDeprecated.js deleted file mode 100644 index dfa5757ef0bdcc..00000000000000 --- a/packages/react-native/Libraries/YellowBox/YellowBoxDeprecated.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import type {IgnorePattern} from '../LogBox/Data/LogBoxData'; - -import React from 'react'; - -const LogBox = require('../LogBox/LogBox').default; - -type Props = $ReadOnly<{}>; - -let YellowBox; -if (__DEV__) { - YellowBox = class extends React.Component { - static ignoreWarnings(patterns: $ReadOnlyArray): void { - console.warn( - 'YellowBox has been replaced with LogBox. Please call LogBox.ignoreLogs() instead.', - ); - - LogBox.ignoreLogs(patterns); - } - - static install(): void { - console.warn( - 'YellowBox has been replaced with LogBox. Please call LogBox.install() instead.', - ); - LogBox.install(); - } - - static uninstall(): void { - console.warn( - 'YellowBox has been replaced with LogBox. Please call LogBox.uninstall() instead.', - ); - LogBox.uninstall(); - } - - render(): React.Node { - return null; - } - }; -} else { - YellowBox = class extends React.Component { - static ignoreWarnings(patterns: $ReadOnlyArray): void { - // Do nothing. - } - - static install(): void { - // Do nothing. - } - - static uninstall(): void { - // Do nothing. - } - - render(): React.Node { - return null; - } - }; -} - -// $FlowFixMe[method-unbinding] -export default (YellowBox: Class> & { - ignoreWarnings($ReadOnlyArray): void, - install(): void, - uninstall(): void, - ... -}); diff --git a/packages/react-native/Libraries/YellowBox/__tests__/YellowBoxDeprecated-test.js b/packages/react-native/Libraries/YellowBox/__tests__/YellowBoxDeprecated-test.js deleted file mode 100644 index 5e972213afe742..00000000000000 --- a/packages/react-native/Libraries/YellowBox/__tests__/YellowBoxDeprecated-test.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - * @format - * @oncall react_native - */ - -'use strict'; - -const LogBox = require('../../LogBox/LogBox').default; -const YellowBox = require('../YellowBoxDeprecated').default; - -describe('YellowBox', () => { - beforeEach(() => { - jest.restoreAllMocks(); - }); - it('calling ignoreWarnings proxies to LogBox.ignoreLogs', () => { - jest.spyOn(LogBox, 'ignoreLogs'); - const consoleWarn = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}); - YellowBox.ignoreWarnings(['foo']); - - // $FlowFixMe[method-unbinding] added when improving typing for this parameters - expect(LogBox.ignoreLogs).toBeCalledWith(['foo']); - expect(consoleWarn).toBeCalledWith( - 'YellowBox has been replaced with LogBox. Please call LogBox.ignoreLogs() instead.', - ); - }); - - it('calling install proxies to LogBox.install', () => { - jest.spyOn(LogBox, 'install'); - const consoleWarn = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}); - YellowBox.install(); - - // $FlowFixMe[method-unbinding] added when improving typing for this parameters - expect(LogBox.install).toBeCalled(); - expect(consoleWarn).toBeCalledWith( - 'YellowBox has been replaced with LogBox. Please call LogBox.install() instead.', - ); - }); - - it('calling uninstall proxies to LogBox.uninstall', () => { - jest.spyOn(LogBox, 'uninstall'); - const consoleWarn = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}); - YellowBox.uninstall(); - - // $FlowFixMe[method-unbinding] added when improving typing for this parameters - expect(LogBox.uninstall).toBeCalled(); - expect(consoleWarn).toBeCalledWith( - 'YellowBox has been replaced with LogBox. Please call LogBox.uninstall() instead.', - ); - }); -}); diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 6221e95f846176..61b771370c1a85 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -9553,17 +9553,6 @@ declare export default typeof WebSocketInterceptor; " `; -exports[`public API should not change unintentionally Libraries/YellowBox/YellowBoxDeprecated.js 1`] = ` -"type Props = $ReadOnly<{}>; -declare export default Class> & { - ignoreWarnings($ReadOnlyArray): void, - install(): void, - uninstall(): void, - ... -}; -" -`; - exports[`public API should not change unintentionally Libraries/promiseRejectionTrackingOptions.js 1`] = ` "declare let rejectionTrackingOptions: $NonMaybeType[0]>; declare export default typeof rejectionTrackingOptions; @@ -9686,7 +9675,6 @@ declare module.exports: { get useWindowDimensions(): useWindowDimensions, get UTFSequence(): UTFSequence, get Vibration(): Vibration, - get YellowBox(): YellowBox, get DeviceEventEmitter(): RCTDeviceEventEmitter, get DynamicColorIOS(): DynamicColorIOS, get NativeAppEventEmitter(): RCTNativeAppEventEmitter, diff --git a/packages/react-native/index.js b/packages/react-native/index.js index 75f84a11d66ed2..3346c87bdb7913 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -96,7 +96,6 @@ import typeof Platform from './Libraries/Utilities/Platform'; import typeof useColorScheme from './Libraries/Utilities/useColorScheme'; import typeof useWindowDimensions from './Libraries/Utilities/useWindowDimensions'; import typeof Vibration from './Libraries/Vibration/Vibration'; -import typeof YellowBox from './Libraries/YellowBox/YellowBoxDeprecated'; import typeof DevMenu from './src/private/devmenu/DevMenu'; const warnOnce = require('./Libraries/Utilities/warnOnce'); @@ -358,9 +357,6 @@ module.exports = { get Vibration(): Vibration { return require('./Libraries/Vibration/Vibration').default; }, - get YellowBox(): YellowBox { - return require('./Libraries/YellowBox/YellowBoxDeprecated').default; - }, // Plugins get DeviceEventEmitter(): RCTDeviceEventEmitter { diff --git a/packages/react-native/types/__typetests__/index.tsx b/packages/react-native/types/__typetests__/index.tsx index 40f0f4523eb6a3..0a34cea9e4a1d7 100644 --- a/packages/react-native/types/__typetests__/index.tsx +++ b/packages/react-native/types/__typetests__/index.tsx @@ -111,7 +111,6 @@ import { View, ViewStyle, VirtualizedList, - YellowBox, findNodeHandle, requireNativeComponent, useColorScheme, @@ -1968,9 +1967,6 @@ const PushNotificationTest = () => { }); }; -// YellowBox -const YellowBoxTest = () => ; - // Appearance const DarkMode = () => { const color = useColorScheme(); diff --git a/packages/react-native/types/index.d.ts b/packages/react-native/types/index.d.ts index 63fc7e3096dac8..3300b10a65e7a5 100644 --- a/packages/react-native/types/index.d.ts +++ b/packages/react-native/types/index.d.ts @@ -148,7 +148,6 @@ export * from '../Libraries/Utilities/Dimensions'; export * from '../Libraries/Utilities/PixelRatio'; export * from '../Libraries/Utilities/Platform'; export * from '../Libraries/Vibration/Vibration'; -export * from '../Libraries/YellowBox/YellowBoxDeprecated'; export * from '../Libraries/vendor/core/ErrorUtils'; export { EmitterSubscription, @@ -184,10 +183,6 @@ declare global { groupCollapsed(label?: string): void; groupEnd(): void; group(label?: string): void; - /** - * @deprecated Use LogBox.ignoreLogs(patterns) instead - */ - ignoredYellowBox: string[]; } var console: Console; From 4d6785bdb53a94d650364ef7b5821fab16c39ae3 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Thu, 30 Jan 2025 07:07:08 -0800 Subject: [PATCH 04/17] Migrate LayoutAnimation and Linking libraries to use export syntax (#49021) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49021 ## Motivation Modernising the RN codebase to allow for modern Flow tooling to process it. ## This diff - Migrates files in `Libraries/LayoutAnimation/*.js` and `Libraries/Linking/*.js` to use the `export` syntax. - Updates deep-imports of these files to use `.default` - Updates jest mocks - Updates the current iteration of API snapshots (intended). Changelog: [General][Breaking] - Deep imports to modules inside `Libraries/LayoutAnimation` and `Libraries/Linking` with `require` syntax need to be appended with '.default'. Reviewed By: huntie Differential Revision: D68782429 fbshipit-source-id: c9ea4fadbc44587a165d311b054fcd03444842c8 --- .../Keyboard/__tests__/Keyboard-test.js | 3 ++- .../Libraries/Core/setUpDeveloperTools.js | 2 +- .../Libraries/JSInspector/InspectorAgent.js | 2 +- .../Libraries/JSInspector/NetworkAgent.js | 2 +- .../LayoutAnimation/LayoutAnimation.js | 2 +- .../Libraries/Linking/Linking.d.ts | 6 +++--- .../react-native/Libraries/Linking/Linking.js | 18 ++++++++++-------- .../__snapshots__/public-api-test.js.snap | 11 ++++++----- packages/react-native/index.js | 4 ++-- packages/react-native/jest/setup.js | 19 +++++++++++-------- 10 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/react-native/Libraries/Components/Keyboard/__tests__/Keyboard-test.js b/packages/react-native/Libraries/Components/Keyboard/__tests__/Keyboard-test.js index 6b2d7d89495a79..f560ced1caa727 100644 --- a/packages/react-native/Libraries/Components/Keyboard/__tests__/Keyboard-test.js +++ b/packages/react-native/Libraries/Components/Keyboard/__tests__/Keyboard-test.js @@ -9,7 +9,8 @@ * @oncall react_native */ -const LayoutAnimation = require('../../../LayoutAnimation/LayoutAnimation'); +const LayoutAnimation = + require('../../../LayoutAnimation/LayoutAnimation').default; const dismissKeyboard = require('../../../Utilities/dismissKeyboard'); const Keyboard = require('../Keyboard').default; diff --git a/packages/react-native/Libraries/Core/setUpDeveloperTools.js b/packages/react-native/Libraries/Core/setUpDeveloperTools.js index a881f83b4c3d46..27edd12da2a200 100644 --- a/packages/react-native/Libraries/Core/setUpDeveloperTools.js +++ b/packages/react-native/Libraries/Core/setUpDeveloperTools.js @@ -19,7 +19,7 @@ declare var console: {[string]: $FlowFixMe}; if (__DEV__) { // Set up inspector const JSInspector = require('../JSInspector/JSInspector'); - JSInspector.registerAgent(require('../JSInspector/NetworkAgent')); + JSInspector.registerAgent(require('../JSInspector/NetworkAgent').default); // Note we can't check if console is "native" because it would appear "native" in JSC and Hermes. // We also can't check any properties that don't exist in the Chrome worker environment. diff --git a/packages/react-native/Libraries/JSInspector/InspectorAgent.js b/packages/react-native/Libraries/JSInspector/InspectorAgent.js index 833a143247f777..921ce972410a38 100644 --- a/packages/react-native/Libraries/JSInspector/InspectorAgent.js +++ b/packages/react-native/Libraries/JSInspector/InspectorAgent.js @@ -24,4 +24,4 @@ class InspectorAgent { } } -module.exports = InspectorAgent; +export default InspectorAgent; diff --git a/packages/react-native/Libraries/JSInspector/NetworkAgent.js b/packages/react-native/Libraries/JSInspector/NetworkAgent.js index 495b5c9f625647..10cf147490e4f7 100644 --- a/packages/react-native/Libraries/JSInspector/NetworkAgent.js +++ b/packages/react-native/Libraries/JSInspector/NetworkAgent.js @@ -293,4 +293,4 @@ class NetworkAgent extends InspectorAgent { } } -module.exports = NetworkAgent; +export default NetworkAgent; diff --git a/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js b/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js index 0e5cec3bb57a51..7078d533e7ad03 100644 --- a/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js +++ b/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js @@ -197,4 +197,4 @@ const LayoutAnimation = { setEnabled, }; -module.exports = LayoutAnimation; +export default LayoutAnimation; diff --git a/packages/react-native/Libraries/Linking/Linking.d.ts b/packages/react-native/Libraries/Linking/Linking.d.ts index 36dbe9f558f9e3..55d7aa33cb5803 100644 --- a/packages/react-native/Libraries/Linking/Linking.d.ts +++ b/packages/react-native/Libraries/Linking/Linking.d.ts @@ -10,7 +10,7 @@ import {NativeEventEmitter} from '../EventEmitter/NativeEventEmitter'; import {EmitterSubscription} from '../vendor/emitter/EventEmitter'; -export interface LinkingStatic extends NativeEventEmitter { +export interface LinkingImpl extends NativeEventEmitter { /** * Add a handler to Linking changes by listening to the `url` event type * and providing the handler @@ -57,5 +57,5 @@ export interface LinkingStatic extends NativeEventEmitter { ): Promise; } -export const Linking: LinkingStatic; -export type Linking = LinkingStatic; +export const Linking: LinkingImpl; +export type Linking = LinkingImpl; diff --git a/packages/react-native/Libraries/Linking/Linking.js b/packages/react-native/Libraries/Linking/Linking.js index e5900c11326244..b42b38d322fe35 100644 --- a/packages/react-native/Libraries/Linking/Linking.js +++ b/packages/react-native/Libraries/Linking/Linking.js @@ -21,13 +21,7 @@ type LinkingEventDefinitions = { url: [{url: string}], }; -/** - * `Linking` gives you a general interface to interact with both incoming - * and outgoing app links. - * - * See https://reactnative.dev/docs/linking - */ -class Linking extends NativeEventEmitter { +class LinkingImpl extends NativeEventEmitter { constructor() { super(Platform.OS === 'ios' ? nullthrows(NativeLinkingManager) : undefined); } @@ -129,4 +123,12 @@ class Linking extends NativeEventEmitter { } } -module.exports = (new Linking(): Linking); +const Linking: LinkingImpl = new LinkingImpl(); + +/** + * `Linking` gives you a general interface to interact with both incoming + * and outgoing app links. + * + * See https://reactnative.dev/docs/linking + */ +export default Linking; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 61b771370c1a85..b5903211123e33 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -5406,7 +5406,7 @@ declare class InspectorAgent { constructor(eventSender: EventSender): void; sendEvent(name: string, params: mixed): void; } -declare module.exports: InspectorAgent; +declare export default typeof InspectorAgent; " `; @@ -5461,7 +5461,7 @@ declare class NetworkAgent extends InspectorAgent { }; interceptor(): Interceptor; } -declare module.exports: NetworkAgent; +declare export default typeof NetworkAgent; " `; @@ -5497,7 +5497,7 @@ declare const LayoutAnimation: { spring: (onAnimationDidEnd?: OnAnimationDidEndCallback) => void, setEnabled: setEnabled, }; -declare module.exports: LayoutAnimation; +declare export default typeof LayoutAnimation; " `; @@ -5505,7 +5505,7 @@ exports[`public API should not change unintentionally Libraries/Linking/Linking. "type LinkingEventDefinitions = { url: [{ url: string }], }; -declare class Linking extends NativeEventEmitter { +declare class LinkingImpl extends NativeEventEmitter { constructor(): void; addEventListener>( eventType: K, @@ -5525,7 +5525,8 @@ declare class Linking extends NativeEventEmitter { ): Promise; _validateURL(url: string): void; } -declare module.exports: Linking; +declare const Linking: LinkingImpl; +declare export default typeof Linking; " `; diff --git a/packages/react-native/index.js b/packages/react-native/index.js index 3346c87bdb7913..c37681399584ec 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -277,10 +277,10 @@ module.exports = { return require('./Libraries/Components/Keyboard/Keyboard').default; }, get LayoutAnimation(): LayoutAnimation { - return require('./Libraries/LayoutAnimation/LayoutAnimation'); + return require('./Libraries/LayoutAnimation/LayoutAnimation').default; }, get Linking(): Linking { - return require('./Libraries/Linking/Linking'); + return require('./Libraries/Linking/Linking').default; }, get LogBox(): LogBox { return require('./Libraries/LogBox/LogBox').default; diff --git a/packages/react-native/jest/setup.js b/packages/react-native/jest/setup.js index a5482d9cdf3879..aab3d8148bf5c3 100644 --- a/packages/react-native/jest/setup.js +++ b/packages/react-native/jest/setup.js @@ -236,14 +236,17 @@ jest }, })) .mock('../Libraries/Linking/Linking', () => ({ - openURL: jest.fn(), - canOpenURL: jest.fn(() => Promise.resolve(true)), - openSettings: jest.fn(), - addEventListener: jest.fn(() => ({ - remove: jest.fn(), - })), - getInitialURL: jest.fn(() => Promise.resolve()), - sendIntent: jest.fn(), + __esModule: true, + default: { + openURL: jest.fn(), + canOpenURL: jest.fn(() => Promise.resolve(true)), + openSettings: jest.fn(), + addEventListener: jest.fn(() => ({ + remove: jest.fn(), + })), + getInitialURL: jest.fn(() => Promise.resolve()), + sendIntent: jest.fn(), + }, })) // Mock modules defined by the native layer (ex: Objective-C, Java) .mock('../Libraries/BatchedBridge/NativeModules', () => ({ From 8f7f3be9af73c2356d5d31e4b7315f6b11b0926a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 30 Jan 2025 07:11:43 -0800 Subject: [PATCH 05/17] Add support for document instance in React Native (#32260) (#49051) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49051 ## Summary We're adding support for `Document` instances in React Native (as `ReactNativeDocument` instances) in https://github.com/facebook/react-native/pull/49012 , which requires the React Fabric renderer to handle its lifecycle. This modifies the renderer to create those document instances and associate them with the React root, and provides a new method for React Native to access them given its containerTag / rootTag. ## How did you test this change? Tested e2e in https://github.com/facebook/react-native/pull/49012 manually syncing these changes. DiffTrain build for [b2357ecd8203341a3668a96d32d68dd519e5430d](https://github.com/facebook/react/commit/b2357ecd8203341a3668a96d32d68dd519e5430d) Reviewed By: javache Differential Revision: D68839346 fbshipit-source-id: 589ddce15d0f32ba3eab1c03306c405d38616723 --- .../react-native/Libraries/Renderer/shims/ReactNativeTypes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js index 85d273893178b8..7a3c7410ab664a 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js @@ -7,7 +7,7 @@ * @noformat * @nolint * @flow strict - * @generated SignedSource<> + * @generated SignedSource<<694ba49f9b85f1cc713053fe7628684a>> */ import type {ElementRef, ElementType, MixedElement} from 'react'; @@ -232,6 +232,7 @@ export opaque type Node = mixed; export opaque type InternalInstanceHandle = mixed; type PublicInstance = mixed; type PublicTextInstance = mixed; +export opaque type PublicRootInstance = mixed; export type ReactFabricType = { findHostInstance_DEPRECATED( From 3dab9c66ba145c3014df5944a6ef58fe6ab82c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 30 Jan 2025 07:11:43 -0800 Subject: [PATCH 06/17] Expose new method in Fabric renderer to access root instance from root tag (#49011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49011 Changelog: [internal] This exposes the new `getPublicInstanceFromRoot` method from the React renderer in our RN façades, preparing for the new change to implement the document interface in RN. Reviewed By: javache Differential Revision: D68767143 fbshipit-source-id: 9a3403f9bc1612b402305695d084497a46ee4480 --- .../Libraries/ReactNative/RendererImplementation.js | 9 +++++++++ .../Libraries/Renderer/shims/ReactNativeTypes.js | 3 +++ .../__tests__/__snapshots__/public-api-test.js.snap | 1 + 3 files changed, 13 insertions(+) diff --git a/packages/react-native/Libraries/ReactNative/RendererImplementation.js b/packages/react-native/Libraries/ReactNative/RendererImplementation.js index e2d25090794b94..7ff71985c03987 100644 --- a/packages/react-native/Libraries/ReactNative/RendererImplementation.js +++ b/packages/react-native/Libraries/ReactNative/RendererImplementation.js @@ -162,3 +162,12 @@ export function getPublicInstanceFromInternalInstanceHandle( internalInstanceHandle, ); } + +export function getPublicInstanceFromRootTag( + rootTag: number, +): mixed /*PublicRootInstance | null*/ { + // This is only available in Fabric + return require('../Renderer/shims/ReactFabric').default.getPublicInstanceFromRootTag( + rootTag, + ); +} diff --git a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js index 7a3c7410ab664a..f7ed3267d017bb 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js @@ -262,6 +262,9 @@ export type ReactFabricType = { getPublicInstanceFromInternalInstanceHandle( internalInstanceHandle: InternalInstanceHandle, ): PublicInstance | PublicTextInstance | null, + getPublicInstanceFromRootTag( + rootTag: number, + ): PublicRootInstance | null, ... }; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index b5903211123e33..1817129aa11caf 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -7622,6 +7622,7 @@ declare export function getNodeFromInternalInstanceHandle( declare export function getPublicInstanceFromInternalInstanceHandle( internalInstanceHandle: InternalInstanceHandle ): mixed; +declare export function getPublicInstanceFromRootTag(rootTag: number): mixed; " `; From 2436e3ba8473e655c1a3d8e278dd7d084e149316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 30 Jan 2025 07:11:43 -0800 Subject: [PATCH 07/17] Implement ReactNativeDocument (#49012) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49012 Changelog: [internal] (This is internal for now, until we rollout the DOM APIs in stable). This refines the concept of root elements from the merged proposal for [DOM Traversal & Layout APIs](https://github.com/react-native-community/discussions-and-proposals/blob/main/proposals/0607-dom-traversal-and-layout-apis.md). The original proposal included a reference to have the root node in the tree as `getRootNode()` and no other methods/accessors to access it. This makes the following changes: * The root node is a new abstraction in React Native implementing the concept of `Document` from Web. `node.getRootNode()`, as well as `node.ownerDocument` now return instances to this node (except when the node is detached, in which case `getRootNode` returns the node itself, aligning with the spec). * The existing root node in the shadow tree is exposed as the `documentElement` of the new document instance. It would be the first and only child of the document instance, and the topmost parent of all the host nodes rendered in the tree. In terms of APIs: * Implements `getRootNode` correctly, according to the specified semantics. * Adds `ownerDocument` to the `ReadOnlyNode` interface. * Adds the `ReactNativeDocument` interface, which extends `ReadOnlyNode` (with no new methods on its own, which will be added in a following PR). NOTE: This is currently gated under `ReactNativeFeatureFlags.enableDOMDocumentAPI` feature flag, which is disabled by default. Reviewed By: yungsters Differential Revision: D67526381 fbshipit-source-id: dff3645469e7ea2b2026dbbaa94d9fd0e00291be --- .../Libraries/ReactNative/FabricUIManager.js | 2 +- .../ReactFabricPublicInstance.js | 115 +- ...actFabricPublicInstance-benchmark-itest.js | 13 +- .../__snapshots__/public-api-test.js.snap | 47 +- .../react/nativemodule/dom/NativeDOM.cpp | 123 +- .../react/nativemodule/dom/NativeDOM.h | 9 + .../components/root/RootShadowNode.cpp | 5 + .../renderer/components/root/RootShadowNode.h | 2 + .../react/renderer/core/ShadowNodeFamily.cpp | 9 + .../react/renderer/core/ShadowNodeFamily.h | 5 +- .../ReactCommon/react/renderer/dom/DOM.cpp | 14 +- .../ReactCommon/react/renderer/dom/DOM.h | 6 + .../ReactNativeFeatureFlags.config.js | 11 + .../featureflags/ReactNativeFeatureFlags.js | 8 +- .../webapis/dom/nodes/ReactNativeDocument.js | 100 ++ .../webapis/dom/nodes/ReactNativeElement.js | 50 +- .../dom/nodes/ReadOnlyCharacterData.js | 4 +- .../private/webapis/dom/nodes/ReadOnlyNode.js | 78 +- .../__tests__/ReactNativeDocument-itest.js | 203 +++ .../ReactNativeElementWithDocument-itest.js | 1323 +++++++++++++++++ .../dom/nodes/internals/NodeInternals.js | 102 +- ...eactNativeDocumentElementInstanceHandle.js | 55 + .../ReactNativeDocumentInstanceHandle.js | 43 + .../webapis/dom/nodes/specs/NativeDOM.js | 60 +- .../internals/MutationObserverManager.js | 6 +- 25 files changed, 2246 insertions(+), 147 deletions(-) create mode 100644 packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js create mode 100644 packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js create mode 100644 packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElementWithDocument-itest.js create mode 100644 packages/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentElementInstanceHandle.js create mode 100644 packages/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentInstanceHandle.js diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index ba125fe3987b1b..97888852f30524 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow strict + * @flow strict-local * @format */ diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js index 0d9d385cba10e6..fadc102da4be4c 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js @@ -13,33 +13,74 @@ * instances and get some data from them (like their instance handle / fiber). */ -import type ReactNativeElement from '../../../src/private/webapis/dom/nodes/ReactNativeElement'; -import type ReadOnlyText from '../../../src/private/webapis/dom/nodes/ReadOnlyText'; +import type ReactNativeDocumentT from '../../../src/private/webapis/dom/nodes/ReactNativeDocument'; +import typeof * as ReactNativeDocumentModuleT from '../../../src/private/webapis/dom/nodes/ReactNativeDocument'; +import type ReactNativeElementT from '../../../src/private/webapis/dom/nodes/ReactNativeElement'; +import type ReadOnlyTextT from '../../../src/private/webapis/dom/nodes/ReadOnlyText'; import typeof * as RendererProxyT from '../../ReactNative/RendererProxy'; import type { InternalInstanceHandle, Node, + PublicRootInstance, ViewConfig, } from '../../Renderer/shims/ReactNativeTypes'; import type {RootTag} from '../RootTag'; -import type ReactFabricHostComponent from './ReactFabricHostComponent'; +import type ReactFabricHostComponentT from './ReactFabricHostComponent'; import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; // Lazy loaded to avoid evaluating the module when using the legacy renderer. -let PublicInstanceClass: - | Class - | Class; -let ReadOnlyTextClass: Class; - -// Lazy loaded to avoid evaluating the module when using the legacy renderer. +let ReactNativeDocumentModuleObject: ?ReactNativeDocumentModuleT; +let ReactFabricHostComponentClass: Class; +let ReactNativeElementClass: Class; +let ReadOnlyTextClass: Class; let RendererProxy: RendererProxyT; -// This is just a temporary placeholder so ReactFabric doesn't crash when synced. -type PublicRootInstance = null; +function getReactNativeDocumentModule(): ReactNativeDocumentModuleT { + if (ReactNativeDocumentModuleObject == null) { + // We initialize this lazily to avoid a require cycle. + ReactNativeDocumentModuleObject = require('../../../src/private/webapis/dom/nodes/ReactNativeDocument'); + } + + return ReactNativeDocumentModuleObject; +} + +function getReactNativeElementClass(): Class { + if (ReactNativeElementClass == null) { + ReactNativeElementClass = + require('../../../src/private/webapis/dom/nodes/ReactNativeElement').default; + } + return ReactNativeElementClass; +} + +function getReactFabricHostComponentClass(): Class { + if (ReactFabricHostComponentClass == null) { + ReactFabricHostComponentClass = + require('./ReactFabricHostComponent').default; + } + return ReactFabricHostComponentClass; +} + +function getReadOnlyTextClass(): Class { + if (ReadOnlyTextClass == null) { + ReadOnlyTextClass = + require('../../../src/private/webapis/dom/nodes/ReadOnlyText').default; + } + return ReadOnlyTextClass; +} export function createPublicRootInstance(rootTag: RootTag): PublicRootInstance { - // This is just a placeholder so ReactFabric doesn't crash when synced. + if ( + ReactNativeFeatureFlags.enableAccessToHostTreeInFabric() && + ReactNativeFeatureFlags.enableDOMDocumentAPI() + ) { + const ReactNativeDocumentModule = getReactNativeDocumentModule(); + + // $FlowExpectedError[incompatible-return] + return ReactNativeDocumentModule.createReactNativeDocument(rootTag); + } + + // $FlowExpectedError[incompatible-return] return null; } @@ -47,42 +88,42 @@ export function createPublicInstance( tag: number, viewConfig: ViewConfig, internalInstanceHandle: InternalInstanceHandle, - ownerDocument: PublicRootInstance, -): ReactFabricHostComponent | ReactNativeElement { - if (PublicInstanceClass == null) { - // We don't use inline requires in react-native, so this forces lazy loading - // the right module to avoid eagerly loading both. - if (ReactNativeFeatureFlags.enableAccessToHostTreeInFabric()) { - PublicInstanceClass = - require('../../../src/private/webapis/dom/nodes/ReactNativeElement').default; - } else { - PublicInstanceClass = require('./ReactFabricHostComponent').default; - } + ownerDocument: ReactNativeDocumentT, +): ReactFabricHostComponentT | ReactNativeElementT { + if (ReactNativeFeatureFlags.enableAccessToHostTreeInFabric()) { + const ReactNativeElement = getReactNativeElementClass(); + return new ReactNativeElement( + tag, + viewConfig, + internalInstanceHandle, + ownerDocument, + ); + } else { + const ReactFabricHostComponent = getReactFabricHostComponentClass(); + return new ReactFabricHostComponent( + tag, + viewConfig, + internalInstanceHandle, + ); } - - return new PublicInstanceClass(tag, viewConfig, internalInstanceHandle); } export function createPublicTextInstance( internalInstanceHandle: InternalInstanceHandle, - ownerDocument: PublicRootInstance, -): ReadOnlyText { - if (ReadOnlyTextClass == null) { - ReadOnlyTextClass = - require('../../../src/private/webapis/dom/nodes/ReadOnlyText').default; - } - - return new ReadOnlyTextClass(internalInstanceHandle); + ownerDocument: ReactNativeDocumentT, +): ReadOnlyTextT { + const ReadOnlyText = getReadOnlyTextClass(); + return new ReadOnlyText(internalInstanceHandle, ownerDocument); } export function getNativeTagFromPublicInstance( - publicInstance: ReactFabricHostComponent | ReactNativeElement, + publicInstance: ReactFabricHostComponentT | ReactNativeElementT, ): number { return publicInstance.__nativeTag; } export function getNodeFromPublicInstance( - publicInstance: ReactFabricHostComponent | ReactNativeElement, + publicInstance: ReactFabricHostComponentT | ReactNativeElementT, ): ?Node { // Avoid loading ReactFabric if using an instance from the legacy renderer. if (publicInstance.__internalInstanceHandle == null) { @@ -93,12 +134,13 @@ export function getNodeFromPublicInstance( RendererProxy = require('../../ReactNative/RendererProxy'); } return RendererProxy.getNodeFromInternalInstanceHandle( + // $FlowExpectedError[incompatible-call] __internalInstanceHandle is always an InternalInstanceHandle from React when we get here. publicInstance.__internalInstanceHandle, ); } export function getInternalInstanceHandleFromPublicInstance( - publicInstance: ReactFabricHostComponent | ReactNativeElement, + publicInstance: ReactFabricHostComponentT | ReactNativeElementT, ): InternalInstanceHandle { // TODO(T174762768): Remove this once OSS versions of renderers will be synced. // $FlowExpectedError[prop-missing] Keeping this for backwards-compatibility with the renderers versions in open source. @@ -107,5 +149,6 @@ export function getInternalInstanceHandleFromPublicInstance( return publicInstance._internalInstanceHandle; } + // $FlowExpectedError[incompatible-return] __internalInstanceHandle is always an InternalInstanceHandle from React when we get here. return publicInstance.__internalInstanceHandle; } diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-benchmark-itest.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-benchmark-itest.js index 0bd47f3dde636b..6fbffd7a5d8d4c 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-benchmark-itest.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/ReactFabricPublicInstance-benchmark-itest.js @@ -10,6 +10,7 @@ */ import '../../../Core/InitializeCore.js'; +import type ReactNativeDocument from '../../../../src/private/webapis/dom/nodes/ReactNativeDocument'; import type { InternalInstanceHandle, ViewConfig, @@ -29,14 +30,20 @@ const viewConfig: ViewConfig = { }; // $FlowExpectedError[incompatible-type] const internalInstanceHandle: InternalInstanceHandle = {}; +// $FlowExpectedError[incompatible-type] +const ownerDocument: ReactNativeDocument = {}; +/* eslint-disable no-new */ Fantom.unstable_benchmark .suite('ReactNativeElement vs. ReactFabricHostComponent') .add('ReactNativeElement', () => { - // eslint-disable-next-line no-new - new ReactNativeElement(tag, viewConfig, internalInstanceHandle); + new ReactNativeElement( + tag, + viewConfig, + internalInstanceHandle, + ownerDocument, + ); }) .add('ReactFabricHostComponent', () => { - // eslint-disable-next-line no-new new ReactFabricHostComponent(tag, viewConfig, internalInstanceHandle); }); diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 1817129aa11caf..a280e7b152e478 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -7509,28 +7509,27 @@ exports[`public API should not change unintentionally Libraries/ReactNative/Reac `; exports[`public API should not change unintentionally Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js 1`] = ` -"type PublicRootInstance = null; -declare export function createPublicRootInstance( +"declare export function createPublicRootInstance( rootTag: RootTag ): PublicRootInstance; declare export function createPublicInstance( tag: number, viewConfig: ViewConfig, internalInstanceHandle: InternalInstanceHandle, - ownerDocument: PublicRootInstance -): ReactFabricHostComponent | ReactNativeElement; + ownerDocument: ReactNativeDocumentT +): ReactFabricHostComponentT | ReactNativeElementT; declare export function createPublicTextInstance( internalInstanceHandle: InternalInstanceHandle, - ownerDocument: PublicRootInstance -): ReadOnlyText; + ownerDocument: ReactNativeDocumentT +): ReadOnlyTextT; declare export function getNativeTagFromPublicInstance( - publicInstance: ReactFabricHostComponent | ReactNativeElement + publicInstance: ReactFabricHostComponentT | ReactNativeElementT ): number; declare export function getNodeFromPublicInstance( - publicInstance: ReactFabricHostComponent | ReactNativeElement + publicInstance: ReactFabricHostComponentT | ReactNativeElementT ): ?Node; declare export function getInternalInstanceHandleFromPublicInstance( - publicInstance: ReactFabricHostComponent | ReactNativeElement + publicInstance: ReactFabricHostComponentT | ReactNativeElementT ): InternalInstanceHandle; " `; @@ -11306,18 +11305,38 @@ declare export default class DOMRectReadOnly { " `; +exports[`public API should not change unintentionally src/private/webapis/dom/nodes/ReactNativeDocument.js 1`] = ` +"declare export default class ReactNativeDocument extends ReadOnlyNode { + _documentElement: ReactNativeElement; + constructor( + rootTag: RootTag, + instanceHandle: ReactNativeDocumentInstanceHandle + ): void; + get documentElement(): ReactNativeElement; + get nodeName(): string; + get nodeType(): number; + get nodeValue(): null; + get textContent(): null; +} +declare export function createReactNativeDocument( + rootTag: RootTag +): ReactNativeDocument; +" +`; + exports[`public API should not change unintentionally src/private/webapis/dom/nodes/ReactNativeElement.js 1`] = ` "declare class ReactNativeElementMethods extends ReadOnlyElement implements INativeMethods { __nativeTag: number; - __internalInstanceHandle: InternalInstanceHandle; + __internalInstanceHandle: InstanceHandle; __viewConfig: ViewConfig; constructor( tag: number, viewConfig: ViewConfig, - internalInstanceHandle: InternalInstanceHandle + instanceHandle: InstanceHandle, + ownerDocument: ReactNativeDocument ): void; get offsetHeight(): number; get offsetLeft(): number; @@ -11389,7 +11408,10 @@ declare export function getBoundingClientRect( exports[`public API should not change unintentionally src/private/webapis/dom/nodes/ReadOnlyNode.js 1`] = ` "declare export default class ReadOnlyNode { - constructor(internalInstanceHandle: InternalInstanceHandle): void; + constructor( + instanceHandle: InstanceHandle, + ownerDocument: ReactNativeDocument | null + ): void; get childNodes(): NodeList; get firstChild(): ReadOnlyNode | null; get isConnected(): boolean; @@ -11398,6 +11420,7 @@ exports[`public API should not change unintentionally src/private/webapis/dom/no get nodeName(): string; get nodeType(): number; get nodeValue(): string | null; + get ownerDocument(): ReactNativeDocument | null; get parentElement(): ReadOnlyElement | null; get parentNode(): ReadOnlyNode | null; get previousSibling(): ReadOnlyNode | null; diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp index 8cee5575d58f99..41821f60f51578 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp @@ -34,6 +34,19 @@ static RootShadowNode::Shared getCurrentShadowTreeRevision( return shadowTreeRevisionProvider->getCurrentRevision(surfaceId); } +static RootShadowNode::Shared getCurrentShadowTreeRevision( + facebook::jsi::Runtime& runtime, + jsi::Value& nativeNodeReference) { + if (nativeNodeReference.isNumber()) { + return getCurrentShadowTreeRevision( + runtime, static_cast(nativeNodeReference.asNumber())); + } + + return getCurrentShadowTreeRevision( + runtime, + shadowNodeFromValue(runtime, nativeNodeReference)->getSurfaceId()); +} + static facebook::react::PointerEventsProcessor& getPointerEventsProcessorFromRuntime(facebook::jsi::Runtime& runtime) { return facebook::react::UIManagerBinding::getBinding(runtime) @@ -59,6 +72,10 @@ getArrayOfInstanceHandlesFromShadowNodes( return nonNullInstanceHandles; } +static bool isRootShadowNode(const ShadowNode& shadowNode) { + return shadowNode.getTraits().check(ShadowNodeTraits::Trait::RootNodeKind); +} + #pragma mark - NativeDOM NativeDOM::NativeDOM(std::shared_ptr jsInvoker) @@ -70,12 +87,56 @@ double NativeDOM::compareDocumentPosition( jsi::Runtime& rt, jsi::Value nativeNodeReference, jsi::Value otherNativeNodeReference) { - auto shadowNode = shadowNodeFromValue(rt, nativeNodeReference); - auto otherShadowNode = shadowNodeFromValue(rt, otherNativeNodeReference); - auto currentRevision = - getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); - if (otherShadowNode == nullptr || currentRevision == nullptr) { - return 0; + auto currentRevision = getCurrentShadowTreeRevision(rt, nativeNodeReference); + if (currentRevision == nullptr) { + return dom::DOCUMENT_POSITION_DISCONNECTED; + } + + ShadowNode::Shared shadowNode; + ShadowNode::Shared otherShadowNode; + + // Check if document references are used + if (nativeNodeReference.isNumber() || otherNativeNodeReference.isNumber()) { + if (nativeNodeReference.isNumber() && otherNativeNodeReference.isNumber()) { + // Both are documents (and equality is handled in JS directly). + return dom::DOCUMENT_POSITION_DISCONNECTED; + } else if (nativeNodeReference.isNumber()) { + // Only the first is a document + auto surfaceId = nativeNodeReference.asNumber(); + shadowNode = currentRevision; + otherShadowNode = shadowNodeFromValue(rt, otherNativeNodeReference); + + if (isRootShadowNode(*otherShadowNode)) { + // If the other is a root node, we just need to check if it is its + // `documentElement` + return (surfaceId == otherShadowNode->getSurfaceId()) + ? dom::DOCUMENT_POSITION_CONTAINED_BY | + dom::DOCUMENT_POSITION_FOLLOWING + : dom::DOCUMENT_POSITION_DISCONNECTED; + } else { + // Otherwise, we'll use the root node to represent the document + // (the result should be the same) + } + } else { + // Only the second is a document + auto otherSurfaceId = otherNativeNodeReference.asNumber(); + shadowNode = shadowNodeFromValue(rt, nativeNodeReference); + otherShadowNode = getCurrentShadowTreeRevision(rt, otherSurfaceId); + + if (isRootShadowNode(*shadowNode)) { + // If this is a root node, we just need to check if the other is its + // document. + return (otherSurfaceId == shadowNode->getSurfaceId()) + ? dom::DOCUMENT_POSITION_CONTAINS | dom::DOCUMENT_POSITION_PRECEDING + : dom::DOCUMENT_POSITION_DISCONNECTED; + } else { + // Otherwise, we'll use the root node to represent the document + // (the result should be the same) + } + } + } else { + shadowNode = shadowNodeFromValue(rt, nativeNodeReference); + otherShadowNode = shadowNodeFromValue(rt, otherNativeNodeReference); } return dom::compareDocumentPosition( @@ -85,21 +146,35 @@ double NativeDOM::compareDocumentPosition( std::vector NativeDOM::getChildNodes( jsi::Runtime& rt, jsi::Value nativeNodeReference) { - auto shadowNode = shadowNodeFromValue(rt, nativeNodeReference); - auto currentRevision = - getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + auto currentRevision = getCurrentShadowTreeRevision(rt, nativeNodeReference); if (currentRevision == nullptr) { return std::vector{}; } - auto childNodes = dom::getChildNodes(currentRevision, *shadowNode); + // The only child node of the document is the root node. + if (nativeNodeReference.isNumber()) { + return getArrayOfInstanceHandlesFromShadowNodes({currentRevision}, rt); + } + + auto childNodes = dom::getChildNodes( + currentRevision, *shadowNodeFromValue(rt, nativeNodeReference)); return getArrayOfInstanceHandlesFromShadowNodes(childNodes, rt); } jsi::Value NativeDOM::getParentNode( jsi::Runtime& rt, jsi::Value nativeNodeReference) { + // The document does not have a parent node. + if (nativeNodeReference.isNumber()) { + return jsi::Value::undefined(); + } + auto shadowNode = shadowNodeFromValue(rt, nativeNodeReference); + if (isRootShadowNode(*shadowNode)) { + // The parent of the root node is the document. + return jsi::Value{shadowNode->getSurfaceId()}; + } + auto currentRevision = getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); if (currentRevision == nullptr) { @@ -115,13 +190,17 @@ jsi::Value NativeDOM::getParentNode( } bool NativeDOM::isConnected(jsi::Runtime& rt, jsi::Value nativeNodeReference) { - auto shadowNode = shadowNodeFromValue(rt, nativeNodeReference); - auto currentRevision = - getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + auto currentRevision = getCurrentShadowTreeRevision(rt, nativeNodeReference); if (currentRevision == nullptr) { return false; } + // The document is connected because we got a value for current revision. + if (nativeNodeReference.isNumber()) { + return true; + } + + auto shadowNode = shadowNodeFromValue(rt, nativeNodeReference); return dom::isConnected(currentRevision, *shadowNode); } @@ -278,6 +357,24 @@ NativeDOM::getOffset(jsi::Runtime& rt, jsi::Value nativeElementReference) { domOffset.left}; } +#pragma mark - Special methods to handle the root node. + +jsi::Value NativeDOM::linkRootNode( + jsi::Runtime& rt, + SurfaceId surfaceId, + jsi::Value instanceHandle) { + auto currentRevision = getCurrentShadowTreeRevision(rt, surfaceId); + if (currentRevision == nullptr) { + return jsi::Value::undefined(); + } + + auto instanceHandleWrapper = + std::make_shared(rt, instanceHandle, surfaceId); + currentRevision->setInstanceHandle(instanceHandleWrapper); + + return valueFromShadowNode(rt, currentRevision); +} + #pragma mark - Legacy layout APIs (for `ReactNativeElement`). void NativeDOM::measure( diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h index 7050faa13213da..268cd1ba85fe6a 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h @@ -17,6 +17,8 @@ #include #endif +#include + namespace facebook::react { class NativeDOM : public NativeDOMCxxSpec { @@ -95,6 +97,13 @@ class NativeDOM : public NativeDOMCxxSpec { /* left: */ double> getOffset(jsi::Runtime& rt, jsi::Value nativeElementReference); +#pragma mark - Special methods to handle the root node. + + jsi::Value linkRootNode( + jsi::Runtime& rt, + SurfaceId surfaceId, + jsi::Value instanceHandle); + #pragma mark - Legacy layout APIs (for `ReactNativeElement`). void measure( diff --git a/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.cpp index ab0290566deb95..860ec2b9d81f38 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.cpp @@ -56,4 +56,9 @@ RootShadowNode::Unshared RootShadowNode::clone( return newRootShadowNode; } +void RootShadowNode::setInstanceHandle( + InstanceHandle::Shared instanceHandle) const { + getFamily().setInstanceHandle(instanceHandle); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.h index 321ea590d4c0a2..fce4754d52c90f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.h @@ -56,6 +56,8 @@ class RootShadowNode final const LayoutContext& layoutContext) const; Transform getTransform() const override; + + void setInstanceHandle(InstanceHandle::Shared instanceHandle) const; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.cpp b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.cpp index a27b816a460da1..543ce2abe847f5 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.cpp @@ -76,6 +76,15 @@ Tag ShadowNodeFamily::getTag() const { return tag_; } +InstanceHandle::Shared ShadowNodeFamily::getInstanceHandle() const { + return instanceHandle_; +} + +void ShadowNodeFamily::setInstanceHandle( + InstanceHandle::Shared& instanceHandle) const { + instanceHandle_ = instanceHandle; +} + ShadowNodeFamily::~ShadowNodeFamily() { if (!hasBeenMounted_ && onUnmountedFamilyDestroyedCallback_ != nullptr) { onUnmountedFamilyDestroyedCallback_(*this); diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h index c06213774192dd..203f895ff7cc16 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h @@ -122,6 +122,9 @@ class ShadowNodeFamily final { */ Tag getTag() const; + InstanceHandle::Shared getInstanceHandle() const; + void setInstanceHandle(InstanceHandle::Shared& instanceHandle) const; + /** * Override destructor to call onUnmountedFamilyDestroyedCallback() for * ShadowViews that were preallocated but never mounted on the screen. @@ -160,7 +163,7 @@ class ShadowNodeFamily final { /* * Weak reference to the React instance handle */ - const InstanceHandle::Shared instanceHandle_; + mutable InstanceHandle::Shared instanceHandle_; /* * `EventEmitter` associated with all nodes of the family. diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp index a9b6645d541aae..811e59cdb1fb93 100644 --- a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp @@ -22,12 +22,6 @@ using facebook::react::Point; using facebook::react::Rect; using facebook::react::Size; -constexpr uint_fast16_t DOCUMENT_POSITION_DISCONNECTED = 1; -constexpr uint_fast16_t DOCUMENT_POSITION_PRECEDING = 2; -constexpr uint_fast16_t DOCUMENT_POSITION_FOLLOWING = 4; -constexpr uint_fast16_t DOCUMENT_POSITION_CONTAINS = 8; -constexpr uint_fast16_t DOCUMENT_POSITION_CONTAINED_BY = 16; - ShadowNode::Shared getShadowNodeInRevision( const RootShadowNode::Shared& currentRevision, const ShadowNode& shadowNode) { @@ -206,12 +200,20 @@ uint_fast16_t compareDocumentPosition( auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision); if (ancestors.empty()) { + if (ShadowNode::sameFamily(*currentRevision, shadowNode)) { + // shadowNode is the root + return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING; + } return DOCUMENT_POSITION_DISCONNECTED; } auto otherAncestors = otherShadowNode.getFamily().getAncestors(*currentRevision); if (otherAncestors.empty()) { + if (ShadowNode::sameFamily(*currentRevision, otherShadowNode)) { + // otherShadowNode is the root + return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING; + } return DOCUMENT_POSITION_DISCONNECTED; } diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.h b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h index bd79309608274e..056c9d3674d6ef 100644 --- a/packages/react-native/ReactCommon/react/renderer/dom/DOM.h +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h @@ -16,6 +16,12 @@ namespace facebook::react::dom { +constexpr uint_fast16_t DOCUMENT_POSITION_DISCONNECTED = 1; +constexpr uint_fast16_t DOCUMENT_POSITION_PRECEDING = 2; +constexpr uint_fast16_t DOCUMENT_POSITION_FOLLOWING = 4; +constexpr uint_fast16_t DOCUMENT_POSITION_CONTAINS = 8; +constexpr uint_fast16_t DOCUMENT_POSITION_CONTAINED_BY = 16; + struct DOMRect { double x = 0; double y = 0; diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 5776f00b7574b8..e28c892c95a247 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -502,6 +502,17 @@ const definitions: FeatureFlagDefinitions = { purpose: 'experimentation', }, }, + enableDOMDocumentAPI: { + defaultValue: false, + metadata: { + dateAdded: '2025-01-28', + description: + 'Enables the DOM Document API, exposing instaces of document through `getRootNode` and `ownerDocument`, and providing access to the `documentElement` representing the root node. ' + + 'This flag will be short-lived, only to test the Document API specifically, and then it will be collapsed into the enableAccessToHostTreeInFabric flag.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + }, fixVirtualizeListCollapseWindowSize: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 1f40abdfca7378..96fc19ceb22e67 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3c609bd4ace6147f8f32af07a5e3cc05>> + * @generated SignedSource<> * @flow strict */ @@ -33,6 +33,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{ disableInteractionManager: Getter, enableAccessToHostTreeInFabric: Getter, enableAnimatedClearImmediateFix: Getter, + enableDOMDocumentAPI: Getter, fixVirtualizeListCollapseWindowSize: Getter, isLayoutAnimationEnabled: Getter, scheduleAnimatedCleanupInMicrotask: Getter, @@ -122,6 +123,11 @@ export const enableAccessToHostTreeInFabric: Getter = createJavaScriptF */ export const enableAnimatedClearImmediateFix: Getter = createJavaScriptFlagGetter('enableAnimatedClearImmediateFix', true); +/** + * Enables the DOM Document API, exposing instaces of document through `getRootNode` and `ownerDocument`, and providing access to the `documentElement` representing the root node. This flag will be short-lived, only to test the Document API specifically, and then it will be collapsed into the enableAccessToHostTreeInFabric flag. + */ +export const enableDOMDocumentAPI: Getter = createJavaScriptFlagGetter('enableDOMDocumentAPI', false); + /** * Fixing an edge case where the current window size is not properly calculated with fast scrolling. Window size collapsed to 1 element even if windowSize more than the current amount of elements */ diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js new file mode 100644 index 00000000000000..97ed4dfe2c0667 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js @@ -0,0 +1,100 @@ +/** + * 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 {RootTag} from '../../../../../Libraries/ReactNative/RootTag'; +import type {ViewConfig} from '../../../../../Libraries/Renderer/shims/ReactNativeTypes'; +import type {ReactNativeDocumentInstanceHandle} from './internals/ReactNativeDocumentInstanceHandle'; + +import { + createReactNativeDocumentElementInstanceHandle, + setNativeElementReferenceForReactNativeDocumentElementInstanceHandle, + setPublicInstanceForReactNativeDocumentElementInstanceHandle, +} from './internals/ReactNativeDocumentElementInstanceHandle'; +import {createReactNativeDocumentInstanceHandle} from './internals/ReactNativeDocumentInstanceHandle'; +import ReactNativeElement from './ReactNativeElement'; +import ReadOnlyNode from './ReadOnlyNode'; +import NativeDOM from './specs/NativeDOM'; + +export default class ReactNativeDocument extends ReadOnlyNode { + _documentElement: ReactNativeElement; + + constructor( + rootTag: RootTag, + instanceHandle: ReactNativeDocumentInstanceHandle, + ) { + super(instanceHandle, null); + this._documentElement = createDocumentElement(rootTag, this); + } + + get documentElement(): ReactNativeElement { + return this._documentElement; + } + + get nodeName(): string { + return '#document'; + } + + get nodeType(): number { + return ReadOnlyNode.DOCUMENT_NODE; + } + + get nodeValue(): null { + return null; + } + + get textContent(): null { + return null; + } +} + +function createDocumentElement( + rootTag: RootTag, + ownerDocument: ReactNativeDocument, +): ReactNativeElement { + // In the case of the document object, React does not create an instance + // handle for it, so we create a custom one. + const instanceHandle = createReactNativeDocumentElementInstanceHandle(); + + // $FlowExpectedError[incompatible-type] + const rootTagIsNumber: number = rootTag; + // $FlowExpectedError[incompatible-type] + const viewConfig: ViewConfig = null; + + const documentElement = new ReactNativeElement( + rootTagIsNumber, + viewConfig, + instanceHandle, + ownerDocument, + ); + + // The root shadow node was created ahead of time without an instance + // handle, so we need to link them now. + const rootShadowNode = NativeDOM.linkRootNode(rootTag, instanceHandle); + setNativeElementReferenceForReactNativeDocumentElementInstanceHandle( + instanceHandle, + rootShadowNode, + ); + setPublicInstanceForReactNativeDocumentElementInstanceHandle( + instanceHandle, + documentElement, + ); + + return documentElement; +} + +export function createReactNativeDocument( + rootTag: RootTag, +): ReactNativeDocument { + const instanceHandle = createReactNativeDocumentInstanceHandle(rootTag); + const document = new ReactNativeDocument(rootTag, instanceHandle); + return document; +} diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js index 7b6bfb8258eb2d..0326779a6ea392 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js @@ -17,8 +17,11 @@ import type { MeasureInWindowOnSuccessCallback, MeasureLayoutOnSuccessCallback, MeasureOnSuccessCallback, + Node as ShadowNode, ViewConfig, } from '../../../../../Libraries/Renderer/shims/ReactNativeTypes'; +import type {InstanceHandle} from './internals/NodeInternals'; +import type ReactNativeDocument from './ReactNativeDocument'; import TextInputState from '../../../../../Libraries/Components/TextInput/TextInputState'; import {getFabricUIManager} from '../../../../../Libraries/ReactNative/FabricUIManager'; @@ -26,11 +29,11 @@ import {create as createAttributePayload} from '../../../../../Libraries/ReactNa import warnForStyleProps from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps'; import { getNativeElementReference, - getPublicInstanceFromInternalInstanceHandle, + getPublicInstanceFromInstanceHandle, setInstanceHandle, + setOwnerDocument, } from './internals/NodeInternals'; import ReadOnlyElement, {getBoundingClientRect} from './ReadOnlyElement'; -import ReadOnlyNode from './ReadOnlyNode'; import NativeDOM from './specs/NativeDOM'; import nullthrows from 'nullthrows'; @@ -61,7 +64,7 @@ class ReactNativeElementMethods { // These need to be accessible from `ReactFabricPublicInstanceUtils`. __nativeTag: number; - __internalInstanceHandle: InternalInstanceHandle; + __internalInstanceHandle: InstanceHandle; __viewConfig: ViewConfig; @@ -70,12 +73,13 @@ class ReactNativeElementMethods constructor( tag: number, viewConfig: ViewConfig, - internalInstanceHandle: InternalInstanceHandle, + instanceHandle: InstanceHandle, + ownerDocument: ReactNativeDocument, ) { - super(internalInstanceHandle); + super(instanceHandle, ownerDocument); this.__nativeTag = tag; - this.__internalInstanceHandle = internalInstanceHandle; + this.__internalInstanceHandle = instanceHandle; this.__viewConfig = viewConfig; } @@ -106,7 +110,7 @@ class ReactNativeElementMethods // in JavaScript yet. if (offset[0] != null) { const offsetParentInstanceHandle = offset[0]; - const offsetParent = getPublicInstanceFromInternalInstanceHandle( + const offsetParent = getPublicInstanceFromInstanceHandle( offsetParentInstanceHandle, ); // $FlowExpectedError[incompatible-type] The value returned by `getOffset` is always an instance handle for `ReadOnlyElement`. @@ -152,14 +156,18 @@ class ReactNativeElementMethods measure(callback: MeasureOnSuccessCallback) { const node = getNativeElementReference(this); if (node != null) { - nullthrows(getFabricUIManager()).measure(node, callback); + // $FlowExpectedError[incompatible-type] This is an element instance so the native node reference is always a shadow node. + const shadowNode: ShadowNode = node; + nullthrows(getFabricUIManager()).measure(shadowNode, callback); } } measureInWindow(callback: MeasureInWindowOnSuccessCallback) { const node = getNativeElementReference(this); if (node != null) { - nullthrows(getFabricUIManager()).measureInWindow(node, callback); + // $FlowExpectedError[incompatible-type] This is an element instance so the native node reference is always a shadow node. + const shadowNode: ShadowNode = node; + nullthrows(getFabricUIManager()).measureInWindow(shadowNode, callback); } } @@ -168,7 +176,7 @@ class ReactNativeElementMethods onSuccess: MeasureLayoutOnSuccessCallback, onFail?: () => void /* currently unused */, ) { - if (!(relativeToNativeNode instanceof ReadOnlyNode)) { + if (!(relativeToNativeNode instanceof ReactNativeElement)) { if (__DEV__) { console.error( 'Warning: ref.measureLayout must be called with a ref to a native component.', @@ -182,9 +190,14 @@ class ReactNativeElementMethods const fromStateNode = getNativeElementReference(relativeToNativeNode); if (toStateNode != null && fromStateNode != null) { + // $FlowExpectedError[incompatible-type] This is an element instance so the native node reference is always a shadow node. + const toStateShadowNode: ShadowNode = toStateNode; + // $FlowExpectedError[incompatible-type] This is an element instance so the native node reference is always a shadow node. + const fromStateShadowNode: ShadowNode = fromStateNode; + nullthrows(getFabricUIManager()).measureLayout( - toStateNode, - fromStateNode, + toStateShadowNode, + fromStateShadowNode, onFail != null ? onFail : noop, onSuccess != null ? onSuccess : noop, ); @@ -204,7 +217,12 @@ class ReactNativeElementMethods const node = getNativeElementReference(this); if (node != null && updatePayload != null) { - nullthrows(getFabricUIManager()).setNativeProps(node, updatePayload); + // $FlowExpectedError[incompatible-type] This is an element instance so the native node reference is always a shadow node. + const shadowNode: ShadowNode = node; + nullthrows(getFabricUIManager()).setNativeProps( + shadowNode, + updatePayload, + ); } } } @@ -216,11 +234,15 @@ function ReactNativeElement( tag: number, viewConfig: ViewConfig, internalInstanceHandle: InternalInstanceHandle, + ownerDocument: ReactNativeDocument, ) { + // Inlined from `ReadOnlyNode` + setOwnerDocument(this, ownerDocument); + setInstanceHandle(this, internalInstanceHandle); + this.__nativeTag = tag; this.__internalInstanceHandle = internalInstanceHandle; this.__viewConfig = viewConfig; - setInstanceHandle(this, internalInstanceHandle); } ReactNativeElement.prototype = Object.create( diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js index 5e6a6ee02fc1d1..2d6857d23a3939 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js @@ -12,7 +12,7 @@ import type ReadOnlyElement from './ReadOnlyElement'; -import {getNativeNodeReference} from './internals/NodeInternals'; +import {getNativeTextReference} from './internals/NodeInternals'; import {getElementSibling} from './internals/Traversal'; import ReadOnlyNode from './ReadOnlyNode'; import NativeDOM from './specs/NativeDOM'; @@ -27,7 +27,7 @@ export default class ReadOnlyCharacterData extends ReadOnlyNode { } get data(): string { - const node = getNativeNodeReference(this); + const node = getNativeTextReference(this); if (node != null) { return NativeDOM.getTextContent(node); diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js index c55c47d9ac41e9..f4dc0d7638b305 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js @@ -10,27 +10,32 @@ // flowlint unsafe-getters-setters:off -import type {InternalInstanceHandle} from '../../../../../Libraries/Renderer/shims/ReactNativeTypes'; import type NodeList from '../oldstylecollections/NodeList'; +import type {InstanceHandle} from './internals/NodeInternals'; +import type ReactNativeDocument from './ReactNativeDocument'; import type ReadOnlyElement from './ReadOnlyElement'; +import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags'; import {createNodeList} from '../oldstylecollections/NodeList'; import { getNativeNodeReference, - getPublicInstanceFromInternalInstanceHandle, + getOwnerDocument, + getPublicInstanceFromInstanceHandle, setInstanceHandle, + setOwnerDocument, } from './internals/NodeInternals'; import NativeDOM from './specs/NativeDOM'; -// We initialize this lazily to avoid a require cycle -// (`ReadOnlyElement` also depends on `ReadOnlyNode`). -let ReadOnlyElementClass: Class; - export default class ReadOnlyNode { - constructor(internalInstanceHandle: InternalInstanceHandle) { + constructor( + instanceHandle: InstanceHandle, + // This will be null for the document node itself. + ownerDocument: ReactNativeDocument | null, + ) { // This constructor is inlined in `ReactNativeElement` so if you modify // this make sure that their implementation stays in sync. - setInstanceHandle(this, internalInstanceHandle); + setOwnerDocument(this, ownerDocument); + setInstanceHandle(this, instanceHandle); } get childNodes(): NodeList { @@ -106,15 +111,14 @@ export default class ReadOnlyNode { ); } + get ownerDocument(): ReactNativeDocument | null { + return getOwnerDocument(this); + } + 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) { + if (parentNode instanceof getReadOnlyElementClass()) { return parentNode; } @@ -134,9 +138,7 @@ export default class ReadOnlyNode { return null; } - return ( - getPublicInstanceFromInternalInstanceHandle(parentInstanceHandle) ?? null - ); + return getPublicInstanceFromInstanceHandle(parentInstanceHandle) ?? null; } get previousSibling(): ReadOnlyNode | null { @@ -186,16 +188,25 @@ export default class ReadOnlyNode { } 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; + if (ReactNativeFeatureFlags.enableDOMDocumentAPI()) { + if (this.isConnected) { + // If this is the document node, then the root node is itself. + return this.ownerDocument ?? this; + } + + return this; + } else { + // 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; } - - return lastKnownParent; } hasChildNodes(): boolean { @@ -239,7 +250,7 @@ export default class ReadOnlyNode { */ static COMMENT_NODE: number = 8; /** - * @deprecated Unused in React Native. + * Document nodes. */ static DOCUMENT_NODE: number = 9; /** @@ -301,9 +312,7 @@ export function getChildNodes( const childNodeInstanceHandles = NativeDOM.getChildNodes(shadowNode); return childNodeInstanceHandles - .map(instanceHandle => - getPublicInstanceFromInternalInstanceHandle(instanceHandle), - ) + .map(instanceHandle => getPublicInstanceFromInstanceHandle(instanceHandle)) .filter(Boolean); } @@ -325,3 +334,12 @@ function getNodeSiblingsAndPosition( return [siblings, position]; } + +let ReadOnlyElementClass; +function getReadOnlyElementClass(): Class { + if (ReadOnlyElementClass == null) { + // We initialize this lazily to avoid a require cycle. + ReadOnlyElementClass = require('./ReadOnlyElement').default; + } + return ReadOnlyElementClass; +} diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js new file mode 100644 index 00000000000000..b4e8fd0a77882a --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js @@ -0,0 +1,203 @@ +/** + * 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 + * @fantom_flags enableAccessToHostTreeInFabric:true + * @fantom_flags enableDOMDocumentAPI:true + */ + +import '../../../../../../Libraries/Core/InitializeCore.js'; + +import View from '../../../../../../Libraries/Components/View/View'; +import ensureInstance from '../../../../utilities/ensureInstance'; +import ReactNativeDocument from '../ReactNativeDocument'; +import ReactNativeElement from '../ReactNativeElement'; +import ReadOnlyNode from '../ReadOnlyNode'; +import Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; +import * as React from 'react'; + +describe('ReactNativeDocument', () => { + it('is connected until the surface is destroyed', () => { + let lastNode; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastNode = node; + }} + />, + ); + }); + + const element = ensureInstance(lastNode, ReactNativeElement); + const document = ensureInstance(element.ownerDocument, ReactNativeDocument); + + expect(document.isConnected).toBe(true); + + Fantom.runTask(() => { + root.render(<>); + }); + + expect(document.isConnected).toBe(true); + + Fantom.runTask(() => { + root.destroy(); + }); + + expect(document.isConnected).toBe(false); + }); + + it('allows traversal as a regular node', () => { + let lastNode; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastNode = node; + }} + />, + ); + }); + + const element = ensureInstance(lastNode, ReactNativeElement); + const document = ensureInstance(element.ownerDocument, ReactNativeDocument); + + expect(document.childNodes.length).toBe(1); + expect(document.childNodes[0]).toBe(document.documentElement); + expect(document.documentElement.parentNode).toBe(document); + expect(document.documentElement.childNodes.length).toBe(1); + expect(document.documentElement.childNodes[0]).toBe(element); + expect(element.parentNode).toBe(document.documentElement); + }); + + it('implements the abstract methods from ReadOnlyNode', () => { + let lastNode; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastNode = node; + }} + />, + ); + }); + + const element = ensureInstance(lastNode, ReactNativeElement); + const document = ensureInstance(element.ownerDocument, ReactNativeDocument); + + expect(document.nodeName).toBe('#document'); + expect(document.nodeType).toBe(ReadOnlyNode.DOCUMENT_NODE); + expect(document.nodeValue).toBe(null); + expect(document.textContent).toBe(null); + }); + + it('implements compareDocumentPosition correctly', () => { + let lastNode; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastNode = node; + }} + />, + ); + }); + + const element = ensureInstance(lastNode, ReactNativeElement); + const document = ensureInstance(element.ownerDocument, ReactNativeDocument); + const documentElement = document.documentElement; + + /* eslint-disable no-bitwise */ + + expect(document.compareDocumentPosition(document)).toBe(0); + expect( + document.documentElement.compareDocumentPosition( + document.documentElement, + ), + ).toBe(0); + + expect(document.compareDocumentPosition(documentElement)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY | + ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING, + ); + expect(document.compareDocumentPosition(element)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY | + ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING, + ); + expect(documentElement.compareDocumentPosition(document)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINS | + ReadOnlyNode.DOCUMENT_POSITION_PRECEDING, + ); + expect(documentElement.compareDocumentPosition(element)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY | + ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING, + ); + expect(element.compareDocumentPosition(document)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINS | + ReadOnlyNode.DOCUMENT_POSITION_PRECEDING, + ); + expect(element.compareDocumentPosition(documentElement)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINS | + ReadOnlyNode.DOCUMENT_POSITION_PRECEDING, + ); + }); + + it('is released when the root is destroyed', () => { + let lastNode; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastNode = node; + }} + />, + ); + }); + + let maybeWeakNode; + let maybeWeakDocument; + Fantom.runTask(() => { + maybeWeakDocument = new WeakRef( + ensureInstance( + ensureInstance(lastNode, ReactNativeElement).ownerDocument, + ReactNativeDocument, + ), + ); + maybeWeakNode = new WeakRef(ensureInstance(lastNode, ReactNativeElement)); + }); + + const weakDocument = nullthrows(maybeWeakDocument); + expect(weakDocument.deref()).toBeInstanceOf(ReactNativeDocument); + + const weakNode = nullthrows(maybeWeakNode); + expect(weakNode.deref()).toBeInstanceOf(ReactNativeElement); + + Fantom.runTask(() => { + root.destroy(); + }); + + Fantom.runTask(() => { + global.gc(); + }); + + expect(lastNode).toBe(null); + expect(weakNode.deref()).toBe(undefined); + expect(weakDocument.deref()).toBe(undefined); + }); +}); diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElementWithDocument-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElementWithDocument-itest.js new file mode 100644 index 00000000000000..08f0a018380ea0 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElementWithDocument-itest.js @@ -0,0 +1,1323 @@ +/** + * 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 + * @fantom_flags enableAccessToHostTreeInFabric:true + * @fantom_flags enableDOMDocumentAPI:true + */ + +import '../../../../../../Libraries/Core/InitializeCore.js'; + +import ScrollView from '../../../../../../Libraries/Components/ScrollView/ScrollView'; +import View from '../../../../../../Libraries/Components/View/View'; +import { + NativeText, + NativeVirtualText, +} from '../../../../../../Libraries/Text/TextNativeComponent'; +import ensureInstance from '../../../../utilities/ensureInstance'; +import HTMLCollection from '../../oldstylecollections/HTMLCollection'; +import NodeList from '../../oldstylecollections/NodeList'; +import ReactNativeElement from '../ReactNativeElement'; +import ReadOnlyElement from '../ReadOnlyElement'; +import ReadOnlyNode from '../ReadOnlyNode'; +import Fantom from '@react-native/fantom'; +import * as React from 'react'; + +function ensureReactNativeElement(value: mixed): ReactNativeElement { + return ensureInstance(value, ReactNativeElement); +} + +/* eslint-disable no-bitwise */ + +describe('ReactNativeElement', () => { + it('should be used to create public instances when the `enableAccessToHostTreeInFabric` feature flag is enabled', () => { + let node; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + node = receivedNode; + }} + />, + ); + }); + + expect(node).toBeInstanceOf(ReactNativeElement); + }); + + describe('extends `ReadOnlyNode`', () => { + it('should be an instance of `ReadOnlyNode`', () => { + let lastNode; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastNode = node; + }} + />, + ); + }); + + expect(lastNode).toBeInstanceOf(ReadOnlyNode); + }); + + describe('nodeType', () => { + it('returns ReadOnlyNode.ELEMENT_NODE', () => { + let lastParentNode; + let lastChildNodeA; + let lastChildNodeB; + let lastChildNodeC; + + // Initial render with 3 children + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastParentNode = node; + }}> + { + lastChildNodeA = node; + }} + /> + { + lastChildNodeB = node; + }} + /> + { + lastChildNodeC = node; + }} + /> + , + ); + }); + + const parentNode = ensureReactNativeElement(lastParentNode); + const childNodeA = ensureReactNativeElement(lastChildNodeA); + const childNodeB = ensureReactNativeElement(lastChildNodeB); + const childNodeC = ensureReactNativeElement(lastChildNodeC); + + expect(parentNode.nodeType).toBe(ReadOnlyNode.ELEMENT_NODE); + expect(childNodeA.nodeType).toBe(ReadOnlyNode.ELEMENT_NODE); + expect(childNodeB.nodeType).toBe(ReadOnlyNode.ELEMENT_NODE); + expect(childNodeC.nodeType).toBe(ReadOnlyNode.ELEMENT_NODE); + }); + }); + + describe('nodeValue', () => { + it('returns null', () => { + let lastParentNode; + let lastChildNodeA; + let lastChildNodeB; + let lastChildNodeC; + + // Initial render with 3 children + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastParentNode = node; + }}> + { + lastChildNodeA = node; + }} + /> + { + lastChildNodeB = node; + }} + /> + { + lastChildNodeC = node; + }} + /> + , + ); + }); + + const parentNode = ensureReactNativeElement(lastParentNode); + const childNodeA = ensureReactNativeElement(lastChildNodeA); + const childNodeB = ensureReactNativeElement(lastChildNodeB); + const childNodeC = ensureReactNativeElement(lastChildNodeC); + + expect(parentNode.nodeValue).toBe(null); + expect(childNodeA.nodeValue).toBe(null); + expect(childNodeB.nodeValue).toBe(null); + expect(childNodeC.nodeValue).toBe(null); + }); + }); + + describe('childNodes / hasChildNodes()', () => { + it('returns updated child nodes information', () => { + let lastParentNode; + let lastChildNodeA; + let lastChildNodeB; + let lastChildNodeC; + + // Initial render with 3 children + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastParentNode = node; + }}> + { + lastChildNodeA = node; + }} + /> + { + lastChildNodeB = node; + }} + /> + { + lastChildNodeC = node; + }} + /> + , + ); + }); + + const parentNode = ensureReactNativeElement(lastParentNode); + const childNodeA = ensureReactNativeElement(lastChildNodeA); + const childNodeB = ensureReactNativeElement(lastChildNodeB); + const childNodeC = ensureReactNativeElement(lastChildNodeC); + + const childNodes = parentNode.childNodes; + expect(childNodes).toBeInstanceOf(NodeList); + expect(childNodes.length).toBe(3); + expect(childNodes[0]).toBe(childNodeA); + expect(childNodes[1]).toBe(childNodeB); + expect(childNodes[2]).toBe(childNodeC); + + expect(parentNode.hasChildNodes()).toBe(true); + + // Remove one of the children + Fantom.runTask(() => { + root.render( + + + + , + ); + }); + + const childNodesAfterUpdate = parentNode.childNodes; + expect(childNodesAfterUpdate).toBeInstanceOf(NodeList); + expect(childNodesAfterUpdate.length).toBe(2); + expect(childNodesAfterUpdate[0]).toBe(childNodeA); + expect(childNodesAfterUpdate[1]).toBe(childNodeB); + + expect(parentNode.hasChildNodes()).toBe(true); + + // Unmount node + Fantom.runTask(() => { + root.render(<>); + }); + + const childNodesAfterUnmount = parentNode.childNodes; + expect(childNodesAfterUnmount).toBeInstanceOf(NodeList); + expect(childNodesAfterUnmount.length).toBe(0); + + expect(parentNode.hasChildNodes()).toBe(false); + }); + }); + + describe('getRootNode()', () => { + // This is the desired implementation (not implemented yet). + it('returns a root node representing the document', () => { + let lastParentANode; + let lastParentBNode; + let lastChildANode; + let lastChildBNode; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + <> + { + lastParentANode = node; + }}> + { + lastChildANode = node; + }} + /> + + { + lastParentBNode = node; + }}> + { + lastChildBNode = node; + }} + /> + + , + ); + }); + + const parentANode = ensureReactNativeElement(lastParentANode); + const childANode = ensureReactNativeElement(lastChildANode); + const parentBNode = ensureReactNativeElement(lastParentBNode); + const childBNode = ensureReactNativeElement(lastChildBNode); + + expect(childANode.getRootNode()).toBe(childBNode.getRootNode()); + const document = childANode.getRootNode(); + + expect(document.childNodes.length).toBe(1); + expect(document.childNodes[0]).toBeInstanceOf(ReactNativeElement); + + const documentElement = document.childNodes[0]; + expect(documentElement.childNodes[0]).toBeInstanceOf( + ReactNativeElement, + ); + expect(documentElement.childNodes[0]).toBe(parentANode); + expect(documentElement.childNodes[1]).toBe(parentBNode); + + Fantom.runTask(() => { + root.render( + <> + + + + , + ); + }); + + expect(parentANode.getRootNode()).toBe(document); + expect(childANode.getRootNode()).toBe(document); + + // The root node of a disconnected node is itself + expect(parentBNode.getRootNode()).toBe(parentBNode); + expect(childBNode.getRootNode()).toBe(childBNode); + }); + }); + + describe('firstChild / lastChild / previousSibling / nextSibling / parentNode / parentElement', () => { + it('return updated relative nodes', () => { + let lastParentNode; + let lastChildNodeA; + let lastChildNodeB; + let lastChildNodeC; + + // Initial render with 3 children + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastParentNode = node; + }}> + { + lastChildNodeA = node; + }} + /> + { + lastChildNodeB = node; + }} + /> + { + lastChildNodeC = node; + }} + /> + , + ); + }); + + const parentNode = ensureReactNativeElement(lastParentNode); + const childNodeA = ensureReactNativeElement(lastChildNodeA); + const childNodeB = ensureReactNativeElement(lastChildNodeB); + const childNodeC = ensureReactNativeElement(lastChildNodeC); + + expect(parentNode.isConnected).toBe(true); + expect(parentNode.firstChild).toBe(childNodeA); + expect(parentNode.lastChild).toBe(childNodeC); + expect(parentNode.previousSibling).toBe(null); + expect(parentNode.nextSibling).toBe(null); + + expect(childNodeA.isConnected).toBe(true); + expect(childNodeA.firstChild).toBe(null); + expect(childNodeA.lastChild).toBe(null); + expect(childNodeA.previousSibling).toBe(null); + expect(childNodeA.nextSibling).toBe(childNodeB); + expect(childNodeA.parentNode).toBe(parentNode); + expect(childNodeA.parentElement).toBe(parentNode); + + expect(childNodeB.isConnected).toBe(true); + expect(childNodeB.firstChild).toBe(null); + expect(childNodeB.lastChild).toBe(null); + expect(childNodeB.previousSibling).toBe(childNodeA); + expect(childNodeB.nextSibling).toBe(childNodeC); + expect(childNodeB.parentNode).toBe(parentNode); + expect(childNodeB.parentElement).toBe(parentNode); + + expect(childNodeC.isConnected).toBe(true); + expect(childNodeC.firstChild).toBe(null); + expect(childNodeC.lastChild).toBe(null); + expect(childNodeC.previousSibling).toBe(childNodeB); + expect(childNodeC.nextSibling).toBe(null); + expect(childNodeC.parentNode).toBe(parentNode); + expect(childNodeC.parentElement).toBe(parentNode); + + // Remove one of the children + Fantom.runTask(() => { + root.render( + + + + , + ); + }); + + expect(parentNode.isConnected).toBe(true); + expect(parentNode.firstChild).toBe(childNodeA); + expect(parentNode.lastChild).toBe(childNodeB); + expect(parentNode.previousSibling).toBe(null); + expect(parentNode.nextSibling).toBe(null); + + expect(childNodeA.isConnected).toBe(true); + expect(childNodeA.firstChild).toBe(null); + expect(childNodeA.lastChild).toBe(null); + expect(childNodeA.previousSibling).toBe(null); + expect(childNodeA.nextSibling).toBe(childNodeB); + expect(childNodeA.parentNode).toBe(parentNode); + expect(childNodeA.parentElement).toBe(parentNode); + + expect(childNodeB.isConnected).toBe(true); + expect(childNodeB.firstChild).toBe(null); + expect(childNodeB.lastChild).toBe(null); + expect(childNodeB.previousSibling).toBe(childNodeA); + expect(childNodeB.nextSibling).toBe(null); + expect(childNodeB.parentNode).toBe(parentNode); + expect(childNodeB.parentElement).toBe(parentNode); + + // Disconnected + expect(childNodeC.isConnected).toBe(false); + expect(childNodeC.firstChild).toBe(null); + expect(childNodeC.lastChild).toBe(null); + expect(childNodeC.previousSibling).toBe(null); + expect(childNodeC.nextSibling).toBe(null); + expect(childNodeC.parentNode).toBe(null); + expect(childNodeC.parentElement).toBe(null); + + // Unmount node + Fantom.runTask(() => { + root.render(<>); + }); + + // Disconnected + expect(parentNode.isConnected).toBe(false); + expect(parentNode.firstChild).toBe(null); + expect(parentNode.lastChild).toBe(null); + expect(parentNode.previousSibling).toBe(null); + expect(parentNode.nextSibling).toBe(null); + expect(parentNode.parentNode).toBe(null); + expect(parentNode.parentElement).toBe(null); + + // Disconnected + expect(childNodeA.isConnected).toBe(false); + expect(childNodeA.firstChild).toBe(null); + expect(childNodeA.lastChild).toBe(null); + expect(childNodeA.previousSibling).toBe(null); + expect(childNodeA.nextSibling).toBe(null); + expect(childNodeA.parentNode).toBe(null); + expect(childNodeA.parentElement).toBe(null); + + // Disconnected + expect(childNodeB.isConnected).toBe(false); + expect(childNodeB.firstChild).toBe(null); + expect(childNodeB.lastChild).toBe(null); + expect(childNodeB.previousSibling).toBe(null); + expect(childNodeB.nextSibling).toBe(null); + expect(childNodeB.parentNode).toBe(null); + expect(childNodeB.parentElement).toBe(null); + + // Disconnected + expect(childNodeC.isConnected).toBe(false); + expect(childNodeC.firstChild).toBe(null); + expect(childNodeC.lastChild).toBe(null); + expect(childNodeC.previousSibling).toBe(null); + expect(childNodeC.nextSibling).toBe(null); + expect(childNodeC.parentNode).toBe(null); + expect(childNodeC.parentElement).toBe(null); + }); + }); + + describe('compareDocumentPosition / contains', () => { + it('handles containment, order and connection', () => { + let lastParentNode; + let lastChildNodeA; + let lastChildNodeAA; + let lastChildNodeB; + let lastChildNodeBB; + + // Initial render with 2 children + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastParentNode = node; + }}> + { + lastChildNodeA = node; + }}> + { + lastChildNodeAA = node; + }} + /> + + { + lastChildNodeB = node; + }}> + { + lastChildNodeBB = node; + }} + /> + + , + ); + }); + + const parentNode = ensureReactNativeElement(lastParentNode); + const childNodeA = ensureReactNativeElement(lastChildNodeA); + const childNodeAA = ensureReactNativeElement(lastChildNodeAA); + const childNodeB = ensureReactNativeElement(lastChildNodeB); + const childNodeBB = ensureReactNativeElement(lastChildNodeBB); + + // Node/self + expect(parentNode.compareDocumentPosition(parentNode)).toBe(0); + expect(parentNode.contains(parentNode)).toBe(true); + // Parent/child + expect(parentNode.compareDocumentPosition(childNodeA)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY | + ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING, + ); + expect(parentNode.contains(childNodeA)).toBe(true); + // Child/parent + expect(childNodeA.compareDocumentPosition(parentNode)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINS | + ReadOnlyNode.DOCUMENT_POSITION_PRECEDING, + ); + expect(childNodeA.contains(parentNode)).toBe(false); + // Grandparent/grandchild + expect(parentNode.compareDocumentPosition(childNodeAA)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY | + ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING, + ); + expect(parentNode.contains(childNodeAA)).toBe(true); + // Grandchild/grandparent + expect(childNodeAA.compareDocumentPosition(parentNode)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_CONTAINS | + ReadOnlyNode.DOCUMENT_POSITION_PRECEDING, + ); + expect(childNodeAA.contains(parentNode)).toBe(false); + // Sibling/sibling + expect(childNodeA.compareDocumentPosition(childNodeB)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING, + ); + expect(childNodeA.contains(childNodeB)).toBe(false); + // Sibling/sibling + expect(childNodeB.compareDocumentPosition(childNodeA)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_PRECEDING, + ); + expect(childNodeB.contains(childNodeA)).toBe(false); + // Cousing/cousing + expect(childNodeAA.compareDocumentPosition(childNodeBB)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING, + ); + expect(childNodeAA.contains(childNodeBB)).toBe(false); + // Cousing/cousing + expect(childNodeBB.compareDocumentPosition(childNodeAA)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_PRECEDING, + ); + expect(childNodeBB.contains(childNodeAA)).toBe(false); + + // Remove one of the children + Fantom.runTask(() => { + root.render( + + + + , + ); + }); + + // Node/disconnected + expect(parentNode.compareDocumentPosition(childNodeAA)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + expect(parentNode.contains(childNodeAA)).toBe(false); + // Disconnected/node + expect(childNodeAA.compareDocumentPosition(parentNode)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + expect(childNodeAA.contains(parentNode)).toBe(false); + // Disconnected/disconnected + expect(childNodeAA.compareDocumentPosition(childNodeBB)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + expect(childNodeAA.contains(childNodeBB)).toBe(false); + // Disconnected/disconnected + expect(childNodeBB.compareDocumentPosition(childNodeAA)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + expect(childNodeBB.contains(childNodeAA)).toBe(false); + // Disconnected/self + expect(childNodeBB.compareDocumentPosition(childNodeBB)).toBe(0); + expect(childNodeBB.contains(childNodeBB)).toBe(true); + + let lastAltParentNode; + + // Similar structure in a different tree + const root2 = Fantom.createRoot(); + Fantom.runTask(() => { + root2.render( + { + lastAltParentNode = node; + }}> + + + , + ); + }); + + const altParentNode = ensureReactNativeElement(lastAltParentNode); + + // Node/same position in different tree + expect(altParentNode.compareDocumentPosition(parentNode)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + expect(parentNode.compareDocumentPosition(altParentNode)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + expect(parentNode.contains(altParentNode)).toBe(false); + expect(altParentNode.contains(parentNode)).toBe(false); + + // Node/child position in different tree + expect(altParentNode.compareDocumentPosition(childNodeA)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + expect(childNodeA.compareDocumentPosition(altParentNode)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + expect(altParentNode.contains(childNodeA)).toBe(false); + expect(childNodeA.contains(altParentNode)).toBe(false); + + // Unmounted root + Fantom.runTask(() => { + root.destroy(); + }); + + expect(parentNode.compareDocumentPosition(parentNode)).toBe(0); + expect(parentNode.contains(parentNode)).toBe(true); + + expect(parentNode.compareDocumentPosition(childNodeA)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + expect(parentNode.compareDocumentPosition(altParentNode)).toBe( + ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED, + ); + }); + }); + }); + + describe('extends `ReadOnlyElement`', () => { + it('should be an instance of `ReadOnlyElement`', () => { + let lastNode; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastNode = node; + }} + />, + ); + }); + + expect(lastNode).toBeInstanceOf(ReadOnlyElement); + }); + + describe('children / childElementCount', () => { + it('return updated element children information', () => { + let lastParentElement; + let lastChildElementA; + let lastChildElementB; + let lastChildElementC; + + // Initial render with 3 children + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastParentElement = element; + }}> + { + lastChildElementA = element; + }} + /> + { + lastChildElementB = element; + }} + /> + { + lastChildElementC = element; + }} + /> + , + ); + }); + + const parentElement = ensureReactNativeElement(lastParentElement); + const childElementA = ensureReactNativeElement(lastChildElementA); + const childElementB = ensureReactNativeElement(lastChildElementB); + const childElementC = ensureReactNativeElement(lastChildElementC); + + const children = parentElement.children; + expect(children).toBeInstanceOf(HTMLCollection); + expect(children.length).toBe(3); + expect(children[0]).toBe(childElementA); + expect(children[1]).toBe(childElementB); + expect(children[2]).toBe(childElementC); + + expect(parentElement.childElementCount).toBe(3); + + // Remove one of the children + Fantom.runTask(() => { + root.render( + + + + , + ); + }); + + const childrenAfterUpdate = parentElement.children; + expect(childrenAfterUpdate).toBeInstanceOf(HTMLCollection); + expect(childrenAfterUpdate.length).toBe(2); + expect(childrenAfterUpdate[0]).toBe(childElementA); + expect(childrenAfterUpdate[1]).toBe(childElementB); + + expect(parentElement.childElementCount).toBe(2); + + // Unmount node + Fantom.runTask(() => { + root.render(<>); + }); + + const childrenAfterUnmount = parentElement.children; + expect(childrenAfterUnmount).toBeInstanceOf(HTMLCollection); + expect(childrenAfterUnmount.length).toBe(0); + + expect(parentElement.childElementCount).toBe(0); + }); + }); + + describe('firstElementChild / lastElementChild / previousElementSibling / nextElementSibling', () => { + it('return updated relative elements', () => { + let lastParentElement; + let lastChildElementA; + let lastChildElementB; + let lastChildElementC; + + // Initial render with 3 children + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastParentElement = element; + }}> + { + lastChildElementA = element; + }} + /> + { + lastChildElementB = element; + }} + /> + { + lastChildElementC = element; + }} + /> + , + ); + }); + + const parentElement = ensureReactNativeElement(lastParentElement); + const childElementA = ensureReactNativeElement(lastChildElementA); + const childElementB = ensureReactNativeElement(lastChildElementB); + const childElementC = ensureReactNativeElement(lastChildElementC); + + expect(parentElement.firstElementChild).toBe(childElementA); + expect(parentElement.lastElementChild).toBe(childElementC); + expect(parentElement.previousElementSibling).toBe(null); + expect(parentElement.nextElementSibling).toBe(null); + + expect(childElementA.firstElementChild).toBe(null); + expect(childElementA.lastElementChild).toBe(null); + expect(childElementA.previousElementSibling).toBe(null); + expect(childElementA.nextElementSibling).toBe(childElementB); + + expect(childElementB.firstElementChild).toBe(null); + expect(childElementB.lastElementChild).toBe(null); + expect(childElementB.previousElementSibling).toBe(childElementA); + expect(childElementB.nextElementSibling).toBe(childElementC); + + expect(childElementC.firstElementChild).toBe(null); + expect(childElementC.lastElementChild).toBe(null); + expect(childElementC.previousElementSibling).toBe(childElementB); + expect(childElementC.nextElementSibling).toBe(null); + + // Remove one of the children + Fantom.runTask(() => { + root.render( + + + + , + ); + }); + + expect(parentElement.firstElementChild).toBe(childElementA); + expect(parentElement.lastElementChild).toBe(childElementB); + expect(parentElement.previousElementSibling).toBe(null); + expect(parentElement.nextElementSibling).toBe(null); + + expect(childElementA.firstElementChild).toBe(null); + expect(childElementA.lastElementChild).toBe(null); + expect(childElementA.previousElementSibling).toBe(null); + expect(childElementA.nextElementSibling).toBe(childElementB); + + expect(childElementB.firstElementChild).toBe(null); + expect(childElementB.lastElementChild).toBe(null); + expect(childElementB.previousElementSibling).toBe(childElementA); + expect(childElementB.nextElementSibling).toBe(null); + + // Disconnected + expect(childElementC.firstElementChild).toBe(null); + expect(childElementC.lastElementChild).toBe(null); + expect(childElementC.previousElementSibling).toBe(null); + expect(childElementC.nextElementSibling).toBe(null); + + // Unmount node + Fantom.runTask(() => { + root.render(<>); + }); + + // Disconnected + expect(parentElement.firstElementChild).toBe(null); + expect(parentElement.lastElementChild).toBe(null); + expect(parentElement.previousElementSibling).toBe(null); + expect(parentElement.nextElementSibling).toBe(null); + + // Disconnected + expect(childElementA.firstElementChild).toBe(null); + expect(childElementA.lastElementChild).toBe(null); + expect(childElementA.previousElementSibling).toBe(null); + expect(childElementA.nextElementSibling).toBe(null); + + // Disconnected + expect(childElementB.firstElementChild).toBe(null); + expect(childElementB.lastElementChild).toBe(null); + expect(childElementB.previousElementSibling).toBe(null); + expect(childElementB.nextElementSibling).toBe(null); + + // Disconnected + expect(childElementC.firstElementChild).toBe(null); + expect(childElementC.lastElementChild).toBe(null); + expect(childElementC.previousElementSibling).toBe(null); + expect(childElementC.nextElementSibling).toBe(null); + }); + }); + + describe('textContent', () => { + it('should return the concatenated values of all its text node descendants (using DFS)', () => { + let lastParentNode; + let lastChildNodeA; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastParentNode = node; + }}> + Hello + { + lastChildNodeA = node; + }}> + world! + + , + ); + }); + + const parentNode = ensureReactNativeElement(lastParentNode); + const childNodeA = ensureReactNativeElement(lastChildNodeA); + + expect(parentNode.textContent).toBe('Hello world!'); + expect(childNodeA.textContent).toBe('world!'); + + let lastChildNodeB; + Fantom.runTask(() => { + root.render( + + Hello + + world + + { + lastChildNodeB = node; + }}> + + + again + and again! + + + + , + ); + }); + + const childNodeB = ensureReactNativeElement(lastChildNodeB); + + expect(parentNode.textContent).toBe('Hello world again and again!'); + expect(childNodeA.textContent).toBe('world '); + expect(childNodeB.textContent).toBe('again and again!'); + }); + }); + + describe('getBoundingClientRect', () => { + it('returns a DOMRect with its size and position, or an empty DOMRect when disconnected', () => { + let lastElement; + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + { + lastElement = element; + }} + />, + ); + }); + + const element = ensureReactNativeElement(lastElement); + + const boundingClientRect = element.getBoundingClientRect(); + expect(boundingClientRect).toBeInstanceOf(DOMRect); + expect(boundingClientRect.x).toBe(5); + expect(boundingClientRect.y).toBeCloseTo(10.33); + expect(boundingClientRect.width).toBeCloseTo(50.33); + expect(boundingClientRect.height).toBeCloseTo(100.33); + + Fantom.runTask(() => { + root.render(); + }); + + const boundingClientRectAfterUnmount = element.getBoundingClientRect(); + expect(boundingClientRectAfterUnmount).toBeInstanceOf(DOMRect); + expect(boundingClientRectAfterUnmount.x).toBe(0); + expect(boundingClientRectAfterUnmount.y).toBe(0); + expect(boundingClientRectAfterUnmount.width).toBe(0); + expect(boundingClientRectAfterUnmount.height).toBe(0); + }); + }); + + describe('scrollLeft / scrollTop', () => { + it('return the scroll position on each axis', () => { + let lastElement; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastElement = element; + }} + />, + ); + }); + + const element = ensureReactNativeElement(lastElement); + + expect(element.scrollLeft).toBeCloseTo(5.1); + expect(element.scrollTop).toBeCloseTo(10.2); + + Fantom.runTask(() => { + root.render(); + }); + + expect(element.scrollLeft).toBe(0); + expect(element.scrollTop).toBe(0); + }); + }); + + describe('scrollWidth / scrollHeight', () => { + it('return the scroll size on each axis', () => { + let lastElement; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastElement = element; + }}> + + , + ); + }); + + const element = ensureReactNativeElement(lastElement); + + expect(element.scrollWidth).toBe(200); + expect(element.scrollHeight).toBe(1500); + + Fantom.runTask(() => { + root.render(); + }); + + expect(element.scrollWidth).toBe(0); + expect(element.scrollHeight).toBe(0); + }); + }); + + describe('clientWidth / clientHeight', () => { + it('return the inner size of the node', () => { + let lastElement; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastElement = element; + }} + />, + ); + }); + + const element = ensureReactNativeElement(lastElement); + + expect(element.clientWidth).toBe(200); + expect(element.clientHeight).toBe(250); + + Fantom.runTask(() => { + root.render(); + }); + + expect(element.clientWidth).toBe(0); + expect(element.clientHeight).toBe(0); + }); + }); + + describe('clientLeft / clientTop', () => { + it('return the border size of the node', () => { + let lastElement; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastElement = element; + }} + />, + ); + }); + + const element = ensureReactNativeElement(lastElement); + + expect(element.clientLeft).toBe(200); + expect(element.clientTop).toBe(250); + + Fantom.runTask(() => { + root.render(); + }); + + expect(element.clientLeft).toBe(0); + expect(element.clientTop).toBe(0); + }); + }); + + describe('id', () => { + it('returns the current `id` prop from the node', () => { + let lastElement; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastElement = element; + }} + />, + ); + }); + + const element = ensureReactNativeElement(lastElement); + + expect(element.id).toBe(''); + }); + + it('returns the current `nativeID` prop from the node', () => { + let lastElement; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastElement = element; + }} + />, + ); + }); + + const element = ensureReactNativeElement(lastElement); + + expect(element.id).toBe(''); + }); + }); + + describe('tagName', () => { + it('returns the normalized tag name for the node', () => { + let lastElement; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastElement = element; + }} + />, + ); + }); + + const element = ensureReactNativeElement(lastElement); + + expect(element.tagName).toBe('RN:View'); + }); + }); + }); + + describe('extends `ReactNativeElement`', () => { + it('should be an instance of `ReactNativeElement`', () => { + let lastNode; + + // Initial render with 3 children + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastNode = node; + }} + />, + ); + }); + + const node = ensureReactNativeElement(lastNode); + expect(node).toBeInstanceOf(ReactNativeElement); + }); + + describe('offsetWidth / offsetHeight', () => { + it('return the rounded width and height, or 0 when disconnected', () => { + let lastElement; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastElement = element; + }} + />, + ); + }); + + const element = ensureReactNativeElement(lastElement); + + expect(element.offsetWidth).toBe(50); + expect(element.offsetHeight).toBe(100); + + Fantom.runTask(() => { + root.render(); + }); + + expect(element.offsetWidth).toBe(0); + expect(element.offsetHeight).toBe(0); + }); + }); + + describe('offsetParent / offsetTop / offsetLeft', () => { + it('retun the rounded offset values and the parent, or null and zeros when disconnected or hidden', () => { + let lastParentElement; + let lastElement; + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + { + lastParentElement = element; + }}> + { + lastElement = element; + }} + /> + , + ); + }); + + const parentElement = ensureReactNativeElement(lastParentElement); + const element = ensureReactNativeElement(lastElement); + + expect(element.offsetTop).toBe(11); + expect(element.offsetLeft).toBe(5); + expect(element.offsetParent).toBe(parentElement); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect(element.offsetTop).toBe(0); + expect(element.offsetLeft).toBe(0); + expect(element.offsetParent).toBe(null); + + Fantom.runTask(() => { + root.render(); + }); + + expect(element.offsetTop).toBe(0); + expect(element.offsetLeft).toBe(0); + expect(element.offsetParent).toBe(null); + }); + }); + }); +}); diff --git a/packages/react-native/src/private/webapis/dom/nodes/internals/NodeInternals.js b/packages/react-native/src/private/webapis/dom/nodes/internals/NodeInternals.js index b4a6c2a5e2e05f..77cd8a796f036a 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/internals/NodeInternals.js +++ b/packages/react-native/src/private/webapis/dom/nodes/internals/NodeInternals.js @@ -9,6 +9,7 @@ */ import type {InternalInstanceHandle} from '../../../../../../Libraries/Renderer/shims/ReactNativeTypes'; +import type ReactNativeDocument from '../ReactNativeDocument'; import type ReadOnlyCharacterData from '../ReadOnlyCharacterData'; import type ReadOnlyElement from '../ReadOnlyElement'; import type ReadOnlyNode from '../ReadOnlyNode'; @@ -17,6 +18,24 @@ import type { NativeNodeReference, NativeTextReference, } from '../specs/NativeDOM'; +import type {ReactNativeDocumentElementInstanceHandle} from './ReactNativeDocumentElementInstanceHandle'; +import type {ReactNativeDocumentInstanceHandle} from './ReactNativeDocumentInstanceHandle'; + +import { + getNativeElementReferenceFromReactNativeDocumentElementInstanceHandle, + getPublicInstanceFromReactNativeDocumentElementInstanceHandle, + isReactNativeDocumentElementInstanceHandle, +} from './ReactNativeDocumentElementInstanceHandle'; +import { + getNativeNodeReferenceFromReactNativeDocumentInstanceHandle, + getPublicInstanceFromReactNativeDocumentInstanceHandle, + isReactNativeDocumentInstanceHandle, +} from './ReactNativeDocumentInstanceHandle'; + +export type InstanceHandle = + | InternalInstanceHandle // component managed by React + | ReactNativeDocumentElementInstanceHandle // root element managed by React Native + | ReactNativeDocumentInstanceHandle; // document node managed by React Native let RendererProxy; function getRendererProxy() { @@ -29,50 +48,97 @@ function getRendererProxy() { } const INSTANCE_HANDLE_KEY = Symbol('internalInstanceHandle'); +const OWNER_DOCUMENT_KEY = Symbol('ownerDocument'); -export function getInstanceHandle(node: ReadOnlyNode): InternalInstanceHandle { +export function getInstanceHandle(node: ReadOnlyNode): InstanceHandle { // $FlowExpectedError[prop-missing] return node[INSTANCE_HANDLE_KEY]; } export function setInstanceHandle( node: ReadOnlyNode, - instanceHandle: InternalInstanceHandle, + instanceHandle: InstanceHandle, ): void { // $FlowExpectedError[prop-missing] node[INSTANCE_HANDLE_KEY] = instanceHandle; } +export function getOwnerDocument( + node: ReadOnlyNode, +): ReactNativeDocument | null { + // $FlowExpectedError[prop-missing] + return node[OWNER_DOCUMENT_KEY] ?? null; +} + +export function setOwnerDocument( + node: ReadOnlyNode, + ownerDocument: ReactNativeDocument | null, +): void { + // $FlowExpectedError[prop-missing] + node[OWNER_DOCUMENT_KEY] = ownerDocument; +} + +export function getPublicInstanceFromInstanceHandle( + instanceHandle: InstanceHandle, +): ?ReadOnlyNode { + if (isReactNativeDocumentInstanceHandle(instanceHandle)) { + return getPublicInstanceFromReactNativeDocumentInstanceHandle( + instanceHandle, + ); + } + + if (isReactNativeDocumentElementInstanceHandle(instanceHandle)) { + return getPublicInstanceFromReactNativeDocumentElementInstanceHandle( + instanceHandle, + ); + } + + const mixedPublicInstance = + getRendererProxy().getPublicInstanceFromInternalInstanceHandle( + instanceHandle, + ); + + // $FlowExpectedError[incompatible-return] React defines public instances as "mixed" because it can't access the definition from React Native. + return mixedPublicInstance; +} + export function getNativeNodeReference( node: ReadOnlyNode, ): ?NativeNodeReference { + const instanceHandle = getInstanceHandle(node); + + if (isReactNativeDocumentInstanceHandle(instanceHandle)) { + return getNativeNodeReferenceFromReactNativeDocumentInstanceHandle( + instanceHandle, + ); + } + + if (isReactNativeDocumentElementInstanceHandle(instanceHandle)) { + return getNativeElementReferenceFromReactNativeDocumentElementInstanceHandle( + instanceHandle, + ); + } + // $FlowExpectedError[incompatible-return] - return getRendererProxy().getNodeFromInternalInstanceHandle( - getInstanceHandle(node), - ); + return getRendererProxy().getNodeFromInternalInstanceHandle(instanceHandle); } export function getNativeElementReference( node: ReadOnlyElement, ): ?NativeElementReference { + // $FlowExpectedError[incompatible-cast] We know ReadOnlyElement instances provide InternalInstanceHandle + const instanceHandle = getInstanceHandle(node) as InternalInstanceHandle; + // $FlowExpectedError[incompatible-return] - return getNativeNodeReference(node); + return getRendererProxy().getNodeFromInternalInstanceHandle(instanceHandle); } export function getNativeTextReference( node: ReadOnlyCharacterData, ): ?NativeTextReference { - // $FlowExpectedError[incompatible-return] - return getNativeNodeReference(node); -} + // $FlowExpectedError[incompatible-cast] We know ReadOnlyText instances provide InternalInstanceHandle + const instanceHandle = getInstanceHandle(node) as InternalInstanceHandle; -export function getPublicInstanceFromInternalInstanceHandle( - instanceHandle: InternalInstanceHandle, -): ?ReadOnlyNode { - const mixedPublicInstance = - getRendererProxy().getPublicInstanceFromInternalInstanceHandle( - instanceHandle, - ); - // $FlowExpectedError[incompatible-return] React defines public instances as "mixed" because it can't access the definition from React Native. - return mixedPublicInstance; + // $FlowExpectedError[incompatible-return] + return getRendererProxy().getNodeFromInternalInstanceHandle(instanceHandle); } diff --git a/packages/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentElementInstanceHandle.js b/packages/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentElementInstanceHandle.js new file mode 100644 index 00000000000000..69d595d50e04ae --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentElementInstanceHandle.js @@ -0,0 +1,55 @@ +/** + * 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 ReadOnlyNode from '../ReadOnlyNode'; +import type {NativeElementReference} from '../specs/NativeDOM'; + +class ReactNativeDocumentElementInstanceHandleImpl { + publicInstance: ?ReadOnlyNode; + nativeElementReference: ?NativeElementReference; +} + +export opaque type ReactNativeDocumentElementInstanceHandle = ReactNativeDocumentElementInstanceHandleImpl; + +export function createReactNativeDocumentElementInstanceHandle(): ReactNativeDocumentElementInstanceHandle { + return new ReactNativeDocumentElementInstanceHandleImpl(); +} + +export function getNativeElementReferenceFromReactNativeDocumentElementInstanceHandle( + instanceHandle: ReactNativeDocumentElementInstanceHandle, +): ?NativeElementReference { + return instanceHandle.nativeElementReference; +} + +export function setNativeElementReferenceForReactNativeDocumentElementInstanceHandle( + instanceHandle: ReactNativeDocumentElementInstanceHandle, + nativeElementReference: ?NativeElementReference, +): void { + instanceHandle.nativeElementReference = nativeElementReference; +} + +export function getPublicInstanceFromReactNativeDocumentElementInstanceHandle( + instanceHandle: ReactNativeDocumentElementInstanceHandle, +): ?ReadOnlyNode { + return instanceHandle.publicInstance; +} + +export function setPublicInstanceForReactNativeDocumentElementInstanceHandle( + instanceHandle: ReactNativeDocumentElementInstanceHandle, + publicInstance: ?ReadOnlyNode, +): void { + instanceHandle.publicInstance = publicInstance; +} + +export function isReactNativeDocumentElementInstanceHandle( + instanceHandle: mixed, +): instanceHandle is ReactNativeDocumentElementInstanceHandle { + return instanceHandle instanceof ReactNativeDocumentElementInstanceHandleImpl; +} diff --git a/packages/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentInstanceHandle.js b/packages/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentInstanceHandle.js new file mode 100644 index 00000000000000..658d65dabb7c50 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/nodes/internals/ReactNativeDocumentInstanceHandle.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. + * + * @format + * @flow strict-local + */ + +import type {RootTag} from '../../../../../../Libraries/ReactNative/RootTag'; +import type ReactNativeDocument from '../ReactNativeDocument'; +import type {NativeNodeReference} from '../specs/NativeDOM'; + +import * as RendererProxy from '../../../../../../Libraries/ReactNative/RendererProxy'; + +export opaque type ReactNativeDocumentInstanceHandle = RootTag; + +export function createReactNativeDocumentInstanceHandle( + rootTag: RootTag, +): ReactNativeDocumentInstanceHandle { + return rootTag; +} + +export function getNativeNodeReferenceFromReactNativeDocumentInstanceHandle( + instanceHandle: ReactNativeDocumentInstanceHandle, +): ?NativeNodeReference { + return instanceHandle; +} + +export function getPublicInstanceFromReactNativeDocumentInstanceHandle( + instanceHandle: ReactNativeDocumentInstanceHandle, +): ?ReactNativeDocument { + // $FlowExpectedError[incompatible-return] React defines public instances as "mixed" because it can't access the definition from React Native. + return RendererProxy.getPublicInstanceFromRootTag(Number(instanceHandle)); +} + +export function isReactNativeDocumentInstanceHandle( + instanceHandle: mixed, + // $FlowExpectedError[incompatible-type-guard] +): instanceHandle is ReactNativeDocumentInstanceHandle { + return typeof instanceHandle === 'number' && instanceHandle % 10 === 1; +} diff --git a/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js b/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js index e1b5b4f3f00e27..79b073f47ac328 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js +++ b/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js @@ -4,22 +4,25 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow strict + * @flow strict-local * @format */ -import type { - InternalInstanceHandle as InstanceHandle, - Node as ShadowNode, -} from '../../../../../../Libraries/Renderer/shims/ReactNativeTypes'; +import type {RootTag} from '../../../../../../Libraries/ReactNative/RootTag'; +import type {Node as ShadowNode} from '../../../../../../Libraries/Renderer/shims/ReactNativeTypes'; import type {TurboModule} from '../../../../../../Libraries/TurboModule/RCTExport'; +import type {InstanceHandle} from '../internals/NodeInternals'; import * as TurboModuleRegistry from '../../../../../../Libraries/TurboModule/TurboModuleRegistry'; import nullthrows from 'nullthrows'; export opaque type NativeElementReference = ShadowNode; export opaque type NativeTextReference = ShadowNode; -export type NativeNodeReference = NativeElementReference | NativeTextReference; + +export type NativeNodeReference = + | NativeElementReference + | NativeTextReference + | RootTag; export type MeasureInWindowOnSuccessCallback = ( x: number, @@ -122,6 +125,15 @@ export interface Spec extends TurboModule { nativeElementReference: mixed /* NativeElementReference */, ) => $ReadOnlyArray /* [offsetParent: ?InstanceHandle, top: number, left: number] */; + /* + * Special methods to handle the root node. + */ + + +linkRootNode?: ( + rootTag: number /* RootTag */, + instanceHandle: mixed /* InstanceHandle */, + ) => mixed /* ?NativeElementReference */; + /** * Legacy layout APIs (for `ReactNativeElement`). */ @@ -353,6 +365,29 @@ export interface RefinedSpec { ], >; + /* + * Special methods to handle the root node. + */ + + /** + * In React Native, surfaces that represent trees (similar to a `Document` on + * Web) are created in native first, and then populated from JavaScript. + * + * Because React does not create this special node, we need a way to link + * the JavaScript instance with that node, which is what this method allows. + * + * It also allows the implementation of `Node.prototype.ownerDocument` and + * `Node.prototype.getRootNode` + * (see https://developer.mozilla.org/en-US/docs/Web/API/Node/ownerDocument and + * https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode). + * + * Returns a shadow node representing the root node if it is still mounted. + */ + +linkRootNode: ( + rootTag: RootTag, + instanceHandle: InstanceHandle, + ) => ?NativeElementReference; + /** * Legacy layout APIs */ @@ -503,6 +538,19 @@ const NativeDOM: RefinedSpec = { >); }, + /* + * Special methods to handle the root node. + */ + + linkRootNode(rootTag, instanceHandle) { + // $FlowExpectedError[incompatible-cast] + return (nullthrows(RawNativeDOM?.linkRootNode)( + // $FlowExpectedError[incompatible-call] + rootTag, + instanceHandle, + ): ?NativeElementReference); + }, + /** * Legacy layout APIs */ diff --git a/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js b/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js index 049251a3c282b4..f0c676c1562faa 100644 --- a/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js +++ b/packages/react-native/src/private/webapis/mutationobserver/internals/MutationObserverManager.js @@ -25,11 +25,9 @@ import type MutationObserver, { import type MutationRecord from '../MutationRecord'; import * as Systrace from '../../../../../Libraries/Performance/Systrace'; +import {getPublicInstanceFromInternalInstanceHandle} from '../../../../../Libraries/ReactNative/RendererProxy'; import warnOnce from '../../../../../Libraries/Utilities/warnOnce'; -import { - getNativeNodeReference, - getPublicInstanceFromInternalInstanceHandle, -} from '../../dom/nodes/internals/NodeInternals'; +import {getNativeNodeReference} from '../../dom/nodes/internals/NodeInternals'; import {createMutationRecord} from '../MutationRecord'; import NativeMutationObserver from '../specs/NativeMutationObserver'; From 105e3ab8374799b5a503dd6874c5cc7fab1c3f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 30 Jan 2025 07:11:43 -0800 Subject: [PATCH 08/17] Implement additional traversal methods on ReactNativeDocument (#49013) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49013 Changelog: [internal] Adds `Document`-specific traversal methods to `ReactNativeDocument`: * `childElementCount` * `children` * `firstElementChild` * `lastElementChild` Reviewed By: yungsters Differential Revision: D67693032 fbshipit-source-id: 1e3279586ece809c5c3584279c07f991cadf0fc6 --- .../__snapshots__/public-api-test.js.snap | 4 ++++ .../src/private/setup/setUpDOM.js | 5 ++++ .../webapis/dom/nodes/ReactNativeDocument.js | 20 ++++++++++++++++ .../__tests__/ReactNativeDocument-itest.js | 24 +++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index a280e7b152e478..939d22dbed45ed 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -11312,7 +11312,11 @@ exports[`public API should not change unintentionally src/private/webapis/dom/no rootTag: RootTag, instanceHandle: ReactNativeDocumentInstanceHandle ): void; + get childElementCount(): number; + get children(): HTMLCollection; get documentElement(): ReactNativeElement; + get firstElementChild(): ReadOnlyElement | null; + get lastElementChild(): ReadOnlyElement | null; get nodeName(): string; get nodeType(): number; get nodeValue(): null; diff --git a/packages/react-native/src/private/setup/setUpDOM.js b/packages/react-native/src/private/setup/setUpDOM.js index f815c86c4e842f..f4fc5552239157 100644 --- a/packages/react-native/src/private/setup/setUpDOM.js +++ b/packages/react-native/src/private/setup/setUpDOM.js @@ -29,6 +29,11 @@ export default function setUpDOM() { () => require('../webapis/dom/geometry/DOMRectReadOnly').default, ); + polyfillGlobal( + 'HTMLCollection', + () => require('../webapis/dom/oldstylecollections/HTMLCollection').default, + ); + polyfillGlobal( 'NodeList', () => require('../webapis/dom/oldstylecollections/NodeList').default, diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js index 97ed4dfe2c0667..09e227be4e72b2 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js @@ -12,8 +12,11 @@ import type {RootTag} from '../../../../../Libraries/ReactNative/RootTag'; import type {ViewConfig} from '../../../../../Libraries/Renderer/shims/ReactNativeTypes'; +import type HTMLCollection from '../oldstylecollections/HTMLCollection'; import type {ReactNativeDocumentInstanceHandle} from './internals/ReactNativeDocumentInstanceHandle'; +import type ReadOnlyElement from './ReadOnlyElement'; +import {createHTMLCollection} from '../oldstylecollections/HTMLCollection'; import { createReactNativeDocumentElementInstanceHandle, setNativeElementReferenceForReactNativeDocumentElementInstanceHandle, @@ -35,10 +38,27 @@ export default class ReactNativeDocument extends ReadOnlyNode { this._documentElement = createDocumentElement(rootTag, this); } + get childElementCount(): number { + // just `documentElement`. + return 1; + } + + get children(): HTMLCollection { + return createHTMLCollection([this.documentElement]); + } + get documentElement(): ReactNativeElement { return this._documentElement; } + get firstElementChild(): ReadOnlyElement | null { + return this.documentElement; + } + + get lastElementChild(): ReadOnlyElement | null { + return this.documentElement; + } + get nodeName(): string { return '#document'; } diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js index b4e8fd0a77882a..8071a493e3ed31 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js @@ -80,6 +80,30 @@ describe('ReactNativeDocument', () => { expect(element.parentNode).toBe(document.documentElement); }); + it('allows traversal through document-specific methods', () => { + let lastNode; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + { + lastNode = node; + }} + />, + ); + }); + + const element = ensureInstance(lastNode, ReactNativeElement); + const document = ensureInstance(element.ownerDocument, ReactNativeDocument); + + expect(document.childElementCount).toBe(1); + expect(document.firstElementChild).toBe(document.documentElement); + expect(document.lastElementChild).toBe(document.documentElement); + expect(document.children).toBeInstanceOf(HTMLCollection); + expect([...document.children]).toEqual([document.documentElement]); + }); + it('implements the abstract methods from ReadOnlyNode', () => { let lastNode; From 4101a2f0b6a4a6b65a921d54431c2dfabb93272c Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Thu, 30 Jan 2025 07:16:54 -0800 Subject: [PATCH 09/17] Add compat layer for react-native-codegen and processColorArray (#49063) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49063 ## Motivation Modernising the RN codebase to allow for modern Flow tooling to process it. ## This diff - Updates react-native-codegen to generate ViewConfigs that are compatible with react-native both before and after the export syntax migration. Changelog: [Internal] Reviewed By: huntie Differential Revision: D68894819 fbshipit-source-id: fca46c1b91c15e22f1e1128ce8621c05341e2fe6 --- .../components/__snapshots__/GenerateViewConfigJs-test.js.snap | 2 +- .../src/generators/components/GenerateViewConfigJs.js | 2 +- .../__tests__/__snapshots__/GenerateViewConfigJs-test.js.snap | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-codegen/e2e/__tests__/components/__snapshots__/GenerateViewConfigJs-test.js.snap b/packages/react-native-codegen/e2e/__tests__/components/__snapshots__/GenerateViewConfigJs-test.js.snap index aa5b60893842a9..d1b1922f82b818 100644 --- a/packages/react-native-codegen/e2e/__tests__/components/__snapshots__/GenerateViewConfigJs-test.js.snap +++ b/packages/react-native-codegen/e2e/__tests__/components/__snapshots__/GenerateViewConfigJs-test.js.snap @@ -31,7 +31,7 @@ export const __INTERNAL_VIEW_CONFIG = { radii: true, colors: { - process: require('react-native/Libraries/StyleSheet/processColorArray').default, + process: ((req) => 'default' in req ? req.default : req)(require('react-native/Libraries/StyleSheet/processColorArray')), }, srcs: true, diff --git a/packages/react-native-codegen/src/generators/components/GenerateViewConfigJs.js b/packages/react-native-codegen/src/generators/components/GenerateViewConfigJs.js index c7e0227428c4bc..64ecdd03a87bbb 100644 --- a/packages/react-native-codegen/src/generators/components/GenerateViewConfigJs.js +++ b/packages/react-native-codegen/src/generators/components/GenerateViewConfigJs.js @@ -91,7 +91,7 @@ function getReactDiffProcessValue(typeAnnotation: PropTypeAnnotation) { switch (typeAnnotation.elementType.name) { case 'ColorPrimitive': return j.template - .expression`{ process: require('react-native/Libraries/StyleSheet/processColorArray').default }`; + .expression`{ process: ((req) => 'default' in req ? req.default : req)(require('react-native/Libraries/StyleSheet/processColorArray')) }`; case 'ImageSourcePrimitive': case 'PointPrimitive': case 'EdgeInsetsPrimitive': diff --git a/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateViewConfigJs-test.js.snap b/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateViewConfigJs-test.js.snap index eecbbe3eb73044..da84fbab1ec871 100644 --- a/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateViewConfigJs-test.js.snap +++ b/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GenerateViewConfigJs-test.js.snap @@ -31,7 +31,7 @@ export const __INTERNAL_VIEW_CONFIG = { radii: true, colors: { - process: require('react-native/Libraries/StyleSheet/processColorArray').default, + process: ((req) => 'default' in req ? req.default : req)(require('react-native/Libraries/StyleSheet/processColorArray')), }, srcs: true, From 82cc465645d7a765707f703a14b4af809a018df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 30 Jan 2025 07:20:52 -0800 Subject: [PATCH 10/17] Clean up disableEventLoopOnBridgeless feature flag (#49065) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49065 Changelog: [internal] Cleaning up the flag because it's no longer necessary. Reviewed By: sammy-SC Differential Revision: D68892995 fbshipit-source-id: 4e0290bfb11181dc388e6590af1b82581588b9ee --- .../Libraries/Core/setUpTimers.js | 109 +++++++----------- .../NativeReactNativeFeatureFlags.cpp | 9 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 10 -- .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 6 files changed, 48 insertions(+), 94 deletions(-) diff --git a/packages/react-native/Libraries/Core/setUpTimers.js b/packages/react-native/Libraries/Core/setUpTimers.js index 8dd5188f9f2dc7..db8baa3e0587f0 100644 --- a/packages/react-native/Libraries/Core/setUpTimers.js +++ b/packages/react-native/Libraries/Core/setUpTimers.js @@ -10,9 +10,6 @@ 'use strict'; -const ReactNativeFeatureFlags = require('../../src/private/featureflags/ReactNativeFeatureFlags'); -const NativeReactNativeFeatureFlags = - require('../../src/private/featureflags/specs/NativeReactNativeFeatureFlags').default; const {polyfillGlobal} = require('../Utilities/PolyfillFunctions'); if (__DEV__) { @@ -21,19 +18,46 @@ if (__DEV__) { } } -const isEventLoopEnabled = (() => { - if (NativeReactNativeFeatureFlags == null) { - return false; - } +// In bridgeless mode, timers are host functions installed from cpp. +if (global.RN$Bridgeless === true) { + // This is the flag that tells React to use `queueMicrotask` to batch state + // updates, instead of using the scheduler to schedule a regular task. + // We use a global variable because we don't currently have any other + // mechanism to pass feature flags from RN to React in OSS. + global.RN$enableMicrotasksInReact = true; - return ( - ReactNativeFeatureFlags.enableBridgelessArchitecture() && - !ReactNativeFeatureFlags.disableEventLoopOnBridgeless() + polyfillGlobal( + 'queueMicrotask', + () => + require('../../src/private/webapis/microtasks/specs/NativeMicrotasks') + .default.queueMicrotask, ); -})(); -// In bridgeless mode, timers are host functions installed from cpp. -if (global.RN$Bridgeless !== true) { + // We shim the immediate APIs via `queueMicrotask` to maintain the backward + // compatibility. + polyfillGlobal( + 'setImmediate', + () => require('./Timers/immediateShim').setImmediate, + ); + polyfillGlobal( + 'clearImmediate', + () => require('./Timers/immediateShim').clearImmediate, + ); + + polyfillGlobal( + 'requestIdleCallback', + () => + require('../../src/private/webapis/idlecallbacks/specs/NativeIdleCallbacks') + .default.requestIdleCallback, + ); + + polyfillGlobal( + 'cancelIdleCallback', + () => + require('../../src/private/webapis/idlecallbacks/specs/NativeIdleCallbacks') + .default.cancelIdleCallback, + ); +} else { /** * Set up timers. * You can use this module directly, or just require InitializeCore. @@ -59,67 +83,22 @@ if (global.RN$Bridgeless !== true) { defineLazyTimer('cancelAnimationFrame'); defineLazyTimer('requestIdleCallback'); defineLazyTimer('cancelIdleCallback'); -} else if (isEventLoopEnabled) { - polyfillGlobal( - 'requestIdleCallback', - () => - require('../../src/private/webapis/idlecallbacks/specs/NativeIdleCallbacks') - .default.requestIdleCallback, - ); - - polyfillGlobal( - 'cancelIdleCallback', - () => - require('../../src/private/webapis/idlecallbacks/specs/NativeIdleCallbacks') - .default.cancelIdleCallback, - ); -} - -// We need to check if the native module is available before accessing the -// feature flag, because otherwise the API would throw an error in the legacy -// architecture in OSS, where the native module isn't available. -if (isEventLoopEnabled) { - // This is the flag that tells React to use `queueMicrotask` to batch state - // updates, instead of using the scheduler to schedule a regular task. - // We use a global variable because we don't currently have any other - // mechanism to pass feature flags from RN to React in OSS. - global.RN$enableMicrotasksInReact = true; + // Polyfill it with promise (regardless it's polyfilled or native) otherwise. polyfillGlobal( 'queueMicrotask', - () => - require('../../src/private/webapis/microtasks/specs/NativeMicrotasks') - .default.queueMicrotask, + () => require('./Timers/queueMicrotask.js').default, ); - // We shim the immediate APIs via `queueMicrotask` to maintain the backward - // compatibility. + // When promise was polyfilled hence is queued to the RN microtask queue, + // we polyfill the immediate APIs as aliases to the ReactNativeMicrotask APIs. + // Note that in bridgeless mode, immediate APIs are installed from cpp. polyfillGlobal( 'setImmediate', - () => require('./Timers/immediateShim').setImmediate, + () => require('./Timers/JSTimers').default.queueReactNativeMicrotask, ); polyfillGlobal( 'clearImmediate', - () => require('./Timers/immediateShim').clearImmediate, - ); -} else { - // Polyfill it with promise (regardless it's polyfilled or native) otherwise. - polyfillGlobal( - 'queueMicrotask', - () => require('./Timers/queueMicrotask.js').default, + () => require('./Timers/JSTimers').default.clearReactNativeMicrotask, ); - - // When promise was polyfilled hence is queued to the RN microtask queue, - // we polyfill the immediate APIs as aliases to the ReactNativeMicrotask APIs. - // Note that in bridgeless mode, immediate APIs are installed from cpp. - if (global.RN$Bridgeless !== true) { - polyfillGlobal( - 'setImmediate', - () => require('./Timers/JSTimers').default.queueReactNativeMicrotask, - ); - polyfillGlobal( - 'clearImmediate', - () => require('./Timers/JSTimers').default.clearReactNativeMicrotask, - ); - } } diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 4bbbba6d75185a..ec190b4a154e94 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6b314d87b09e65d8c70a0d3f4caf953a>> + * @generated SignedSource<<0d5df5cac07efb95bfd5f0984f960a0b>> */ /** @@ -49,13 +49,6 @@ bool NativeReactNativeFeatureFlags::commonTestFlagWithoutNativeImplementation( return false; } -bool NativeReactNativeFeatureFlags::disableEventLoopOnBridgeless( - jsi::Runtime& /*runtime*/) { - // This flag is configured with `skipNativeAPI: true`. - // TODO(T204838867): Implement support for optional methods in C++ TM codegen and remove the method definition altogether. - return false; -} - bool NativeReactNativeFeatureFlags::disableMountItemReorderingAndroid( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::disableMountItemReorderingAndroid(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7b6eca3462ae51..e5557dfaa7bdc7 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3b3ce0cfc8578f517393b89dc76a9aac>> + * @generated SignedSource<<44a75d78e4910acb405e79e9953f563a>> */ /** @@ -39,8 +39,6 @@ class NativeReactNativeFeatureFlags bool commonTestFlagWithoutNativeImplementation(jsi::Runtime& runtime); - bool disableEventLoopOnBridgeless(jsi::Runtime& runtime); - bool disableMountItemReorderingAndroid(jsi::Runtime& runtime); bool enableAccumulatedUpdatesInRawPropsAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index e28c892c95a247..b9bda4a20a5f84 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -57,16 +57,6 @@ const testDefinitions: FeatureFlagDefinitions = { const definitions: FeatureFlagDefinitions = { common: { ...testDefinitions.common, - disableEventLoopOnBridgeless: { - defaultValue: false, - metadata: { - description: - 'The bridgeless architecture enables the event loop by default. This feature flag allows us to force disabling it in specific instances.', - expectedReleaseValue: true, - purpose: 'release', - }, - skipNativeAPI: true, - }, disableMountItemReorderingAndroid: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 96fc19ceb22e67..2899d14ed52865 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<7d66477e7b024c551085f068a6445014>> * @flow strict */ @@ -50,7 +50,6 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ ...ReactNativeFeatureFlagsJsOnly, commonTestFlag: Getter, commonTestFlagWithoutNativeImplementation: Getter, - disableEventLoopOnBridgeless: Getter, disableMountItemReorderingAndroid: Getter, enableAccumulatedUpdatesInRawPropsAndroid: Getter, enableBridgelessArchitecture: Getter, @@ -176,10 +175,6 @@ export const commonTestFlag: Getter = createNativeFlagGetter('commonTes * Common flag for testing (without native implementation). Do NOT modify. */ export const commonTestFlagWithoutNativeImplementation: Getter = createNativeFlagGetter('commonTestFlagWithoutNativeImplementation', false, true); -/** - * The bridgeless architecture enables the event loop by default. This feature flag allows us to force disabling it in specific instances. - */ -export const disableEventLoopOnBridgeless: Getter = createNativeFlagGetter('disableEventLoopOnBridgeless', false, true); /** * Prevent FabricMountingManager from reordering mountitems, which may lead to invalid state on the UI thread */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index eb63439b22de93..f055ab637b4b26 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<73d10e1985377162b75ee60e5bcad8dd>> * @flow strict */ @@ -25,7 +25,6 @@ import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboMod export interface Spec extends TurboModule { +commonTestFlag?: () => boolean; +commonTestFlagWithoutNativeImplementation?: () => boolean; - +disableEventLoopOnBridgeless?: () => boolean; +disableMountItemReorderingAndroid?: () => boolean; +enableAccumulatedUpdatesInRawPropsAndroid?: () => boolean; +enableBridgelessArchitecture?: () => boolean; From 8783196ee540f8f78ce60ad20800338cc7645194 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Thu, 30 Jan 2025 07:27:03 -0800 Subject: [PATCH 11/17] Migrate files in Libraries/EventEmitter and Libraries/Image to use export syntax (#49020) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49020 ## Motivation Modernising the RN codebase to allow for modern Flow tooling to process it. ## This diff - Migrates the `Libraries/EventEmitter/*.js` and `Libraries/Image/*.js` files to use the `export` syntax. - Updates deep-imports of these files to use `.default` - Updates the current iteration of API snapshots (intended). Changelog: [General][Breaking] - Deep imports to modules inside `Libraries/EventEmitter` and `Libraries/Image/*.js` with `require` syntax need to be appended with '.default'. Reviewed By: huntie Differential Revision: D68780876 fbshipit-source-id: bd8e702aba33878e38df6d9c89bec27e7c8df0ac --- .../Libraries/Core/setUpBatchedBridge.js | 5 +++-- .../Libraries/Core/setUpReactDevTools.js | 3 ++- .../Libraries/EventEmitter/RCTEventEmitter.js | 2 +- .../EventEmitter/RCTNativeAppEventEmitter.js | 2 +- .../Libraries/Image/AssetRegistry.js | 4 +++- .../Libraries/Image/AssetSourceResolver.js | 2 +- .../Libraries/Image/Image.android.js | 2 +- .../react-native/Libraries/Image/Image.ios.js | 2 +- .../Libraries/Image/Image.js.flow | 2 +- .../Libraries/Image/ImageBackground.js | 2 +- .../Image/ImageViewNativeComponent.js | 4 ++-- .../Libraries/Image/RelativeImageStub.js | 4 +++- .../Libraries/Image/__tests__/Image-test.js | 2 +- .../Image/__tests__/ImageBackground-test.js | 2 +- .../assetRelativePathInSnapshot-test.js | 2 +- .../__tests__/resolveAssetSource-test.js | 4 ++-- .../Libraries/Image/nativeImageSource.js | 2 +- .../Libraries/Image/resolveAssetSource.js | 6 ++++-- .../getNativeComponentAttributes.js | 2 +- .../ReactNativePrivateInterface.js | 2 +- .../__snapshots__/public-api-test.js.snap | 20 ++++++++++--------- packages/react-native/index.js | 6 +++--- packages/react-native/jest/setup.js | 11 +++++++--- .../examples/Image/ImageCapInsetsExample.js | 3 ++- 24 files changed, 56 insertions(+), 40 deletions(-) diff --git a/packages/react-native/Libraries/Core/setUpBatchedBridge.js b/packages/react-native/Libraries/Core/setUpBatchedBridge.js index 158bf65a872794..32116c5d160fd9 100644 --- a/packages/react-native/Libraries/Core/setUpBatchedBridge.js +++ b/packages/react-native/Libraries/Core/setUpBatchedBridge.js @@ -21,8 +21,9 @@ registerModule( 'RCTDeviceEventEmitter', () => require('../EventEmitter/RCTDeviceEventEmitter').default, ); -registerModule('RCTNativeAppEventEmitter', () => - require('../EventEmitter/RCTNativeAppEventEmitter'), +registerModule( + 'RCTNativeAppEventEmitter', + () => require('../EventEmitter/RCTNativeAppEventEmitter').default, ); registerModule('GlobalPerformanceLogger', () => require('../Utilities/GlobalPerformanceLogger'), diff --git a/packages/react-native/Libraries/Core/setUpReactDevTools.js b/packages/react-native/Libraries/Core/setUpReactDevTools.js index 2d3e32749e11b6..62f8ba68f9296d 100644 --- a/packages/react-native/Libraries/Core/setUpReactDevTools.js +++ b/packages/react-native/Libraries/Core/setUpReactDevTools.js @@ -209,7 +209,8 @@ if (__DEV__) { ); // 3. Fallback to attempting to connect WS-based RDT frontend - const RCTNativeAppEventEmitter = require('../EventEmitter/RCTNativeAppEventEmitter'); + const RCTNativeAppEventEmitter = + require('../EventEmitter/RCTNativeAppEventEmitter').default; RCTNativeAppEventEmitter.addListener( 'RCTDevMenuShown', connectToWSBasedReactDevToolsFrontend, diff --git a/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js b/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js index d191b20baaa500..0619aa712cb28c 100644 --- a/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js +++ b/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js @@ -18,4 +18,4 @@ const RCTEventEmitter = { }, }; -module.exports = RCTEventEmitter; +export default RCTEventEmitter; diff --git a/packages/react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter.js b/packages/react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter.js index bae9fa70f1475d..e9275b622d518f 100644 --- a/packages/react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter.js +++ b/packages/react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter.js @@ -15,4 +15,4 @@ import RCTDeviceEventEmitter from './RCTDeviceEventEmitter'; * adding all event listeners directly to RCTNativeAppEventEmitter. */ const RCTNativeAppEventEmitter = RCTDeviceEventEmitter; -module.exports = RCTNativeAppEventEmitter; +export default RCTNativeAppEventEmitter; diff --git a/packages/react-native/Libraries/Image/AssetRegistry.js b/packages/react-native/Libraries/Image/AssetRegistry.js index e8b73b2554afa4..1209f16eb2ae0b 100644 --- a/packages/react-native/Libraries/Image/AssetRegistry.js +++ b/packages/react-native/Libraries/Image/AssetRegistry.js @@ -12,7 +12,9 @@ import type {PackagerAsset} from '@react-native/assets-registry/registry'; -module.exports = require('@react-native/assets-registry/registry') as { +const AssetRegistry = require('@react-native/assets-registry/registry') as { registerAsset: (asset: PackagerAsset) => number, getAssetByID: (assetId: number) => PackagerAsset, }; + +module.exports = AssetRegistry; diff --git a/packages/react-native/Libraries/Image/AssetSourceResolver.js b/packages/react-native/Libraries/Image/AssetSourceResolver.js index c9c9ac6a3d54e0..d7e2032706c93b 100644 --- a/packages/react-native/Libraries/Image/AssetSourceResolver.js +++ b/packages/react-native/Libraries/Image/AssetSourceResolver.js @@ -210,4 +210,4 @@ class AssetSourceResolver { pickScale; } -module.exports = AssetSourceResolver; +export default AssetSourceResolver; diff --git a/packages/react-native/Libraries/Image/Image.android.js b/packages/react-native/Libraries/Image/Image.android.js index 08fbe9d1faca02..e5ee3d5a495b4c 100644 --- a/packages/react-native/Libraries/Image/Image.android.js +++ b/packages/react-native/Libraries/Image/Image.android.js @@ -319,4 +319,4 @@ const styles = StyleSheet.create({ }, }); -module.exports = Image; +export default Image; diff --git a/packages/react-native/Libraries/Image/Image.ios.js b/packages/react-native/Libraries/Image/Image.ios.js index b3b9e61e50b9ba..337411aff73107 100644 --- a/packages/react-native/Libraries/Image/Image.ios.js +++ b/packages/react-native/Libraries/Image/Image.ios.js @@ -249,4 +249,4 @@ const styles = StyleSheet.create({ }, }); -module.exports = Image; +export default Image; diff --git a/packages/react-native/Libraries/Image/Image.js.flow b/packages/react-native/Libraries/Image/Image.js.flow index d77f80eee6639a..2afd5d87ba27a6 100644 --- a/packages/react-native/Libraries/Image/Image.js.flow +++ b/packages/react-native/Libraries/Image/Image.js.flow @@ -10,4 +10,4 @@ import type {Image} from './ImageTypes.flow'; -declare module.exports: Image; +declare export default Image; diff --git a/packages/react-native/Libraries/Image/ImageBackground.js b/packages/react-native/Libraries/Image/ImageBackground.js index 3e983dc0050ef7..2eeee82b77311c 100644 --- a/packages/react-native/Libraries/Image/ImageBackground.js +++ b/packages/react-native/Libraries/Image/ImageBackground.js @@ -103,4 +103,4 @@ class ImageBackground extends React.Component { } } -module.exports = ImageBackground; +export default ImageBackground; diff --git a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js index 22ac430c2190ef..6b5632a1f365dc 100644 --- a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js +++ b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js @@ -83,7 +83,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = validAttributes: { blurRadius: true, defaultSource: { - process: require('./resolveAssetSource'), + process: require('./resolveAssetSource').default, }, internal_analyticTag: true, resizeMethod: true, @@ -146,7 +146,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = diff: require('../Utilities/differ/insetsDiffer'), }, defaultSource: { - process: require('./resolveAssetSource'), + process: require('./resolveAssetSource').default, }, internal_analyticTag: true, resizeMode: true, diff --git a/packages/react-native/Libraries/Image/RelativeImageStub.js b/packages/react-native/Libraries/Image/RelativeImageStub.js index fbb125a89b1e70..355ff92c22aa6d 100644 --- a/packages/react-native/Libraries/Image/RelativeImageStub.js +++ b/packages/react-native/Libraries/Image/RelativeImageStub.js @@ -15,7 +15,7 @@ const AssetRegistry = require('@react-native/assets-registry/registry'); -module.exports = (AssetRegistry.registerAsset({ +const RelativeImageStub = (AssetRegistry.registerAsset({ __packager_asset: true, fileSystemLocation: '/full/path/to/directory', httpServerLocation: '/assets/full/path/to/directory', @@ -26,3 +26,5 @@ module.exports = (AssetRegistry.registerAsset({ name: 'icon', type: 'png', }): number); + +module.exports = RelativeImageStub; diff --git a/packages/react-native/Libraries/Image/__tests__/Image-test.js b/packages/react-native/Libraries/Image/__tests__/Image-test.js index fde878bcbd8391..9220f6ebec7d78 100644 --- a/packages/react-native/Libraries/Image/__tests__/Image-test.js +++ b/packages/react-native/Libraries/Image/__tests__/Image-test.js @@ -18,7 +18,7 @@ import NativeImageLoaderIOS from '../NativeImageLoaderIOS'; import {act, create} from 'react-test-renderer'; const render = require('../../../jest/renderer'); -const Image = require('../Image'); +const Image = require('../Image').default; const ImageInjection = require('../ImageInjection'); const React = require('react'); diff --git a/packages/react-native/Libraries/Image/__tests__/ImageBackground-test.js b/packages/react-native/Libraries/Image/__tests__/ImageBackground-test.js index d13158dc9ae7c0..c4c0edd1899fef 100644 --- a/packages/react-native/Libraries/Image/__tests__/ImageBackground-test.js +++ b/packages/react-native/Libraries/Image/__tests__/ImageBackground-test.js @@ -12,7 +12,7 @@ 'use strict'; const render = require('../../../jest/renderer'); -const ImageBackground = require('../ImageBackground'); +const ImageBackground = require('../ImageBackground').default; const React = require('react'); describe('ImageBackground', () => { diff --git a/packages/react-native/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js b/packages/react-native/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js index 780c701007700c..7ec3a69d6f140f 100644 --- a/packages/react-native/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js +++ b/packages/react-native/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js @@ -14,7 +14,7 @@ jest.disableAutomock(); const {create} = require('../../../jest/renderer'); const View = require('../../Components/View/View').default; -const Image = require('../Image'); +const Image = require('../Image').default; const React = require('react'); it('renders assets based on relative path', async () => { diff --git a/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js b/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js index e924b3dc61c38f..8fcb0768a84525 100644 --- a/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/packages/react-native/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -20,7 +20,7 @@ describe('resolveAssetSource', () => { jest.resetModules(); AssetRegistry = require('@react-native/assets-registry/registry'); - resolveAssetSource = require('../resolveAssetSource'); + resolveAssetSource = require('../resolveAssetSource').default; NativeSourceCode = require('../../NativeModules/specs/NativeSourceCode').default; Platform = require('../../Utilities/Platform'); @@ -434,7 +434,7 @@ describe('resolveAssetSource', () => { }); describe('resolveAssetSource.pickScale', () => { - const resolveAssetSource = require('../resolveAssetSource'); + const resolveAssetSource = require('../resolveAssetSource').default; it('picks matching scale', () => { expect(resolveAssetSource.pickScale([1], 2)).toBe(1); diff --git a/packages/react-native/Libraries/Image/nativeImageSource.js b/packages/react-native/Libraries/Image/nativeImageSource.js index 8a98e3940c01cb..04835eea300d52 100644 --- a/packages/react-native/Libraries/Image/nativeImageSource.js +++ b/packages/react-native/Libraries/Image/nativeImageSource.js @@ -61,4 +61,4 @@ function nativeImageSource(spec: NativeImageSourceSpec): ImageURISource { }; } -module.exports = nativeImageSource; +export default nativeImageSource; diff --git a/packages/react-native/Libraries/Image/resolveAssetSource.js b/packages/react-native/Libraries/Image/resolveAssetSource.js index d65ab945d5822f..9531b4b53a119d 100644 --- a/packages/react-native/Libraries/Image/resolveAssetSource.js +++ b/packages/react-native/Libraries/Image/resolveAssetSource.js @@ -11,11 +11,13 @@ // Utilities for resolving an asset into a `source` for e.g. `Image` import type {ResolvedAssetSource} from './AssetSourceResolver'; +import typeof AssetSourceResolverT from './AssetSourceResolver'; import type {ImageSource} from './ImageSource'; import SourceCode from '../NativeModules/specs/NativeSourceCode'; -const AssetSourceResolver = require('./AssetSourceResolver'); +const AssetSourceResolver: AssetSourceResolverT = + require('./AssetSourceResolver').default; const {pickScale} = require('./AssetUtils'); const AssetRegistry = require('@react-native/assets-registry/registry'); @@ -140,4 +142,4 @@ function resolveAssetSource(source: ?ImageSource): ?ResolvedAssetSource { resolveAssetSource.pickScale = pickScale; resolveAssetSource.setCustomSourceTransformer = setCustomSourceTransformer; resolveAssetSource.addCustomSourceTransformer = addCustomSourceTransformer; -module.exports = resolveAssetSource; +export default resolveAssetSource; diff --git a/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js b/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js index eb9d8be716b553..04e8e103f576c6 100644 --- a/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js +++ b/packages/react-native/Libraries/ReactNative/getNativeComponentAttributes.js @@ -14,7 +14,7 @@ import processBoxShadow from '../StyleSheet/processBoxShadow'; const ReactNativeStyleAttributes = require('../Components/View/ReactNativeStyleAttributes').default; -const resolveAssetSource = require('../Image/resolveAssetSource'); +const resolveAssetSource = require('../Image/resolveAssetSource').default; const processBackgroundImage = require('../StyleSheet/processBackgroundImage').default; const processColor = require('../StyleSheet/processColor').default; diff --git a/packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js b/packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js index cda20167f2c163..16e43aafcfcb9d 100644 --- a/packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js +++ b/packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js @@ -48,7 +48,7 @@ module.exports = { return require('../Utilities/Platform'); }, get RCTEventEmitter(): RCTEventEmitter { - return require('../EventEmitter/RCTEventEmitter'); + return require('../EventEmitter/RCTEventEmitter').default; }, get ReactNativeViewConfigRegistry(): ReactNativeViewConfigRegistry { return require('../Renderer/shims/ReactNativeViewConfigRegistry'); diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 939d22dbed45ed..b9dbe3f48282e4 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -4710,13 +4710,13 @@ declare export default IEventEmitter; exports[`public API should not change unintentionally Libraries/EventEmitter/RCTEventEmitter.js 1`] = ` "declare const RCTEventEmitter: { register(eventEmitter: any): void }; -declare module.exports: RCTEventEmitter; +declare export default typeof RCTEventEmitter; " `; exports[`public API should not change unintentionally Libraries/EventEmitter/RCTNativeAppEventEmitter.js 1`] = ` "declare const RCTNativeAppEventEmitter: typeof RCTDeviceEventEmitter; -declare module.exports: RCTNativeAppEventEmitter; +declare export default typeof RCTNativeAppEventEmitter; " `; @@ -4799,10 +4799,11 @@ declare export default typeof EventPolyfill; `; exports[`public API should not change unintentionally Libraries/Image/AssetRegistry.js 1`] = ` -"declare module.exports: { +"declare const AssetRegistry: { registerAsset: (asset: PackagerAsset) => number, getAssetByID: (assetId: number) => PackagerAsset, }; +declare module.exports: AssetRegistry; " `; @@ -4835,7 +4836,7 @@ declare class AssetSourceResolver { fromSource(source: string): ResolvedAssetSource; static pickScale: (scales: Array, deviceScale?: number) => number; } -declare module.exports: AssetSourceResolver; +declare export default typeof AssetSourceResolver; " `; @@ -4850,7 +4851,7 @@ declare export function getUrlCacheBreaker(): string; `; exports[`public API should not change unintentionally Libraries/Image/Image.js.flow 1`] = ` -"declare module.exports: Image; +"declare export default Image; " `; @@ -4868,7 +4869,7 @@ exports[`public API should not change unintentionally Libraries/Image/ImageBackg _captureRef: $FlowFixMe; render(): React.Node; } -declare module.exports: ImageBackground; +declare export default typeof ImageBackground; " `; @@ -5144,7 +5145,8 @@ declare export default typeof NativeImageStoreIOS; `; exports[`public API should not change unintentionally Libraries/Image/RelativeImageStub.js 1`] = ` -"declare module.exports: number; +"declare const RelativeImageStub: number; +declare module.exports: RelativeImageStub; " `; @@ -5171,13 +5173,13 @@ exports[`public API should not change unintentionally Libraries/Image/nativeImag width: number, }>; declare function nativeImageSource(spec: NativeImageSourceSpec): ImageURISource; -declare module.exports: nativeImageSource; +declare export default typeof nativeImageSource; " `; exports[`public API should not change unintentionally Libraries/Image/resolveAssetSource.js 1`] = ` "declare function resolveAssetSource(source: ?ImageSource): ?ResolvedAssetSource; -declare module.exports: resolveAssetSource; +declare export default typeof resolveAssetSource; " `; diff --git a/packages/react-native/index.js b/packages/react-native/index.js index c37681399584ec..f511a34e63a2cb 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -128,10 +128,10 @@ module.exports = { return require('./Libraries/Lists/FlatList').default; }, get Image(): Image { - return require('./Libraries/Image/Image'); + return require('./Libraries/Image/Image').default; }, get ImageBackground(): ImageBackground { - return require('./Libraries/Image/ImageBackground'); + return require('./Libraries/Image/ImageBackground').default; }, get InputAccessoryView(): InputAccessoryView { return require('./Libraries/Components/TextInput/InputAccessoryView') @@ -367,7 +367,7 @@ module.exports = { .DynamicColorIOS; }, get NativeAppEventEmitter(): RCTNativeAppEventEmitter { - return require('./Libraries/EventEmitter/RCTNativeAppEventEmitter'); + return require('./Libraries/EventEmitter/RCTNativeAppEventEmitter').default; }, get NativeModules(): NativeModules { return require('./Libraries/BatchedBridge/NativeModules').default; diff --git a/packages/react-native/jest/setup.js b/packages/react-native/jest/setup.js index aab3d8148bf5c3..0585cc8e7a02ab 100644 --- a/packages/react-native/jest/setup.js +++ b/packages/react-native/jest/setup.js @@ -120,9 +120,14 @@ jest }, }, })) - .mock('../Libraries/Image/Image', () => - mockComponent('../Libraries/Image/Image'), - ) + .mock('../Libraries/Image/Image', () => ({ + __esModule: true, + default: mockComponent( + '../Libraries/Image/Image', + /* instanceMethods */ null, + /* isESModule */ true, + ), + })) .mock('../Libraries/Text/Text', () => ({ __esModule: true, default: mockComponent( diff --git a/packages/rn-tester/js/examples/Image/ImageCapInsetsExample.js b/packages/rn-tester/js/examples/Image/ImageCapInsetsExample.js index ef8fb08e120c06..98bfa745ff5769 100644 --- a/packages/rn-tester/js/examples/Image/ImageCapInsetsExample.js +++ b/packages/rn-tester/js/examples/Image/ImageCapInsetsExample.js @@ -12,7 +12,8 @@ const React = require('react'); const ReactNative = require('react-native'); -const nativeImageSource = require('react-native/Libraries/Image/nativeImageSource'); +const nativeImageSource = + require('react-native/Libraries/Image/nativeImageSource').default; const {Image, StyleSheet, Text, View} = ReactNative; type Props = $ReadOnly<{}>; From 566a45e28c76865326aa7f920701a3489d8c166e Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Thu, 30 Jan 2025 08:45:24 -0800 Subject: [PATCH 12/17] adjust rntester readme to use "yarn prepare-ios" (#49067) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49067 ## Changelog: [Internal] [Fixed] - fixed rntester readme to guide users to launch the correct ios prepare command Reviewed By: cipolleschi Differential Revision: D68897627 fbshipit-source-id: 93d30b2728f452e27448d0ef468ee148296f2324 --- packages/rn-tester/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rn-tester/README.md b/packages/rn-tester/README.md index 0d30905bb0fe3f..23dabf9fd47c55 100644 --- a/packages/rn-tester/README.md +++ b/packages/rn-tester/README.md @@ -31,7 +31,7 @@ If you are still having a problem after doing the clean up (which can happen if Both macOS and Xcode are required. 1. `cd packages/rn-tester` 2. Install [Bundler](https://bundler.io/): `gem install bundler`. We use bundler to install the right version of [CocoaPods](https://cocoapods.org/) locally. -3. Install Bundler and CocoaPods dependencies: `bundle install && bundle exec pod install` or `yarn setup-ios-hermes`. In order to use JSC instead of Hermes engine, run: `USE_HERMES=0 bundle exec pod install` or `yarn setup-ios-jsc` instead. +3. Install Bundler and CocoaPods dependencies: `bundle install && bundle exec pod install` or `yarn prepare-ios`. In order to use JSC instead of Hermes engine, run: `USE_HERMES=0 bundle exec pod install` or `yarn prepare-ios --arch old --jsvm jsc` instead. 4. Open the generated `RNTesterPods.xcworkspace`. This is not checked in, as it is generated by CocoaPods. Do not open `RNTesterPods.xcodeproj` directly. #### Note for Apple Silicon users From accde40e1b686ac259e483d47296094bcdc777b2 Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Acosta Date: Thu, 30 Jan 2025 11:04:19 -0800 Subject: [PATCH 13/17] Fix gapbetween making backgroundColor render when width/height is 0 (#49074) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49074 This used to not be noticeable when we were clipping the background even without a border, after fixing that, we got line when the width/height was 0 This is again not an issue with new Background and Border since they take a slightly different approach Diff that caused the issue D68279400 ie. {F1974794589} Changelog: [Internal] Reviewed By: javache Differential Revision: D68843649 fbshipit-source-id: a25ace46b604690e3385c49d6f4bb3a4163bc594 --- .../drawable/CSSBackgroundDrawable.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java index 549bebd68ae81d..d95b81e139b3a0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java @@ -648,7 +648,6 @@ private void updatePath() { } // Clip border ONLY if at least one edge is non-transparent - float pathAdjustment = 0f; if (Color.alpha(colorLeft) != 0 || Color.alpha(colorTop) != 0 || Color.alpha(colorRight) != 0 @@ -659,10 +658,6 @@ private void updatePath() { mInnerClipTempRectForBorderRadius.bottom -= borderWidth.bottom; mInnerClipTempRectForBorderRadius.left += borderWidth.left; mInnerClipTempRectForBorderRadius.right -= borderWidth.right; - - // only close gap between border and main path if we draw the border, otherwise - // we wind up pixelating small pixel-radius curves - pathAdjustment = mGapBetweenPaths; } mTempRectForCenterDrawPath.top += borderWidth.top * 0.5f; @@ -716,11 +711,21 @@ private void updatePath() { // border. mGapBetweenPaths is used to slightly enlarge the rectangle // (mInnerClipTempRectForBorderRadius), ensuring the border can be // drawn on top without the gap. + // only close gap between border and main path if we draw the border, otherwise + // we wind up pixelating small pixel-radius curves mBackgroundColorRenderPath.addRoundRect( - mInnerClipTempRectForBorderRadius.left - pathAdjustment, - mInnerClipTempRectForBorderRadius.top - pathAdjustment, - mInnerClipTempRectForBorderRadius.right + pathAdjustment, - mInnerClipTempRectForBorderRadius.bottom + pathAdjustment, + (borderWidth.left > 0) + ? mInnerClipTempRectForBorderRadius.left - mGapBetweenPaths + : mInnerClipTempRectForBorderRadius.left, + (borderWidth.top > 0) + ? mInnerClipTempRectForBorderRadius.top - mGapBetweenPaths + : mInnerClipTempRectForBorderRadius.top, + (borderWidth.right > 0) + ? mInnerClipTempRectForBorderRadius.right + mGapBetweenPaths + : mInnerClipTempRectForBorderRadius.right, + (borderWidth.bottom > 0) + ? mInnerClipTempRectForBorderRadius.bottom + mGapBetweenPaths + : mInnerClipTempRectForBorderRadius.bottom, new float[] { innerTopLeftRadiusX, innerTopLeftRadiusY, From 9ba4dd81db08c401ef04fa60800585bf39c6dab3 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 30 Jan 2025 11:30:05 -0800 Subject: [PATCH 14/17] Delete Libraries/JSInspector (#49019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49019 Removes the `JSInspector` class and its dependencies. - This was related to the legacy `ReactCommon/inspector/` subsystem (D4021490) — which added a compat layer from JavaScriptCore to CDP for an earlier version of Chrome debugging. - The JS components of this system (`JSInspector.js`, `NetworkAgent.js`) were added in D4021516. `ReactCommon/inspector/` has since been deleted and these components are no longer load bearing. - We intend to replace this logic (at least, the archaic `XHRInterceptor` behaviour, which worked at one point) with native debugger `Network` domain support in our C++ layer. **Changes** - Remove all modules under `Libraries/JSInspector/`. - Remove all `XHRInterceptor` call sites. - Remove the `JSInspector.registerAgent()` mount point in `setUpDeveloperTools.js`. - Exclude `Libraries/Core/setUp*` from `public-api-test` (these are side-effect setup files with no exported API). Changelog: [General][Breaking] - Remove legacy Libraries/JSInspector modules Reviewed By: christophpurrer Differential Revision: D68780147 fbshipit-source-id: 3d11cc89886a91055e6b69ac6f0609c288965801 --- .../Libraries/Core/setUpDeveloperTools.js | 4 - .../Libraries/JSInspector/InspectorAgent.js | 27 -- .../Libraries/JSInspector/JSInspector.js | 33 -- .../Libraries/JSInspector/NetworkAgent.js | 296 ------------------ .../Libraries/Network/XMLHttpRequest_new.js | 49 --- .../Libraries/Network/XMLHttpRequest_old.js | 49 --- .../__snapshots__/public-api-test.js.snap | 132 -------- .../Libraries/__tests__/public-api-test.js | 1 + 8 files changed, 1 insertion(+), 590 deletions(-) delete mode 100644 packages/react-native/Libraries/JSInspector/InspectorAgent.js delete mode 100644 packages/react-native/Libraries/JSInspector/JSInspector.js delete mode 100644 packages/react-native/Libraries/JSInspector/NetworkAgent.js diff --git a/packages/react-native/Libraries/Core/setUpDeveloperTools.js b/packages/react-native/Libraries/Core/setUpDeveloperTools.js index 27edd12da2a200..dd82f729eb5527 100644 --- a/packages/react-native/Libraries/Core/setUpDeveloperTools.js +++ b/packages/react-native/Libraries/Core/setUpDeveloperTools.js @@ -17,10 +17,6 @@ declare var console: {[string]: $FlowFixMe}; * You can use this module directly, or just require InitializeCore. */ if (__DEV__) { - // Set up inspector - const JSInspector = require('../JSInspector/JSInspector'); - JSInspector.registerAgent(require('../JSInspector/NetworkAgent').default); - // Note we can't check if console is "native" because it would appear "native" in JSC and Hermes. // We also can't check any properties that don't exist in the Chrome worker environment. // So we check a navigator property that's set to a particular value ("Netscape") in all real browsers. diff --git a/packages/react-native/Libraries/JSInspector/InspectorAgent.js b/packages/react-native/Libraries/JSInspector/InspectorAgent.js deleted file mode 100644 index 921ce972410a38..00000000000000 --- a/packages/react-native/Libraries/JSInspector/InspectorAgent.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow strict - */ - -'use strict'; - -export type EventSender = (name: string, params: mixed) => void; - -class InspectorAgent { - _eventSender: EventSender; - - constructor(eventSender: EventSender) { - this._eventSender = eventSender; - } - - sendEvent(name: string, params: mixed) { - this._eventSender(name, params); - } -} - -export default InspectorAgent; diff --git a/packages/react-native/Libraries/JSInspector/JSInspector.js b/packages/react-native/Libraries/JSInspector/JSInspector.js deleted file mode 100644 index 0a6388e3d3dd37..00000000000000 --- a/packages/react-native/Libraries/JSInspector/JSInspector.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow strict - */ - -'use strict'; - -import type {EventSender} from './InspectorAgent'; - -interface Agent { - constructor(eventSender: EventSender): void; -} - -// Flow doesn't support static declarations in interface -type AgentClass = Class & {DOMAIN: string, ...}; - -const JSInspector = { - registerAgent(type: AgentClass) { - if (global.__registerInspectorAgent) { - global.__registerInspectorAgent(type); - } - }, - getTimestamp(): number { - return global.__inspectorTimestamp(); - }, -}; - -module.exports = JSInspector; diff --git a/packages/react-native/Libraries/JSInspector/NetworkAgent.js b/packages/react-native/Libraries/JSInspector/NetworkAgent.js deleted file mode 100644 index 10cf147490e4f7..00000000000000 --- a/packages/react-native/Libraries/JSInspector/NetworkAgent.js +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow strict-local - */ - -'use strict'; - -import type EventSender from './InspectorAgent'; - -import InspectorAgent from './InspectorAgent'; - -const XMLHttpRequest = require('../Network/XMLHttpRequest'); -const JSInspector = require('./JSInspector'); - -type RequestId = string; - -type LoaderId = string; -type FrameId = string; -type Timestamp = number; - -type Headers = {[string]: string}; - -// We don't currently care about this -type ResourceTiming = null; - -type ResourceType = - | 'Document' - | 'Stylesheet' - | 'Image' - | 'Media' - | 'Font' - | 'Script' - | 'TextTrack' - | 'XHR' - | 'Fetch' - | 'EventSource' - | 'WebSocket' - | 'Manifest' - | 'Other'; - -type SecurityState = - | 'unknown' - | 'neutral' - | 'insecure' - | 'warning' - | 'secure' - | 'info'; -type BlockedReason = - | 'csp' - | 'mixed-content' - | 'origin' - | 'inspector' - | 'subresource-filter' - | 'other'; - -type StackTrace = null; - -type Initiator = { - type: 'script' | 'other', - stackTrace?: StackTrace, - url?: string, - lineNumber?: number, - ... -}; - -type ResourcePriority = 'VeryLow' | 'Low' | 'Medium' | 'High' | 'VeryHigh'; - -type Request = { - url: string, - method: string, - headers: Headers, - postData?: string, - mixedContentType?: 'blockable' | 'optionally-blockable' | 'none', - initialPriority: ResourcePriority, - ... -}; - -type Response = { - url: string, - status: number, - statusText: string, - headers: Headers, - headersText?: string, - mimeType: string, - requestHeaders?: Headers, - requestHeadersText?: string, - connectionReused: boolean, - connectionId: number, - fromDiskCache?: boolean, - encodedDataLength: number, - timing?: ResourceTiming, - securityState: SecurityState, - ... -}; - -type RequestWillBeSentEvent = { - requestId: RequestId, - frameId: FrameId, - loaderId: LoaderId, - documentURL: string, - request: Request, - timestamp: Timestamp, - initiator: Initiator, - redirectResponse?: Response, - // This is supposed to be optional but the inspector crashes without it, - // see https://bugs.chromium.org/p/chromium/issues/detail?id=653138 - type: ResourceType, - ... -}; - -type ResponseReceivedEvent = { - requestId: RequestId, - frameId: FrameId, - loaderId: LoaderId, - timestamp: Timestamp, - type: ResourceType, - response: Response, - ... -}; - -type DataReceived = { - requestId: RequestId, - timestamp: Timestamp, - dataLength: number, - encodedDataLength: number, - ... -}; - -type LoadingFinishedEvent = { - requestId: RequestId, - timestamp: Timestamp, - encodedDataLength: number, - ... -}; - -type LoadingFailedEvent = { - requestId: RequestId, - timestamp: Timestamp, - type: ResourceType, - errorText: string, - canceled?: boolean, - blockedReason?: BlockedReason, - ... -}; - -class Interceptor { - _agent: NetworkAgent; - _requests: Map; - - constructor(agent: NetworkAgent) { - this._agent = agent; - this._requests = new Map(); - } - - getData(requestId: string): ?string { - return this._requests.get(requestId); - } - - requestSent(id: number, url: string, method: string, headers: Headers) { - const requestId = String(id); - this._requests.set(requestId, ''); - - const request: Request = { - url, - method, - headers, - initialPriority: 'Medium', - }; - const event: RequestWillBeSentEvent = { - requestId, - documentURL: '', - frameId: '1', - loaderId: '1', - request, - timestamp: JSInspector.getTimestamp(), - initiator: { - // TODO(blom): Get stack trace - // If type is 'script' the inspector will try to execute - // `stack.callFrames[0]` - type: 'other', - }, - type: 'Other', - }; - this._agent.sendEvent('requestWillBeSent', event); - } - - responseReceived(id: number, url: string, status: number, headers: Headers) { - const requestId = String(id); - const response: Response = { - url, - status, - statusText: String(status), - headers, - // TODO(blom) refined headers, can we get this? - requestHeaders: {}, - mimeType: this._getMimeType(headers), - connectionReused: false, - connectionId: -1, - encodedDataLength: 0, - securityState: 'unknown', - }; - - const event: ResponseReceivedEvent = { - requestId, - frameId: '1', - loaderId: '1', - timestamp: JSInspector.getTimestamp(), - type: 'Other', - response, - }; - this._agent.sendEvent('responseReceived', event); - } - - dataReceived(id: number, data: string) { - const requestId = String(id); - const existingData = this._requests.get(requestId) || ''; - this._requests.set(requestId, existingData.concat(data)); - const event: DataReceived = { - requestId, - timestamp: JSInspector.getTimestamp(), - dataLength: data.length, - encodedDataLength: data.length, - }; - this._agent.sendEvent('dataReceived', event); - } - - loadingFinished(id: number, encodedDataLength: number) { - const event: LoadingFinishedEvent = { - requestId: String(id), - timestamp: JSInspector.getTimestamp(), - encodedDataLength, - }; - this._agent.sendEvent('loadingFinished', event); - } - - loadingFailed(id: number, error: string) { - const event: LoadingFailedEvent = { - requestId: String(id), - timestamp: JSInspector.getTimestamp(), - type: 'Other', - errorText: error, - }; - this._agent.sendEvent('loadingFailed', event); - } - - _getMimeType(headers: Headers): string { - const contentType = headers['Content-Type'] || ''; - return contentType.split(';')[0]; - } -} - -type EnableArgs = { - maxResourceBufferSize?: number, - maxTotalBufferSize?: number, - ... -}; - -class NetworkAgent extends InspectorAgent { - static DOMAIN: string = 'Network'; - - _sendEvent: EventSender; - _interceptor: ?Interceptor; - - enable({maxResourceBufferSize, maxTotalBufferSize}: EnableArgs) { - this._interceptor = new Interceptor(this); - XMLHttpRequest.setInterceptor(this._interceptor); - } - - disable() { - XMLHttpRequest.setInterceptor(null); - this._interceptor = null; - } - - getResponseBody({requestId}: {requestId: RequestId, ...}): { - body: ?string, - base64Encoded: boolean, - ... - } { - return {body: this.interceptor().getData(requestId), base64Encoded: false}; - } - - interceptor(): Interceptor { - if (this._interceptor) { - return this._interceptor; - } else { - throw Error('_interceptor can not be null'); - } - } -} - -export default NetworkAgent; diff --git a/packages/react-native/Libraries/Network/XMLHttpRequest_new.js b/packages/react-native/Libraries/Network/XMLHttpRequest_new.js index 2b3ec300ef5624..1eec800735d4d0 100644 --- a/packages/react-native/Libraries/Network/XMLHttpRequest_new.js +++ b/packages/react-native/Libraries/Network/XMLHttpRequest_new.js @@ -45,19 +45,6 @@ export type ResponseType = | 'text'; export type Response = ?Object | string; -type XHRInterceptor = interface { - requestSent(id: number, url: string, method: string, headers: Object): void, - responseReceived( - id: number, - url: string, - status: number, - headers: Object, - ): void, - dataReceived(id: number, data: string): void, - loadingFinished(id: number, encodedDataLength: number): void, - loadingFailed(id: number, error: string): void, -}; - // The native blob module is optional so inject it here if available. if (BlobManager.isAvailable) { BlobManager.addNetworkingHandler(); @@ -133,7 +120,6 @@ class XMLHttpRequest extends EventTarget { static LOADING: number = LOADING; static DONE: number = DONE; - static _interceptor: ?XHRInterceptor = null; static _profiling: boolean = false; UNSENT: number = UNSENT; @@ -171,10 +157,6 @@ class XMLHttpRequest extends EventTarget { _startTime: ?number = null; _performanceLogger: IPerformanceLogger = GlobalPerformanceLogger; - static setInterceptor(interceptor: ?XHRInterceptor) { - XMLHttpRequest._interceptor = interceptor; - } - static enableProfiling(enableProfiling: boolean): void { XMLHttpRequest._profiling = enableProfiling; } @@ -304,14 +286,6 @@ class XMLHttpRequest extends EventTarget { // exposed for testing __didCreateRequest(requestId: number): void { this._requestId = requestId; - - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.requestSent( - requestId, - this._url || '', - this._method || 'GET', - this._headers, - ); } // exposed for testing @@ -349,14 +323,6 @@ class XMLHttpRequest extends EventTarget { } else { delete this.responseURL; } - - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.responseReceived( - requestId, - responseURL || this._url || '', - status, - responseHeaders || {}, - ); } } @@ -367,9 +333,6 @@ class XMLHttpRequest extends EventTarget { this._response = response; this._cachedResponse = undefined; // force lazy recomputation this.setReadyState(this.LOADING); - - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.dataReceived(requestId, response); } __didReceiveIncrementalData( @@ -392,8 +355,6 @@ class XMLHttpRequest extends EventTarget { 'Track:XMLHttpRequest:Incremental Data: ' + this._getMeasureURL(), ); } - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.dataReceived(requestId, responseText); this.setReadyState(this.LOADING); this.__didReceiveDataProgress(requestId, progress, total); @@ -443,16 +404,6 @@ class XMLHttpRequest extends EventTarget { end: performance.now(), }); } - if (error) { - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.loadingFailed(requestId, error); - } else { - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.loadingFinished( - requestId, - this._response.length, - ); - } } } diff --git a/packages/react-native/Libraries/Network/XMLHttpRequest_old.js b/packages/react-native/Libraries/Network/XMLHttpRequest_old.js index 0f6b288529d550..8797ccb8937da1 100644 --- a/packages/react-native/Libraries/Network/XMLHttpRequest_old.js +++ b/packages/react-native/Libraries/Network/XMLHttpRequest_old.js @@ -34,19 +34,6 @@ export type ResponseType = | 'text'; export type Response = ?Object | string; -type XHRInterceptor = interface { - requestSent(id: number, url: string, method: string, headers: Object): void, - responseReceived( - id: number, - url: string, - status: number, - headers: Object, - ): void, - dataReceived(id: number, data: string): void, - loadingFinished(id: number, encodedDataLength: number): void, - loadingFailed(id: number, error: string): void, -}; - // The native blob module is optional so inject it here if available. if (BlobManager.isAvailable) { BlobManager.addNetworkingHandler(); @@ -101,7 +88,6 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { static LOADING: number = LOADING; static DONE: number = DONE; - static _interceptor: ?XHRInterceptor = null; static _profiling: boolean = false; UNSENT: number = UNSENT; @@ -149,10 +135,6 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { _startTime: ?number = null; _performanceLogger: IPerformanceLogger = GlobalPerformanceLogger; - static setInterceptor(interceptor: ?XHRInterceptor) { - XMLHttpRequest._interceptor = interceptor; - } - static enableProfiling(enableProfiling: boolean): void { XMLHttpRequest._profiling = enableProfiling; } @@ -282,14 +264,6 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { // exposed for testing __didCreateRequest(requestId: number): void { this._requestId = requestId; - - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.requestSent( - requestId, - this._url || '', - this._method || 'GET', - this._headers, - ); } // exposed for testing @@ -325,14 +299,6 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { } else { delete this.responseURL; } - - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.responseReceived( - requestId, - responseURL || this._url || '', - status, - responseHeaders || {}, - ); } } @@ -343,9 +309,6 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { this._response = response; this._cachedResponse = undefined; // force lazy recomputation this.setReadyState(this.LOADING); - - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.dataReceived(requestId, response); } __didReceiveIncrementalData( @@ -368,8 +331,6 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { 'Track:XMLHttpRequest:Incremental Data: ' + this._getMeasureURL(), ); } - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.dataReceived(requestId, responseText); this.setReadyState(this.LOADING); this.__didReceiveDataProgress(requestId, progress, total); @@ -417,16 +378,6 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { end: performance.now(), }); } - if (error) { - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.loadingFailed(requestId, error); - } else { - XMLHttpRequest._interceptor && - XMLHttpRequest._interceptor.loadingFinished( - requestId, - this._response.length, - ); - } } } diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index b9dbe3f48282e4..650faac4468296 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -4594,44 +4594,6 @@ declare export default typeof registerCallableModule; " `; -exports[`public API should not change unintentionally Libraries/Core/setUpAlert.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpBatchedBridge.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpDeveloperTools.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpErrorHandling.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpGlobals.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpNavigator.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpPerformance.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpReactDevTools.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpReactRefresh.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpRegeneratorRuntime.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpSegmentFetcher.js 1`] = ` -"export type FetchSegmentFunction = typeof __fetchSegment; -declare function __fetchSegment( - segmentId: number, - options: $ReadOnly<{ - otaBuildNumber: ?string, - requestedModuleName: string, - segmentHash: string, - }>, - callback: (?Error) => void -): void; -" -`; - -exports[`public API should not change unintentionally Libraries/Core/setUpTimers.js 1`] = `""`; - -exports[`public API should not change unintentionally Libraries/Core/setUpXHR.js 1`] = `""`; - exports[`public API should not change unintentionally Libraries/Debugging/DebuggingOverlay.js 1`] = ` "type DebuggingOverlayHandle = { highlightTraceUpdates(updates: TraceUpdate[]): void, @@ -5401,72 +5363,6 @@ declare export default typeof TouchHistoryMath; " `; -exports[`public API should not change unintentionally Libraries/JSInspector/InspectorAgent.js 1`] = ` -"export type EventSender = (name: string, params: mixed) => void; -declare class InspectorAgent { - _eventSender: EventSender; - constructor(eventSender: EventSender): void; - sendEvent(name: string, params: mixed): void; -} -declare export default typeof InspectorAgent; -" -`; - -exports[`public API should not change unintentionally Libraries/JSInspector/JSInspector.js 1`] = ` -"interface Agent { - constructor(eventSender: EventSender): void; -} -type AgentClass = Class & { DOMAIN: string, ... }; -declare const JSInspector: { - registerAgent(type: AgentClass): void, - getTimestamp(): number, -}; -declare module.exports: JSInspector; -" -`; - -exports[`public API should not change unintentionally Libraries/JSInspector/NetworkAgent.js 1`] = ` -"type RequestId = string; -type Headers = { [string]: string }; -declare class Interceptor { - _agent: NetworkAgent; - _requests: Map; - constructor(agent: NetworkAgent): void; - getData(requestId: string): ?string; - requestSent(id: number, url: string, method: string, headers: Headers): void; - responseReceived( - id: number, - url: string, - status: number, - headers: Headers - ): void; - dataReceived(id: number, data: string): void; - loadingFinished(id: number, encodedDataLength: number): void; - loadingFailed(id: number, error: string): void; - _getMimeType(headers: Headers): string; -} -type EnableArgs = { - maxResourceBufferSize?: number, - maxTotalBufferSize?: number, - ... -}; -declare class NetworkAgent extends InspectorAgent { - static DOMAIN: string; - _sendEvent: EventSender; - _interceptor: ?Interceptor; - enable(EnableArgs): void; - disable(): void; - getResponseBody({ requestId: RequestId, ... }): { - body: ?string, - base64Encoded: boolean, - ... - }; - interceptor(): Interceptor; -} -declare export default typeof NetworkAgent; -" -`; - exports[`public API should not change unintentionally Libraries/LayoutAnimation/LayoutAnimation.js 1`] = ` "export type LayoutAnimationConfig = LayoutAnimationConfig_; type OnAnimationDidEndCallback = () => void; @@ -6593,18 +6489,6 @@ export type ResponseType = | \\"json\\" | \\"text\\"; export type Response = ?Object | string; -type XHRInterceptor = interface { - requestSent(id: number, url: string, method: string, headers: Object): void, - responseReceived( - id: number, - url: string, - status: number, - headers: Object - ): void, - dataReceived(id: number, data: string): void, - loadingFinished(id: number, encodedDataLength: number): void, - loadingFailed(id: number, error: string): void, -}; declare class XMLHttpRequestEventTarget extends EventTarget { get onload(): EventCallback | null; set onload(listener: ?EventCallback): void; @@ -6627,7 +6511,6 @@ declare class XMLHttpRequest extends EventTarget { static HEADERS_RECEIVED: number; static LOADING: number; static DONE: number; - static _interceptor: ?XHRInterceptor; static _profiling: boolean; UNSENT: number; OPENED: number; @@ -6659,7 +6542,6 @@ declare class XMLHttpRequest extends EventTarget { _incrementalEvents: boolean; _startTime: ?number; _performanceLogger: IPerformanceLogger; - static setInterceptor(interceptor: ?XHRInterceptor): void; static enableProfiling(enableProfiling: boolean): void; constructor(): void; _reset(): void; @@ -6736,18 +6618,6 @@ export type ResponseType = | \\"json\\" | \\"text\\"; export type Response = ?Object | string; -type XHRInterceptor = interface { - requestSent(id: number, url: string, method: string, headers: Object): void, - responseReceived( - id: number, - url: string, - status: number, - headers: Object - ): void, - dataReceived(id: number, data: string): void, - loadingFinished(id: number, encodedDataLength: number): void, - loadingFailed(id: number, error: string): void, -}; declare class XMLHttpRequestEventTarget extends EventTarget { onload: ?Function; onloadstart: ?Function; @@ -6763,7 +6633,6 @@ declare class XMLHttpRequest extends EventTarget { static HEADERS_RECEIVED: number; static LOADING: number; static DONE: number; - static _interceptor: ?XHRInterceptor; static _profiling: boolean; UNSENT: number; OPENED: number; @@ -6803,7 +6672,6 @@ declare class XMLHttpRequest extends EventTarget { _incrementalEvents: boolean; _startTime: ?number; _performanceLogger: IPerformanceLogger; - static setInterceptor(interceptor: ?XHRInterceptor): void; static enableProfiling(enableProfiling: boolean): void; constructor(): void; _reset(): void; diff --git a/packages/react-native/Libraries/__tests__/public-api-test.js b/packages/react-native/Libraries/__tests__/public-api-test.js index 512d86bd755b42..f71b2f06ea4a6b 100644 --- a/packages/react-native/Libraries/__tests__/public-api-test.js +++ b/packages/react-native/Libraries/__tests__/public-api-test.js @@ -30,6 +30,7 @@ const SHARED_PATTERNS = [ const JS_LIBRARIES_FILES_PATTERN = 'Libraries/**/*.{js,flow}'; const JS_LIBRARIES_FILES_IGNORE_PATTERNS = [ ...SHARED_PATTERNS, + 'Libraries/Core/setUp*', 'Libraries/NewAppScreen/components/**', // Non source files 'Libraries/Renderer/implementations/**', From 9afa3596cbe25e51a1c77ef88559fab8a6ab7e2f Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Thu, 30 Jan 2025 12:21:54 -0800 Subject: [PATCH 15/17] Make DevSettingsActivity internal (#49073) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49073 This activity should be internal as it's exposed by ReactNative's Debug Manifest. No need to have it public. I checked that there are no OSS usages of it: https://www.google.com/url?q=https://github.com/search?type%3Dcode%26q%3DNOT%2Bis%253Afork%2BNOT%2Borg%253Afacebook%2BNOT%2Brepo%253Areact-native-tvos%252Freact-native-tvos%2BNOT%2Brepo%253Anuagoz%252Freact-native%2BNOT%2Brepo%253A2lambda123%252Freact-native%2BNOT%2Brepo%253Apvinis%252Freact-native---investigation%2BNOT%2Brepo%253Abeanchips%252Ffacebookreactnative%2BNOT%2Brepo%253AfabOnReact%252Freact-native-notes%2BNOT%2Buser%253Ahuntie%2Bcom.facebook.react.devsupport.DevSettingsActivity&sa=D&source=editors&ust=1738261498882961&usg=AOvVaw295OXKV-8dAbdsMTY5usSx Changelog: [Internal] [Changed] - Reviewed By: mdvacca Differential Revision: D68904656 fbshipit-source-id: b98b417e60a3e8ebba0c9946959ee43ea963b066 --- packages/react-native/ReactAndroid/api/ReactAndroid.api | 5 ----- .../com/facebook/react/devsupport/DevSettingsActivity.kt | 5 +++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index c5b26fda74921c..4eebfab03e31e7 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2096,11 +2096,6 @@ public abstract interface class com/facebook/react/devsupport/DevServerHelper$Pa public abstract fun onPackagerReloadCommand ()V } -public final class com/facebook/react/devsupport/DevSettingsActivity : android/preference/PreferenceActivity { - public fun ()V - public fun onCreate (Landroid/os/Bundle;)V -} - public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/facebook/react/devsupport/interfaces/DevSupportManager { protected final field mReactInstanceDevHelper Lcom/facebook/react/devsupport/ReactInstanceDevHelper; public fun (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSettingsActivity.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSettingsActivity.kt index 6eaf92ac4b986d..ee484b8621d3ae 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSettingsActivity.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSettingsActivity.kt @@ -12,15 +12,16 @@ package com.facebook.react.devsupport import android.os.Bundle import android.preference.PreferenceActivity import com.facebook.react.R +import com.facebook.react.devsupport.interfaces.DevSupportManager /** * Activity that display developers settings. Should be added to the debug manifest of the app. Can * be triggered through the developers option menu displayed by [DevSupportManager]. */ -public class DevSettingsActivity : PreferenceActivity() { +internal class DevSettingsActivity : PreferenceActivity() { @Deprecated("Deprecated in Java") - public override fun onCreate(savedInstanceState: Bundle?) { + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) title = application.resources.getString(R.string.catalyst_settings_title) addPreferencesFromResource(R.xml.rn_dev_preferences) From 64c2a52ca908587e95eb5b5858c809c4acb78f06 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Thu, 30 Jan 2025 14:13:57 -0800 Subject: [PATCH 16/17] Remove unnecessary `public` keyword (#49062) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49062 Another round of cleanup for the `public` keyword that I found around. Those are unnecessary here as those classes are `internal` and we should remove them. Changelog: [Internal] [Changed] - Reviewed By: mdvacca Differential Revision: D68894182 fbshipit-source-id: 6f7bac6051e17785a1bfb0d544950250429c71cb --- .../java/com/facebook/react/devsupport/DevSupportSoLoader.kt | 2 +- .../com/facebook/react/runtime/internal/bolts/Executors.kt | 4 ++-- .../main/java/com/facebook/react/uimanager/BlendModeHelper.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportSoLoader.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportSoLoader.kt index 992cdcbaf95cf8..a7887ad88a7c2b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportSoLoader.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportSoLoader.kt @@ -14,7 +14,7 @@ internal object DevSupportSoLoader { @JvmStatic @Synchronized - public fun staticInit() { + fun staticInit() { if (didInit) { return } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Executors.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Executors.kt index d6815157eec6bf..61b1982bd33c26 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Executors.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/internal/bolts/Executors.kt @@ -24,8 +24,8 @@ import java.util.concurrent.Executor * threads. */ internal object Executors { - @JvmField public val UI_THREAD: Executor = UIThreadExecutor() - @JvmField public val IMMEDIATE: Executor = ImmediateExecutor() + @JvmField val UI_THREAD: Executor = UIThreadExecutor() + @JvmField val IMMEDIATE: Executor = ImmediateExecutor() private class UIThreadExecutor : Executor { override fun execute(command: Runnable) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BlendModeHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BlendModeHelper.kt index 25d789dd5c2c0e..1dd376372e7231 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BlendModeHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BlendModeHelper.kt @@ -19,7 +19,7 @@ internal object BlendModeHelper { /** @see https://www.w3.org/TR/compositing-1/#mix-blend-mode */ @JvmStatic - public fun parseMixBlendMode(mixBlendMode: String?): BlendMode? { + fun parseMixBlendMode(mixBlendMode: String?): BlendMode? { if (mixBlendMode == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return null } @@ -46,6 +46,6 @@ internal object BlendModeHelper { } @JvmStatic - public fun needsIsolatedLayer(view: ViewGroup): Boolean = + fun needsIsolatedLayer(view: ViewGroup): Boolean = view.children.any { it.getTag(R.id.mix_blend_mode) != null } } From 4ccb2f2aa2365377d4f4f54512abb2a24ca0a4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Fri, 31 Jan 2025 02:02:08 -0800 Subject: [PATCH 17/17] Add transform that strips private properties in build types script (#49060) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49060 We want to hide private properties from JS public API interface. The stripPrivateProperties transform removes all private nodes of type ObjectTypeProperty, Property, PropertyDefinition and MethodDefinition. There is also a change in transforms reducer that incorporates `print` function from hermes-transform which modifies the code base on the transformed ast (transformed.mutatedCode seems to be a code before the transform operation). ## Changelog: [Internal] - Added transform that strips private properties in build-types script Reviewed By: huntie Differential Revision: D68892853 fbshipit-source-id: 5035fd4339aa6294d972e7aff0eb563f48d4c3d2 --- .../__tests__/stripPrivateProperties-test.js | 40 +++++++++++++++++++ .../transforms/stripPrivateProperties.js | 21 +++++++++- .../build/build-types/translateSourceFile.js | 8 +++- 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 scripts/build/build-types/transforms/__tests__/stripPrivateProperties-test.js diff --git a/scripts/build/build-types/transforms/__tests__/stripPrivateProperties-test.js b/scripts/build/build-types/transforms/__tests__/stripPrivateProperties-test.js new file mode 100644 index 00000000000000..df97f5719a02c1 --- /dev/null +++ b/scripts/build/build-types/transforms/__tests__/stripPrivateProperties-test.js @@ -0,0 +1,40 @@ +/** + * 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 + */ + +const stripPrivateProperties = require('../stripPrivateProperties.js'); +const {parse, print} = require('hermes-transform'); + +const prettierOptions = {parser: 'babel'}; + +async function translate(code: string): Promise { + const parsed = await parse(code); + const result = await stripPrivateProperties(parsed); + return print(result.ast, result.mutatedCode, prettierOptions); +} + +describe('stripPrivateProperties', () => { + test('should strip private properties', async () => { + const code = `const Foo = { + foo: 'foo', + bar() {}, + _privateFoo: 'privateFoo', + _privateBar() {}, + }`; + const result = await translate(code); + expect(result).toMatchInlineSnapshot(` + "const Foo = { + foo: \\"foo\\", + bar() {}, + }; + " + `); + }); +}); diff --git a/scripts/build/build-types/transforms/stripPrivateProperties.js b/scripts/build/build-types/transforms/stripPrivateProperties.js index e7ceb7e232e7cf..f5f38601d36798 100644 --- a/scripts/build/build-types/transforms/stripPrivateProperties.js +++ b/scripts/build/build-types/transforms/stripPrivateProperties.js @@ -18,7 +18,26 @@ import type {ParseResult} from 'hermes-transform/dist/transform/parse'; const {transformAST} = require('hermes-transform/dist/transform/transformAST'); const visitors /*: TransformVisitor */ = context => ({ - // TODO + ObjectTypeProperty(node) /*: void */ { + if (node.key.type === 'Identifier' && node.key.name.startsWith('_')) { + context.removeNode(node); + } + }, + Property(node) /*: void */ { + if (node.key.type === 'Identifier' && node.key.name.startsWith('_')) { + context.removeNode(node); + } + }, + PropertyDefinition(node) /*: void */ { + if (node.key.type === 'Identifier' && node.key.name.startsWith('_')) { + context.removeNode(node); + } + }, + MethodDefinition(node) /*: void */ { + if (node.key.type === 'Identifier' && node.key.name.startsWith('_')) { + context.removeNode(node); + } + }, }); async function stripPrivateProperties( diff --git a/scripts/build/build-types/translateSourceFile.js b/scripts/build/build-types/translateSourceFile.js index ef1ad061e33ad6..6a98d5e525b5df 100644 --- a/scripts/build/build-types/translateSourceFile.js +++ b/scripts/build/build-types/translateSourceFile.js @@ -15,7 +15,7 @@ import type {TransformASTResult} from 'hermes-transform/dist/transform/transform */ const translate = require('flow-api-translator'); -const {parse} = require('hermes-transform'); +const {parse, print} = require('hermes-transform'); /*:: type TransformFn = (ParseResult) => Promise; @@ -55,10 +55,14 @@ async function applyTransforms( return transforms.reduce((input, transform) => { return input.then(async result => { const transformed = await transform(result); + const code = transformed.astWasMutated + ? await print(transformed.ast, transformed.mutatedCode, prettierOptions) + : transformed.mutatedCode; + return { ...result, ast: transformed.ast, - code: transformed.mutatedCode, + code, }; }); }, Promise.resolve(source));