From dc959c92719085c18a4c3aee2bbc4b03ef7e05ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 30 Jan 2023 04:03:31 -0800 Subject: [PATCH 01/65] Update `react-native-xcode.sh` to use `PROJECT_DIR` from Xcode (#35970) Summary: In a mono-repo the `react-native` package could be hoisted compared to the app directory, in which case it's not a good strategy for the `react-native-xcode.sh` script to guess the app project root relative to the location of itself. Instead I suggest to relying on a build setting provided by Xcode to derive the default app path. I could have use the `SRCROOT` instead. According to https://stackoverflow.com/questions/36323031/what-the-different-between-srcroot-and-project-dir this is equivalent and also a bit less ambiguous as I see it. I.e. I would expect most Xcode projects to be located in the `ios` directory of the app. As a workaround, before this merge, users can add the following to their "Bundle React Native code and images" build phase or `ios/.xcode.env` file: ```shell export PROJECT_ROOT="$PROJECT_DIR/.." ``` This build phase can also be used for users wanting to revert this default behaviour once merged. ## Changelog [iOS] [Changed] - Changed default `PROJECT_ROOT` (used in when bundling for iOS) to rely on the `PROJECT_DIR` build setting. Pull Request resolved: https://github.com/facebook/react-native/pull/35970 Test Plan: I've updated this locally and verified this does indeed pick up the correct app path - even in a mono-repo. To verify this: - Instantiate the template with this patch applied. - Update the "Run scheme"'s "Build Configuration" to "Release". - Build the app without errors. Reviewed By: cortinico Differential Revision: D42842636 Pulled By: cipolleschi fbshipit-source-id: 040c31ac59a8abec5f5b38f795c8e74649420bac --- scripts/react-native-xcode.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/react-native-xcode.sh b/scripts/react-native-xcode.sh index 30a5ac2227afa4..54a4eb9de34262 100755 --- a/scripts/react-native-xcode.sh +++ b/scripts/react-native-xcode.sh @@ -58,9 +58,8 @@ esac # Path to react-native folder inside node_modules REACT_NATIVE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -# The project should be located next to where react-native is installed -# in node_modules. -PROJECT_ROOT=${PROJECT_ROOT:-"$REACT_NATIVE_DIR/../.."} +# Most projects have their project root, one level up from their Xcode project dir (the "ios" directory) +PROJECT_ROOT=${PROJECT_ROOT:-"$PROJECT_DIR/.."} cd "$PROJECT_ROOT" || exit From da270d038c271d6b82de568621b49e38739372c6 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Jan 2023 04:25:26 -0800 Subject: [PATCH 02/65] Restore Dynamic framework with JSC in the Old Architecture Summary: I discovered that 0.69 and 0.70 could run React Native as Dynamic framework with JSC and starting from 0.71 that's not possible anymore. This diff restore that possibility. ## Changelog [iOS][Fixed] - Add Back dynamic framework support for the old architecture Reviewed By: cortinico Differential Revision: D42829137 fbshipit-source-id: 848672f714d8bab133e42f5e3b80202b350d5261 --- React-Core.podspec | 4 ++++ React/CoreModules/React-CoreModules.podspec | 1 + 2 files changed, 5 insertions(+) diff --git a/React-Core.podspec b/React-Core.podspec index b0bc4959222338..0048703bf5ba80 100644 --- a/React-Core.podspec +++ b/React-Core.podspec @@ -114,4 +114,8 @@ Pod::Spec.new do |s| s.dependency "React-jsiexecutor", version s.dependency "Yoga" s.dependency "glog" + + if ENV['USE_HERMES'] == "0" + s.dependency 'React-jsc' + end end diff --git a/React/CoreModules/React-CoreModules.podspec b/React/CoreModules/React-CoreModules.podspec index 90c8adf1bc27f7..89f79da17b46d1 100644 --- a/React/CoreModules/React-CoreModules.podspec +++ b/React/CoreModules/React-CoreModules.podspec @@ -44,4 +44,5 @@ Pod::Spec.new do |s| s.dependency "React-RCTImage", version s.dependency "ReactCommon/turbomodule/core", version s.dependency "React-jsi", version + s.dependency 'React-RCTBlob' end From b3040ec6244da6ea274654abfd84516de4f5bf52 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Jan 2023 04:25:26 -0800 Subject: [PATCH 03/65] Restore Dynamic framework with Hermes in the Old Architecture Summary: I discovered that 0.69 could run React Native as Dynamic framework with Hermes and starting from 0.70 that's not possible anymore. This diff restore that possibility. Notice that now Hermes provisdes JSI and Dynamic Frameworks requires that all the dependencies are explicitly defined, therefore, whenever we have a pod that depended on `React-jsi`, now it also has to explicitly depends on `hermes-engine` ## Changelog [iOS][Fixed] - Add Back dynamic framework support for the Old Architecture with Hermes Reviewed By: cortinico Differential Revision: D42829728 fbshipit-source-id: a660e3b1e346ec6cf3ceb8771dd8bceb0dbcb13a --- Libraries/Blob/React-RCTBlob.podspec | 4 ++++ React-Core.podspec | 23 ++++++++++++++----- ReactCommon/ReactCommon.podspec | 8 ++++++- ReactCommon/cxxreact/React-cxxreact.podspec | 4 ++++ ReactCommon/hermes/React-hermes.podspec | 1 + .../jsiexecutor/React-jsiexecutor.podspec | 4 ++++ 6 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Libraries/Blob/React-RCTBlob.podspec b/Libraries/Blob/React-RCTBlob.podspec index 3d69eeced3ac9b..b3a557dbaeac81 100644 --- a/Libraries/Blob/React-RCTBlob.podspec +++ b/Libraries/Blob/React-RCTBlob.podspec @@ -45,4 +45,8 @@ Pod::Spec.new do |s| s.dependency "React-Core/RCTBlobHeaders", version s.dependency "React-Core/RCTWebSocket", version s.dependency "React-RCTNetwork", version + + if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1" + s.dependency "hermes-engine" + end end diff --git a/React-Core.podspec b/React-Core.podspec index 0048703bf5ba80..31db1b23d9a3ed 100644 --- a/React-Core.podspec +++ b/React-Core.podspec @@ -75,12 +75,20 @@ Pod::Spec.new do |s| s.subspec "Default" do |ss| ss.source_files = "React/**/*.{c,h,m,mm,S,cpp}" - ss.exclude_files = "React/CoreModules/**/*", - "React/DevSupport/**/*", - "React/Fabric/**/*", - "React/FBReactNativeSpec/**/*", - "React/Tests/**/*", - "React/Inspector/**/*" + exclude_files = [ + "React/CoreModules/**/*", + "React/DevSupport/**/*", + "React/Fabric/**/*", + "React/FBReactNativeSpec/**/*", + "React/Tests/**/*", + "React/Inspector/**/*" + ] + # If we are using Hermes (the default is use hermes, so USE_HERMES can be nil), we don't have jsc installed + # So we have to exclude the JSCExecutorFactory + if ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == "1" + exclude_files = exclude_files.append("React/CxxBridge/JSCExecutorFactory.{h,mm}") + end + ss.exclude_files = exclude_files ss.private_header_files = "React/Cxx*/*.h" end @@ -117,5 +125,8 @@ Pod::Spec.new do |s| if ENV['USE_HERMES'] == "0" s.dependency 'React-jsc' + else + s.dependency 'React-hermes' + s.dependency 'hermes-engine' end end diff --git a/ReactCommon/ReactCommon.podspec b/ReactCommon/ReactCommon.podspec index 39701310f6af11..da424368772a11 100644 --- a/ReactCommon/ReactCommon.podspec +++ b/ReactCommon/ReactCommon.podspec @@ -19,7 +19,7 @@ end folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32 -Wno-gnu-zero-variadic-macro-arguments' folly_version = '2021.07.22.00' boost_compiler_flags = '-Wno-documentation' - +using_hermes = ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == "1" Pod::Spec.new do |s| s.name = "ReactCommon" s.module_name = "ReactCommon" @@ -49,6 +49,9 @@ Pod::Spec.new do |s| s.dependency "React-logger", version ss.dependency "DoubleConversion" ss.dependency "glog" + if using_hermes + ss.dependency "hermes-engine" + end ss.subspec "bridging" do |sss| sss.dependency "React-jsi", version @@ -56,6 +59,9 @@ Pod::Spec.new do |s| sss.exclude_files = "react/bridging/tests" sss.header_dir = "react/bridging" sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ReactCommon\" \"$(PODS_ROOT)/RCT-Folly\"" } + if using_hermes + sss.dependency "hermes-engine" + end end ss.subspec "core" do |sss| diff --git a/ReactCommon/cxxreact/React-cxxreact.podspec b/ReactCommon/cxxreact/React-cxxreact.podspec index a2c66cf7eb5c4b..26747a5b69fd1f 100644 --- a/ReactCommon/cxxreact/React-cxxreact.podspec +++ b/ReactCommon/cxxreact/React-cxxreact.podspec @@ -47,4 +47,8 @@ Pod::Spec.new do |s| s.dependency "React-perflogger", version s.dependency "React-jsi", version s.dependency "React-logger", version + + if ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == "1" + s.dependency 'hermes-engine' + end end diff --git a/ReactCommon/hermes/React-hermes.podspec b/ReactCommon/hermes/React-hermes.podspec index 96bb1d0e152f62..425ef3c84c4005 100644 --- a/ReactCommon/hermes/React-hermes.podspec +++ b/ReactCommon/hermes/React-hermes.podspec @@ -53,4 +53,5 @@ Pod::Spec.new do |s| s.dependency "glog" s.dependency "RCT-Folly/Futures", folly_version s.dependency "hermes-engine" + s.dependency "React-jsi" end diff --git a/ReactCommon/jsiexecutor/React-jsiexecutor.podspec b/ReactCommon/jsiexecutor/React-jsiexecutor.podspec index 23a29b9ff45614..e97969d5e846bf 100644 --- a/ReactCommon/jsiexecutor/React-jsiexecutor.podspec +++ b/ReactCommon/jsiexecutor/React-jsiexecutor.podspec @@ -41,4 +41,8 @@ Pod::Spec.new do |s| s.dependency "RCT-Folly", folly_version s.dependency "DoubleConversion" s.dependency "glog" + + if ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == "1" + s.dependency 'hermes-engine' + end end From 8056cd7f05d7959fce658af3a29619e7d0af3cd9 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 30 Jan 2023 04:25:26 -0800 Subject: [PATCH 04/65] Add Tests in CircleCI to check dynamic frameworks with the old arch (#36003) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36003 This diff adds 4 tests in CircleCI to make sure we don't regress in the support of Dynamic Frameworks for the old architecture. ## Changelog [iOS][Fixed] - Add CircleCI tests for dynamic frameworks with the Old Architecture. Reviewed By: cortinico Differential Revision: D42829895 fbshipit-source-id: 5669be45d4f55161a11a6ece161b2a2aa384a644 --- .circleci/config.yml | 66 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 464cef6c10bfdd..14cc5daaabc2b4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -853,7 +853,7 @@ jobs: default: "StaticLibraries" description: Which kind of option we want to use for `use_frameworks!` type: enum - enum: ["StaticLibraries", "StaticFrameworks"] #TODO: Add "DynamicFrameworks" + enum: ["StaticLibraries", "StaticFrameworks", "DynamicFrameworks"] environment: - PROJECT_NAME: "iOSTemplateProject" - HERMES_WS_DIR: *hermes_workspace_root @@ -906,6 +906,8 @@ jobs: if [[ << parameters.use_frameworks >> == "StaticFrameworks" ]]; then export USE_FRAMEWORKS=static + elif [[ << parameters.use_frameworks >> == "DynamicFrameworks" ]]; then + export USE_FRAMEWORKS=dynamic fi bundle exec pod install @@ -1636,7 +1638,7 @@ workflows: flavor: ["Debug", "Release"] jsengine: ["Hermes", "JSC"] flipper: ["WithFlipper", "WithoutFlipper"] - use_frameworks: [ "StaticLibraries", "StaticFrameworks" ] #TODO: make it works with DynamicFrameworks + use_frameworks: ["StaticLibraries", "StaticFrameworks", "DynamicFrameworks"] exclude: - architecture: "NewArch" flavor: "Release" @@ -1648,11 +1650,21 @@ workflows: jsengine: "Hermes" flipper: "WithFlipper" use_frameworks: "StaticFrameworks" + - architecture: "NewArch" + flavor: "Release" + jsengine: "Hermes" + flipper: "WithFlipper" + use_frameworks: "DynamicFrameworks" - architecture: "NewArch" flavor: "Release" jsengine: "Hermes" flipper: "WithoutFlipper" use_frameworks: "StaticFrameworks" + - architecture: "NewArch" + flavor: "Release" + jsengine: "Hermes" + flipper: "WithoutFlipper" + use_frameworks: "DynamicFrameworks" - architecture: "NewArch" flavor: "Release" jsengine: "JSC" @@ -1663,11 +1675,21 @@ workflows: jsengine: "JSC" flipper: "WithFlipper" use_frameworks: "StaticFrameworks" + - architecture: "NewArch" + flavor: "Release" + jsengine: "JSC" + flipper: "WithFlipper" + use_frameworks: "DynamicFrameworks" - architecture: "NewArch" flavor: "Release" jsengine: "JSC" flipper: "WithoutFlipper" use_frameworks: "StaticFrameworks" + - architecture: "NewArch" + flavor: "Release" + jsengine: "JSC" + flipper: "WithoutFlipper" + use_frameworks: "DynamicFrameworks" - architecture: "OldArch" flavor: "Release" jsengine: "Hermes" @@ -1678,6 +1700,11 @@ workflows: jsengine: "Hermes" flipper: "WithFlipper" use_frameworks: "StaticFrameworks" + - architecture: "OldArch" + flavor: "Release" + jsengine: "Hermes" + flipper: "WithFlipper" + use_frameworks: "DynamicFrameworks" - architecture: "OldArch" flavor: "Release" jsengine: "JSC" @@ -1688,6 +1715,11 @@ workflows: jsengine: "JSC" flipper: "WithFlipper" use_frameworks: "StaticFrameworks" + - architecture: "OldArch" + flavor: "Release" + jsengine: "JSC" + flipper: "WithFlipper" + use_frameworks: "DynamicFrameworks" - architecture: "NewArch" flavor: "Debug" jsengine: "Hermes" @@ -1718,6 +1750,36 @@ workflows: jsengine: "JSC" flipper: "WithFlipper" use_frameworks: "StaticFrameworks" + - architecture: "NewArch" + flavor: "Debug" + jsengine: "Hermes" + flipper: "WithFlipper" + use_frameworks: "DynamicFrameworks" + - architecture: "NewArch" + flavor: "Debug" + jsengine: "Hermes" + flipper: "WithoutFlipper" + use_frameworks: "DynamicFrameworks" + - architecture: "NewArch" + flavor: "Debug" + jsengine: "JSC" + flipper: "WithFlipper" + use_frameworks: "DynamicFrameworks" + - architecture: "NewArch" + flavor: "Debug" + jsengine: "JSC" + flipper: "WithoutFlipper" + use_frameworks: "DynamicFrameworks" + - architecture: "OldArch" + flavor: "Debug" + jsengine: "Hermes" + flipper: "WithFlipper" + use_frameworks: "DynamicFrameworks" + - architecture: "OldArch" + flavor: "Debug" + jsengine: "JSC" + flipper: "WithFlipper" + use_frameworks: "DynamicFrameworks" - test_ios_rntester: requires: - build_hermes_macos From 9856c334bd0b8637d599b5450e66a611ac01e99c Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Mon, 30 Jan 2023 06:22:12 -0800 Subject: [PATCH 05/65] fix(publishing-bumped-packages): look for status code instaead of stderr (#36004) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36004 Changelog: [Internal] This fixes CircleCI job, which is responsible for publishing bumped packages. We should not check for `stderr`, apparently `npm` uses it to store debug information: - https://github.com/npm/npm/issues/118#issuecomment-325440 So we've tried to use this on 0.71-stable before and it succesfully published one package, but have exited right after it because `stderr` was not empty Reviewed By: cortinico, cipolleschi Differential Revision: D42836212 fbshipit-source-id: 6f2a9a512121683268fe6aae6a187fccb8d9dfbc --- scripts/monorepo/find-and-publish-all-bumped-packages.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/monorepo/find-and-publish-all-bumped-packages.js b/scripts/monorepo/find-and-publish-all-bumped-packages.js index 17e06596c3af2b..f700f5c9c9889d 100644 --- a/scripts/monorepo/find-and-publish-all-bumped-packages.js +++ b/scripts/monorepo/find-and-publish-all-bumped-packages.js @@ -103,15 +103,15 @@ const findAndPublishAllBumpedPackages = () => { const npmOTPFlag = NPM_CONFIG_OTP ? `--otp ${NPM_CONFIG_OTP}` : ''; - const {stderr} = spawnSync('npm', ['publish', `${npmOTPFlag}`], { + const {status, stderr} = spawnSync('npm', ['publish', `${npmOTPFlag}`], { cwd: packageAbsolutePath, shell: true, stdio: 'pipe', encoding: 'utf-8', }); - if (stderr) { + if (status !== 0) { console.log( - `\u274c Failed to publish version ${nextVersion} of ${packageManifest.name}:`, + `\u274c Failed to publish version ${nextVersion} of ${packageManifest.name}. npm publish exited with code ${status}:`, ); console.log(stderr); From 84c15475266815d5d57f10a6da106cb4150711d3 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Mon, 30 Jan 2023 06:34:32 -0800 Subject: [PATCH 06/65] refactor: Renamed emitObject to emitGenericObject (#35981) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/35981 This reflects better what the former `emitObject` was doing - emitting a generic object. It would also allow us to rename `emitPartial` to `emitObject` in the next diff whence the function name `emitObject` will be free. Changelog: [Internal] - Renamed emitObject to emitGenericObject Reviewed By: christophpurrer Differential Revision: D42740871 fbshipit-source-id: 1b9112464695b8abeccc95eed908b0b45a0e3bf2 --- .../src/parsers/__tests__/parsers-primitives-test.js | 8 ++++---- .../src/parsers/flow/modules/index.js | 8 ++++---- .../src/parsers/parsers-primitives.js | 4 ++-- .../src/parsers/typescript/modules/index.js | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js b/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js index 53a03bafec54d3..fd30944ea97f52 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js @@ -20,7 +20,7 @@ const { emitFloat, emitNumber, emitInt32, - emitObject, + emitGenericObject, emitPartial, emitPromise, emitRootTag, @@ -431,10 +431,10 @@ describe('emitPromise', () => { }); }); -describe('emitObject', () => { +describe('emitGenericObject', () => { describe('when nullable is true', () => { it('returns nullable type annotation', () => { - const result = emitObject(true); + const result = emitGenericObject(true); const expected = { type: 'NullableTypeAnnotation', typeAnnotation: { @@ -447,7 +447,7 @@ describe('emitObject', () => { }); describe('when nullable is false', () => { it('returns non nullable type annotation', () => { - const result = emitObject(false); + const result = emitGenericObject(false); const expected = { type: 'GenericObjectTypeAnnotation', }; diff --git a/packages/react-native-codegen/src/parsers/flow/modules/index.js b/packages/react-native-codegen/src/parsers/flow/modules/index.js index eff16b613d8a07..b8d2b22c5702ea 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/index.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/index.js @@ -41,7 +41,7 @@ const { emitFunction, emitNumber, emitInt32, - emitObject, + emitGenericObject, emitPartial, emitPromise, emitRootTag, @@ -154,7 +154,7 @@ function translateTypeAnnotation( } case 'UnsafeObject': case 'Object': { - return emitObject(nullable); + return emitGenericObject(nullable); } case '$Partial': { if (typeAnnotation.typeParameters.params.length !== 1) { @@ -221,7 +221,7 @@ function translateTypeAnnotation( parser, ); // no need to do further checking - return emitObject(nullable); + return emitGenericObject(nullable); } } @@ -298,7 +298,7 @@ function translateTypeAnnotation( if (cxxOnly) { return emitMixed(nullable); } else { - return emitObject(nullable); + return emitGenericObject(nullable); } } default: { diff --git a/packages/react-native-codegen/src/parsers/parsers-primitives.js b/packages/react-native-codegen/src/parsers/parsers-primitives.js index e9af9b41bbbd46..a3ce61e7f494a9 100644 --- a/packages/react-native-codegen/src/parsers/parsers-primitives.js +++ b/packages/react-native-codegen/src/parsers/parsers-primitives.js @@ -236,7 +236,7 @@ function emitPromise( } } -function emitObject( +function emitGenericObject( nullable: boolean, ): Nullable { return wrapNullable(nullable, { @@ -380,7 +380,7 @@ module.exports = { emitFunction, emitInt32, emitNumber, - emitObject, + emitGenericObject, emitPartial, emitPromise, emitRootTag, diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/index.js b/packages/react-native-codegen/src/parsers/typescript/modules/index.js index 6097b5b11824c8..98978beba95642 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -41,7 +41,7 @@ const { emitFunction, emitNumber, emitInt32, - emitObject, + emitGenericObject, emitPartial, emitPromise, emitRootTag, @@ -171,7 +171,7 @@ function translateTypeAnnotation( } case 'UnsafeObject': case 'Object': { - return emitObject(nullable); + return emitGenericObject(nullable); } case 'Partial': { if (typeAnnotation.typeParameters.params.length !== 1) { @@ -275,7 +275,7 @@ function translateTypeAnnotation( parser, ); // no need to do further checking - return emitObject(nullable); + return emitGenericObject(nullable); } } From 112c89e8105d1322dd1ebf072f9757c9d994e20b Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Mon, 30 Jan 2023 06:34:32 -0800 Subject: [PATCH 07/65] refactor: Renamed emitPartial to emitObject (#35982) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/35982 This reflects better what the former `emitPartial` was doing - emitting an object with set props. Changelog: [Internal] - Renamed emitPartial to emitObject Reviewed By: christophpurrer Differential Revision: D42740958 fbshipit-source-id: 4f974c6911bc129e698323a8039bbd7aa7602461 --- .../src/parsers/__tests__/parsers-primitives-test.js | 8 ++++---- .../src/parsers/flow/modules/index.js | 4 ++-- .../src/parsers/parsers-primitives.js | 4 ++-- .../src/parsers/typescript/modules/index.js | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js b/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js index fd30944ea97f52..92f391d3bcb24f 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js @@ -21,7 +21,7 @@ const { emitNumber, emitInt32, emitGenericObject, - emitPartial, + emitObject, emitPromise, emitRootTag, emitVoid, @@ -457,7 +457,7 @@ describe('emitGenericObject', () => { }); }); -describe('emitPartial', () => { +describe('emitObject', () => { describe('when nullable is true', () => { it('returns nullable type annotation', () => { const props = [ @@ -477,7 +477,7 @@ describe('emitPartial', () => { }, ]; - const result = emitPartial(true, props); + const result = emitObject(true, props); const expected = { type: 'NullableTypeAnnotation', @@ -509,7 +509,7 @@ describe('emitPartial', () => { }, ]; - const result = emitPartial(false, props); + const result = emitObject(false, props); const expected = { type: 'ObjectTypeAnnotation', diff --git a/packages/react-native-codegen/src/parsers/flow/modules/index.js b/packages/react-native-codegen/src/parsers/flow/modules/index.js index b8d2b22c5702ea..612d4c4ed2ccfe 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/index.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/index.js @@ -42,7 +42,7 @@ const { emitNumber, emitInt32, emitGenericObject, - emitPartial, + emitObject, emitPromise, emitRootTag, emitVoid, @@ -188,7 +188,7 @@ function translateTypeAnnotation( }; }); - return emitPartial(nullable, properties); + return emitObject(nullable, properties); } default: { return translateDefault( diff --git a/packages/react-native-codegen/src/parsers/parsers-primitives.js b/packages/react-native-codegen/src/parsers/parsers-primitives.js index a3ce61e7f494a9..23a7495b1654af 100644 --- a/packages/react-native-codegen/src/parsers/parsers-primitives.js +++ b/packages/react-native-codegen/src/parsers/parsers-primitives.js @@ -244,7 +244,7 @@ function emitGenericObject( }); } -function emitPartial( +function emitObject( nullable: boolean, properties: Array<$FlowFixMe>, ): Nullable { @@ -381,7 +381,7 @@ module.exports = { emitInt32, emitNumber, emitGenericObject, - emitPartial, + emitObject, emitPromise, emitRootTag, emitVoid, diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/index.js b/packages/react-native-codegen/src/parsers/typescript/modules/index.js index 98978beba95642..1d0a222b48d828 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -42,7 +42,7 @@ const { emitNumber, emitInt32, emitGenericObject, - emitPartial, + emitObject, emitPromise, emitRootTag, emitVoid, @@ -207,7 +207,7 @@ function translateTypeAnnotation( }, ); - return emitPartial(nullable, properties); + return emitObject(nullable, properties); } default: { return translateDefault( From c0004092f935ad892d4a1acf38fb184f1140bfd2 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Mon, 30 Jan 2023 06:50:24 -0800 Subject: [PATCH 08/65] RNGP - Properly set the `jsRootDir` default value (#35992) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/35992 Fixes https://github.com/software-mansion/react-native-gesture-handler/issues/2382 I've just realized that the default value fo `jsRootDir` is not entirely correct. That's the root of the folder where the codegen should run. For apps, it should be defaulted to `root` (i.e. ../../) For libraries, it should be defaulted to `../` (currently is root). This causes a problem where libraries without either a `codegenConfig` or a `react{ jsRootDir = ... }` specified in the build.gradle will be invoking the codegen and generating duplicated symbols. Changelog: [Android] [Fixed] - RNGP - Properly set the `jsRootDir` default value Reviewed By: cipolleschi Differential Revision: D42806411 fbshipit-source-id: ffe45f9684a22494cc2e4d0a19de9077cb341365 --- .../src/main/kotlin/com/facebook/react/ReactExtension.kt | 4 ++-- .../src/main/kotlin/com/facebook/react/ReactPlugin.kt | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt index d805dabe31c9c7..47e24225bd4aec 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt @@ -129,9 +129,9 @@ abstract class ReactExtension @Inject constructor(project: Project) { /** * The root directory for all JS files for the app. * - * Default: [root] (i.e. ${rootProject.dir}/../) + * Default: the parent folder of the `/android` folder. */ - val jsRootDir: DirectoryProperty = objects.directoryProperty().convention(root.get()) + val jsRootDir: DirectoryProperty = objects.directoryProperty() /** * The library name that will be used for the codegen artifacts. diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index 8fd0a54dbd3932..48fff59c790a38 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -110,6 +110,15 @@ class ReactPlugin : Plugin { // First, we set up the output dir for the codegen. val generatedSrcDir = File(project.buildDir, "generated/source/codegen") + // We specify the default value (convention) for jsRootDir. + // It's the root folder for apps (so ../../ from the Gradle project) + // and the package folder for library (so ../ from the Gradle project) + if (isLibrary) { + extension.jsRootDir.convention(project.layout.projectDirectory.dir("../")) + } else { + extension.jsRootDir.convention(extension.root) + } + val buildCodegenTask = project.tasks.register("buildCodegenCLI", BuildCodegenCLITask::class.java) { it.codegenDir.set(extension.codegenDir) From 47903d0c62de9399a5cd0993a59e4af0bdea4f0c Mon Sep 17 00:00:00 2001 From: Genki Kondo Date: Mon, 30 Jan 2023 11:10:51 -0800 Subject: [PATCH 09/65] Restore scroll position when scroll view is hidden and shown Summary: ScrollViews don't properly maintain position where they are hidden and shown. On iOS, when a UIScrollView (or its ancestor) is hidden, its scroll position is set to 0 (its window also becomes nil). When it is shown again, its scroll position is not restored. When a scroll is attempted when the scroll view is hidden, we keep track of the last known offset before it was hidden. Then, in updateLayoutMetrics (which is triggered when the view is shown), we apply the pending offset if there is one. This is [consistent with Android's behavior in ReactScrollView.java](https://www.internalfb.com/code/fbsource/[2930f8c146af62ad63673c8d34e9876b77634c05]/xplat/js/react-native-github/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java?lines=289). Changelog: [Internal][Fixed] - In onLayoutChange, only scroll if the view is shown and the content view is ready Reviewed By: cipolleschi Differential Revision: D42815359 fbshipit-source-id: 4b209c1e54edf3f5c0bea902b48450a1a2e9661a --- .../ScrollView/RCTScrollViewComponentView.mm | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 8c95cdbc0b9e18..ae41b9cb8dfd2b 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -25,6 +25,12 @@ using namespace facebook::react; +struct PendingOffset { + bool isPending; + CGPoint offset; + CGPoint lastOffset; +}; + static CGFloat const kClippingLeeway = 44.0; static UIScrollViewKeyboardDismissMode RCTUIKeyboardDismissModeFromProps(ScrollViewProps const &props) @@ -99,6 +105,8 @@ @implementation RCTScrollViewComponentView { BOOL _shouldUpdateContentInsetAdjustmentBehavior; CGPoint _contentOffsetWhenClipped; + + PendingOffset _pendingOffset; } + (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view @@ -173,6 +181,12 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics _containerView.transform = transform; _scrollView.transform = transform; } + + // If there is a pending offset, apply it + if (_pendingOffset.isPending) { + [self scrollTo:_pendingOffset.offset.x y:_pendingOffset.offset.y animated:false]; + _pendingOffset.isPending = false; + } } - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps @@ -421,6 +435,13 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView [self _updateStateWithContentOffset]; } + // If the view is hidden, then set as pending offset. Apply it later on + // updateLayoutMetrics. + if (_scrollView.window == nil && !_pendingOffset.isPending) { + _pendingOffset.offset = _pendingOffset.lastOffset; + _pendingOffset.isPending = true; + } + NSTimeInterval now = CACurrentMediaTime(); if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) { _lastScrollEventDispatchTime = now; @@ -431,6 +452,8 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag); } + _pendingOffset.lastOffset = _scrollView.contentOffset; + [self _remountChildrenIfNeeded]; } From 9e65ba2b7b9afbd36d7ea7d422c2b46427913aa1 Mon Sep 17 00:00:00 2001 From: Genki Kondo Date: Mon, 30 Jan 2023 11:55:13 -0800 Subject: [PATCH 10/65] In onLayout, only scroll if the content view is ready Summary: ScrollViews don't properly maintain position where they are hidden and shown. There is an edge case where on onLayout for a ScrollView, its content may not have been laid out yet. This happens in some cases when a scroll view is hidden via display: 'none' (resulting in setVisibility(INVISIBLE)). Check that the content view is laid out before attempting a scroll. Changelog: [Internal][Fixed] - In onLayout, only scroll if the content view is ready Reviewed By: sshic Differential Revision: D42794750 fbshipit-source-id: 654a380bcae306da2704d3e190423c8de125833d --- .../scroll/ReactHorizontalScrollView.java | 19 +++++++++++-------- .../react/views/scroll/ReactScrollView.java | 19 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index 5f734256571277..c5271853b4d09c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -356,14 +356,17 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) { mScrollXAfterMeasure = NO_SCROLL_POSITION; } - // Call with the present values in order to re-layout if necessary - // If a "pending" value has been set, we restore that value. - // That value gets cleared by reactScrollTo. - int scrollToX = - pendingContentOffsetX != UNSET_CONTENT_OFFSET ? pendingContentOffsetX : getScrollX(); - int scrollToY = - pendingContentOffsetY != UNSET_CONTENT_OFFSET ? pendingContentOffsetY : getScrollY(); - scrollTo(scrollToX, scrollToY); + // Apply pending contentOffset in case it was set before the view was laid out. + if (isContentReady()) { + // If a "pending" content offset value has been set, we restore that value. + // Upon call to scrollTo, the "pending" values will be re-set. + int scrollToX = + pendingContentOffsetX != UNSET_CONTENT_OFFSET ? pendingContentOffsetX : getScrollX(); + int scrollToY = + pendingContentOffsetY != UNSET_CONTENT_OFFSET ? pendingContentOffsetY : getScrollY(); + scrollTo(scrollToX, scrollToY); + } + ReactScrollViewHelper.emitLayoutEvent(this); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 7a619882dfdd14..e42e648f56a6f7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -286,14 +286,17 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { - // Call with the present values in order to re-layout if necessary - // If a "pending" content offset value has been set, we restore that value. - // Upon call to scrollTo, the "pending" values will be re-set. - int scrollToX = - pendingContentOffsetX != UNSET_CONTENT_OFFSET ? pendingContentOffsetX : getScrollX(); - int scrollToY = - pendingContentOffsetY != UNSET_CONTENT_OFFSET ? pendingContentOffsetY : getScrollY(); - scrollTo(scrollToX, scrollToY); + // Apply pending contentOffset in case it was set before the view was laid out. + if (isContentReady()) { + // If a "pending" content offset value has been set, we restore that value. + // Upon call to scrollTo, the "pending" values will be re-set. + int scrollToX = + pendingContentOffsetX != UNSET_CONTENT_OFFSET ? pendingContentOffsetX : getScrollX(); + int scrollToY = + pendingContentOffsetY != UNSET_CONTENT_OFFSET ? pendingContentOffsetY : getScrollY(); + scrollTo(scrollToX, scrollToY); + } + ReactScrollViewHelper.emitLayoutEvent(this); } From 115dbe9433464d78dcc98db3bd0ef423670345b6 Mon Sep 17 00:00:00 2001 From: Genki Kondo Date: Mon, 30 Jan 2023 14:06:37 -0800 Subject: [PATCH 11/65] In onLayoutChange, only scroll if the view is shown and the content view is ready Summary: ScrollViews don't properly maintain position where they are hidden and shown. When a ScrollView's content is laid out, onLayoutChange is triggered. This is also fired when the views are hidden, which is not desirable as the layout may not be accurate when the view is hidden. Check that the scroll view is showing before attempting a scroll. Changelog: [Internal][Fixed] - In onLayoutChange, only scroll if the view is shown and the content view is ready Reviewed By: sshic Differential Revision: D42808119 fbshipit-source-id: 0197ae55fa7d80e52c2ea483609e62d512a117f3 --- .../facebook/react/views/scroll/ReactScrollView.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index e42e648f56a6f7..2442f7b2013d55 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -1127,10 +1127,12 @@ public void onLayoutChange( mMaintainVisibleContentPositionHelper.updateScrollPosition(); } - int currentScrollY = getScrollY(); - int maxScrollY = getMaxScrollY(); - if (currentScrollY > maxScrollY) { - scrollTo(getScrollX(), maxScrollY); + if (isShown() && isContentReady()) { + int currentScrollY = getScrollY(); + int maxScrollY = getMaxScrollY(); + if (currentScrollY > maxScrollY) { + scrollTo(getScrollX(), maxScrollY); + } } } From 8568b937335b152c2836e7790c1db75e93365787 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 30 Jan 2023 15:13:31 -0800 Subject: [PATCH 12/65] react-native: Use number literals in TypeScript types for `FileReader` and `XMLHttpRequest` states (#36000) Summary: Mostly to improve compat in codebases where `lib.dom.d.ts` is loaded alongside RN. TS 5.0 updates to `lib.dom.d.ts` added number literals for these states as well so the abstract `number` type from RN was no longer compatible. Forward-port of https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64144 Underlying flow types: - https://github.com/facebook/react-native/blob/v0.71.1/Libraries/Blob/FileReader.js#L33-L35 - https://github.com/facebook/react-native/blob/v0.71.1/Libraries/Network/XMLHttpRequest.js#L54-L58 ## Changelog [GENERAL] [CHANGED] - Use number literals in TypeScript types for `FileReader` and `XMLHttpRequest` states Pull Request resolved: https://github.com/facebook/react-native/pull/36000 Test Plan: - [x] https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64144 green Reviewed By: christophpurrer Differential Revision: D42849886 Pulled By: jacdebug fbshipit-source-id: c3cf2ac4f5f53ab889a9190583486da4627d3dcc --- types/modules/globals.d.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/types/modules/globals.d.ts b/types/modules/globals.d.ts index 042fd06cb1f017..3efd94c55fc8d2 100644 --- a/types/modules/globals.d.ts +++ b/types/modules/globals.d.ts @@ -260,11 +260,11 @@ interface XMLHttpRequest extends EventTarget, XMLHttpRequestEventTarget { overrideMimeType(mime: string): void; send(data?: any): void; setRequestHeader(header: string, value: string): void; - readonly DONE: number; - readonly HEADERS_RECEIVED: number; - readonly LOADING: number; - readonly OPENED: number; - readonly UNSENT: number; + readonly DONE: 4; + readonly HEADERS_RECEIVED: 2; + readonly LOADING: 3; + readonly OPENED: 1; + readonly UNSENT: 0; addEventListener( type: K, listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, @@ -280,11 +280,11 @@ interface XMLHttpRequest extends EventTarget, XMLHttpRequestEventTarget { declare var XMLHttpRequest: { prototype: XMLHttpRequest; new (): XMLHttpRequest; - readonly DONE: number; - readonly HEADERS_RECEIVED: number; - readonly LOADING: number; - readonly OPENED: number; - readonly UNSENT: number; + readonly DONE: 4; + readonly HEADERS_RECEIVED: 2; + readonly LOADING: 3; + readonly OPENED: 1; + readonly UNSENT: 0; }; interface XMLHttpRequestEventTargetEventMap { @@ -551,9 +551,9 @@ interface FileReader extends EventTarget { // readAsBinaryString(blob: Blob): void; readAsDataURL(blob: Blob): void; readAsText(blob: Blob, encoding?: string): void; - readonly DONE: number; - readonly EMPTY: number; - readonly LOADING: number; + readonly DONE: 2; + readonly EMPTY: 0; + readonly LOADING: 1; addEventListener( type: K, listener: (this: FileReader, ev: FileReaderEventMap[K]) => any, @@ -571,7 +571,7 @@ interface FileReader extends EventTarget { declare var FileReader: { prototype: FileReader; new (): FileReader; - readonly DONE: number; - readonly EMPTY: number; - readonly LOADING: number; + readonly DONE: 2; + readonly EMPTY: 0; + readonly LOADING: 1; }; From 83743e1edf5be24405f776625bdb48aa077aa77d Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Mon, 30 Jan 2023 17:05:25 -0800 Subject: [PATCH 13/65] Update React Native's synced sort-imports lint rule Summary: Changelog: [Internal][Changed] - Update synced sort-import lint rule to newest version Reviewed By: yungsters Differential Revision: D42779224 fbshipit-source-id: bd388c258e5882331fd20d7313b4717a6b88f611 --- tools/eslint/rules/sort-imports.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/eslint/rules/sort-imports.js b/tools/eslint/rules/sort-imports.js index 42312a93c1e47e..0c3c2f50e080e9 100644 --- a/tools/eslint/rules/sort-imports.js +++ b/tools/eslint/rules/sort-imports.js @@ -6,7 +6,7 @@ * * To regenerate this file, please run this command on Meta's monorepo: * @codegen-command : xplat/js/tools/sort-imports/scripts/build.sh - * @generated SignedSource<> + * @generated SignedSource<<7b94a136bcb0a75c0ed8a3e4fea96e2b>> * @nolint */ -"use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}function t(e){var t=e.default;if("function"==typeof t){var n=function(){return t.apply(this,arguments)};n.prototype=t.prototype}else n={};return Object.defineProperty(n,"__esModule",{value:!0}),Object.keys(e).forEach((function(t){var r=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(n,t,r.get?r:{enumerable:!0,get:function(){return e[t]}})})),n}const n=e(require("path")).default,r=__dirname.split(n.sep).includes("xplat");function i(e){if(("Literal"===e.type||"JSXText"===e.type)&&"string"==typeof e.value)return e.value;if("BinaryExpression"===e.type&&"+"===e.operator){const t=i(e.left),n=i(e.right);if(null!=t&&null!=n)return t+n}return null}function o(e){if("Identifier"===e.type)return e.name;if("ThisExpression"===e.type)return"this";if("MemberExpression"===e.type){const t=o(e.object),n=e.computed?i(e.property):o(e.property);if(null!=t&&null!=n)return t+"."+n}else if("TypeCastExpression"===e.type)return o(e.expression);return null}function a(e){return e.callee?o(e.callee):null}function l(e,t,n){const r=e.getSourceCode().getText(),i=function(e,t){if(t.line<1)throw new RangeError("Line number "+t.line+" is before the start of file");const n=/\r\n|\r|\n|\u2028|\u2029/g;let r={index:0};for(let i=1;i=e.length)throw new RangeError("computed offset "+t+" is past the end of file");const n=/\r\n|\r|\n|\u2028|\u2029/g;let r,i={index:0},o=0;do{r=i,i=n.exec(e),++o}while(i&&i.index"string"==typeof e&&e.charCodeAt(0)>=97,c=e=>"string"==typeof e&&e.charCodeAt(0)<=90;function p(e){return null!=e&&"VariableDeclaration"===e.type&&"Program"===e.parent.type&&1===e.declarations.length&&null!=e.declarations[0].init&&m(e.declarations[0].init)}function f(e){return null!=e&&"VariableDeclarator"===e.type&&"VariableDeclaration"===e.parent.type&&"Program"===e.parent.parent.type&&1===e.parent.declarations.length&&m(e.init)}function m(e){return null!=e&&(d(e)||y(e)||h(e))}function g(e){return null!=e&&"CallExpression"===e.type&&e.callee&&"Identifier"===e.callee.type&&"requireNUX"===e.callee.name&&2===e.arguments.length&&"Literal"===e.arguments[0].type}function d(e){return"CallExpression"===e.type&&e.callee&&"Identifier"===e.callee.type&&"require"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type}function y(e){return"CallExpression"===e.type&&"Identifier"===e.callee.type&&"requireDeferred"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type&&"string"==typeof e.arguments[0].value}function h(e){return"CallExpression"===e.type&&"Identifier"===e.callee.type&&"requireDeferredForDisplay"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type&&"string"==typeof e.arguments[0].value}function x(e){return"ImportDeclaration"===e.type&&(null==e.importKind||"value"===e.importKind)}function b(e){return"ImportDeclaration"===e.type&&("type"===e.importKind||"typeof"===e.importKind)}function C(e){return x(e)||b(e)}function S(e){return null!=e&&"CallExpression"===e.type&&e.callee&&"Identifier"===e.callee.type&&"JSResource"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type}function v(e){return null!=e&&"CallExpression"===e.type&&e.callee&&"Identifier"===e.callee.type&&"JSResourceForInteraction"===e.callee.name&&2===e.arguments.length&&"Literal"===e.arguments[0].type}function E(e){return null!=e&&"CallExpression"===e.type&&"Identifier"===e.callee.type&&"ClientJSResource"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type&&"string"==typeof e.arguments[0].value}function I(e){if(null==e||"CallExpression"!==e.type||null==e.callee)return!1;let t;return"Identifier"===e.callee.type&&(t=e.callee),"MemberExpression"===e.callee.type&&"Identifier"===e.callee.object.type&&(t=e.callee.object),null!=t&&"requireCond"===t.name&&e.arguments.length>0}function T(e){return 0===e.indexOf("m#")?e.substring(2):e}function R(e,t,n=1){return"CallExpression"!==e.type||e.arguments.length!==n||null!=t&&"string"==typeof t&&a(e)!==t||"Literal"!==e.arguments[0].type||"string"!=typeof e.arguments[0].value?null:T(e.arguments[0].value)}function D(e,t,n){const r=e.arguments;return I(e)?3!==r.length?null:(null==t||r[0]&&"Literal"===r[0].type&&r[0].value===t)&&(null==n||r[1]&&"Literal"===r[1].type&&r[1].value===n)?r[2]&&"ObjectExpression"===r[2].type?r[2].properties.reduce(((e,t)=>{if("Property"===t.type&&"Literal"===t.value.type){let n;if("Identifier"===t.key.type)n=t.key.name;else{if("Literal"!==t.key.type)return e;n=String(t.key.value)}e[n]="string"==typeof t.value.value?T(t.value.value):null}return e}),{}):r[2]&&"Literal"===r[2].type&&"string"==typeof r[2].value?T(r[2].value):null:null:null}function w(e){return R(e,"JSResource")}function N(e){return null==e?null:m(e)?"string"!=typeof e.arguments[0].value?null:T(e.arguments[0].value):null}function q(e,t){const n=e.getSourceCode().ast.body.find((e=>"VariableDeclaration"===e.type&&1===e.declarations.length&&"VariableDeclarator"===e.declarations[0].type&&null!=e.declarations[0].init&&N(e.declarations[0].init)===t));return n||null}function k(e){let t=e;for(;"TypeCastExpression"===t.type;)t=t.expression;return t}function L(e){if("value"!==e.importKind)return!0;return e.specifiers.every((e=>"ImportSpecifier"===e.type&&("type"===e.importKind||"typeof"===e.importKind)))}function O(e,t){const n=e.getSourceCode().ast.body.find((e=>"ImportDeclaration"===e.type&&e.source.value===t&&!L(e)));return n||null}var P={asAnyKindOfRequireCall:function(e){return m(e)?e:null},asAnyKindOfRequireVariableDeclaration:function(e){return p(e)?e:null},asAnyKindOfRequireVariableDeclarator:function(e){return f(e)?e:null},getAnyRequiredModuleName:function(e,t=Object.freeze({})){const n=N(e)||w(e)||function(e){return R(e,"JSResourceForInteraction",2)}(e)||function(e){if(!g(e))return null;if(2!==e.arguments.length)return null;if("string"!=typeof e.arguments[1].value)return null;return T(e.arguments[1].value)}(e);return null!=n?n:D(e,t.condType,t.condition)},getBaseNode:function(e){let t=e;for(;"MemberExpression"===t.type;)t=t.object;return t},getBinding:s,getBootloadedModuleNames:function(e){return"ExpressionStatement"===e.type&&e.expression&&"CallExpression"===e.expression.type&&e.expression.callee&&"MemberExpression"===e.expression.callee.type&&e.expression.callee.object&&"Bootloader"===e.expression.callee.object.name&&e.expression.callee.property&&"loadModules"===e.expression.callee.property.name&&e.expression.arguments.length>0&&"ArrayExpression"===e.expression.arguments[0].type&&e.expression.arguments[0].elements.length>0?e.expression.arguments[0].elements.map((e=>"Literal"===e.type&&"string"==typeof e.value?e.value:null)).filter(Boolean):null},getCalleeName:a,getConstantStringExpression:i,getCurrentClassName:function(e){const t=e.getAncestors().find((e=>"ClassDeclaration"===e.type));return t&&"ClassDeclaration"===t.type&&null!=t.id?t.id.name:null},getEnglishForNth:function(e){return["first","second","third","fourth","fifth","sixth"][e]},getFullyQualifiedIdentifier:o,getJSResourceModuleName:w,getJSXMemberOrNamespaceRoot:function(e){let t=e;for(;"JSXIdentifier"!==t.type;)if("JSXMemberExpression"===t.type)t=t.object;else{if("JSXNamespacedName"!==t.type)throw new Error("unexpected "+t.type);t=t.namespace}return t},getLocOffset:function(e,t,n){return l(e,t.loc.start,n)},getLocOffsetOfLoc:l,getName:function(e){const t=k(e);return"Identifier"===t.type?t.name:"Literal"===t.type?String(t.value):null},getObjectPropertyName:function(e){if("Property"!==e.type&&"PropertyDefinition"!==e.type&&"MethodDefinition"!==e.type)return null;const t=e.key;return"Identifier"!==t.type||e.computed?function(e){switch(e.type){case"Literal":switch(e.literalType){case"bigint":return e.bigint;case"null":return"null";case"regexp":return`/${e.regex.pattern}/${e.regex.flags}`;default:return String(e.value)}case"TemplateLiteral":if(0===e.expressions.length&&1===e.quasis.length)return e.quasis[0].value.cooked}return null}(t):t.name},getParamComments:function(e,t){return t.params.map((function(t){const n=e.getSourceCode().getCommentsBefore(t);return n[n.length-1]}))},getPropertyName:function(e){return"MemberExpression"!==e.type?null:e.computed?i(e.property):"Identifier"!==e.property.type?null:e.property.name},getPropTokens:function(e,t){const n=e.getSourceCode().getTokens(t),r=[];return n.forEach(((e,t,n)=>{"JSXIdentifier"===e.type&&"Punctuator"===n[t+1].type&&"="===n[t+1].value&&r.push(e)})),r},getRequireCondModules:D,getRequireModuleName:N,getRequireModuleNode:q,getReturnComment:function(e,t){return e.getSourceCode().getCommentsBefore(t.body)[0]},getValueImportNode:O,hasValueImport:function(e){return e.getSourceCode().ast.body.some((e=>"ImportDeclaration"===e.type&&!L(e)))},getVariable:function(e,t){let n=t;for(;n;){const t=n.set.get(e);if(t)return t;n=n.upper}return null},insertRequireStatement:function(e,t,n,r=""){if(""===n)throw new Error("Name must be a string with length larger than 0");const i=`const ${n} = require('${n}${r}');`,o=[];e.getSourceCode().ast.body.forEach((e=>{if("VariableDeclaration"===e.type&&1===e.declarations.length&&"VariableDeclarator"===e.declarations[0].type&&e.declarations[0].init){const t=N(e.declarations[0].init);null!=t&&o.push({name:t,node:e})}}));const a=o.find((e=>e.name>=n));if(a){if(a.name.replace(r,"")===n)return[];const e=o[0];if(u(a.name)&&c(n)){if(e!==a){const e=a.node.range[0];return[t.removeRange([e-1,e-1]),t.insertTextBefore(a.node,i+"\n\n")]}return[t.insertTextBefore(a.node,i+"\n\n")]}return[t.insertTextBefore(a.node,i+"\n")]}if(o.length>0){const e=o[o.length-1],r=c(e.name)&&u(n)?"\n":"";return[t.insertTextAfter(e.node,"\n"+i+r)]}{const n=e.getSourceCode().ast.body,r=n[0];return"ExpressionStatement"===r.type&&"Literal"===r.expression.type&&"use strict"===r.expression.value?[t.insertTextBefore(n[1],i+"\n")]:[t.insertTextBefore(n[0],i+"\n")]}},insertValueImportStatement:function(e,t,n,r=""){if(""===n)throw new Error("Name must be a string with length larger than 0");const i=`import ${n} from '${n}${r}';`,o=[];e.getSourceCode().ast.body.forEach((e=>{if(x(e)){const t=function(e){if(m(e))return"string"!=typeof e.arguments[0].value?null:T(e.arguments[0].value);if(C(e))return e.source.value;return null}(e);null!=t&&o.push({name:t,node:e})}}));const a=o.find((e=>e.name>=n));if(a){if(a.name.replace(r,"")===n)return[];const e=o[0];if(u(a.name)&&c(n)){if(e!==a){const e=a.node.range[0];return[t.removeRange([e-1,e-1]),t.insertTextBefore(a.node,i+"\n\n")]}return[t.insertTextBefore(a.node,i+"\n\n")]}return[t.insertTextBefore(a.node,i+"\n")]}if(o.length>0){const e=o[o.length-1],r=c(e.name)&&u(n)?"\n":"";return[t.insertTextAfter(e.node,"\n"+i+r)]}{const n=e.getSourceCode().ast.body,r=n[0];return"ExpressionStatement"===r.type&&"Literal"===r.expression.type&&"use strict"===r.expression.value?[t.insertTextBefore(n[1],i+"\n")]:[t.insertTextBefore(n[0],i+"\n")]}},isAnyKindOfImport:C,isAnyKindOfModuleCall:function(e){return m(e)||I(e)||S(e)||v(e)||E(e)||g(e)},isAnyKindOfRequireCall:m,isAnyKindOfRequireVariableDeclaration:p,isAnyKindOfRequireVariableDeclarator:f,isClientJSResource:E,isFbSourceRepo:r,isGraphQLTemplate:function(e){return"Identifier"===e.tag.type&&"graphql"===e.tag.name&&1===e.quasi.quasis.length},isInsideMethod:function(e,t){return e.getAncestors().some((e=>"MethodDefinition"===e.type&&"Identifier"===e.key.type&&e.key.name===t))},isJSResource:S,isJSResourceForInteraction:v,isModuleRef:function(e){return"Literal"===e.type&&"string"==typeof e.value&&e.value.startsWith("m#")},isOnlyTypeImport:L,isOnlyTypeExport:function(e){return"type"===e.exportKind},isReferenced:function(e){const t=e.parent;switch(t.type){case"MemberExpression":case"JSXMemberExpression":return t.property===e&&!0===t.computed||t.object===e;case"MetaProperty":case"ImportDefaultSpecifier":case"ImportNamespaceSpecifier":case"ImportSpecifier":case"LabeledStatement":case"RestElement":case"ObjectPattern":case"ArrayPattern":return!1;case"Property":case"MethodDefinition":return t.key===e&&t.computed;case"VariableDeclarator":case"ClassDeclaration":case"ClassExpression":return t.id!==e;case"ArrowFunctionExpression":case"FunctionDeclaration":case"FunctionExpression":for(let n=0;ne.writeExpr));return null!=i?i.writeExpr:null},stripModuleRef:T,uncast:k};var j=t(Object.freeze({__proto__:null,isCommaOrSemiToken:function(e){return"Punctuator"===e.type&&(","===e.value||";"===e.value)}}));const{isCommaOrSemiToken:B}=j;var F={getInlineComments:function(e,t,n=B){const r=e.ast.comments.filter((e=>e.loc.start.line===t.loc.end.line&&e.range[0]>t.range[1])).sort(((e,t)=>e.range[0]-t.range[0])),i=[];let o=t;for(const t of r){const r=e.getTokensBetween(o,t);if(r.length>0){if(!n)break;if(!r.every(n))break}i.push(t),o=t}return i},getLeadingComments:function(e,t,n){const r=e.getCommentsBefore(t),i=[];let o=t;for(let t=r.length-1;t>=0;t-=1){const a=r[t];if(a===n)break;if(a.loc.end.line===o.loc.start.line){i.unshift(a),o=a;continue}if(a.loc.end.line!==o.loc.start.line-1)break;const l=e.getTokenBefore(a);if(l&&l.loc.end.line===a.loc.start.line)break;i.unshift(a),o=a}return i}};var M=function(e){return e.getSourceCode().ast.docblock?.comment};const{getInlineComments:A,getLeadingComments:V}=F,{isCommaOrSemiToken:$}=j;function K(e){return"@"===e[0]||":"===e[0]}const J=/\d{1,3}(\.\d+)?%/;function _(e){switch(typeof e){case"number":return{isSafeNumericString:!0,isPercentage:!1};case"boolean":return{isSafeNumericString:!1,isPercentage:!1}}return isNaN(e)||isNaN(parseFloat(e))?J.test(e)?{isSafeNumericString:!0,isPercentage:!0}:{isSafeNumericString:!1,isPercentage:!1}:{isSafeNumericString:!0,isPercentage:!1}}function X(e){return e.reduce((([e,t],[n,r])=>[Math.min(e,n),Math.max(t,r)]),[Number.MAX_SAFE_INTEGER,0])}var U={compareNames:function(e,t,n=!1){if("number"==typeof e&&"number"==typeof t)return e-t;const r=String(e),i=String(t);if(""===r&&""!==i)return-1;if(""!==r&&""===i)return 1;if(""===r&&""===i)return 0;const{isSafeNumericString:o,isPercentage:a}=_(r),{isSafeNumericString:l,isPercentage:s}=_(i);if(o&&l){const e=Number.parseFloat(r),t=Number.parseFloat(i);if(e===t){if(!a&&s)return-1;if(a&&!s)return 1}return e-t}if(n){const e=K(r),t=K(i);if(!e&&t)return-1;if(e&&!t)return 1}const u=o||r[0].toLowerCase()===r[0].toUpperCase(),c=l||i[0].toLowerCase()===i[0].toUpperCase();if(!u&&c)return 1;if(u&&!c)return-1;if(!u&&!c){const e=r[0].toLowerCase()===r[0],t=i[0].toLowerCase()===i[0];if(!e&&t)return-1;if(e&&!t)return 1}return r.localeCompare(i,"en",{caseFirst:"upper",sensitivity:"base"})},getEncompassingRange:X,getNodeTextWithComments:function(e,t,n,{shouldIncludeNextTokenInRange:r=$,ensureTextFollowsNode:i,inlineCommentIgnoreToken:o}={}){const a=A(e,t,o),l=[...V(e,t,n).map((({range:e})=>e)),t.range,...a.map((({range:e})=>e))],s=e.getTokenAfter(t);s&&!0===r?.(s)&&l.push(s.range);const u=X(l);let c=e.text.slice(u[0],u[1]);const p=t.range[1]-u[0];if(null!=i){e.getTokenAfter(t)?.value!==i&&(c=c.slice(0,p)+i+c.slice(p))}return{range:u,text:c}},isComma:function(e){return"Punctuator"===e.type&&","===e.value}};const z=P,{getInlineComments:G,getLeadingComments:Q}=F,W=M,{compareNames:H}=U,Y=0,Z={default:1,namespace:2,named:3},ee={default:4,namespace:5,named:6},te={default:7,namespace:8,named:9},ne=0,re=1,ie=2,oe=3,ae=99;var le={meta:{fixable:"code",messages:{incorrectOrder:"Requires should be sorted alphabetically"}},create(e){const t=e.getSourceCode(),n=function(e){const t=W(e);if(null!=t)return t;const n=e.getSourceCode().ast,r=n.body.length>0?n.body[0]:n,i=e.getSourceCode().getCommentsBefore(r)[0];return i&&"Block"===i.type?i:null}(e);if(n&&(n.value.includes("* @generated")||n.value.includes("* @partially-generated")))return{};const r=Object.freeze({typeImport:{priority:10,uppercase:[],lowercase:[],tiebreakFunction:s},valueImport:{priority:20,uppercase:[],lowercase:[],tiebreakFunction:s},requiresUsedByOtherRequires:{priority:30,uppercase:[],lowercase:[],tiebreakFunction:u},require:{priority:40,uppercase:[],lowercase:[],tiebreakFunction:u}}),i=[];let o=null,a=null;const l=new Set;return{Program(n){for(const e of n.body)switch(e.type){case"ImportDeclaration":if("type"===e.importKind||"typeof"===e.importKind)g(r.typeImport,e,e.source.value,!0);else{const n=t.getLastToken(e.source,(e=>"from"===e.value));0===e.specifiers.length&&null==n?d(e):g(r.valueImport,e,e.source.value)}break;case"VariableDeclaration":{const t=e.declarations[0]?.init;if(1!==e.declarations.length||null==t){d(e);break}f(t,e);break}default:d(e)}const s=[];for(const e of Object.keys(r)){const t=r[e];s.push({priority:t.priority,nodes:t.uppercase.sort(((e,n)=>c(e,n,t.tiebreakFunction)))}),s.push({priority:t.priority+5,nodes:t.lowercase.sort(((e,n)=>c(e,n,t.tiebreakFunction)))})}function u(e){return[e.leadingComments.length?e.leadingComments.map((e=>t.getText(e))).join("\n")+"\n":"",e.inlineComments.length?" "+e.inlineComments.map((e=>t.getText(e))).join(" "):""]}const p=s.filter((e=>0!==e.nodes.length)).sort(((e,t)=>e.priority-t.priority)).map((e=>e.nodes.map((e=>{const[n,r]=u(e),i=function(e,t){const n=t.node,r=e.getText(n),i=(()=>{if("ImportDeclaration"===n.type&&null!=n.specifiers.find((e=>"ImportSpecifier"===e.type))){const t=e.getFirstToken(n,(e=>"Punctuator"===e.type&&"{"===e.value)),r=e.getFirstToken(n,(e=>"Punctuator"===e.type&&"}"===e.value));return null==t||null==r?null:e.getText().substring(t.range[0],r.range[1])}if("VariableDeclaration"===n.type&&"ObjectPattern"===n.declarations[0].id.type){const t=n.declarations[0].id.typeAnnotation,r=e.getText(n.declarations[0].id);return t?r.substr(0,t.range[0]-n.declarations[0].id.range[0]):r}return null})();if(null==i)return r;let o=[],a=null;"ImportDeclaration"===n.type?o=n.specifiers.map((t=>"ImportDefaultSpecifier"===t.type||"ImportNamespaceSpecifier"===t.type?null:{leadingComments:e.getCommentsBefore(t),name:t.imported.name,node:t})).filter(Boolean):"VariableDeclaration"===n.type&&"ObjectPattern"===n.declarations[0].id.type&&(o=n.declarations[0].id.properties.map((t=>{if("ExperimentalRestProperty"===t.type||"RestElement"===t.type)return a=t,null;const n=t.key,r="Literal"===n.type?String(n.value):"Identifier"===n.type?t.computed?null:n.name:null;return null==r?null:{leadingComments:e.getCommentsBefore(t),name:r,node:t}})).filter(Boolean));if(o.length<=1)return r;const l=i.indexOf("\n")>=0,s=o.sort(((e,t)=>H(e.name,t.name))).map((e=>e.node));null!=a&&s.push(a);const u=s.map((t=>{const n=e.getCommentsBefore(t).map((t=>e.getText(t))),r=n.length?n.join(""):"";return l?(r?" "+r+"\n":"")+" "+e.getText(t):(r?r+" ":"")+e.getText(t)})),c=(()=>{const t=[];if("ImportDeclaration"===n.type){if(t.push(...e.getCommentsBefore(n.source)),l&&null!=n.specifiers.find((e=>"ImportSpecifier"===e.type))){const r=e.getTokenBefore(n.source,(e=>"Punctuator"===e.type&&"}"===e.value));null!=r&&t.push(...e.getCommentsBefore(r))}}else if("VariableDeclaration"===n.type&&"ObjectPattern"===n.declarations[0].id.type){const r=e.getLastToken(n.declarations[0].id);null!=r&&t.push(...e.getCommentsBefore(r))}return t})(),p=l&&c.length?c.map((t=>" "+e.getText(t))).join("\n")+"\n":"",f=l?"\n"+u.map((e=>e.includes("...")?`${e}\n`:`${e},\n`)).join("")+p:u.join(", ");return r.replace(i,`{${f}}`)}(t,e);return n+i+r})).join("\n"))).join("\n\n"),m=o;if(null==m||null==a)return;const y=m.leadingComments.length?m.leadingComments[0].range[0]:m.node.range[0],h=a.inlineComments.length>0?a.inlineComments[a.inlineComments.length-1].range[1]:a.node.range[1];t.getText(m.node,m.node.range[0]-y,h-m.node.range[1])!==p&&e.report({node:m.node,messageId:"incorrectOrder",fix(e){const n=i.filter((({node:e})=>e.range[0]>=y&&e.range[1]<=h)).map((e=>{const[n,r]=u(e),i=t.getText(e.node);return{range:e.node.range,text:n+i+r}})).flat().concat(t.getAllComments().filter((e=>e.range[0]>=y&&e.range[1]<=h&&!l.has(e))).map((e=>({range:e.range,text:t.getText(e)})))).sort(((e,t)=>e.range[0]-t.range[0]||e.range[1]-t.range[1])).map((({text:e})=>e)).join("\n");return e.replaceTextRange([y,h],[p,n].filter(Boolean).join("\n\n"))}})}};function s(e){if(0===e.specifiers.length)return Y;const t=(()=>{switch(e.importKind){default:case"value":return Z;case"type":return ee;case"typeof":return te}})();return e.specifiers.find((e=>"ImportDefaultSpecifier"===e.type))?t.default:e.specifiers.find((e=>"ImportNamespaceSpecifier"===e.type))?t.namespace:t.named}function u(e){if("ExpressionStatement"===e.type)return ne;switch(e.declarations[0].id.type){case"Identifier":return re;case"ObjectPattern":return ie;case"ArrayPattern":return oe}return ae}function c(e,t,n){const r=H(e.moduleName,t.moduleName);if(0!==r)return r;const i=n(e.node)-n(t.node);return 0!==i?i:e.node.loc.start.line-t.node.loc.start.line}function p(e,t){if(null==e)throw new Error("Missing required module name");return null!=t?`${e}_${t}`:e}function f(e,n,i){const o=z.getRequireModuleName(e);if(z.isRequire(e)){const e=p(o,i);g("requireCond"===e||"requireDeferred"===e||"requireDeferredForDisplay"===e?r.requiresUsedByOtherRequires:r.require,n,p(o,i))}else if(z.isRequireDeferred(e)||z.isRequireDeferredForDisplay(e))g(r.require,n,p(o,i));else{if(z.isRequireCond(e)){if("VariableDeclaration"===n.type){const e=t.getText(n.declarations[0].id);return void g(r.require,n,p(e,i))}}else{if("MemberExpression"===e.type)return void f(e.object,n,p(t.getText(e.property),i));if("CallExpression"===e.type)return void f(e.callee,n,i)}d(n)}}function m(e){e.leadingComments.forEach((e=>l.add(e))),e.inlineComments.forEach((e=>l.add(e)));t.getCommentsInside(e.node).forEach((e=>l.add(e)))}function g(e,r,i,l=!1){const s={inlineComments:G(t,r),leadingComments:Q(t,r,n),moduleName:i,node:r};if(l)e.uppercase.push(s);else{const t=i[0]||"";t.toLowerCase()===t?e.lowercase.push(s):e.uppercase.push(s)}null==o&&(o=s),a=s,m(s)}function d(e){const r={inlineComments:G(t,e),leadingComments:Q(t,e,n),node:e};i.push(r),m(r)}}};const se=le;var ue={...se,create:e=>e.getSourceCode().getText().includes("@generated SignedSource<<")?{}:se.create(e)};module.exports=ue; +"use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}function t(e){var t=e.default;if("function"==typeof t){var n=function(){return t.apply(this,arguments)};n.prototype=t.prototype}else n={};return Object.defineProperty(n,"__esModule",{value:!0}),Object.keys(e).forEach((function(t){var r=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(n,t,r.get?r:{enumerable:!0,get:function(){return e[t]}})})),n}const n=e(require("path")).default,r=__dirname.split(n.sep).includes("xplat");function i(e){if(("Literal"===e.type||"JSXText"===e.type)&&"string"==typeof e.value)return e.value;if("BinaryExpression"===e.type&&"+"===e.operator){const t=i(e.left),n=i(e.right);if(null!=t&&null!=n)return t+n}return null}function o(e){if("Identifier"===e.type)return e.name;if("ThisExpression"===e.type)return"this";if("MemberExpression"===e.type){const t=o(e.object),n=e.computed?i(e.property):o(e.property);if(null!=t&&null!=n)return t+"."+n}else if("TypeCastExpression"===e.type)return o(e.expression);return null}function a(e){return e.callee?o(e.callee):null}function l(e,t,n){const r=e.getSourceCode().getText(),i=function(e,t){if(t.line<1)throw new RangeError("Line number "+t.line+" is before the start of file");const n=/\r\n|\r|\n|\u2028|\u2029/g;let r={index:0};for(let i=1;i=e.length)throw new RangeError("computed offset "+t+" is past the end of file");const n=/\r\n|\r|\n|\u2028|\u2029/g;let r,i={index:0},o=0;do{r=i,i=n.exec(e),++o}while(i&&i.index"string"==typeof e&&e.charCodeAt(0)>=97,c=e=>"string"==typeof e&&e.charCodeAt(0)<=90;function p(e){return null!=e&&"VariableDeclaration"===e.type&&"Program"===e.parent.type&&1===e.declarations.length&&null!=e.declarations[0].init&&m(e.declarations[0].init)}function f(e){return null!=e&&"VariableDeclarator"===e.type&&"VariableDeclaration"===e.parent.type&&"Program"===e.parent.parent.type&&1===e.parent.declarations.length&&m(e.init)}function m(e){return null!=e&&(d(e)||y(e)||h(e))}function g(e){return null!=e&&"CallExpression"===e.type&&e.callee&&"Identifier"===e.callee.type&&"requireNUX"===e.callee.name&&2===e.arguments.length&&"Literal"===e.arguments[0].type}function d(e){return"CallExpression"===e.type&&e.callee&&"Identifier"===e.callee.type&&"require"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type}function y(e){return"CallExpression"===e.type&&"Identifier"===e.callee.type&&"requireDeferred"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type&&"string"==typeof e.arguments[0].value}function h(e){return"CallExpression"===e.type&&"Identifier"===e.callee.type&&"requireDeferredForDisplay"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type&&"string"==typeof e.arguments[0].value}function x(e){return"ImportDeclaration"===e.type&&(null==e.importKind||"value"===e.importKind)}function b(e){return"ImportDeclaration"===e.type&&("type"===e.importKind||"typeof"===e.importKind)}function C(e){return x(e)||b(e)}function S(e){return null!=e&&"CallExpression"===e.type&&e.callee&&"Identifier"===e.callee.type&&"JSResource"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type}function v(e){return null!=e&&"CallExpression"===e.type&&e.callee&&"Identifier"===e.callee.type&&"JSResourceForInteraction"===e.callee.name&&2===e.arguments.length&&"Literal"===e.arguments[0].type}function E(e){return null!=e&&"CallExpression"===e.type&&"Identifier"===e.callee.type&&"ClientJSResource"===e.callee.name&&1===e.arguments.length&&"Literal"===e.arguments[0].type&&"string"==typeof e.arguments[0].value}function I(e){if(null==e||"CallExpression"!==e.type||null==e.callee)return!1;let t;return"Identifier"===e.callee.type&&(t=e.callee),"MemberExpression"===e.callee.type&&"Identifier"===e.callee.object.type&&(t=e.callee.object),null!=t&&"requireCond"===t.name&&e.arguments.length>0}function T(e){return 0===e.indexOf("m#")?e.substring(2):e}function R(e,t,n=1){return"CallExpression"!==e.type||e.arguments.length!==n||null!=t&&"string"==typeof t&&a(e)!==t||"Literal"!==e.arguments[0].type||"string"!=typeof e.arguments[0].value?null:T(e.arguments[0].value)}function D(e,t,n){const r=e.arguments;return I(e)?3!==r.length?null:(null==t||r[0]&&"Literal"===r[0].type&&r[0].value===t)&&(null==n||r[1]&&"Literal"===r[1].type&&r[1].value===n)?r[2]&&"ObjectExpression"===r[2].type?r[2].properties.reduce(((e,t)=>{if("Property"===t.type&&"Literal"===t.value.type){let n;if("Identifier"===t.key.type)n=t.key.name;else{if("Literal"!==t.key.type)return e;n=String(t.key.value)}e[n]="string"==typeof t.value.value?T(t.value.value):null}return e}),{}):r[2]&&"Literal"===r[2].type&&"string"==typeof r[2].value?T(r[2].value):null:null:null}function w(e){return R(e,"JSResource")}function N(e){return null==e?null:m(e)?"string"!=typeof e.arguments[0].value?null:T(e.arguments[0].value):null}function q(e,t){const n=e.getSourceCode().ast.body.find((e=>"VariableDeclaration"===e.type&&1===e.declarations.length&&"VariableDeclarator"===e.declarations[0].type&&null!=e.declarations[0].init&&N(e.declarations[0].init)===t));return n||null}function k(e){let t=e;for(;"TypeCastExpression"===t.type;)t=t.expression;return t}function L(e){if("value"!==e.importKind)return!0;return e.specifiers.every((e=>"ImportSpecifier"===e.type&&("type"===e.importKind||"typeof"===e.importKind)))}function O(e,t){const n=e.getSourceCode().ast.body.find((e=>"ImportDeclaration"===e.type&&e.source.value===t&&!L(e)));return n||null}var P={asAnyKindOfRequireCall:function(e){return m(e)?e:null},asAnyKindOfRequireVariableDeclaration:function(e){return p(e)?e:null},asAnyKindOfRequireVariableDeclarator:function(e){return f(e)?e:null},getAnyRequiredModuleName:function(e,t=Object.freeze({})){const n=N(e)||w(e)||function(e){return R(e,"JSResourceForInteraction",2)}(e)||function(e){if(!g(e))return null;if(2!==e.arguments.length)return null;if("string"!=typeof e.arguments[1].value)return null;return T(e.arguments[1].value)}(e);return null!=n?n:D(e,t.condType,t.condition)},getBaseNode:function(e){let t=e;for(;"MemberExpression"===t.type;)t=t.object;return t},getBinding:s,getBootloadedModuleNames:function(e){return"ExpressionStatement"===e.type&&e.expression&&"CallExpression"===e.expression.type&&e.expression.callee&&"MemberExpression"===e.expression.callee.type&&e.expression.callee.object&&"Bootloader"===e.expression.callee.object.name&&e.expression.callee.property&&"loadModules"===e.expression.callee.property.name&&e.expression.arguments.length>0&&"ArrayExpression"===e.expression.arguments[0].type&&e.expression.arguments[0].elements.length>0?e.expression.arguments[0].elements.map((e=>"Literal"===e.type&&"string"==typeof e.value?e.value:null)).filter(Boolean):null},getCalleeName:a,getConstantStringExpression:i,getCurrentClassName:function(e){const t=e.getAncestors().find((e=>"ClassDeclaration"===e.type));return t&&"ClassDeclaration"===t.type&&null!=t.id?t.id.name:null},getEnglishForNth:function(e){return["first","second","third","fourth","fifth","sixth"][e]},getFullyQualifiedIdentifier:o,getJSResourceModuleName:w,getJSXMemberOrNamespaceRoot:function(e){let t=e;for(;"JSXIdentifier"!==t.type;)if("JSXMemberExpression"===t.type)t=t.object;else{if("JSXNamespacedName"!==t.type)throw new Error("unexpected "+t.type);t=t.namespace}return t},getLocOffset:function(e,t,n){return l(e,t.loc.start,n)},getLocOffsetOfLoc:l,getName:function(e){const t=k(e);return"Identifier"===t.type?t.name:"Literal"===t.type?String(t.value):null},getObjectPropertyName:function(e){if("Property"!==e.type&&"PropertyDefinition"!==e.type&&"MethodDefinition"!==e.type)return null;const t=e.key;return"Identifier"!==t.type||e.computed?function(e){switch(e.type){case"Literal":switch(e.literalType){case"bigint":return e.bigint;case"null":return"null";case"regexp":return`/${e.regex.pattern}/${e.regex.flags}`;default:return String(e.value)}case"TemplateLiteral":if(0===e.expressions.length&&1===e.quasis.length)return e.quasis[0].value.cooked}return null}(t):t.name},getParamComments:function(e,t){return t.params.map((function(t){const n=e.getSourceCode().getCommentsBefore(t);return n[n.length-1]}))},getPropertyName:function(e){return"MemberExpression"!==e.type?null:e.computed?i(e.property):"Identifier"!==e.property.type?null:e.property.name},getPropTokens:function(e,t){const n=e.getSourceCode().getTokens(t),r=[];return n.forEach(((e,t,n)=>{"JSXIdentifier"===e.type&&"Punctuator"===n[t+1].type&&"="===n[t+1].value&&r.push(e)})),r},getRequireCondModules:D,getRequireModuleName:N,getRequireModuleNode:q,getReturnComment:function(e,t){return e.getSourceCode().getCommentsBefore(t.body)[0]},getValueImportNode:O,hasValueImport:function(e){return e.getSourceCode().ast.body.some((e=>"ImportDeclaration"===e.type&&!L(e)))},getVariable:function(e,t){let n=t;for(;n;){const t=n.set.get(e);if(t)return t;n=n.upper}return null},insertRequireStatement:function(e,t,n,r=""){if(""===n)throw new Error("Name must be a string with length larger than 0");const i=`const ${n} = require('${n}${r}');`,o=[];e.getSourceCode().ast.body.forEach((e=>{if("VariableDeclaration"===e.type&&1===e.declarations.length&&"VariableDeclarator"===e.declarations[0].type&&e.declarations[0].init){const t=N(e.declarations[0].init);null!=t&&o.push({name:t,node:e})}}));const a=o.find((e=>e.name>=n));if(a){if(a.name.replace(r,"")===n)return[];const e=o[0];if(u(a.name)&&c(n)){if(e!==a){const e=a.node.range[0];return[t.removeRange([e-1,e-1]),t.insertTextBefore(a.node,i+"\n\n")]}return[t.insertTextBefore(a.node,i+"\n\n")]}return[t.insertTextBefore(a.node,i+"\n")]}if(o.length>0){const e=o[o.length-1],r=c(e.name)&&u(n)?"\n":"";return[t.insertTextAfter(e.node,"\n"+i+r)]}{const n=e.getSourceCode().ast.body,r=n[0];return"ExpressionStatement"===r.type&&"Literal"===r.expression.type&&"use strict"===r.expression.value?[t.insertTextBefore(n[1],i+"\n")]:[t.insertTextBefore(n[0],i+"\n")]}},insertValueImportStatement:function(e,t,n,r=""){if(""===n)throw new Error("Name must be a string with length larger than 0");const i=`import ${n} from '${n}${r}';`,o=[];e.getSourceCode().ast.body.forEach((e=>{if(x(e)){const t=function(e){if(m(e))return"string"!=typeof e.arguments[0].value?null:T(e.arguments[0].value);if(C(e))return e.source.value;return null}(e);null!=t&&o.push({name:t,node:e})}}));const a=o.find((e=>e.name>=n));if(a){if(a.name.replace(r,"")===n)return[];const e=o[0];if(u(a.name)&&c(n)){if(e!==a){const e=a.node.range[0];return[t.removeRange([e-1,e-1]),t.insertTextBefore(a.node,i+"\n\n")]}return[t.insertTextBefore(a.node,i+"\n\n")]}return[t.insertTextBefore(a.node,i+"\n")]}if(o.length>0){const e=o[o.length-1],r=c(e.name)&&u(n)?"\n":"";return[t.insertTextAfter(e.node,"\n"+i+r)]}{const n=e.getSourceCode().ast.body,r=n[0];return"ExpressionStatement"===r.type&&"Literal"===r.expression.type&&"use strict"===r.expression.value?[t.insertTextBefore(n[1],i+"\n")]:[t.insertTextBefore(n[0],i+"\n")]}},isAnyKindOfImport:C,isAnyKindOfModuleCall:function(e){return m(e)||I(e)||S(e)||v(e)||E(e)||g(e)},isAnyKindOfRequireCall:m,isAnyKindOfRequireVariableDeclaration:p,isAnyKindOfRequireVariableDeclarator:f,isClientJSResource:E,isFbSourceRepo:r,isGraphQLTemplate:function(e){return"Identifier"===e.tag.type&&"graphql"===e.tag.name&&1===e.quasi.quasis.length},isInsideMethod:function(e,t){return e.getAncestors().some((e=>"MethodDefinition"===e.type&&"Identifier"===e.key.type&&e.key.name===t))},isJSResource:S,isJSResourceForInteraction:v,isModuleRef:function(e){return"Literal"===e.type&&"string"==typeof e.value&&e.value.startsWith("m#")},isOnlyTypeImport:L,isOnlyTypeExport:function(e){return"type"===e.exportKind},isReferenced:function(e){const t=e.parent;switch(t.type){case"MemberExpression":case"JSXMemberExpression":return t.property===e&&!0===t.computed||t.object===e;case"MetaProperty":case"ImportDefaultSpecifier":case"ImportNamespaceSpecifier":case"ImportSpecifier":case"LabeledStatement":case"RestElement":case"ObjectPattern":case"ArrayPattern":return!1;case"Property":case"MethodDefinition":return t.key===e&&t.computed;case"VariableDeclarator":case"ClassDeclaration":case"ClassExpression":return t.id!==e;case"ArrowFunctionExpression":case"FunctionDeclaration":case"FunctionExpression":for(let n=0;ne.writeExpr));return null!=i?i.writeExpr:null},stripModuleRef:T,uncast:k};var j=t(Object.freeze({__proto__:null,isCommaOrSemiToken:function(e){return"Punctuator"===e.type&&(","===e.value||";"===e.value)}}));const{isCommaOrSemiToken:B}=j;var F={getInlineComments:function(e,t,n=B){const r=e.ast.comments.filter((e=>e.loc.start.line===t.loc.end.line&&e.range[0]>t.range[1])).sort(((e,t)=>e.range[0]-t.range[0])),i=[];let o=t;for(const t of r){const r=e.getTokensBetween(o,t);if(r.length>0){if(!n)break;if(!r.every(n))break}i.push(t),o=t}return i},getLeadingComments:function(e,t,n){const r=e.getCommentsBefore(t),i=[];let o=t;for(let t=r.length-1;t>=0;t-=1){const a=r[t];if(a===n)break;if(a.loc.end.line===o.loc.start.line){i.unshift(a),o=a;continue}if(a.loc.end.line!==o.loc.start.line-1)break;const l=e.getTokenBefore(a);if(l&&l.loc.end.line===a.loc.start.line)break;i.unshift(a),o=a}return i}};var M=function(e){return e.getSourceCode().ast.docblock?.comment};const{getInlineComments:A,getLeadingComments:V}=F,{isCommaOrSemiToken:$}=j;function K(e){return"@"===e[0]||":"===e[0]}const J=/\d{1,3}(\.\d+)?%/;function _(e){switch(typeof e){case"number":return{isSafeNumericString:!0,isPercentage:!1};case"boolean":return{isSafeNumericString:!1,isPercentage:!1}}return isNaN(e)||isNaN(parseFloat(e))?J.test(e)?{isSafeNumericString:!0,isPercentage:!0}:{isSafeNumericString:!1,isPercentage:!1}:{isSafeNumericString:!0,isPercentage:!1}}function X(e){return e.reduce((([e,t],[n,r])=>[Math.min(e,n),Math.max(t,r)]),[Number.MAX_SAFE_INTEGER,0])}var U={compareNames:function(e,t,n=!1){if("number"==typeof e&&"number"==typeof t)return e-t;const r=String(e),i=String(t);if(""===r&&""!==i)return-1;if(""!==r&&""===i)return 1;if(""===r&&""===i)return 0;const{isSafeNumericString:o,isPercentage:a}=_(r),{isSafeNumericString:l,isPercentage:s}=_(i);if(o&&l){const e=Number.parseFloat(r),t=Number.parseFloat(i);if(e===t){if(!a&&s)return-1;if(a&&!s)return 1}return e-t}if(n){const e=K(r),t=K(i);if(!e&&t)return-1;if(e&&!t)return 1}const u=o||r[0].toLowerCase()===r[0].toUpperCase(),c=l||i[0].toLowerCase()===i[0].toUpperCase();if(!u&&c)return 1;if(u&&!c)return-1;if(!u&&!c){const e=r[0].toLowerCase()===r[0],t=i[0].toLowerCase()===i[0];if(!e&&t)return-1;if(e&&!t)return 1}return r.localeCompare(i,"en",{caseFirst:"upper",sensitivity:"base"})},getEncompassingRange:X,getNodeTextWithComments:function(e,t,n,{shouldIncludeNextTokenInRange:r=$,ensureTextFollowsNode:i,inlineCommentIgnoreToken:o}={}){const a=A(e,t,o),l=[...V(e,t,n).map((({range:e})=>e)),t.range,...a.map((({range:e})=>e))],s=e.getTokenAfter(t);s&&!0===r?.(s)&&l.push(s.range);const u=X(l);let c=e.text.slice(u[0],u[1]);const p=t.range[1]-u[0];if(null!=i){e.getTokenAfter(t)?.value!==i&&(c=c.slice(0,p)+i+c.slice(p))}return{range:u,text:c}},isComma:function(e){return"Punctuator"===e.type&&","===e.value}};const z=P,{getInlineComments:G,getLeadingComments:Q}=F,W=M,{compareNames:H}=U,Y=0,Z={default:1,namespace:2,named:3},ee={default:4,namespace:5,named:6},te={default:7,namespace:8,named:9},ne=0,re=1,ie=2,oe=3,ae=99;var le={meta:{fixable:"code",messages:{incorrectOrder:"Requires should be sorted alphabetically"}},create(e){const t=e.getSourceCode(),n=function(e){const t=W(e);if(null!=t)return t;const n=e.getSourceCode().ast,r=n.body.length>0?n.body[0]:n,i=e.getSourceCode().getCommentsBefore(r)[0];return i&&"Block"===i.type?i:null}(e);if(n&&(n.value.includes("* @generated")||n.value.includes("* @partially-generated")))return{};const r=Object.freeze({typeImport:{priority:10,uppercase:[],lowercase:[],tiebreakFunction:s},valueImport:{priority:20,uppercase:[],lowercase:[],tiebreakFunction:s},requiresUsedByOtherRequires:{priority:30,uppercase:[],lowercase:[],tiebreakFunction:u},require:{priority:40,uppercase:[],lowercase:[],tiebreakFunction:u}}),i=[];let o=null,a=null;const l=new Set;return{Program(n){for(const e of n.body)switch(e.type){case"ImportDeclaration":if("type"===e.importKind||"typeof"===e.importKind)g(r.typeImport,e,e.source.value,!0);else{const n=t.getLastToken(e.source,(e=>"from"===e.value));0===e.specifiers.length&&null==n?d(e):g(r.valueImport,e,e.source.value)}break;case"VariableDeclaration":{const t=e.declarations[0]?.init;if(1!==e.declarations.length||null==t){d(e);break}f(t,e);break}default:d(e)}const s=[];for(const e of Object.keys(r)){const t=r[e];s.push({priority:t.priority,nodes:t.uppercase.sort(((e,n)=>c(e,n,t.tiebreakFunction)))}),s.push({priority:t.priority+5,nodes:t.lowercase.sort(((e,n)=>c(e,n,t.tiebreakFunction)))})}function u(e){return[e.leadingComments.length?e.leadingComments.map((e=>t.getText(e))).join("\n")+"\n":"",e.inlineComments.length?" "+e.inlineComments.map((e=>t.getText(e))).join(" "):""]}const p=s.filter((e=>0!==e.nodes.length)).sort(((e,t)=>e.priority-t.priority)).map((e=>e.nodes.map((e=>{const[n,r]=u(e),i=function(e,t){const n=t.node,r=e.getText(n),i=(()=>{if("ImportDeclaration"===n.type&&null!=n.specifiers.find((e=>"ImportSpecifier"===e.type))){const t=e.getFirstToken(n,(e=>"Punctuator"===e.type&&"{"===e.value)),r=e.getFirstToken(n,(e=>"Punctuator"===e.type&&"}"===e.value));return null==t||null==r?null:e.getText().substring(t.range[0],r.range[1])}if("VariableDeclaration"===n.type&&"ObjectPattern"===n.declarations[0].id.type){const t=n.declarations[0].id.typeAnnotation,r=e.getText(n.declarations[0].id);return t?r.substr(0,t.range[0]-n.declarations[0].id.range[0]):r}return null})();if(null==i)return r;let o=[],a=null;"ImportDeclaration"===n.type?o=n.specifiers.map((t=>"ImportDefaultSpecifier"===t.type||"ImportNamespaceSpecifier"===t.type?null:{leadingComments:e.getCommentsBefore(t),name:t.imported.name,node:t})).filter(Boolean):"VariableDeclaration"===n.type&&"ObjectPattern"===n.declarations[0].id.type&&(o=n.declarations[0].id.properties.map((t=>{if("ExperimentalRestProperty"===t.type||"RestElement"===t.type)return a=t,null;const n=t.key,r="Literal"===n.type?String(n.value):"Identifier"===n.type?t.computed?null:n.name:null;return null==r?null:{leadingComments:e.getCommentsBefore(t),name:r,node:t}})).filter(Boolean));if(o.length<=1)return r;const l=i.indexOf("\n")>=0,s=o.sort(((e,t)=>H(e.name,t.name))).map((e=>e.node));null!=a&&s.push(a);const u=s.map((t=>{const n=e.getCommentsBefore(t).map((t=>e.getText(t))),r=n.length?n.join(""):"";return l?(r?" "+r+"\n":"")+" "+e.getText(t):(r?r+" ":"")+e.getText(t)})),c=(()=>{const t=[];if("ImportDeclaration"===n.type){if(t.push(...e.getCommentsBefore(n.source)),l&&null!=n.specifiers.find((e=>"ImportSpecifier"===e.type))){const r=e.getTokenBefore(n.source,(e=>"Punctuator"===e.type&&"}"===e.value));null!=r&&t.push(...e.getCommentsBefore(r))}}else if("VariableDeclaration"===n.type&&"ObjectPattern"===n.declarations[0].id.type){const r=e.getLastToken(n.declarations[0].id);null!=r&&t.push(...e.getCommentsBefore(r))}return t})(),p=l&&c.length?c.map((t=>" "+e.getText(t))).join("\n")+"\n":"",f=l?"\n"+u.map((e=>e.includes("...")?`${e}\n`:`${e},\n`)).join("")+p:u.join(", ");return r.replace(i,(()=>`{${f}}`))}(t,e);return n+i+r})).join("\n"))).join("\n\n"),m=o;if(null==m||null==a)return;const y=m.leadingComments.length?m.leadingComments[0].range[0]:m.node.range[0],h=a.inlineComments.length>0?a.inlineComments[a.inlineComments.length-1].range[1]:a.node.range[1];t.getText(m.node,m.node.range[0]-y,h-m.node.range[1])!==p&&e.report({node:m.node,messageId:"incorrectOrder",fix(e){const n=i.filter((({node:e})=>e.range[0]>=y&&e.range[1]<=h)).map((e=>{const[n,r]=u(e),i=t.getText(e.node);return{range:e.node.range,text:n+i+r}})).flat().concat(t.getAllComments().filter((e=>e.range[0]>=y&&e.range[1]<=h&&!l.has(e))).map((e=>({range:e.range,text:t.getText(e)})))).sort(((e,t)=>e.range[0]-t.range[0]||e.range[1]-t.range[1])).map((({text:e})=>e)).join("\n");return e.replaceTextRange([y,h],[p,n].filter(Boolean).join("\n\n"))}})}};function s(e){if(0===e.specifiers.length)return Y;const t=(()=>{switch(e.importKind){default:case"value":return Z;case"type":return ee;case"typeof":return te}})();return e.specifiers.find((e=>"ImportDefaultSpecifier"===e.type))?t.default:e.specifiers.find((e=>"ImportNamespaceSpecifier"===e.type))?t.namespace:t.named}function u(e){if("ExpressionStatement"===e.type)return ne;switch(e.declarations[0].id.type){case"Identifier":return re;case"ObjectPattern":return ie;case"ArrayPattern":return oe}return ae}function c(e,t,n){const r=H(e.moduleName,t.moduleName);if(0!==r)return r;const i=n(e.node)-n(t.node);return 0!==i?i:e.node.loc.start.line-t.node.loc.start.line}function p(e,t){if(null==e)throw new Error("Missing required module name");return null!=t?`${e}_${t}`:e}function f(e,n,i){const o=z.getRequireModuleName(e);if(z.isRequire(e)){const e=p(o,i);g("requireCond"===e||"requireDeferred"===e||"requireDeferredForDisplay"===e?r.requiresUsedByOtherRequires:r.require,n,p(o,i))}else if(z.isRequireDeferred(e)||z.isRequireDeferredForDisplay(e))g(r.require,n,p(o,i));else{if(z.isRequireCond(e)){if("VariableDeclaration"===n.type){const e=t.getText(n.declarations[0].id);return void g(r.require,n,p(e,i))}}else{if("MemberExpression"===e.type)return void f(e.object,n,p(t.getText(e.property),i));if("CallExpression"===e.type)return void f(e.callee,n,i)}d(n)}}function m(e){e.leadingComments.forEach((e=>l.add(e))),e.inlineComments.forEach((e=>l.add(e)));t.getCommentsInside(e.node).forEach((e=>l.add(e)))}function g(e,r,i,l=!1){const s={inlineComments:G(t,r),leadingComments:Q(t,r,n),moduleName:i,node:r};if(l)e.uppercase.push(s);else{const t=i[0]||"";t.toLowerCase()===t?e.lowercase.push(s):e.uppercase.push(s)}null==o&&(o=s),a=s,m(s)}function d(e){const r={inlineComments:G(t,e),leadingComments:Q(t,e,n),node:e};i.push(r),m(r)}}};const se=le;var ue={...se,create:e=>e.getSourceCode().getText().includes("@generated SignedSource<<")?{}:se.create(e)};module.exports=ue; From 86852f8cb5049f94d60e8a5423daccf0fb23cf84 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 31 Jan 2023 02:54:19 -0800 Subject: [PATCH 14/65] Fix displayed name when codegen dependency is not found (#36013) Summary: The fixed error guides users toward the right missing dependency. The original error pointed to the old name. ## Changelog [INTERNAL] [FIXED] - Display correct codegen dependency name when not found Pull Request resolved: https://github.com/facebook/react-native/pull/36013 Test Plan: This is an error message wording change. Reviewed By: jacdebug Differential Revision: D42881807 Pulled By: cipolleschi fbshipit-source-id: d96fb867cfe27ae922d398ab981f5797cb51a269 --- scripts/codegen/generate-artifacts-executor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/codegen/generate-artifacts-executor.js b/scripts/codegen/generate-artifacts-executor.js index 757ce492c5ae14..38a2b8f22493c7 100644 --- a/scripts/codegen/generate-artifacts-executor.js +++ b/scripts/codegen/generate-artifacts-executor.js @@ -22,9 +22,9 @@ const os = require('os'); const path = require('path'); const RN_ROOT = path.join(__dirname, '../..'); - +const CODEGEN_DEPENDENCY_NAME = '@react-native/codegen'; const CODEGEN_REPO_PATH = `${RN_ROOT}/packages/react-native-codegen`; -const CODEGEN_NPM_PATH = `${RN_ROOT}/../@react-native/codegen`; +const CODEGEN_NPM_PATH = `${RN_ROOT}/../${CODEGEN_DEPENDENCY_NAME}`; const CORE_LIBRARIES = new Set(['rncore', 'FBReactNativeSpec']); const REACT_NATIVE_DEPENDENCY_NAME = 'react-native'; @@ -293,7 +293,7 @@ function getCodeGenCliPath() { } else if (fs.existsSync(CODEGEN_NPM_PATH)) { codegenCliPath = CODEGEN_NPM_PATH; } else { - throw "error: Could not determine react-native-codegen location. Try running 'yarn install' or 'npm install' in your project root."; + throw `error: Could not determine ${CODEGEN_DEPENDENCY_NAME} location. Try running 'yarn install' or 'npm install' in your project root.`; } return codegenCliPath; } From 9e4b4ef2d5073c0359b7854b264fec68db78b188 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Tue, 31 Jan 2023 08:00:08 -0800 Subject: [PATCH 15/65] Remove the `react-native-bot` context from CircleCI Summary: While working on T143721371 I've noticed that we still have an exposed context for `react-native-bot` which gives access to `PAT_TOKEN` and `PAT_USERNAME`. As those two variables are unused, we can fully clean this us. Changelog: [Internal] [Changed] - Remove the `react-native-bot` context from CircleCI Reviewed By: cipolleschi Differential Revision: D42886196 fbshipit-source-id: 4eba7a53557fe7af7d87650052630eea2d2d3934 --- .circleci/config.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 14cc5daaabc2b4..c86f1ce8fafa9d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1454,9 +1454,7 @@ jobs: - download_gradle_dependencies # START: Stables and nightlies - # This conditional step sets up the necessary credentials for publishing react-native to npm, - # and for interacting with GitHub as the react-native-bot account. Important: these steps - # should not be allowed to run on commits from pull requests. + # This conditional step sets up the necessary credentials for publishing react-native to npm. - when: condition: or: @@ -1840,7 +1838,6 @@ workflows: # This job will trigger when a version tag is pushed (by package_release) - build_npm_package: name: build_and_publish_npm_package - context: react-native-bot release_type: "release" filters: *only_release_tags requires: @@ -1876,7 +1873,6 @@ workflows: - prepare_hermes_workspace - build_npm_package: name: build_and_publish_npm_package - context: react-native-bot release_type: "dry-run" requires: - build_hermesc_linux From b086e5dc0aaaf7f98d23e32d79da039f7b9b6087 Mon Sep 17 00:00:00 2001 From: Dmitry Rykun Date: Tue, 31 Jan 2023 08:01:08 -0800 Subject: [PATCH 16/65] chore: Add changelog for 0.68.6 and 0.69.8 (#36010) Summary: Create changelog for 0.69.8 ## Changelog [Internal] [Changed] - add changelog entry for 0.69.8 Pull Request resolved: https://github.com/facebook/react-native/pull/36010 Test Plan: N/A Reviewed By: rshest Differential Revision: D42882254 Pulled By: dmytrorykun fbshipit-source-id: 2cf12ab1898d292bd525d9c357203c69b58b3247 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14b140c5945c56..d29e2d3891bb54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -744,6 +744,14 @@ Read the [announcement blogpost here](https://reactnative.dev/blog/2023/01/12/ve - Add GitHub token permissions for workflows ([3da3d82320](https://github.com/facebook/react-native/commit/3da3d82320bd035c6bd361a82ea12a70dba4e851) by [@varunsh-coder](https://github.com/varunsh-coder)) - Bump RCT-Folly to 2021-07-22 ([68f3a42fc7](https://github.com/facebook/react-native/commit/68f3a42fc7380051714253f43b42175de361f8bd) by [@luissantana](https://github.com/luissantana)) +## v0.69.8 + +### Fixed + +#### Android specific + +- Mitigation for Samsung TextInput Hangs ([be69c8b5a7](https://github.com/facebook/react-native/commit/be69c8b5a77ae60cced1b2af64e48b90d9955be5) by [@NickGerleman](https://github.com/NickGerleman)) + ## v0.69.7 ### Fixed @@ -1044,6 +1052,14 @@ Read the [announcement blogpost here](https://reactnative.dev/blog/2023/01/12/ve - Encode URL params in URLSearchParams.toString() ([1042a8012f](https://github.com/facebook/react-native/commit/1042a8012fb472bd5c882b469fe507dd6279d562) by [@sshic](https://github.com/sshic)) +## v0.68.6 + +### Fixed + +#### Android specific + +- Mitigation for Samsung TextInput Hangs ([be69c8b5a7](https://github.com/facebook/react-native/commit/be69c8b5a77ae60cced1b2af64e48b90d9955be5) by [@NickGerleman](https://github.com/NickGerleman)) + ## v0.68.5 ### Fixed From dea48e22489f7dc4289d738016cf7521e89c9d77 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 31 Jan 2023 09:17:55 -0800 Subject: [PATCH 17/65] React Native sync for revisions 17f6912...48b687f MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This sync includes the following changes: - **[48b687fc9](https://github.com/facebook/react/commit/48b687fc9 )**: [trusted types][www] Add enableTrustedTypesIntegration flag back in ([#26016](https://github.com/facebook/react/pull/26016)) //// - **[9b1423cc0](https://github.com/facebook/react/commit/9b1423cc0 )**: Revert "Hold host functions in var" ([#26079](https://github.com/facebook/react/pull/26079)) //// - **[ce09ace9a](https://github.com/facebook/react/commit/ce09ace9a )**: Improve Error Messages when Access Client References ([#26059](https://github.com/facebook/react/pull/26059)) //// - **[0652bdbd1](https://github.com/facebook/react/commit/0652bdbd1 )**: Add flow types to Maps in ReactNativeViewConfigRegistry.js ([#26064](https://github.com/facebook/react/pull/26064)) //// - **[ee8509801](https://github.com/facebook/react/commit/ee8509801 )**: [cleanup] remove deletedTreeCleanUpLevel feature flag ([#25529](https://github.com/facebook/react/pull/25529)) //// - **[0e31dd028](https://github.com/facebook/react/commit/0e31dd028 )**: Remove findDOMNode www shim ([#25998](https://github.com/facebook/react/pull/25998)) //// - **[379dd741e](https://github.com/facebook/react/commit/379dd741e )**: [www] set enableTrustedTypesIntegration to false ([#25997](https://github.com/facebook/react/pull/25997)) //// - **[555ece0cd](https://github.com/facebook/react/commit/555ece0cd )**: Don't warn about concurrently rendering contexts if we finished rendering ([#22797](https://github.com/facebook/react/pull/22797)) //// - **[0fce6bb49](https://github.com/facebook/react/commit/0fce6bb49 )**: [cleanup] remove feature flags warnAboutDefaultPropsOnFunctionComponents and warnAboutStringRefs ([#25980](https://github.com/facebook/react/pull/25980)) //// - **[7002a6743](https://github.com/facebook/react/commit/7002a6743 )**: [cleanup] remove unused values from ReactFeatureFlags.www-dynamic ([#25575](https://github.com/facebook/react/pull/25575)) //// - **[a48e54f2b](https://github.com/facebook/react/commit/a48e54f2b )**: [cleanup] remove old feature flag warnAboutDeprecatedLifecycles ([#25978](https://github.com/facebook/react/pull/25978)) //// - **[0f4a83596](https://github.com/facebook/react/commit/0f4a83596 )**: Remove duplicate JSResourceReferenceImpl mock ([#25976](https://github.com/facebook/react/pull/25976)) //// - **[c49131669](https://github.com/facebook/react/commit/c49131669 )**: Remove unused Flow suppressions ([#25977](https://github.com/facebook/react/pull/25977)) //// - **[afe6521e1](https://github.com/facebook/react/commit/afe6521e1 )**: Refactor: remove useless parameter ([#25923](https://github.com/facebook/react/pull/25923)) //// - **[34464fb16](https://github.com/facebook/react/commit/34464fb16 )**: Upgrade to Flow 0.196.3 ([#25974](https://github.com/facebook/react/pull/25974)) //// - **[e2424f33b](https://github.com/facebook/react/commit/e2424f33b )**: [flow] enable exact_empty_objects ([#25973](https://github.com/facebook/react/pull/25973)) //// - **[0b4f44302](https://github.com/facebook/react/commit/0b4f44302 )**: [flow] enable enforce_local_inference_annotations ([#25921](https://github.com/facebook/react/pull/25921)) //// - **[0b974418c](https://github.com/facebook/react/commit/0b974418c )**: [Fizz] Fork Fizz instruction set for inline script and external runtime ([#25862](https://github.com/facebook/react/pull/25862)) //// - **[5379b6123](https://github.com/facebook/react/commit/5379b6123 )**: Batch sync, default and continuous lanes ([#25700](https://github.com/facebook/react/pull/25700)) //// - **[bbf4d2211](https://github.com/facebook/react/commit/bbf4d2211 )**: Update import for babel-code-frame in build script ([#25963](https://github.com/facebook/react/pull/25963)) //// - **[b83baf63f](https://github.com/facebook/react/commit/b83baf63f )**: Transform updates to support Flow this annotation syntax ([#25918](https://github.com/facebook/react/pull/25918)) //// - **[c2d655207](https://github.com/facebook/react/commit/c2d655207 )**: Unify `use` and `renderDidSuspendDelayIfPossible` implementations ([#25922](https://github.com/facebook/react/pull/25922)) //// - **[48274a43a](https://github.com/facebook/react/commit/48274a43a )**: Remove vestigial Suspense batching logic ([#25861](https://github.com/facebook/react/pull/25861)) //// - **[de7d1c907](https://github.com/facebook/react/commit/de7d1c907 )**: Add `fetchPriority` to `` and `` ([#25927](https://github.com/facebook/react/pull/25927)) //// - **[81d4ee9ca](https://github.com/facebook/react/commit/81d4ee9ca )**: reconciler docs: fix small typo - "mode" (instead of "node") ([#25863](https://github.com/facebook/react/pull/25863)) //// - **[5fcf1a4b4](https://github.com/facebook/react/commit/5fcf1a4b4 )**: Bugfix: Synchronous ping during render phase sometimes unwinds the stack, leading to crash ([#25851](https://github.com/facebook/react/pull/25851)) //// - **[2b1fb91a5](https://github.com/facebook/react/commit/2b1fb91a5 )**: ESLint upgrade to use hermes-eslint ([#25915](https://github.com/facebook/react/pull/25915)) //// - **[fabef7a6b](https://github.com/facebook/react/commit/fabef7a6b )**: Resubmit Add HydrationSyncLane ([#25878](https://github.com/facebook/react/pull/25878)) //// - **[7efa9e597](https://github.com/facebook/react/commit/7efa9e597 )**: Fix unwinding context during selective hydration ([#25876](https://github.com/facebook/react/pull/25876)) //// - **[84a0a171e](https://github.com/facebook/react/commit/84a0a171e )**: Rename experimental useEvent to useEffectEvent ([#25881](https://github.com/facebook/react/pull/25881)) //// - **[4dda96a40](https://github.com/facebook/react/commit/4dda96a40 )**: [react-www] remove forked bundle ([#25866](https://github.com/facebook/react/pull/25866)) //// - **[9c09c1cd6](https://github.com/facebook/react/commit/9c09c1cd6 )**: Revert "Fork ReactDOMSharedInternals for www ([#25791](https://github.com/facebook/react/pull/25791))" ([#25864](https://github.com/facebook/react/pull/25864)) //// - **[996e4c0d5](https://github.com/facebook/react/commit/996e4c0d5 )**: Offscreen add attach ([#25603](https://github.com/facebook/react/pull/25603)) //// - **[b14d7fa4b](https://github.com/facebook/react/commit/b14d7fa4b )**: Add support for setNativeProps to Fabric ([#25737](https://github.com/facebook/react/pull/25737)) //// - **[819687279](https://github.com/facebook/react/commit/819687279 )**: [Float] Fix typo in ReactDOMResourceValidation.js ([#25798](https://github.com/facebook/react/pull/25798)) //// - **[5dfc485f6](https://github.com/facebook/react/commit/5dfc485f6 )**: fix tests for when float is off ([#25839](https://github.com/facebook/react/pull/25839)) //// - **[bfcbf3306](https://github.com/facebook/react/commit/bfcbf3306 )**: toString children of title ([#25838](https://github.com/facebook/react/pull/25838)) //// - **[d4bc16a7d](https://github.com/facebook/react/commit/d4bc16a7d )**: Revert "[react-www] remove forked bundle" ([#25837](https://github.com/facebook/react/pull/25837)) //// - **[d69b2cf82](https://github.com/facebook/react/commit/d69b2cf82 )**: [bug fix] revert values in ReactFiberFlags to keep consistency for devtools ([#25832](https://github.com/facebook/react/pull/25832)) //// - **[645ae2686](https://github.com/facebook/react/commit/645ae2686 )**: [react-www] remove forked bundle ([#25831](https://github.com/facebook/react/pull/25831)) //// - **[d807eb52c](https://github.com/facebook/react/commit/d807eb52c )**: Revert recent hydration changes ([#25812](https://github.com/facebook/react/pull/25812)) //// - **[2ccfa657d](https://github.com/facebook/react/commit/2ccfa657d )**: Fork ReactDOMSharedInternals for www ([#25791](https://github.com/facebook/react/pull/25791)) //// - **[f0534ae94](https://github.com/facebook/react/commit/f0534ae94 )**: Avoid replaying SelectiveHydrationException in dev ([#25754](https://github.com/facebook/react/pull/25754)) //// - **[7fab379d8](https://github.com/facebook/react/commit/7fab379d8 )**: fix link to ReactDOMHostconfig in reconciler docs ([#25788](https://github.com/facebook/react/pull/25788)) //// - **[500c8aa08](https://github.com/facebook/react/commit/500c8aa08 )**: Add component name to StrictMode error message ([#25718](https://github.com/facebook/react/pull/25718)) //// - **[353c30252](https://github.com/facebook/react/commit/353c30252 )**: Hold host functions in var ([#25741](https://github.com/facebook/react/pull/25741)) //// Changelog: [General][Changed] - React Native sync for revisions 17f6912...48b687f jest_e2e[run_all_tests] Reviewed By: rubennorte Differential Revision: D42855483 fbshipit-source-id: c244a595bb2d490a23b333c1b16d04a459ec94fc --- Libraries/Renderer/REVISION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Renderer/REVISION b/Libraries/Renderer/REVISION index 86ed1c8121de3c..2ef55796759cfe 100644 --- a/Libraries/Renderer/REVISION +++ b/Libraries/Renderer/REVISION @@ -1 +1 @@ -17f6912a44e2ce7b88a72488931428da58f19c56 \ No newline at end of file +48b687fc95a172cec8f305312a27d105e5719581 \ No newline at end of file From 597a1ff60b3e1844b4794fb4acd40fa073f2e93b Mon Sep 17 00:00:00 2001 From: gabrieldonadel Date: Tue, 31 Jan 2023 10:58:24 -0800 Subject: [PATCH 18/65] feat: Add logical border block color properties (#35999) Summary: This PR implements logical border block color properties as requested on https://github.com/facebook/react-native/issues/34425. This implementation includes the addition of the following style properties - `borderBlockColor`, equivalent to `borderTopColor` and `borderBottomColor`. - `borderBlockEndColor`, equivalent to `borderBottomColor`. - `borderBlockStartColor`, equivalent to `borderTopColor`. ## Changelog [GENERAL] [ADDED] - Add logical border block color properties Pull Request resolved: https://github.com/facebook/react-native/pull/35999 Test Plan: 1. Open the RNTester app and navigate to the `View` page 2. Test the new style properties through the `Logical Border Color` section
Android iOS
Reviewed By: cipolleschi Differential Revision: D42849911 Pulled By: jacdebug fbshipit-source-id: 822cff5264689c42031d496105537032b5cd31ef --- .../View/ReactNativeStyleAttributes.js | 3 ++ .../Components/View/ViewNativeComponent.js | 9 +++++ Libraries/StyleSheet/StyleSheetTypes.d.ts | 3 ++ Libraries/StyleSheet/StyleSheetTypes.js | 3 ++ React/Views/RCTView.h | 7 ++++ React/Views/RCTView.m | 13 ++++++- React/Views/RCTViewManager.m | 3 ++ .../com/facebook/react/uimanager/Spacing.java | 24 ++++++++++-- .../facebook/react/uimanager/ViewProps.java | 12 ++++++ .../views/view/ReactMapBufferPropSetter.kt | 6 +++ .../view/ReactViewBackgroundDrawable.java | 38 +++++++++++++++++- .../react/views/view/ReactViewManager.java | 8 +++- .../renderer/components/view/primitives.h | 21 ++++++++-- .../components/view/propsConversions.h | 24 ++++++++++++ .../components/view/viewPropConversions.h | 9 ++++- ReactCommon/react/renderer/core/PropsMacros.h | 8 +++- .../rn-tester/js/examples/View/ViewExample.js | 39 +++++++++++++++++++ 17 files changed, 218 insertions(+), 12 deletions(-) diff --git a/Libraries/Components/View/ReactNativeStyleAttributes.js b/Libraries/Components/View/ReactNativeStyleAttributes.js index 7408b96a461432..13d8db56a5fe67 100644 --- a/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -117,6 +117,9 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { */ backfaceVisibility: true, backgroundColor: colorAttributes, + borderBlockColor: colorAttributes, + borderBlockEndColor: colorAttributes, + borderBlockStartColor: colorAttributes, borderBottomColor: colorAttributes, borderBottomEndRadius: true, borderBottomLeftRadius: true, diff --git a/Libraries/Components/View/ViewNativeComponent.js b/Libraries/Components/View/ViewNativeComponent.js index b60bc0a8d1efec..d8052fb9889b02 100644 --- a/Libraries/Components/View/ViewNativeComponent.js +++ b/Libraries/Components/View/ViewNativeComponent.js @@ -85,6 +85,15 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = borderEndColor: { process: require('../../StyleSheet/processColor').default, }, + borderBlockColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderBlockEndColor: { + process: require('../../StyleSheet/processColor').default, + }, + borderBlockStartColor: { + process: require('../../StyleSheet/processColor').default, + }, focusable: true, overflow: true, diff --git a/Libraries/StyleSheet/StyleSheetTypes.d.ts b/Libraries/StyleSheet/StyleSheetTypes.d.ts index f8ea56ae7989ff..f31224e7a62a0c 100644 --- a/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -232,6 +232,9 @@ export interface TransformsStyle { export interface ViewStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle { backfaceVisibility?: 'visible' | 'hidden' | undefined; backgroundColor?: ColorValue | undefined; + borderBlockColor?: ColorValue | undefined; + borderBlockEndColor?: ColorValue | undefined; + borderBlockStartColor?: ColorValue | undefined; borderBottomColor?: ColorValue | undefined; borderBottomEndRadius?: number | undefined; borderBottomLeftRadius?: number | undefined; diff --git a/Libraries/StyleSheet/StyleSheetTypes.js b/Libraries/StyleSheet/StyleSheetTypes.js index 44015f2b02b160..4fc70eed012233 100644 --- a/Libraries/StyleSheet/StyleSheetTypes.js +++ b/Libraries/StyleSheet/StyleSheetTypes.js @@ -698,6 +698,9 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{ borderRightColor?: ____ColorValue_Internal, borderStartColor?: ____ColorValue_Internal, borderTopColor?: ____ColorValue_Internal, + borderBlockColor?: ____ColorValue_Internal, + borderBlockEndColor?: ____ColorValue_Internal, + borderBlockStartColor?: ____ColorValue_Internal, borderRadius?: number | AnimatedNode, borderBottomEndRadius?: number | AnimatedNode, borderBottomLeftRadius?: number | AnimatedNode, diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index adda5c1f016848..abb1966a199f0c 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -86,6 +86,9 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @property (nonatomic, strong) UIColor *borderStartColor; @property (nonatomic, strong) UIColor *borderEndColor; @property (nonatomic, strong) UIColor *borderColor; +@property (nonatomic, strong) UIColor *borderBlockColor; +@property (nonatomic, strong) UIColor *borderBlockEndColor; +@property (nonatomic, strong) UIColor *borderBlockStartColor; /** * Border widths. @@ -97,6 +100,10 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @property (nonatomic, assign) CGFloat borderStartWidth; @property (nonatomic, assign) CGFloat borderEndWidth; @property (nonatomic, assign) CGFloat borderWidth; +// TODO: Implement logical border width logic +@property (nonatomic, assign) CGFloat borderBlockWidth; +@property (nonatomic, assign) CGFloat borderBlockEndWidth; +@property (nonatomic, assign) CGFloat borderBlockStartWidth; /** * Border curve. diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index f9a38f31040f1b..6d7583395e3415 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -745,6 +745,17 @@ - (RCTBorderColors)borderColorsWithTraitCollection:(UITraitCollection *)traitCol UIColor *borderTopColor = _borderTopColor; UIColor *borderBottomColor = _borderBottomColor; + if (_borderBlockColor) { + borderTopColor = _borderBlockColor; + borderBottomColor = _borderBlockColor; + } + if (_borderBlockEndColor) { + borderBottomColor = _borderBlockEndColor; + } + if (_borderBlockStartColor) { + borderTopColor = _borderBlockStartColor; + } + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, *)) { borderColor = [borderColor resolvedColorWithTraitCollection:self.traitCollection]; @@ -924,7 +935,7 @@ -(void)setBorder##side##Color : (UIColor *)color \ } setBorderColor() setBorderColor(Top) setBorderColor(Right) setBorderColor(Bottom) setBorderColor(Left) - setBorderColor(Start) setBorderColor(End) + setBorderColor(Start) setBorderColor(End) setBorderColor(Block) setBorderColor(BlockEnd) setBorderColor(BlockStart) #pragma mark - Border Width diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index e50b4c19d91a50..a866b6f748987d 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -358,6 +358,9 @@ - (RCTShadowView *)shadowView RCT_VIEW_BORDER_PROPERTY(Left) RCT_VIEW_BORDER_PROPERTY(Start) RCT_VIEW_BORDER_PROPERTY(End) +RCT_VIEW_BORDER_PROPERTY(Block) +RCT_VIEW_BORDER_PROPERTY(BlockEnd) +RCT_VIEW_BORDER_PROPERTY(BlockStart) #define RCT_VIEW_BORDER_RADIUS_PROPERTY(SIDE) \ RCT_CUSTOM_VIEW_PROPERTY(border##SIDE##Radius, CGFloat, RCTView) \ diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/Spacing.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/Spacing.java index 36aeb9776da067..9ff37664d52b67 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/Spacing.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/Spacing.java @@ -47,10 +47,18 @@ public class Spacing { * Spacing type that represents all directions (left, top, right, bottom). E.g. {@code margin}. */ public static final int ALL = 8; + /** Spacing type that represents block directions (top, bottom). E.g. {@code marginBlock}. */ + public static final int BLOCK = 9; + /** Spacing type that represents the block end direction (bottom). E.g. {@code marginBlockEnd}. */ + public static final int BLOCK_END = 10; + /** + * Spacing type that represents the block start direction (top). E.g. {@code marginBlockStart}. + */ + public static final int BLOCK_START = 11; private static final int[] sFlagsMap = { 1, /*LEFT*/ 2, /*TOP*/ 4, /*RIGHT*/ 8, /*BOTTOM*/ 16, /*START*/ 32, /*END*/ 64, /*HORIZONTAL*/ - 128, /*VERTICAL*/ 256, /*ALL*/ + 128, /*VERTICAL*/ 256, /*ALL*/ 512, /*BLOCK*/ 1024, /*BLOCK_END*/ 2048, /*BLOCK_START*/ }; private final float[] mSpacing; @@ -96,7 +104,8 @@ public boolean set(int spacingType, float value) { mHasAliasesSet = (mValueFlags & sFlagsMap[ALL]) != 0 || (mValueFlags & sFlagsMap[VERTICAL]) != 0 - || (mValueFlags & sFlagsMap[HORIZONTAL]) != 0; + || (mValueFlags & sFlagsMap[HORIZONTAL]) != 0 + || (mValueFlags & sFlagsMap[BLOCK]) != 0; return true; } @@ -111,7 +120,13 @@ public boolean set(int spacingType, float value) { */ public float get(int spacingType) { float defaultValue = - (spacingType == START || spacingType == END ? YogaConstants.UNDEFINED : mDefaultValue); + (spacingType == START + || spacingType == END + || spacingType == BLOCK + || spacingType == BLOCK_END + || spacingType == BLOCK_START + ? YogaConstants.UNDEFINED + : mDefaultValue); if (mValueFlags == 0) { return defaultValue; @@ -174,6 +189,9 @@ private static float[] newFullSpacingArray() { YogaConstants.UNDEFINED, YogaConstants.UNDEFINED, YogaConstants.UNDEFINED, + YogaConstants.UNDEFINED, + YogaConstants.UNDEFINED, + YogaConstants.UNDEFINED, }; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java index 53cc9783421917..3f76fa7dd3d9b1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -135,6 +135,9 @@ public class ViewProps { public static final String BORDER_RIGHT_COLOR = "borderRightColor"; public static final String BORDER_TOP_COLOR = "borderTopColor"; public static final String BORDER_BOTTOM_COLOR = "borderBottomColor"; + public static final String BORDER_BLOCK_COLOR = "borderBlockColor"; + public static final String BORDER_BLOCK_END_COLOR = "borderBlockEndColor"; + public static final String BORDER_BLOCK_START_COLOR = "borderBlockStartColor"; public static final String BORDER_TOP_START_RADIUS = "borderTopStartRadius"; public static final String BORDER_TOP_END_RADIUS = "borderTopEndRadius"; public static final String BORDER_BOTTOM_START_RADIUS = "borderBottomStartRadius"; @@ -299,6 +302,15 @@ public static boolean isLayoutOnly(ReadableMap map, String prop) { case BORDER_BOTTOM_COLOR: return map.getType(BORDER_BOTTOM_COLOR) == ReadableType.Number && map.getInt(BORDER_BOTTOM_COLOR) == Color.TRANSPARENT; + case BORDER_BLOCK_COLOR: + return map.getType(BORDER_BLOCK_COLOR) == ReadableType.Number + && map.getInt(BORDER_BLOCK_COLOR) == Color.TRANSPARENT; + case BORDER_BLOCK_END_COLOR: + return map.getType(BORDER_BLOCK_END_COLOR) == ReadableType.Number + && map.getInt(BORDER_BLOCK_END_COLOR) == Color.TRANSPARENT; + case BORDER_BLOCK_START_COLOR: + return map.getType(BORDER_BLOCK_START_COLOR) == ReadableType.Number + && map.getInt(BORDER_BLOCK_START_COLOR) == Color.TRANSPARENT; case BORDER_WIDTH: return map.isNull(BORDER_WIDTH) || map.getDouble(BORDER_WIDTH) == 0d; case BORDER_LEFT_WIDTH: diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt index 100cf1c36d3d0e..a7effcdab1752e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt @@ -87,6 +87,9 @@ object ReactMapBufferPropSetter { private const val EDGE_START = 4 private const val EDGE_END = 5 private const val EDGE_ALL = 6 + private const val EDGE_BLOCK = 7 + private const val EDGE_BLOCK_END = 8 + private const val EDGE_BLOCK_START = 9 private const val CORNER_TOP_LEFT = 0 private const val CORNER_TOP_RIGHT = 1 @@ -349,6 +352,9 @@ object ReactMapBufferPropSetter { EDGE_BOTTOM -> 4 EDGE_START -> 5 EDGE_END -> 6 + EDGE_BLOCK -> 7 + EDGE_BLOCK_END -> 8 + EDGE_BLOCK_START -> 9 else -> throw IllegalArgumentException("Unknown key for border color: $key") } val colorValue = entry.intValue diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java index 937abcc20bb392..eded03ee94dfb0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java @@ -359,6 +359,21 @@ private void drawRoundedBackgroundWithBorders(Canvas canvas) { int colorRight = getBorderColor(Spacing.RIGHT); int colorBottom = getBorderColor(Spacing.BOTTOM); + int colorBlock = getBorderColor(Spacing.BLOCK); + int colorBlockStart = getBorderColor(Spacing.BLOCK_START); + int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); + + if (isBorderColorDefined(Spacing.BLOCK)) { + colorBottom = colorBlock; + colorTop = colorBlock; + } + if (isBorderColorDefined(Spacing.BLOCK_END)) { + colorBottom = colorBlockEnd; + } + if (isBorderColorDefined(Spacing.BLOCK_START)) { + colorTop = colorBlockStart; + } + if (borderWidth.top > 0 || borderWidth.bottom > 0 || borderWidth.left > 0 @@ -552,13 +567,19 @@ private void updatePath() { int colorRight = getBorderColor(Spacing.RIGHT); int colorBottom = getBorderColor(Spacing.BOTTOM); int borderColor = getBorderColor(Spacing.ALL); + int colorBlock = getBorderColor(Spacing.BLOCK); + int colorBlockStart = getBorderColor(Spacing.BLOCK_START); + int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); // Clip border ONLY if its color is non transparent if (Color.alpha(colorLeft) != 0 && Color.alpha(colorTop) != 0 && Color.alpha(colorRight) != 0 && Color.alpha(colorBottom) != 0 - && Color.alpha(borderColor) != 0) { + && Color.alpha(borderColor) != 0 + && Color.alpha(colorBlock) != 0 + && Color.alpha(colorBlockStart) != 0 + && Color.alpha(colorBlockEnd) != 0) { mInnerClipTempRectForBorderRadius.top += borderWidth.top; mInnerClipTempRectForBorderRadius.bottom -= borderWidth.bottom; @@ -1128,6 +1149,21 @@ private void drawRectangularBackgroundWithBorders(Canvas canvas) { int colorRight = getBorderColor(Spacing.RIGHT); int colorBottom = getBorderColor(Spacing.BOTTOM); + int colorBlock = getBorderColor(Spacing.BLOCK); + int colorBlockStart = getBorderColor(Spacing.BLOCK_START); + int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); + + if (isBorderColorDefined(Spacing.BLOCK)) { + colorBottom = colorBlock; + colorTop = colorBlock; + } + if (isBorderColorDefined(Spacing.BLOCK_END)) { + colorBottom = colorBlockEnd; + } + if (isBorderColorDefined(Spacing.BLOCK_START)) { + colorTop = colorBlockStart; + } + final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; int colorStart = getBorderColor(Spacing.START); int colorEnd = getBorderColor(Spacing.END); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index 91cbdf7c3b741a..4bd81d72153b0b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -47,6 +47,9 @@ public class ReactViewManager extends ReactClippingViewManager { Spacing.BOTTOM, Spacing.START, Spacing.END, + Spacing.BLOCK, + Spacing.BLOCK_END, + Spacing.BLOCK_START }; private static final int CMD_HOTSPOT_UPDATE = 1; private static final int CMD_SET_PRESSED = 2; @@ -238,7 +241,10 @@ public void setBorderWidth(ReactViewGroup view, int index, float width) { ViewProps.BORDER_TOP_COLOR, ViewProps.BORDER_BOTTOM_COLOR, ViewProps.BORDER_START_COLOR, - ViewProps.BORDER_END_COLOR + ViewProps.BORDER_END_COLOR, + ViewProps.BORDER_BLOCK_COLOR, + ViewProps.BORDER_BLOCK_END_COLOR, + ViewProps.BORDER_BLOCK_START_COLOR }, customType = "Color") public void setBorderColor(ReactViewGroup view, int index, Integer color) { diff --git a/ReactCommon/react/renderer/components/view/primitives.h b/ReactCommon/react/renderer/components/view/primitives.h index 34ea20376f1df9..ec8a276d307db3 100644 --- a/ReactCommon/react/renderer/components/view/primitives.h +++ b/ReactCommon/react/renderer/components/view/primitives.h @@ -99,6 +99,9 @@ struct CascadedRectangleEdges { OptionalT horizontal{}; OptionalT vertical{}; OptionalT all{}; + OptionalT block{}; + OptionalT blockStart{}; + OptionalT blockEnd{}; Counterpart resolve(bool isRTL, T defaults) const { const auto leadingEdge = isRTL ? end : start; @@ -111,10 +114,14 @@ struct CascadedRectangleEdges { return { /* .left = */ left.value_or(leadingEdge.value_or(horizontalOrAllOrDefault)), - /* .top = */ top.value_or(verticalOrAllOrDefault), + /* .top = */ + blockStart.value_or( + block.value_or(top.value_or(verticalOrAllOrDefault))), /* .right = */ right.value_or(trailingEdge.value_or(horizontalOrAllOrDefault)), - /* .bottom = */ bottom.value_or(verticalOrAllOrDefault), + /* .bottom = */ + blockEnd.value_or( + block.value_or(bottom.value_or(verticalOrAllOrDefault))), }; } @@ -128,7 +135,10 @@ struct CascadedRectangleEdges { this->end, this->horizontal, this->vertical, - this->all) == + this->all, + this->block, + this->blockStart, + this->blockEnd) == std::tie( rhs.left, rhs.top, @@ -138,7 +148,10 @@ struct CascadedRectangleEdges { rhs.end, rhs.horizontal, rhs.vertical, - rhs.all); + rhs.all, + rhs.block, + rhs.blockStart, + rhs.blockEnd); } bool operator!=(const CascadedRectangleEdges &rhs) const { diff --git a/ReactCommon/react/renderer/components/view/propsConversions.h b/ReactCommon/react/renderer/components/view/propsConversions.h index 97fa6b6d7eeb02..ad7cc2f3a3488c 100644 --- a/ReactCommon/react/renderer/components/view/propsConversions.h +++ b/ReactCommon/react/renderer/components/view/propsConversions.h @@ -515,6 +515,30 @@ static inline CascadedRectangleEdges convertRawProp( defaultValue.vertical, prefix, suffix); + result.block = convertRawProp( + context, + rawProps, + "Block", + sourceValue.block, + defaultValue.block, + prefix, + suffix); + result.blockEnd = convertRawProp( + context, + rawProps, + "BlockEnd", + sourceValue.blockEnd, + defaultValue.blockEnd, + prefix, + suffix); + result.blockStart = convertRawProp( + context, + rawProps, + "BlockStart", + sourceValue.blockStart, + defaultValue.blockStart, + prefix, + suffix); result.all = convertRawProp( context, rawProps, "", sourceValue.all, defaultValue.all, prefix, suffix); diff --git a/ReactCommon/react/renderer/components/view/viewPropConversions.h b/ReactCommon/react/renderer/components/view/viewPropConversions.h index fe253277752cb4..64a0469a9f7c8b 100644 --- a/ReactCommon/react/renderer/components/view/viewPropConversions.h +++ b/ReactCommon/react/renderer/components/view/viewPropConversions.h @@ -28,6 +28,9 @@ constexpr MapBuffer::Key EDGE_BOTTOM = 3; constexpr MapBuffer::Key EDGE_START = 4; constexpr MapBuffer::Key EDGE_END = 5; constexpr MapBuffer::Key EDGE_ALL = 6; +constexpr MapBuffer::Key EDGE_BLOCK = 7; +constexpr MapBuffer::Key EDGE_BLOCK_START = 8; +constexpr MapBuffer::Key EDGE_BLOCK_END = 9; constexpr MapBuffer::Key CORNER_TOP_LEFT = 0; constexpr MapBuffer::Key CORNER_TOP_RIGHT = 1; @@ -107,13 +110,17 @@ inline MapBuffer convertBorderColors(CascadedBorderColors const &colors) { template MapBuffer convertCascadedEdges(CascadedRectangleEdges const &edges) { - MapBufferBuilder builder(7); + MapBufferBuilder builder(10); putOptionalFloat(builder, EDGE_TOP, optionalFromValue(edges.top)); putOptionalFloat(builder, EDGE_RIGHT, optionalFromValue(edges.right)); putOptionalFloat(builder, EDGE_BOTTOM, optionalFromValue(edges.bottom)); putOptionalFloat(builder, EDGE_LEFT, optionalFromValue(edges.left)); putOptionalFloat(builder, EDGE_START, optionalFromValue(edges.start)); putOptionalFloat(builder, EDGE_END, optionalFromValue(edges.end)); + putOptionalFloat(builder, EDGE_BLOCK, optionalFromValue(edges.block)); + putOptionalFloat(builder, EDGE_BLOCK_END, optionalFromValue(edges.blockEnd)); + putOptionalFloat( + builder, EDGE_BLOCK_START, optionalFromValue(edges.blockStart)); putOptionalFloat(builder, EDGE_ALL, optionalFromValue(edges.all)); return builder.build(); } diff --git a/ReactCommon/react/renderer/core/PropsMacros.h b/ReactCommon/react/renderer/core/PropsMacros.h index 2aa75fd4e4216e..6fa0bd9a90ca48 100644 --- a/ReactCommon/react/renderer/core/PropsMacros.h +++ b/ReactCommon/react/renderer/core/PropsMacros.h @@ -99,7 +99,13 @@ CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \ struct, vertical, prefix "Vertical" suffix, rawValue) \ CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \ - struct, all, prefix "" suffix, rawValue) + struct, all, prefix "" suffix, rawValue) \ + CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \ + struct, block, prefix "Block" suffix, rawValue) \ + CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \ + struct, blockEnd, prefix "BlockEnd" suffix, rawValue) \ + CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \ + struct, blockStart, prefix "BlockStart" suffix, rawValue) // Rebuild a type that contains multiple fields from a single field value #define REBUILD_FIELD_SWITCH_CASE( \ diff --git a/packages/rn-tester/js/examples/View/ViewExample.js b/packages/rn-tester/js/examples/View/ViewExample.js index de2e04acc1aec4..8602101cf521bd 100644 --- a/packages/rn-tester/js/examples/View/ViewExample.js +++ b/packages/rn-tester/js/examples/View/ViewExample.js @@ -738,4 +738,43 @@ exports.examples = [ ); }, }, + { + title: 'Logical Border Color', + render(): React.Node { + return ( + + + + borderBlockColor orange + + + + + borderBlockStartColor purple + borderBlockEndColor green + + + + ); + }, + }, ]; From bf34810c5c188cd1c42e2ac0c52d08790209bc1e Mon Sep 17 00:00:00 2001 From: "Zihan Chen (MSFT)" <53799235+ZihanChen-MSFT@users.noreply.github.com> Date: Tue, 31 Jan 2023 12:05:46 -0800 Subject: [PATCH 19/65] Turbo module codegen support interface with inheritance in module (#36011) Summary: The [previous pull request](https://github.com/facebook/react-native/pull/35812) enables defining interfaces and using them in a turbo module, but all members are flattened, this is not the best option for codegen for object oriented languages. In this pull request, an optional member `baseTypes` is added to aliased objects. Members are still flattened for backward compatibility, if a codegen doesn't care about that nothing needs to be changed. ### Example ```typescript interface A { a : string; } interface B extends A { b : number; } ``` #### Before the previous pull request `interface` is not allowed here, you must use type alias. #### At the previous pull request `interface` is allowed, but it is translated to ```typescript type A = {a : string}; type B = {a : string, b : number}; ``` #### At this pull request Extra information is provided so that you know `B` extends `A`. By comparing `B` to `A` it is easy to know that `B::a` is obtained from `A`. ## Changelog [GENERAL] [CHANGED] - Turbo module codegen support interface with inheritance in module Pull Request resolved: https://github.com/facebook/react-native/pull/36011 Test Plan: `yarn jest react-native-codegen` passed Reviewed By: rshest Differential Revision: D42882650 Pulled By: cipolleschi fbshipit-source-id: 32d502e8a99c4151fae0a1f4dfa60db9ac827963 --- .../react-native-codegen/src/CodegenSchema.js | 3 ++ ...script-module-parser-snapshot-test.js.snap | 54 +++++++++++++++++++ .../src/parsers/typescript/modules/index.js | 28 +++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/packages/react-native-codegen/src/CodegenSchema.js b/packages/react-native-codegen/src/CodegenSchema.js index 8033695ba65513..4a16783ef0fff3 100644 --- a/packages/react-native-codegen/src/CodegenSchema.js +++ b/packages/react-native-codegen/src/CodegenSchema.js @@ -53,6 +53,9 @@ export type VoidTypeAnnotation = $ReadOnly<{ export type ObjectTypeAnnotation<+T> = $ReadOnly<{ type: 'ObjectTypeAnnotation', properties: $ReadOnlyArray>, + + // metadata for objects that generated from interfaces + baseTypes?: $ReadOnlyArray, }>; type FunctionTypeAnnotation<+P, +R> = $ReadOnly<{ diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap index d6f4f9d4e3679e..38fd3e1186724a 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap @@ -1521,6 +1521,56 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_NE } ] }, + 'Base1': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'bar1', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Bar' + } + } + ] + }, + 'Base2': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'bar2', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Bar' + } + } + ] + }, + 'Base3': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'bar2', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Bar' + } + }, + { + 'name': 'bar3', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Bar' + } + } + ], + 'baseTypes': [ + 'Base2' + ] + }, 'Foo': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1556,6 +1606,10 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_NE 'name': 'Bar' } } + ], + 'baseTypes': [ + 'Base1', + 'Base3' ] } }, diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/index.js b/packages/react-native-codegen/src/parsers/typescript/modules/index.js index 1d0a222b48d828..74331eaeb81b8a 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -221,7 +221,26 @@ function translateTypeAnnotation( } } case 'TSInterfaceDeclaration': { - const objectTypeAnnotation = { + const baseTypes = (typeAnnotation.extends ?? []).map( + extend => extend.expression.name, + ); + for (const baseType of baseTypes) { + // ensure base types exist and appear in aliasMap + translateTypeAnnotation( + hasteModuleName, + { + type: 'TSTypeReference', + typeName: {type: 'Identifier', name: baseType}, + }, + types, + aliasMap, + tryParse, + cxxOnly, + parser, + ); + } + + let objectTypeAnnotation = { type: 'ObjectTypeAnnotation', // $FlowFixMe[missing-type-arg] properties: (flattenProperties( @@ -246,8 +265,15 @@ function translateTypeAnnotation( }, ) .filter(Boolean), + baseTypes, }; + if (objectTypeAnnotation.baseTypes.length === 0) { + // The flow checker does not allow adding a member after an object literal is created + // so here I do it in a reverse way + delete objectTypeAnnotation.baseTypes; + } + return typeAliasResolution( typeAliasResolutionStatus, objectTypeAnnotation, From e18787e7b5701a2b2a8c9f9cc4c9a92b2ef13c29 Mon Sep 17 00:00:00 2001 From: Dmitry Rykun Date: Wed, 1 Feb 2023 10:04:51 -0800 Subject: [PATCH 20/65] Update changelog for 0.70.7 (#36018) Summary: This PR updates the Changelog for release 0.70.7 ## Changelog [Internal] - Update Changelog Pull Request resolved: https://github.com/facebook/react-native/pull/36018 Test Plan: CircleCI Reviewed By: cortinico Differential Revision: D42928093 Pulled By: cipolleschi fbshipit-source-id: 25c23eeb47e4ffe34884db5b8adc514c235c2c77 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d29e2d3891bb54..ed7ba6af59f25b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -456,6 +456,24 @@ Read the [announcement blogpost here](https://reactnative.dev/blog/2023/01/12/ve - Bump terser minor version to mitigate CVE-2022-25858 ([743f9ff63b](https://github.com/facebook/react-native/commit/743f9ff63bf1e3825a1788978a9f6bad8ebddc0d) by [@GijsWeterings](https://github.com/GijsWeterings)) +## v0.70.7 + +### Fixed + +#### Android specific + +- Mitigation for Samsung TextInput Hangs ([be69c8b5a7](https://github.com/facebook/react-native/commit/be69c8b5a77ae60cced1b2af64e48b90d9955be5) by [@NickGerleman](https://github.com/NickGerleman)) + +#### iOS Specific + +- Fix the potential race condition when dismissing and presenting modal ([279fb52e03](https://github.com/facebook/react-native/commit/279fb52e033daba60393e400e1ee585e7d067090) by [@wood1986](https://github.com/wood1986)) + +### Added + +#### Android Specific + +- Add `POST_NOTIFICATIONS` and deprecate `POST_NOTIFICATION` ([b5280bbc93](https://github.com/facebook/react-native/commit/b5280bbc93218bd15e2166b8689c1689200bb92c) by [@dcangulo](https://github.com/dcangulo)) + ## v0.70.6 ### Fixed @@ -752,6 +770,14 @@ Read the [announcement blogpost here](https://reactnative.dev/blog/2023/01/12/ve - Mitigation for Samsung TextInput Hangs ([be69c8b5a7](https://github.com/facebook/react-native/commit/be69c8b5a77ae60cced1b2af64e48b90d9955be5) by [@NickGerleman](https://github.com/NickGerleman)) +## v0.69.8 + +### Fixed + +#### Android specific + +- Mitigation for Samsung TextInput Hangs ([be69c8b5a7](https://github.com/facebook/react-native/commit/be69c8b5a77ae60cced1b2af64e48b90d9955be5) by [@NickGerleman](https://github.com/NickGerleman)) + ## v0.69.7 ### Fixed @@ -1060,6 +1086,14 @@ Read the [announcement blogpost here](https://reactnative.dev/blog/2023/01/12/ve - Mitigation for Samsung TextInput Hangs ([be69c8b5a7](https://github.com/facebook/react-native/commit/be69c8b5a77ae60cced1b2af64e48b90d9955be5) by [@NickGerleman](https://github.com/NickGerleman)) +## v0.68.6 + +### Fixed + +#### Android specific + +- Mitigation for Samsung TextInput Hangs ([be69c8b5a7](https://github.com/facebook/react-native/commit/be69c8b5a77ae60cced1b2af64e48b90d9955be5) by [@NickGerleman](https://github.com/NickGerleman)) + ## v0.68.5 ### Fixed From 95b995270ada3116d72b5fc984f8735a12f1f1da Mon Sep 17 00:00:00 2001 From: Lorenzo Sciandra Date: Wed, 1 Feb 2023 10:04:51 -0800 Subject: [PATCH 21/65] add 0.71.2 changelog (#36032) Summary: Adds changelog for new patch. ## Changelog [Internal] [Changed] - add changelog entry for 0.71.2 Pull Request resolved: https://github.com/facebook/react-native/pull/36032 Test Plan: N/A Reviewed By: cortinico Differential Revision: D42928179 Pulled By: cipolleschi fbshipit-source-id: 1eb7b399e58c6e78a6961dc98198fb747df5ceb3 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed7ba6af59f25b..61e7b129eaa2c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## v0.71.2 + +### Added + +- Added AlertOptions argument to the type definition for Alert.prompt to bring it into parity with the js code. ([305ca337c0](https://github.com/facebook/react-native/commit/305ca337c0471c61cb74216bd93ae3f1a232a89f) by [@paulmand3l](https://github.com/paulmand3l)) +- Added missing `accessibilityLabelledBy` TypeScript type ([e162b07982](https://github.com/facebook/react-native/commit/e162b07982cf9481038de71f5dd7bd9b45387f0a) by [@DimitarNestorov](https://github.com/DimitarNestorov)) +- Added missing `accessibilityLanguage` TypeScript type ([71c4f57baf](https://github.com/facebook/react-native/commit/71c4f57baf6683ea4304e15c040d6b6c3b3d2b73) by [@DimitarNestorov](https://github.com/DimitarNestorov)) + +### Changed + +- Bump `react-native-gradle-plugin` to `^0.71.14` in core, `@react-native-community/eslint-config` to `^3.2.0` in starting template ([785bc8d97b](https://github.com/facebook/react-native/commit/785bc8d97b824a2af86ffe46f321471f4952764c) by [@kelset](https://github.com/kelset)) + +### Fixed + +- Add `TextInput`'s `inputMode` TypeScript types ([fac7859863](https://github.com/facebook/react-native/commit/fac7859863c7130740aacc95d0e62417bd8f789e) by [@eps1lon](https://github.com/eps1lon)) +- Fix crash by conditional value of aspectRatio style value ([a8166bd75b](https://github.com/facebook/react-native/commit/a8166bd75b221f967a859d5cc25b3394c4d35301) by [@mym0404](https://github.com/mym0404)) +- Fix TurboModuleRegistry TS type ([c289442848](https://github.com/facebook/react-native/commit/c28944284894a3188b97e3d8bb5b489755852160) by [@janicduplessis](https://github.com/janicduplessis)) +- Fix invariant violation when nesting VirtualizedList inside ListEmptyComponent ([1fef376812](https://github.com/facebook/react-native/commit/1fef37681298c828a07febcd0d975a32f6bc4403) by [@NickGerleman](https://github.com/NickGerleman)) + +#### Android specific + +- [RNGP] Properly set the `jsRootDir` default value ([c0004092f9](https://github.com/facebook/react-native/commit/c0004092f935ad892d4a1acf38fb184f1140bfd2) by [@cortinico](https://github.com/cortinico)) +- Do not use WindowInsetsCompat for Keyboard Events ([32f54877ff](https://github.com/facebook/react-native/commit/32f54877ff788240d24528d208c704ee78e4e761) by [@NickGerleman](https://github.com/NickGerleman)) +- Mitigation for Samsung TextInput Hangs ([4650ef3](https://github.com/facebook/react-native/commit/4650ef36e3d63df6e6a31f00fcf323c53daff2d6) by [@NickGerleman](https://github.com/NickGerleman)) + +#### iOS specific + +- Add Back dynamic framework support for the Old Architecture with Hermes ([b3040ec624](https://github.com/facebook/react-native/commit/b3040ec6244da6ea274654abfd84516de4f5bf52) by [@cipolleschi](https://github.com/cipolleschi)) +- Add Back dynamic framework support for the old architecture ([da270d038c](https://github.com/facebook/react-native/commit/da270d038c271d6b82de568621b49e38739372c6) by [@cipolleschi](https://github.com/cipolleschi)) + ## v0.71.1 ### Added From a0800ffc7a676555aa9e769fc8fd6d3162de0ea6 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Wed, 1 Feb 2023 12:20:40 -0800 Subject: [PATCH 22/65] Add `borderCurve` and `pointerEvents` to `ViewStyle` (#35998) Summary: Forward-porting https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64125 ## Changelog [GENERAL] [FIXED] - Add `borderCurve` and `pointerEvents` to `ViewStyle` Pull Request resolved: https://github.com/facebook/react-native/pull/35998 Test Plan: - [x] https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64125 green Reviewed By: christophpurrer Differential Revision: D42906357 Pulled By: lunaleaps fbshipit-source-id: 6a5763cf7880888462fbabe1a00e560065c9a551 --- Libraries/StyleSheet/StyleSheetTypes.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Libraries/StyleSheet/StyleSheetTypes.d.ts b/Libraries/StyleSheet/StyleSheetTypes.d.ts index f31224e7a62a0c..412eaa55afb61d 100644 --- a/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -242,6 +242,11 @@ export interface ViewStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle { borderBottomStartRadius?: number | undefined; borderBottomWidth?: number | undefined; borderColor?: ColorValue | undefined; + /** + * On iOS 13+, it is possible to change the corner curve of borders. + * @platform ios + */ + borderCurve?: 'circular' | 'continuous' | undefined; borderEndColor?: ColorValue | undefined; borderEndEndRadius?: number | undefined; borderEndStartRadius?: number | undefined; @@ -271,6 +276,10 @@ export interface ViewStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle { * @platform android */ elevation?: number | undefined; + /** + * Controls whether the View can be the target of touch events. + */ + pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto' | undefined; } export type FontVariant = From 1e53f88b72e24ae2654b31f5c8bfbf8a07d51e09 Mon Sep 17 00:00:00 2001 From: Luna Wei Date: Wed, 1 Feb 2023 23:52:16 -0800 Subject: [PATCH 23/65] PointerEvents: Account for root view exits Summary: Changelog: [Internal] Support hovering in/out of root views Prior to this change, we did not have signal when an input device moved out of the root view and so our internal state would not be aware and we would not trigger enter/leave events. This diff starts listening to `HOVER_EXIT` events as dispatched from `onInterceptHoverEvent` and assumes that's the right event to signal a cursor has moved out of the root view. We dispatch the relevant leave events for this case and update our internal state to ensure the next `HOVER_MOVE` in our rootview, will properly dispatch the enter events. ## Investigation for creating this diff Determining the signal for when an inputDevice enters/exits our rootview wasn't straight-forward. From my understanding Android event dispatching follows a similar capture, bubbling phase as web. With `onIntercept..` handlers to swallow events. See this explanation: https://suragch.medium.com/how-touch-events-are-delivered-in-android-eee3b607b038 and this video talk: https://youtu.be/EZAoJU-nUyI?t=929 However when trying to understand hover enter/exit events on the root view, my understanding of this logic broke down. Here's what confused me: * When moving a cursor from inside to outside the root view, I would receive `HOVER_ENTER/EXIT` MotionEvents on `onInterceptHoverEvent` and since we did not swallow them, we'd receive those same events on the bubble up in `onHover`. That makes sense. * However, when I hovered from the rootview into a child view, I would receive MotionEvents of `HOVER_ENTER/HOVER_EXIT` in the `onHoverEvent` handler of the rootview without having seen them in the `onInterceptHoverEvent` (re: capture phase down). This was confusing, where was the capture down? * What tips me off that these events (`HOVER_ENTER/EXIT`) don't follow the classic capture, bubbling model as explained in the linked article, is that I don't receive `HOVER_ENTER/HOVER_EXIT` events for each child view in the root view's `onInterceptHoverEvent`. * Like when a cursor moves from root -> child, I'd expect to motion events 1. exit for the rootview, 2. enter for the child view. But I never receive the 2. from the root view -- * I also wonder if the wording for `HOVER_EXIT` events mean that these events are directly dispatched to the view? Re: ["This action is always delivered to the window or view that was previously under the pointer."](https://developer.android.com/reference/android/view/MotionEvent#ACTION_HOVER_ENTER) * There also seems to be some optimizations around the dispatch path as mentioned in this video at this timestamp: https://youtu.be/EZAoJU-nUyI?t=929 for the UP gesture.. so maybe there's some optimization happening with hover events? I'm not sure how hover events are account for in gesture handling for Android. Reviewed By: vincentriemer Differential Revision: D42817315 fbshipit-source-id: 412c971c1d1e7afc0d67fadcc4417189967fe48c --- .../com/facebook/react/ReactRootView.java | 22 ++--- .../react/uimanager/JSPointerDispatcher.java | 87 +++++++++++++------ .../react/views/modal/ReactModalHostView.java | 8 +- 3 files changed, 75 insertions(+), 42 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 5ac4cd77d23006..9f4339bf490389 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -262,31 +262,31 @@ public boolean onInterceptTouchEvent(MotionEvent ev) { if (shouldDispatchJSTouchEvent(ev)) { dispatchJSTouchEvent(ev); } - dispatchJSPointerEvent(ev); + dispatchJSPointerEvent(ev, true); return super.onInterceptTouchEvent(ev); } - @Override - public boolean onInterceptHoverEvent(MotionEvent ev) { - dispatchJSPointerEvent(ev); - return super.onInterceptHoverEvent(ev); - } - @Override public boolean onTouchEvent(MotionEvent ev) { if (shouldDispatchJSTouchEvent(ev)) { dispatchJSTouchEvent(ev); } - dispatchJSPointerEvent(ev); + dispatchJSPointerEvent(ev, false); super.onTouchEvent(ev); // In case when there is no children interested in handling touch event, we return true from // the root view in order to receive subsequent events related to that gesture return true; } + @Override + public boolean onInterceptHoverEvent(MotionEvent ev) { + dispatchJSPointerEvent(ev, true); + return super.onInterceptHoverEvent(ev); + } + @Override public boolean onHoverEvent(MotionEvent ev) { - dispatchJSPointerEvent(ev); + dispatchJSPointerEvent(ev, false); return super.onHoverEvent(ev); } @@ -343,7 +343,7 @@ public void requestChildFocus(View child, View focused) { super.requestChildFocus(child, focused); } - protected void dispatchJSPointerEvent(MotionEvent event) { + protected void dispatchJSPointerEvent(MotionEvent event, boolean isCapture) { if (mReactInstanceManager == null || !mIsAttachedToInstance || mReactInstanceManager.getCurrentReactContext() == null) { @@ -362,7 +362,7 @@ protected void dispatchJSPointerEvent(MotionEvent event) { if (uiManager != null) { EventDispatcher eventDispatcher = uiManager.getEventDispatcher(); - mJSPointerDispatcher.handleMotionEvent(event, eventDispatcher); + mJSPointerDispatcher.handleMotionEvent(event, eventDispatcher, isCapture); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java index e1f75da6dc6f63..caeea1dec94fc5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java @@ -199,7 +199,8 @@ private PointerEventState createEventState(int activePointerId, MotionEvent moti mHoveringPointerIds); // Creates a copy of hovering pointer ids, as they may be updated } - public void handleMotionEvent(MotionEvent motionEvent, EventDispatcher eventDispatcher) { + public void handleMotionEvent( + MotionEvent motionEvent, EventDispatcher eventDispatcher, boolean isCapture) { // Don't fire any pointer events if child view is handling native gesture if (mChildHandlingNativeGesture != -1) { return; @@ -214,16 +215,41 @@ public void handleMotionEvent(MotionEvent motionEvent, EventDispatcher eventDisp } PointerEventState eventState = createEventState(activePointerId, motionEvent); - List activeHitPath = - eventState.getHitPathByPointerId().get(eventState.getActivePointerId()); - if (activeHitPath == null || activeHitPath.isEmpty()) { - return; + // We've empirically determined that when we get a ACTION_HOVER_EXIT from the root view on the + // `onInterceptHoverEvent`, this means we've exited the root view. + // This logic may be wrong but reasoning about the dispatch sequence for HOVER_ENTER/HOVER_EXIT + // doesn't follow the capture/bubbling sequence like other MotionEvents. See: + // https://developer.android.com/reference/android/view/MotionEvent#ACTION_HOVER_ENTER + // https://suragch.medium.com/how-touch-events-are-delivered-in-android-eee3b607b038 + boolean isExitFromRoot = + isCapture && motionEvent.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT; + + // Calculate the targetTag, with special handling for when we exit the root view. In that case, + // we use the root viewId of the last event + int activeTargetTag; + + List activeHitPath; + if (isExitFromRoot) { + List lastHitPath = mLastHitPathByPointerId.get(eventState.getActivePointerId()); + if (lastHitPath == null || lastHitPath.isEmpty()) { + return; + } + activeTargetTag = lastHitPath.get(lastHitPath.size() - 1).getViewId(); + + // Explicitly make the hit path for this cursor empty + activeHitPath = new ArrayList<>(); + eventState.getHitPathByPointerId().put(activePointerId, activeHitPath); + } else { + activeHitPath = eventState.getHitPathByPointerId().get(activePointerId); + if (activeHitPath == null || activeHitPath.isEmpty()) { + return; + } + activeTargetTag = activeHitPath.get(0).getViewId(); } - TouchTargetHelper.ViewTarget activeViewTarget = activeHitPath.get(0); - int activeTargetTag = activeViewTarget.getViewId(); - + // Dispatch pointer events from the MotionEvents. When we want to ignore an event, we need to + // exit early so we don't record anything about this MotionEvent. switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: @@ -231,7 +257,18 @@ public void handleMotionEvent(MotionEvent motionEvent, EventDispatcher eventDisp break; case MotionEvent.ACTION_HOVER_MOVE: // TODO(luwe) - converge this with ACTION_MOVE - // HOVER_MOVE may occur before DOWN. Add its downTime as a coalescing key + + // If we don't move enough, ignore this event. + float[] eventCoordinates = eventState.getEventCoordinatesByPointerId().get(activePointerId); + float[] lastEventCoordinates = + mLastEventCoordinatesByPointerId != null + && mLastEventCoordinatesByPointerId.containsKey(activePointerId) + ? mLastEventCoordinatesByPointerId.get(activePointerId) + : new float[] {0, 0}; + if (!qualifiedMove(eventCoordinates, lastEventCoordinates)) { + return; + } + onMove(activeTargetTag, eventState, motionEvent, eventDispatcher); break; case MotionEvent.ACTION_MOVE: @@ -257,8 +294,15 @@ public void handleMotionEvent(MotionEvent motionEvent, EventDispatcher eventDisp dispatchCancelEvent(eventState, motionEvent, eventDispatcher); break; case MotionEvent.ACTION_HOVER_ENTER: + // Ignore these events as enters will be calculated from HOVER_MOVE + return; case MotionEvent.ACTION_HOVER_EXIT: - // These are handled by HOVER_MOVE + // For root exits, we need to update our stored eventState to reflect this exit because we + // won't receive future HOVER_MOVE events when cursor is outside root view + if (isExitFromRoot) { + // We've set the hit path for this pointer to be empty to calculate all exits + onMove(activeTargetTag, eventState, motionEvent, eventDispatcher); + } break; default: FLog.w( @@ -267,6 +311,7 @@ public void handleMotionEvent(MotionEvent motionEvent, EventDispatcher eventDisp return; } + // Caching the event state so we have a new "last" mLastHitPathByPointerId = eventState.getHitPathByPointerId(); mLastEventCoordinatesByPointerId = eventState.getEventCoordinatesByPointerId(); mLastButtonState = motionEvent.getButtonState(); @@ -335,7 +380,11 @@ private void dispatchEventForViewTargets( } } - // called on hover_move motion events only + private boolean qualifiedMove(float[] eventCoordinates, float[] lastEventCoordinates) { + return (Math.abs(lastEventCoordinates[0] - eventCoordinates[0]) > ONMOVE_EPSILON + || Math.abs(lastEventCoordinates[1] - eventCoordinates[1]) > ONMOVE_EPSILON); + } + private void onMove( int targetTag, PointerEventState eventState, @@ -343,7 +392,6 @@ private void onMove( EventDispatcher eventDispatcher) { int activePointerId = eventState.getActivePointerId(); - float[] eventCoordinates = eventState.getEventCoordinatesByPointerId().get(activePointerId); List activeHitPath = eventState.getHitPathByPointerId().get(activePointerId); List lastHitPath = @@ -351,21 +399,6 @@ private void onMove( ? mLastHitPathByPointerId.get(activePointerId) : new ArrayList(); - float[] lastEventCoordinates = - mLastEventCoordinatesByPointerId != null - && mLastEventCoordinatesByPointerId.containsKey(activePointerId) - ? mLastEventCoordinatesByPointerId.get(activePointerId) - : new float[] {0, 0}; - - boolean qualifiedMove = - (Math.abs(lastEventCoordinates[0] - eventCoordinates[0]) > ONMOVE_EPSILON - || Math.abs(lastEventCoordinates[1] - eventCoordinates[1]) > ONMOVE_EPSILON); - - // Early exit if active pointer has not moved enough - if (!qualifiedMove) { - return; - } - // hitState is list ordered from inner child -> parent tag // Traverse hitState back-to-front to find the first divergence with lastHitPath // FIXME: this may generate incorrect events when view collapsing changes the hierarchy diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java index 9f408a7668c18f..8c401a3544f1e0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java @@ -534,7 +534,7 @@ private ThemedReactContext getReactContext() { public boolean onInterceptTouchEvent(MotionEvent event) { mJSTouchDispatcher.handleTouchEvent(event, mEventDispatcher); if (mJSPointerDispatcher != null) { - mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher); + mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher, true); } return super.onInterceptTouchEvent(event); } @@ -543,7 +543,7 @@ public boolean onInterceptTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) { mJSTouchDispatcher.handleTouchEvent(event, mEventDispatcher); if (mJSPointerDispatcher != null) { - mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher); + mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher, false); } super.onTouchEvent(event); // In case when there is no children interested in handling touch event, we return true from @@ -554,7 +554,7 @@ public boolean onTouchEvent(MotionEvent event) { @Override public boolean onInterceptHoverEvent(MotionEvent event) { if (mJSPointerDispatcher != null) { - mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher); + mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher, true); } return super.onHoverEvent(event); } @@ -562,7 +562,7 @@ public boolean onInterceptHoverEvent(MotionEvent event) { @Override public boolean onHoverEvent(MotionEvent event) { if (mJSPointerDispatcher != null) { - mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher); + mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher, false); } return super.onHoverEvent(event); } From 6d45e49dc783d0af3a39be2df5e8495541d65e5f Mon Sep 17 00:00:00 2001 From: Lulu Wu Date: Thu, 2 Feb 2023 05:35:47 -0800 Subject: [PATCH 24/65] Align creation of FabricUIManager with bridge Summary: - Bridgeless is using a deprecated FabricUIManager constructor which bridge doesn't use, and is the only one using it, this diff migrated bridgeless to use the same FabricUIManager constructor as bridge - Remove static view config check (mShouldDeallocateEventDispatcher), instead use Bridgeless check since SVC is enabled in Bridgeless but not in Bridge. Changelog: [Android][Changed] - Align creation of FabricUIManager with bridge Reviewed By: javache Differential Revision: D42681489 fbshipit-source-id: b9c7c4a81a98db52e881138cc85be0e85df636d9 --- .../react/fabric/FabricUIManager.java | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index e0947264fd959a..5f3a34038bed36 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -182,9 +182,6 @@ public void onFabricCommitEnd(DevToolsReactPerfLogger.FabricCommitPoint commitPo */ private volatile boolean mDestroyed = false; - // TODO T83943316: Delete this variable once StaticViewConfigs are enabled by default - private volatile boolean mShouldDeallocateEventDispatcher = false; - private boolean mDriveCxxAnimations = false; private long mDispatchViewUpdatesTime = 0l; @@ -209,28 +206,6 @@ public void executeItems(Queue items) { } }; - // TODO T83943316: Deprecate and delete this constructor once StaticViewConfigs are enabled by - // default - @Deprecated - public FabricUIManager( - ReactApplicationContext reactContext, - ViewManagerRegistry viewManagerRegistry, - EventDispatcher eventDispatcher, - EventBeatManager eventBeatManager) { - mDispatchUIFrameCallback = new DispatchUIFrameCallback(reactContext); - mReactApplicationContext = reactContext; - mMountingManager = new MountingManager(viewManagerRegistry, mMountItemExecutor); - mMountItemDispatcher = - new MountItemDispatcher(mMountingManager, new MountItemDispatchListener()); - mEventDispatcher = eventDispatcher; - mShouldDeallocateEventDispatcher = false; - mEventBeatManager = eventBeatManager; - mReactApplicationContext.addLifecycleEventListener(this); - - mViewManagerRegistry = viewManagerRegistry; - mReactApplicationContext.registerComponentCallbacks(viewManagerRegistry); - } - public FabricUIManager( ReactApplicationContext reactContext, ViewManagerRegistry viewManagerRegistry, @@ -241,7 +216,6 @@ public FabricUIManager( mMountItemDispatcher = new MountItemDispatcher(mMountingManager, new MountItemDispatchListener()); mEventDispatcher = new EventDispatcherImpl(reactContext); - mShouldDeallocateEventDispatcher = true; mEventBeatManager = eventBeatManager; mReactApplicationContext.addLifecycleEventListener(this); @@ -467,10 +441,11 @@ public void onCatalystInstanceDestroy() { ViewManagerPropertyUpdater.clear(); - // When using StaticViewConfigs is enabled, FabriUIManager is - // responsible for initializing and deallocating EventDispatcher. + // When StaticViewConfigs is enabled, FabriUIManager is + // responsible for initializing and deallocating EventDispatcher. StaticViewConfigs is enabled + // only in Bridgeless for now. // TODO T83943316: Remove this IF once StaticViewConfigs are enabled by default - if (mShouldDeallocateEventDispatcher) { + if (!ReactFeatureFlags.enableBridgelessArchitecture) { mEventDispatcher.onCatalystInstanceDestroyed(); } } From 5647d79dc97ab2787a9575cb1621725d865b9814 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Thu, 2 Feb 2023 07:07:45 -0800 Subject: [PATCH 25/65] RNGP - Bump AGP to 7.4.1 (#36039) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36039 This change bumps AGP to 7.4.1 so we can remove a lot of the unnecessary changes we had to do to support AGP 7.3.x I've also removed the explicit version in the template as now the AGP version is provided by RNGP. That means that the user will get the correct version they need. This also bumps the default CMake version in user space to 3.22 which resolves a lot of warning when users are building with the New Architecture enabled. Changelog: [Android] [Changed] - Bump AGP to 7.4.1 allow-large-files Reviewed By: cipolleschi Differential Revision: D42960353 fbshipit-source-id: 9065f975c1694d266a86b2d3fe805e6e2b1c4aa1 --- build.gradle.kts | 2 +- .../build.gradle.kts | 2 +- .../kotlin/com/facebook/react/ReactPlugin.kt | 19 ------------------- .../com/facebook/react/TaskConfiguration.kt | 4 +--- packages/rn-tester/android/app/build.gradle | 10 ++++++++++ template/android/build.gradle | 2 +- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index bcdbd6c874f2a8..dde63fc00734fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,7 +33,7 @@ buildscript { gradlePluginPortal() } dependencies { - classpath("com.android.tools.build:gradle:7.3.1") + classpath("com.android.tools.build:gradle:7.4.1") classpath("de.undercouch:gradle-download-task:5.0.1") } } diff --git a/packages/react-native-gradle-plugin/build.gradle.kts b/packages/react-native-gradle-plugin/build.gradle.kts index 5ea92ee71b41c8..8a54bdaaa9e6a2 100644 --- a/packages/react-native-gradle-plugin/build.gradle.kts +++ b/packages/react-native-gradle-plugin/build.gradle.kts @@ -33,7 +33,7 @@ group = "com.facebook.react" dependencies { implementation(gradleApi()) - implementation("com.android.tools.build:gradle:7.3.1") + implementation("com.android.tools.build:gradle:7.4.1") implementation("com.google.code.gson:gson:2.8.9") implementation("com.google.guava:guava:31.0.1-jre") implementation("com.squareup:javapoet:1.13.0") diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index 48fff59c790a38..8aa1c5bbdb0d9a 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -8,7 +8,6 @@ package com.facebook.react import com.android.build.api.variant.AndroidComponentsExtension -import com.android.build.gradle.AppExtension import com.android.build.gradle.internal.tasks.factory.dependsOn import com.facebook.react.tasks.BuildCodegenCLITask import com.facebook.react.tasks.GenerateCodegenArtifactsTask @@ -55,24 +54,6 @@ class ReactPlugin : Plugin { project.configureReactTasks(variant = variant, config = extension) } } - - // This is a legacy AGP api. Needed as AGP 7.3 is not consuming generated resources correctly. - // Can be removed as we bump to AGP 7.4 stable. - // This registers the $buildDir/generated/res/react/ folder as a - // res folder to be consumed with the old AGP Apis which are not broken. - project.extensions.getByType(AppExtension::class.java).apply { - this.applicationVariants.all { variant -> - val isDebuggableVariant = - extension.debuggableVariants.get().any { it.equals(variant.name, ignoreCase = true) } - val targetName = variant.name.replaceFirstChar { it.uppercase() } - val bundleTaskName = "createBundle${targetName}JsAndAssets" - if (!isDebuggableVariant) { - variant.registerGeneratedResFolders( - project.layout.buildDirectory.files("generated/res/react/${variant.name}")) - variant.mergeResourcesProvider.get().dependsOn(bundleTaskName) - } - } - } configureCodegen(project, extension, isLibrary = false) } diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt index 30bad252fd823b..24ba33b297c692 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/TaskConfiguration.kt @@ -72,9 +72,7 @@ internal fun Project.configureReactTasks(variant: Variant, config: ReactExtensio it.hermesFlags.set(config.hermesFlags) it.reactNativeDir.set(config.reactNativeDir) } - // Currently broken inside AGP 7.3 We need to wait for a release of AGP 7.4 in order to use - // the addGeneratedSourceDirectory API. - // variant.sources.res?.addGeneratedSourceDirectory(bundleTask, BundleHermesCTask::resourcesDir) + variant.sources.res?.addGeneratedSourceDirectory(bundleTask, BundleHermesCTask::resourcesDir) variant.sources.assets?.addGeneratedSourceDirectory(bundleTask, BundleHermesCTask::jsBundleDir) } } diff --git a/packages/rn-tester/android/app/build.gradle b/packages/rn-tester/android/app/build.gradle index 964948984f041f..7e409761fab2c9 100644 --- a/packages/rn-tester/android/app/build.gradle +++ b/packages/rn-tester/android/app/build.gradle @@ -77,6 +77,11 @@ def enableProguardInReleaseBuilds = true */ def jscFlavor = 'org.webkit:android-jsc:+' +/** + * This allows to customized the CMake version used for compiling RN Tester. + */ +def cmakeVersion = project(":ReactAndroid").cmake_version + /** * Architectures to build native code for. */ @@ -128,6 +133,11 @@ android { keyPassword MYAPP_RELEASE_KEY_PASSWORD } } + externalNativeBuild { + cmake { + version cmakeVersion + } + } splits { abi { enable enableSeparateBuildPerCPUArchitecture diff --git a/template/android/build.gradle b/template/android/build.gradle index 67d887b03078b6..34ea71819406f6 100644 --- a/template/android/build.gradle +++ b/template/android/build.gradle @@ -15,7 +15,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.3.1") + classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") } } From 37171ec78f377fbae89ce43010f9cf69c1e60fbc Mon Sep 17 00:00:00 2001 From: Jonathan Maurice Date: Thu, 2 Feb 2023 07:50:12 -0800 Subject: [PATCH 26/65] Fix negative value rounding issue for nodes across an axis (#688) Summary: This fix issue https://github.com/facebook/yoga/issues/683 the rounding calculation is incorrect if a node is crossing an axis and it will shrink it's width/height on layout calculation. The following test reproduce the issue : ``` TEST(YogaTest, node_shrink_on_axis) { const YGConfigRef config = YGConfigNew(); const YGNodeRef root = YGNodeNewWithConfig(config); const YGNodeRef child = YGNodeNewWithConfig(config); YGNodeInsertChild(root, child, 0); YGNodeStyleSetWidth(child, 10); YGNodeStyleSetHeight(child, 10); YGNodeStyleSetPosition(root, YGEdgeLeft, -0.75f); YGNodeStyleSetPosition(root, YGEdgeTop, -0.75f); YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR); ASSERT_FLOAT_EQ(YGNodeLayoutGetWidth(child), 10); ASSERT_FLOAT_EQ(YGNodeLayoutGetHeight(child), 10); YGNodeFreeRecursive(root); YGConfigFree(config); } ``` X-link: https://github.com/facebook/yoga/pull/688 Reviewed By: NickGerleman Differential Revision: D13866122 Pulled By: rozele fbshipit-source-id: 4faf8a9efc86723c303f600d730660a2e13d8a73 --- ReactCommon/yoga/yoga/Yoga.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ReactCommon/yoga/yoga/Yoga.cpp b/ReactCommon/yoga/yoga/Yoga.cpp index 2c8f45b02d6270..5072992d9c3e96 100644 --- a/ReactCommon/yoga/yoga/Yoga.cpp +++ b/ReactCommon/yoga/yoga/Yoga.cpp @@ -3701,16 +3701,12 @@ YOGA_EXPORT float YGRoundValueToPixelGrid( scaledValue = scaledValue - fractial + 1.0; } else if (forceCeil) { // Next we check if we need to use forced rounding - scaledValue = scaledValue - fractial + 1.0; + scaledValue = ceil(scaledValue); } else if (forceFloor) { - scaledValue = scaledValue - fractial; + scaledValue = floor(scaledValue); } else { // Finally we just round the value - scaledValue = scaledValue - fractial + - (!YGDoubleIsUndefined(fractial) && - (fractial > 0.5 || YGDoubleEqual(fractial, 0.5)) - ? 1.0 - : 0.0); + scaledValue = round(scaledValue); } return (YGDoubleIsUndefined(scaledValue) || YGDoubleIsUndefined(pointScaleFactor)) From 147a1f6619461c3382f3b8e1d7b71682b152bc8c Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Thu, 2 Feb 2023 09:23:55 -0800 Subject: [PATCH 27/65] Add macro to detect circular dependencies on Cmake Summary: This is a prototype to add circular dependencies detection on CMake for ReactCommon and ReactAndroid. It can be enabled per module and works as follows: ``` set(ALLOWED_HEADER_IMPORT_PATHS react/renderer/graphics react/debug) check_for_circular_dependencies("${ALLOWED_HEADER_IMPORT_PATHS}") ``` The allowed header import path must be manually specified as libraries are exposing wrong header search paths (so can't be reused). The CI will be red till the circular dependency on `graphics` is resolved. Changelog: [Internal] [Changed] - Add macro to detect circular dependencies on Cmake Reviewed By: cipolleschi Differential Revision: D42927036 fbshipit-source-id: b1393dfd43fd042e2ebf1d5b46b24bd9f5e20d58 --- ReactAndroid/src/main/jni/CMakeLists.txt | 3 ++ .../src/main/jni/CircularDepsValidator.cmake | 32 +++++++++++++++++++ .../react/renderer/graphics/CMakeLists.txt | 1 + 3 files changed, 36 insertions(+) create mode 100644 ReactAndroid/src/main/jni/CircularDepsValidator.cmake diff --git a/ReactAndroid/src/main/jni/CMakeLists.txt b/ReactAndroid/src/main/jni/CMakeLists.txt index 7bc36325b689e7..2d1295d2cc5df5 100644 --- a/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/ReactAndroid/src/main/jni/CMakeLists.txt @@ -19,6 +19,9 @@ endif(CCACHE_FOUND) add_link_options(-Wl,--build-id) add_compile_options(-Wall -Werror -std=c++17) +# This exposes the check_for_circular_dependencies macro to all the submodules +include(CircularDepsValidator.cmake) + function(add_react_android_subdir relative_path) add_subdirectory(${REACT_ANDROID_DIR}/${relative_path} ReactAndroid/${relative_path}) endfunction() diff --git a/ReactAndroid/src/main/jni/CircularDepsValidator.cmake b/ReactAndroid/src/main/jni/CircularDepsValidator.cmake new file mode 100644 index 00000000000000..44f8fb93651820 --- /dev/null +++ b/ReactAndroid/src/main/jni/CircularDepsValidator.cmake @@ -0,0 +1,32 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +# Util function that help us verify that you're not including a header file +# which has an invalid import path. Most of the time this is causing a C++ circular dependency. +function(check_for_circular_dependencies allowed_imports_paths) + file(GLOB headers CONFIGURE_DEPENDS *.h) + foreach(file ${headers}) + file(STRINGS ${file} header_file) + while(header_file) + list(POP_FRONT header_file line) + if (line MATCHES "^#include Date: Thu, 2 Feb 2023 09:30:43 -0800 Subject: [PATCH 28/65] Fix null pointer exception in JSPointerDispatcher Summary: Changelog: [Internal] Fix null pointer exception in JSPointerDispatcher Small regression introduced in D42817315 (https://github.com/facebook/react-native/commit/1e53f88b72e24ae2654b31f5c8bfbf8a07d51e09). Reviewed By: javache Differential Revision: D42965308 fbshipit-source-id: c1c4e907605d792f4be05800eb17af26cfea2bac --- .../com/facebook/react/uimanager/JSPointerDispatcher.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java index caeea1dec94fc5..2a462123eaa69b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java @@ -231,7 +231,10 @@ public void handleMotionEvent( List activeHitPath; if (isExitFromRoot) { - List lastHitPath = mLastHitPathByPointerId.get(eventState.getActivePointerId()); + List lastHitPath = + mLastHitPathByPointerId != null + ? mLastHitPathByPointerId.get(eventState.getActivePointerId()) + : null; if (lastHitPath == null || lastHitPath.isEmpty()) { return; } From 69494a7b1f31e6aeb35b9ad6391e923dc5e62255 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Thu, 2 Feb 2023 09:58:01 -0800 Subject: [PATCH 29/65] rename module exports from "aliases" to "aliasMap" (#36042) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36042 Pull Request resolved: https://github.com/facebook/react-native/pull/36028 Use aliasMap both in parsers and generators instead of using aliasMap in half of the places and aliases in the other. This would also allow us to introduce "enumMap" more easily in the next commit. Changelog: [Internal] rename module exports from "aliases" to "aliasMap" Reviewed By: christophpurrer, cipolleschi Differential Revision: D42888752 fbshipit-source-id: cf1929fcebde994d07e5c6bda5ab71106d417b21 --- .../react-native-codegen/src/CodegenSchema.js | 2 +- .../generators/__test_fixtures__/fixtures.js | 2 +- .../generators/modules/GenerateModuleCpp.js | 4 +- .../src/generators/modules/GenerateModuleH.js | 6 +- .../modules/GenerateModuleJavaSpec.js | 4 +- .../modules/GenerateModuleJniCpp.js | 4 +- .../modules/GenerateModuleObjCpp/index.js | 4 +- .../modules/__test_fixtures__/fixtures.js | 20 +++---- .../parsers/__tests__/parsers-commons-test.js | 14 ++--- .../module-parser-snapshot-test.js.snap | 50 ++++++++-------- .../__tests__/module-parser-e2e-test.js | 2 +- .../src/parsers/flow/modules/index.js | 4 +- ...script-module-parser-snapshot-test.js.snap | 58 +++++++++---------- .../typescript-module-parser-e2e-test.js | 2 +- .../src/parsers/typescript/modules/index.js | 4 +- 15 files changed, 90 insertions(+), 90 deletions(-) diff --git a/packages/react-native-codegen/src/CodegenSchema.js b/packages/react-native-codegen/src/CodegenSchema.js index 4a16783ef0fff3..ab0ff056e79613 100644 --- a/packages/react-native-codegen/src/CodegenSchema.js +++ b/packages/react-native-codegen/src/CodegenSchema.js @@ -222,7 +222,7 @@ export type NullableTypeAnnotation<+T: NativeModuleTypeAnnotation> = $ReadOnly<{ export type NativeModuleSchema = $ReadOnly<{ type: 'NativeModule', - aliases: NativeModuleAliasMap, + aliasMap: NativeModuleAliasMap, spec: NativeModuleSpec, moduleName: string, // Use for modules that are not used on other platforms. diff --git a/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js index 8a6963d001a6f3..2a60dc45f888a4 100644 --- a/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js @@ -41,7 +41,7 @@ const SCHEMA_WITH_TM_AND_FC: SchemaType = { }, NativeCalculator: { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: { properties: [ { diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js index ec42fa6ecacd7c..5b366e953530ae 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js @@ -238,11 +238,11 @@ module.exports = { .map((hasteModuleName: string) => { const nativeModule = nativeModules[hasteModuleName]; const { - aliases, + aliasMap, spec: {properties}, moduleName, } = nativeModule; - const resolveAlias = createAliasResolver(aliases); + const resolveAlias = createAliasResolver(aliasMap); const hostFunctions = properties.map(property => serializePropertyIntoHostFunction( hasteModuleName, diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js index 7c3e6e44b26205..a3156c4d7e49c9 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js @@ -341,12 +341,12 @@ module.exports = { const modules = Object.keys(nativeModules).flatMap(hasteModuleName => { const { - aliases, + aliasMap, spec: {properties}, moduleName, } = nativeModules[hasteModuleName]; - const resolveAlias = createAliasResolver(aliases); - const structs = createStructs(moduleName, aliases, resolveAlias); + const resolveAlias = createAliasResolver(aliasMap); + const structs = createStructs(moduleName, aliasMap, resolveAlias); return [ ModuleClassDeclarationTemplate({ diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index 7be905514a699b..aff908ec83cd7c 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -447,7 +447,7 @@ module.exports = { Object.keys(nativeModules).forEach(hasteModuleName => { const { - aliases, + aliasMap, excludedPlatforms, moduleName, spec: {properties}, @@ -455,7 +455,7 @@ module.exports = { if (excludedPlatforms != null && excludedPlatforms.includes('android')) { return; } - const resolveAlias = createAliasResolver(aliases); + const resolveAlias = createAliasResolver(aliasMap); const className = `${hasteModuleName}Spec`; const imports: Set = new Set([ diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js index fb62617998dd36..1c825918fbb55a 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -438,10 +438,10 @@ module.exports = { .sort() .map(hasteModuleName => { const { - aliases, + aliasMap, spec: {properties}, } = nativeModules[hasteModuleName]; - const resolveAlias = createAliasResolver(aliases); + const resolveAlias = createAliasResolver(aliasMap); const translatedMethods = properties .map(property => diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js index 8f8da790b90407..9524327b87137e 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js @@ -132,14 +132,14 @@ module.exports = { const hasteModuleNames: Array = Object.keys(nativeModules).sort(); for (const hasteModuleName of hasteModuleNames) { const { - aliases, + aliasMap, excludedPlatforms, spec: {properties}, } = nativeModules[hasteModuleName]; if (excludedPlatforms != null && excludedPlatforms.includes('iOS')) { continue; } - const resolveAlias = createAliasResolver(aliases); + const resolveAlias = createAliasResolver(aliasMap); const structCollector = new StructCollector(); const methodSerializations: Array = []; diff --git a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js index a0cd29d601e309..0f838c2d836abd 100644 --- a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js @@ -16,7 +16,7 @@ const EMPTY_NATIVE_MODULES: SchemaType = { modules: { NativeSampleTurboModule: { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: { properties: [], }, @@ -29,7 +29,7 @@ const SIMPLE_NATIVE_MODULES: SchemaType = { modules: { NativeSampleTurboModule: { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: { properties: [ { @@ -341,7 +341,7 @@ const TWO_MODULES_DIFFERENT_FILES: SchemaType = { modules: { NativeSampleTurboModule: { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: { properties: [ { @@ -361,7 +361,7 @@ const TWO_MODULES_DIFFERENT_FILES: SchemaType = { }, NativeSampleTurboModule2: { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: { properties: [ { @@ -398,7 +398,7 @@ const COMPLEX_OBJECTS: SchemaType = { modules: { NativeSampleTurboModule: { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: { properties: [ { @@ -764,7 +764,7 @@ const NATIVE_MODULES_WITH_TYPE_ALIASES: SchemaType = { modules: { AliasTurboModule: { type: 'NativeModule', - aliases: { + aliasMap: { Options: { type: 'ObjectTypeAnnotation', properties: [ @@ -899,7 +899,7 @@ const REAL_MODULE_EXAMPLE: SchemaType = { modules: { NativeCameraRollManager: { type: 'NativeModule', - aliases: { + aliasMap: { PhotoIdentifierImage: { type: 'ObjectTypeAnnotation', properties: [ @@ -1226,7 +1226,7 @@ const REAL_MODULE_EXAMPLE: SchemaType = { }, NativeExceptionsManager: { type: 'NativeModule', - aliases: { + aliasMap: { StackFrame: { properties: [ { @@ -1494,7 +1494,7 @@ const CXX_ONLY_NATIVE_MODULES: SchemaType = { modules: { NativeSampleTurboModule: { type: 'NativeModule', - aliases: { + aliasMap: { ObjectAlias: { type: 'ObjectTypeAnnotation', properties: [ @@ -1648,7 +1648,7 @@ const SAMPLE_WITH_UPPERCASE_NAME: SchemaType = { modules: { NativeSampleTurboModule: { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: { properties: [], }, diff --git a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js index 74db8652b9ec0e..bd97c0f757a406 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js @@ -374,7 +374,7 @@ describe('buildSchemaFromConfigType', () => { const moduleSchemaMock = { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: {properties: []}, moduleName: '', }; @@ -657,13 +657,13 @@ describe('buildSchema', () => { const contents = ` import type {ViewProps} from 'ViewPropTypes'; import type {HostComponent} from 'react-native'; - + const codegenNativeComponent = require('codegenNativeComponent'); - + export type ModuleProps = $ReadOnly<{| ...ViewProps, |}>; - + export default (codegenNativeComponent( 'Module', ): HostComponent); @@ -712,11 +712,11 @@ describe('buildSchema', () => { const contents = ` import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; - + export interface Spec extends TurboModule { +getArray: (a: Array) => Array; } - + export default (TurboModuleRegistry.getEnforcing( 'SampleTurboModule', ): Spec); @@ -742,7 +742,7 @@ describe('buildSchema', () => { modules: { fileName: { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: { properties: [ { diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap index 390e3be33c30fd..ce01f78c2627f5 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap @@ -21,7 +21,7 @@ exports[`RN Codegen Flow Parser can generate fixture ANDROID_ONLY_NATIVE_MODULE 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [] }, @@ -39,7 +39,7 @@ exports[`RN Codegen Flow Parser can generate fixture CXX_ONLY_NATIVE_MODULE 1`] 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -220,7 +220,7 @@ exports[`RN Codegen Flow Parser can generate fixture EMPTY_NATIVE_MODULE 1`] = ` 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [] }, @@ -235,7 +235,7 @@ exports[`RN Codegen Flow Parser can generate fixture IOS_ONLY_NATIVE_MODULE 1`] 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -298,7 +298,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ALIASES 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'ObjectAlias': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -495,7 +495,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ARRAY_WI 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -536,7 +536,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ARRAY_WI 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -571,7 +571,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_BASIC_AR 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -637,7 +637,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_BASIC_PA 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -729,7 +729,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_CALLBACK 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -789,7 +789,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_COMPLEX_ 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -848,7 +848,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_COMPLEX_ 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1060,7 +1060,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_COMPLEX_ 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1164,7 +1164,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_FLOAT_AN 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1218,7 +1218,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_NESTED_A 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'Bar': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1309,7 +1309,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_NULLABLE 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1347,7 +1347,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_OBJECT_W 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'DisplayMetricsAndroid': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1436,7 +1436,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_PARTIALS 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'SomeObj': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1547,7 +1547,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_PARTIALS 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'SomeObj': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1642,7 +1642,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_PROMISE 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'SomeObj': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1714,7 +1714,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ROOT_TAG 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1751,7 +1751,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_SIMPLE_O 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1786,7 +1786,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_UNION 1` 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1847,7 +1847,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_UNSAFE_O 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1882,7 +1882,7 @@ exports[`RN Codegen Flow Parser can generate fixture PROMISE_WITH_COMMONLY_USED_ 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'CustomObject': { 'type': 'ObjectTypeAnnotation', 'properties': [ diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/module-parser-e2e-test.js b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/module-parser-e2e-test.js index 07f1ad56afc73e..6cda41648a0013 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/module-parser-e2e-test.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/module-parser-e2e-test.js @@ -60,7 +60,7 @@ type AnimalPointer = Animal; `; function expectAnimalTypeAliasToExist(module: NativeModuleSchema) { - const animalAlias = module.aliases.Animal; + const animalAlias = module.aliasMap.Animal; expect(animalAlias).not.toBe(null); invariant(animalAlias != null, ''); diff --git a/packages/react-native-codegen/src/parsers/flow/modules/index.js b/packages/react-native-codegen/src/parsers/flow/modules/index.js index 612d4c4ed2ccfe..d8fba8e465eff2 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/index.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/index.js @@ -452,7 +452,7 @@ function buildModuleSchema( .reduce( (moduleSchema: NativeModuleSchema, {aliasMap, propertyShape}) => ({ type: 'NativeModule', - aliases: {...moduleSchema.aliases, ...aliasMap}, + aliasMap: {...moduleSchema.aliasMap, ...aliasMap}, spec: { properties: [...moduleSchema.spec.properties, propertyShape], }, @@ -461,7 +461,7 @@ function buildModuleSchema( }), { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: {properties: []}, moduleName, excludedPlatforms: diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap index 38fd3e1186724a..60f9fbaba5a0b0 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap @@ -19,7 +19,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture ANDROID_ONLY_NATIVE_M 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [] }, @@ -37,7 +37,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture CXX_ONLY_NATIVE_MODUL 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -218,7 +218,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture EMPTY_NATIVE_MODULE 1 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [] }, @@ -233,7 +233,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture IOS_ONLY_NATIVE_MODUL 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -296,7 +296,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AL 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'ObjectAlias': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -493,7 +493,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -534,7 +534,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -569,7 +569,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -610,7 +610,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -645,7 +645,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BA 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -711,7 +711,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BA 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -777,7 +777,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BA 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -869,7 +869,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CA 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -929,7 +929,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CO 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -988,7 +988,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CO 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1047,7 +1047,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CO 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1259,7 +1259,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CO 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1363,7 +1363,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_FL 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1417,7 +1417,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_NE 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'Bar': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1508,7 +1508,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_NE 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'Bar': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1669,7 +1669,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_NU 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -1707,7 +1707,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_OB 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'DisplayMetricsAndroid': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1796,7 +1796,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_PA 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'SomeObj': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -1907,7 +1907,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_PA 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'SomeObj': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -2002,7 +2002,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_PR 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': { + 'aliasMap': { 'SomeObj': { 'type': 'ObjectTypeAnnotation', 'properties': [ @@ -2074,7 +2074,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_RO 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -2111,7 +2111,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_SI 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -2146,7 +2146,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_UN 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { @@ -2207,7 +2207,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_UN 'modules': { 'NativeSampleTurboModule': { 'type': 'NativeModule', - 'aliases': {}, + 'aliasMap': {}, 'spec': { 'properties': [ { diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/typescript-module-parser-e2e-test.js b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/typescript-module-parser-e2e-test.js index 507895ba589433..04b471ac808dd0 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/typescript-module-parser-e2e-test.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/typescript-module-parser-e2e-test.js @@ -60,7 +60,7 @@ type AnimalPointer = Animal; `; function expectAnimalTypeAliasToExist(module: NativeModuleSchema) { - const animalAlias = module.aliases.Animal; + const animalAlias = module.aliasMap.Animal; expect(animalAlias).not.toBe(null); invariant(animalAlias != null, ''); diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/index.js b/packages/react-native-codegen/src/parsers/typescript/modules/index.js index 74331eaeb81b8a..0d9c431ef2a22a 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -527,7 +527,7 @@ function buildModuleSchema( (moduleSchema: NativeModuleSchema, {aliasMap, propertyShape}) => { return { type: 'NativeModule', - aliases: {...moduleSchema.aliases, ...aliasMap}, + aliasMap: {...moduleSchema.aliasMap, ...aliasMap}, spec: { properties: [...moduleSchema.spec.properties, propertyShape], }, @@ -537,7 +537,7 @@ function buildModuleSchema( }, { type: 'NativeModule', - aliases: {}, + aliasMap: {}, spec: {properties: []}, moduleName: moduleName, excludedPlatforms: From f731151a901ae7089d06e125d5aee166907e9268 Mon Sep 17 00:00:00 2001 From: Evan Yeung Date: Thu, 2 Feb 2023 10:07:01 -0800 Subject: [PATCH 30/65] Deploy 0.199.0 to xplat Summary: Changelog: [Internal] Reviewed By: SamChou19815 Differential Revision: D42952050 fbshipit-source-id: a0c046860d3229a40ee3bf7f9c8bbc56fd63dc01 --- .flowconfig | 2 +- .flowconfig.android | 2 +- package.json | 2 +- repo-config/package.json | 2 +- yarn.lock | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.flowconfig b/.flowconfig index b332bb2b0c0b9a..de7069656623b9 100644 --- a/.flowconfig +++ b/.flowconfig @@ -74,4 +74,4 @@ untyped-import untyped-type-import [version] -^0.198.2 +^0.199.0 diff --git a/.flowconfig.android b/.flowconfig.android index 2c8f824a6548b3..dc5c94ab04e73d 100644 --- a/.flowconfig.android +++ b/.flowconfig.android @@ -74,4 +74,4 @@ untyped-import untyped-type-import [version] -^0.198.2 +^0.199.0 diff --git a/package.json b/package.json index d8a95a8941f325..0449e26a457ecc 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "ws": "^6.2.2" }, "devDependencies": { - "flow-bin": "^0.198.2", + "flow-bin": "^0.199.0", "hermes-eslint": "0.8.0", "mock-fs": "^5.1.4", "react": "18.2.0", diff --git a/repo-config/package.json b/repo-config/package.json index 5377cf8187ccb2..ff42ee1ffce4df 100644 --- a/repo-config/package.json +++ b/repo-config/package.json @@ -38,7 +38,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native": "^4.0.0", "eslint-plugin-relay": "^1.8.3", - "flow-bin": "^0.198.2", + "flow-bin": "^0.199.0", "inquirer": "^7.1.0", "jest": "^29.2.1", "jest-junit": "^10.0.0", diff --git a/yarn.lock b/yarn.lock index b5f311bbfedace..a671dba9d9fe24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4399,10 +4399,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== -flow-bin@^0.198.2: - version "0.198.2" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.198.2.tgz#e601121a411b4ec5889cc3567d2706369cc510a9" - integrity sha512-PML2zhAZVd8EgzDqS9oSJF+OxUtJ+YLEdVVsVO7d1BwnrXCgiVrLjxiQP8/hur820grzC51Xb2CI3J+ohH/bMg== +flow-bin@^0.199.0: + version "0.199.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.199.0.tgz#e710c0834d4e1032529a633e0cf32d89a102fcfb" + integrity sha512-8N8jn59ghgtDSogFoy1Ld1P8NlfdlVUcXSRADDf8WyX3SMMA6b1SbqraTRXxJDNn0F3WdVBHKdufdUg73y4Nhw== flow-parser@0.*, flow-parser@^0.185.0: version "0.185.0" From 6bbf323d68c8477ac5d8428d9ce030aa6116bdae Mon Sep 17 00:00:00 2001 From: Evan Yeung Date: Thu, 2 Feb 2023 12:05:39 -0800 Subject: [PATCH 31/65] Back out "Deploy 0.199.0 to xplat" Summary: Original commit changeset: a0c046860d32 Original Phabricator Diff: D42952050 (https://github.com/facebook/react-native/commit/f731151a901ae7089d06e125d5aee166907e9268) Changelog: [internal] Reviewed By: samwgoldman Differential Revision: D42972089 fbshipit-source-id: fe2b21d18e1a6a5a147a3ff6a4c0c7d5e6fc7226 --- .flowconfig | 2 +- .flowconfig.android | 2 +- package.json | 2 +- repo-config/package.json | 2 +- yarn.lock | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.flowconfig b/.flowconfig index de7069656623b9..b332bb2b0c0b9a 100644 --- a/.flowconfig +++ b/.flowconfig @@ -74,4 +74,4 @@ untyped-import untyped-type-import [version] -^0.199.0 +^0.198.2 diff --git a/.flowconfig.android b/.flowconfig.android index dc5c94ab04e73d..2c8f824a6548b3 100644 --- a/.flowconfig.android +++ b/.flowconfig.android @@ -74,4 +74,4 @@ untyped-import untyped-type-import [version] -^0.199.0 +^0.198.2 diff --git a/package.json b/package.json index 0449e26a457ecc..d8a95a8941f325 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "ws": "^6.2.2" }, "devDependencies": { - "flow-bin": "^0.199.0", + "flow-bin": "^0.198.2", "hermes-eslint": "0.8.0", "mock-fs": "^5.1.4", "react": "18.2.0", diff --git a/repo-config/package.json b/repo-config/package.json index ff42ee1ffce4df..5377cf8187ccb2 100644 --- a/repo-config/package.json +++ b/repo-config/package.json @@ -38,7 +38,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native": "^4.0.0", "eslint-plugin-relay": "^1.8.3", - "flow-bin": "^0.199.0", + "flow-bin": "^0.198.2", "inquirer": "^7.1.0", "jest": "^29.2.1", "jest-junit": "^10.0.0", diff --git a/yarn.lock b/yarn.lock index a671dba9d9fe24..b5f311bbfedace 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4399,10 +4399,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== -flow-bin@^0.199.0: - version "0.199.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.199.0.tgz#e710c0834d4e1032529a633e0cf32d89a102fcfb" - integrity sha512-8N8jn59ghgtDSogFoy1Ld1P8NlfdlVUcXSRADDf8WyX3SMMA6b1SbqraTRXxJDNn0F3WdVBHKdufdUg73y4Nhw== +flow-bin@^0.198.2: + version "0.198.2" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.198.2.tgz#e601121a411b4ec5889cc3567d2706369cc510a9" + integrity sha512-PML2zhAZVd8EgzDqS9oSJF+OxUtJ+YLEdVVsVO7d1BwnrXCgiVrLjxiQP8/hur820grzC51Xb2CI3J+ohH/bMg== flow-parser@0.*, flow-parser@^0.185.0: version "0.185.0" From e1a1e6aa8030bf11d691c3dcf7abd13b25175027 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Thu, 2 Feb 2023 14:04:12 -0800 Subject: [PATCH 32/65] Revert D13866122: Fix negative value rounding issue for nodes across an axis Differential Revision: D13866122 (https://github.com/facebook/react-native/commit/37171ec78f377fbae89ce43010f9cf69c1e60fbc) Original commit changeset: 4faf8a9efc86 Original Phabricator Diff: D13866122 (https://github.com/facebook/react-native/commit/37171ec78f377fbae89ce43010f9cf69c1e60fbc) fbshipit-source-id: c11919fdd585f09b0422e8db55a8a3d66027676f --- ReactCommon/yoga/yoga/Yoga.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ReactCommon/yoga/yoga/Yoga.cpp b/ReactCommon/yoga/yoga/Yoga.cpp index 5072992d9c3e96..2c8f45b02d6270 100644 --- a/ReactCommon/yoga/yoga/Yoga.cpp +++ b/ReactCommon/yoga/yoga/Yoga.cpp @@ -3701,12 +3701,16 @@ YOGA_EXPORT float YGRoundValueToPixelGrid( scaledValue = scaledValue - fractial + 1.0; } else if (forceCeil) { // Next we check if we need to use forced rounding - scaledValue = ceil(scaledValue); + scaledValue = scaledValue - fractial + 1.0; } else if (forceFloor) { - scaledValue = floor(scaledValue); + scaledValue = scaledValue - fractial; } else { // Finally we just round the value - scaledValue = round(scaledValue); + scaledValue = scaledValue - fractial + + (!YGDoubleIsUndefined(fractial) && + (fractial > 0.5 || YGDoubleEqual(fractial, 0.5)) + ? 1.0 + : 0.0); } return (YGDoubleIsUndefined(scaledValue) || YGDoubleIsUndefined(pointScaleFactor)) From 81d123c3995931e0582941c0a7b9ff3c44442a69 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Fri, 3 Feb 2023 03:03:41 -0800 Subject: [PATCH 33/65] Allow using flow enums in the react-native repo (#923) Summary: X-link: https://github.com/facebook/metro/pull/923 Changelog: [Internal] Allow using flow enums in the react-native repo Reviewed By: christophpurrer, yungsters Differential Revision: D42924379 fbshipit-source-id: b4c720428668ed853a1acfff68d4927e87bd6bc5 --- .flowconfig | 2 ++ .flowconfig.android | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.flowconfig b/.flowconfig index b332bb2b0c0b9a..747437031e30a1 100644 --- a/.flowconfig +++ b/.flowconfig @@ -29,6 +29,8 @@ interface.js flow/ [options] +enums=true + emoji=true exact_by_default=true diff --git a/.flowconfig.android b/.flowconfig.android index 2c8f824a6548b3..2f4a143d1677d3 100644 --- a/.flowconfig.android +++ b/.flowconfig.android @@ -29,6 +29,8 @@ interface.js flow/ [options] +enums=true + emoji=true exact_by_default=true From 209d0190552de142dc500498d553064106e4f7ab Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Fri, 3 Feb 2023 03:03:41 -0800 Subject: [PATCH 34/65] Added an e2e test fixture for Enums and .H+.C TM e2e snapshot tests Summary: * Added an e2e test fixture with all the supported enum variations * Added H and C file TM generators to the TM e2e tests Changelog: [Internal] Added an e2e test fixture for Enums and .H+.C TM e2e snapshot tests Reviewed By: cipolleschi Differential Revision: D42917330 fbshipit-source-id: 8e4adb86b6eb4e2b29a9e6d0cd6e4fd5b002ad1a --- .../modules/NativeEnumTurboModule.js | 68 + .../modules/GenerateModuleCpp-test.js | 52 + .../__tests__/modules/GenerateModuleH-test.js | 52 + .../GenerateModuleCpp-test.js.snap | 1047 +++++ .../GenerateModuleH-test.js.snap | 3601 +++++++++++++++++ .../GenerateModuleObjCpp-test.js.snap | 238 ++ 6 files changed, 5058 insertions(+) create mode 100644 packages/react-native-codegen/e2e/__test_fixtures__/modules/NativeEnumTurboModule.js create mode 100644 packages/react-native-codegen/e2e/__tests__/modules/GenerateModuleCpp-test.js create mode 100644 packages/react-native-codegen/e2e/__tests__/modules/GenerateModuleH-test.js create mode 100644 packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleCpp-test.js.snap create mode 100644 packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleH-test.js.snap diff --git a/packages/react-native-codegen/e2e/__test_fixtures__/modules/NativeEnumTurboModule.js b/packages/react-native-codegen/e2e/__test_fixtures__/modules/NativeEnumTurboModule.js new file mode 100644 index 00000000000000..1dbeeec74d8ec2 --- /dev/null +++ b/packages/react-native-codegen/e2e/__test_fixtures__/modules/NativeEnumTurboModule.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type StateType = {| + state: string, +|}; + +export enum StatusRegularEnum { + Active, + Paused, + Off, +} + +export enum StatusStrEnum { + Active = 'active', + Paused = 'paused', + Off = 'off', +} + +export enum StatusNumEnum { + Active = 2, + Paused = 1, + Off = 0, +} + +export enum StatusFractionEnum { + Active = 0.2, + Paused = 0.1, + Off = 0.0, +} + +export type StateTypeWithEnums = {| + state: string, + regular: StatusRegularEnum, + str: StatusStrEnum, + num: StatusNumEnum, + fraction: StatusFractionEnum, +|}; + +export interface Spec extends TurboModule { + +getStatusRegular: (statusProp: StateType) => StatusRegularEnum; + +getStatusStr: (statusProp: StateType) => StatusStrEnum; + +getStatusNum: (statusProp: StateType) => StatusNumEnum; + +getStatusFraction: (statusProp: StateType) => StatusFractionEnum; + +getStateType: ( + a: StatusRegularEnum, + b: StatusStrEnum, + c: StatusNumEnum, + d: StatusFractionEnum, + ) => StateType; + +getStateTypeWithEnums: ( + paramOfTypeWithEnums: StateTypeWithEnums, + ) => StateTypeWithEnums; +} + +export default (TurboModuleRegistry.getEnforcing( + 'NativeEnumTurboModule', +): Spec); diff --git a/packages/react-native-codegen/e2e/__tests__/modules/GenerateModuleCpp-test.js b/packages/react-native-codegen/e2e/__tests__/modules/GenerateModuleCpp-test.js new file mode 100644 index 00000000000000..662a8fd0ddb3db --- /dev/null +++ b/packages/react-native-codegen/e2e/__tests__/modules/GenerateModuleCpp-test.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +'use strict'; + +const {FlowParser} = require('../../../src/parsers/flow/parser'); +const generator = require('../../../src/generators/modules/GenerateModuleCpp'); +const fs = require('fs'); + +import type {SchemaType} from '../../../src/CodegenSchema'; + +const FIXTURE_DIR = `${__dirname}/../../__test_fixtures__/modules`; + +const parser = new FlowParser(); + +function getModules(): SchemaType { + const filenames: Array = fs.readdirSync(FIXTURE_DIR); + return filenames.reduce( + (accumulator, file) => { + const schema = parser.parseFile(`${FIXTURE_DIR}/${file}`); + return { + modules: { + ...accumulator.modules, + ...schema.modules, + }, + }; + }, + {modules: {}}, + ); +} + +describe('GenerateModuleCpp', () => { + it('can generate an implementation file NativeModule specs', () => { + const libName = 'RNCodegenModuleFixtures'; + const output = generator.generate(libName, getModules(), undefined, false); + expect(output.get(libName + 'JSI-generated.cpp')).toMatchSnapshot(); + }); + + it('can generate a header file NativeModule specs with assume nonnull enabled', () => { + const libName = 'RNCodegenModuleFixtures'; + const output = generator.generate(libName, getModules(), undefined, true); + expect(output.get(libName + 'JSI-generated.cpp')).toMatchSnapshot(); + }); +}); diff --git a/packages/react-native-codegen/e2e/__tests__/modules/GenerateModuleH-test.js b/packages/react-native-codegen/e2e/__tests__/modules/GenerateModuleH-test.js new file mode 100644 index 00000000000000..51b6c8e37b6769 --- /dev/null +++ b/packages/react-native-codegen/e2e/__tests__/modules/GenerateModuleH-test.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +'use strict'; + +const {FlowParser} = require('../../../src/parsers/flow/parser'); +const generator = require('../../../src/generators/modules/GenerateModuleH'); +const fs = require('fs'); + +import type {SchemaType} from '../../../src/CodegenSchema'; + +const FIXTURE_DIR = `${__dirname}/../../__test_fixtures__/modules`; + +const parser = new FlowParser(); + +function getModules(): SchemaType { + const filenames: Array = fs.readdirSync(FIXTURE_DIR); + return filenames.reduce( + (accumulator, file) => { + const schema = parser.parseFile(`${FIXTURE_DIR}/${file}`); + return { + modules: { + ...accumulator.modules, + ...schema.modules, + }, + }; + }, + {modules: {}}, + ); +} + +describe('GenerateModuleH', () => { + it('can generate a header file NativeModule specs', () => { + const libName = 'RNCodegenModuleFixtures'; + const output = generator.generate(libName, getModules(), undefined, false); + expect(output.get(libName + 'JSI.h')).toMatchSnapshot(); + }); + + it('can generate a header file NativeModule specs with assume nonnull enabled', () => { + const libName = 'RNCodegenModuleFixtures'; + const output = generator.generate(libName, getModules(), undefined, true); + expect(output.get(libName + 'JSI.h')).toMatchSnapshot(); + }); +}); diff --git a/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleCpp-test.js.snap b/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleCpp-test.js.snap new file mode 100644 index 00000000000000..06f976b26535a7 --- /dev/null +++ b/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleCpp-test.js.snap @@ -0,0 +1,1047 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenerateModuleCpp can generate a header file NativeModule specs with assume nonnull enabled 1`] = ` +"/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateModuleH.js + */ + +#include \\"RNCodegenModuleFixturesJSI.h\\" + +namespace facebook { +namespace react { + +static jsi::Value __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArray(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getReadOnlyArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getReadOnlyArray(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getArrayWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArrayWithAlias(rt, args[0].asObject(rt).asArray(rt), args[1].asObject(rt).asArray(rt)); +} + +NativeArrayTurboModuleCxxSpecJSI::NativeArrayTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getArray}; + methodMap_[\\"getReadOnlyArray\\"] = MethodMetadata {1, __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getReadOnlyArray}; + methodMap_[\\"getArrayWithAlias\\"] = MethodMetadata {2, __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getArrayWithAlias}; +} +static jsi::Value __hostFunction_NativeBooleanTurboModuleCxxSpecJSI_getBoolean(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBoolean(rt, args[0].asBool()); +} +static jsi::Value __hostFunction_NativeBooleanTurboModuleCxxSpecJSI_getBooleanWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBooleanWithAlias(rt, args[0].asBool()); +} + +NativeBooleanTurboModuleCxxSpecJSI::NativeBooleanTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getBoolean\\"] = MethodMetadata {1, __hostFunction_NativeBooleanTurboModuleCxxSpecJSI_getBoolean}; + methodMap_[\\"getBooleanWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeBooleanTurboModuleCxxSpecJSI_getBooleanWithAlias}; +} +static jsi::Value __hostFunction_NativeCallbackTurboModuleCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeCallbackTurboModuleCxxSpecJSI_getValueWithCallbackWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallbackWithAlias(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} + +NativeCallbackTurboModuleCxxSpecJSI::NativeCallbackTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeCallbackTurboModuleCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithCallbackWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeCallbackTurboModuleCxxSpecJSI_getValueWithCallbackWithAlias}; +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusRegular(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStatusRegular(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusStr(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStatusStr(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusNum(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStatusNum(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusFraction(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStatusFraction(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStateType(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStateType(rt, args[0].asString(rt), args[1].asString(rt), args[2].asNumber(), args[3].asNumber()); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStateTypeWithEnums(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStateTypeWithEnums(rt, args[0].asObject(rt)); +} + +NativeEnumTurboModuleCxxSpecJSI::NativeEnumTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"NativeEnumTurboModule\\", jsInvoker) { + methodMap_[\\"getStatusRegular\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusRegular}; + methodMap_[\\"getStatusStr\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusStr}; + methodMap_[\\"getStatusNum\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusNum}; + methodMap_[\\"getStatusFraction\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusFraction}; + methodMap_[\\"getStateType\\"] = MethodMetadata {4, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStateType}; + methodMap_[\\"getStateTypeWithEnums\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStateTypeWithEnums}; +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getBool(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getNumber(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getString(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getArray(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asArray(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getObject(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValueWithPromise(rt); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} + +NativeNullableTurboModuleCxxSpecJSI::NativeNullableTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getObject}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {0, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeNumberTurboModuleCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumber(rt, args[0].asNumber()); +} +static jsi::Value __hostFunction_NativeNumberTurboModuleCxxSpecJSI_getNumberWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumberWithAlias(rt, args[0].asNumber()); +} + +NativeNumberTurboModuleCxxSpecJSI::NativeNumberTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeNumberTurboModuleCxxSpecJSI_getNumber}; + methodMap_[\\"getNumberWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeNumberTurboModuleCxxSpecJSI_getNumberWithAlias}; +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getGenericObject(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObjectReadOnly(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getGenericObjectReadOnly(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObjectWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getGenericObjectWithAlias(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_difficultObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->difficultObject(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} + +NativeObjectTurboModuleCxxSpecJSI::NativeObjectTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getGenericObject\\"] = MethodMetadata {1, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObject}; + methodMap_[\\"getGenericObjectReadOnly\\"] = MethodMetadata {1, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObjectReadOnly}; + methodMap_[\\"getGenericObjectWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObjectWithAlias}; + methodMap_[\\"difficultObject\\"] = MethodMetadata {1, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_difficultObject}; + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getConstants}; +} +static jsi::Value __hostFunction_NativeOptionalObjectTurboModuleCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} + +NativeOptionalObjectTurboModuleCxxSpecJSI::NativeOptionalObjectTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeOptionalObjectTurboModuleCxxSpecJSI_getConstants}; +} +static jsi::Value __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getSomeObj(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getSomeObj(rt); +} +static jsi::Value __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getPartialSomeObj(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getPartialSomeObj(rt); +} +static jsi::Value __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getSomeObjFromPartialSomeObj(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getSomeObjFromPartialSomeObj(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getPartialPartial(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getPartialPartial(rt, args[0].asObject(rt), args[1].asObject(rt)); +} + +NativePartialAnnotationTurboModuleCxxSpecJSI::NativePartialAnnotationTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"NativePartialAnnotationTurboModule\\", jsInvoker) { + methodMap_[\\"getSomeObj\\"] = MethodMetadata {0, __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getSomeObj}; + methodMap_[\\"getPartialSomeObj\\"] = MethodMetadata {0, __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getPartialSomeObj}; + methodMap_[\\"getSomeObjFromPartialSomeObj\\"] = MethodMetadata {1, __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getSomeObjFromPartialSomeObj}; + methodMap_[\\"getPartialPartial\\"] = MethodMetadata {2, __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getPartialPartial}; +} +static jsi::Value __hostFunction_NativePromiseTurboModuleCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromise(rt, args[0].asBool()); +} +static jsi::Value __hostFunction_NativePromiseTurboModuleCxxSpecJSI_getValueWithPromiseWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromiseWithAlias(rt, args[0].asString(rt)); +} + +NativePromiseTurboModuleCxxSpecJSI::NativePromiseTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativePromiseTurboModuleCxxSpecJSI_getValueWithPromise}; + methodMap_[\\"getValueWithPromiseWithAlias\\"] = MethodMetadata {1, __hostFunction_NativePromiseTurboModuleCxxSpecJSI_getValueWithPromiseWithAlias}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBool(rt, args[0].asBool()); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumber(rt, args[0].asNumber()); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getString(rt, args[0].asString(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArray(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObject(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObjectShape(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getAlias(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getRootTag(rt, args[0].getNumber()); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValue(rt, args[0].asNumber(), args[1].asString(rt), args[2].asObject(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromise(rt, args[0].asBool()); +} + +NativeSampleTurboModuleCxxSpecJSI::NativeSampleTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBool(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumber(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getString(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArray(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObject(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObjectShape(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getAlias(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getRootTag(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValue(rt, args[0].asObject(rt).asArray(rt), args[1].asObject(rt).asArray(rt), args[2].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromise(rt, args[0].asObject(rt).asArray(rt)); +} + +NativeSampleTurboModuleArraysCxxSpecJSI::NativeSampleTurboModuleArraysCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleArrays\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getBool(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getNumber(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getString(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asString(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getArray(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asArray(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getObject(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getObjectShape(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getAlias(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getRootTag(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].getNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValue(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber()), args[1].isNull() || args[1].isUndefined() ? std::nullopt : std::make_optional(args[1].asString(rt)), args[2].isNull() || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValueWithPromise(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} + +NativeSampleTurboModuleNullableCxxSpecJSI::NativeSampleTurboModuleNullableCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleNullable\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getBool(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getNumber(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getString(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asString(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getArray(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asArray(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getObject(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObjectShape(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getAlias(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getRootTag(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].getNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValue(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber()), count < 1 || args[1].isNull() || args[1].isUndefined() ? std::nullopt : std::make_optional(args[1].asString(rt)), count < 2 || args[2].isNull() || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asFunction(rt))); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValueWithPromise(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} + +NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI::NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleNullableAndOptional\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBool(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumber(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getString(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asString(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArray(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asArray(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObject(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObjectShape(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getAlias(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getRootTag(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].getNumber())); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValue(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber()), count < 1 || args[1].isNull() || args[1].isUndefined() ? std::nullopt : std::make_optional(args[1].asString(rt)), count < 2 || args[2].isNull() || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asFunction(rt))); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromise(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); +} + +NativeSampleTurboModuleOptionalCxxSpecJSI::NativeSampleTurboModuleOptionalCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleOptional\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeStringTurboModuleCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getString(rt, args[0].asString(rt)); +} +static jsi::Value __hostFunction_NativeStringTurboModuleCxxSpecJSI_getStringWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStringWithAlias(rt, args[0].asString(rt)); +} + +NativeStringTurboModuleCxxSpecJSI::NativeStringTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeStringTurboModuleCxxSpecJSI_getString}; + methodMap_[\\"getStringWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeStringTurboModuleCxxSpecJSI_getStringWithAlias}; +} + + +} // namespace react +} // namespace facebook +" +`; + +exports[`GenerateModuleCpp can generate an implementation file NativeModule specs 1`] = ` +"/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateModuleH.js + */ + +#include \\"RNCodegenModuleFixturesJSI.h\\" + +namespace facebook { +namespace react { + +static jsi::Value __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArray(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getReadOnlyArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getReadOnlyArray(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getArrayWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArrayWithAlias(rt, args[0].asObject(rt).asArray(rt), args[1].asObject(rt).asArray(rt)); +} + +NativeArrayTurboModuleCxxSpecJSI::NativeArrayTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getArray}; + methodMap_[\\"getReadOnlyArray\\"] = MethodMetadata {1, __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getReadOnlyArray}; + methodMap_[\\"getArrayWithAlias\\"] = MethodMetadata {2, __hostFunction_NativeArrayTurboModuleCxxSpecJSI_getArrayWithAlias}; +} +static jsi::Value __hostFunction_NativeBooleanTurboModuleCxxSpecJSI_getBoolean(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBoolean(rt, args[0].asBool()); +} +static jsi::Value __hostFunction_NativeBooleanTurboModuleCxxSpecJSI_getBooleanWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBooleanWithAlias(rt, args[0].asBool()); +} + +NativeBooleanTurboModuleCxxSpecJSI::NativeBooleanTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getBoolean\\"] = MethodMetadata {1, __hostFunction_NativeBooleanTurboModuleCxxSpecJSI_getBoolean}; + methodMap_[\\"getBooleanWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeBooleanTurboModuleCxxSpecJSI_getBooleanWithAlias}; +} +static jsi::Value __hostFunction_NativeCallbackTurboModuleCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeCallbackTurboModuleCxxSpecJSI_getValueWithCallbackWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallbackWithAlias(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} + +NativeCallbackTurboModuleCxxSpecJSI::NativeCallbackTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeCallbackTurboModuleCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithCallbackWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeCallbackTurboModuleCxxSpecJSI_getValueWithCallbackWithAlias}; +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusRegular(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStatusRegular(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusStr(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStatusStr(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusNum(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStatusNum(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusFraction(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStatusFraction(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStateType(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStateType(rt, args[0].asString(rt), args[1].asString(rt), args[2].asNumber(), args[3].asNumber()); +} +static jsi::Value __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStateTypeWithEnums(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStateTypeWithEnums(rt, args[0].asObject(rt)); +} + +NativeEnumTurboModuleCxxSpecJSI::NativeEnumTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"NativeEnumTurboModule\\", jsInvoker) { + methodMap_[\\"getStatusRegular\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusRegular}; + methodMap_[\\"getStatusStr\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusStr}; + methodMap_[\\"getStatusNum\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusNum}; + methodMap_[\\"getStatusFraction\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStatusFraction}; + methodMap_[\\"getStateType\\"] = MethodMetadata {4, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStateType}; + methodMap_[\\"getStateTypeWithEnums\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleCxxSpecJSI_getStateTypeWithEnums}; +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getBool(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getNumber(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getString(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getArray(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asArray(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getObject(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValueWithPromise(rt); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} + +NativeNullableTurboModuleCxxSpecJSI::NativeNullableTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getObject}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {0, __hostFunction_NativeNullableTurboModuleCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeNumberTurboModuleCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumber(rt, args[0].asNumber()); +} +static jsi::Value __hostFunction_NativeNumberTurboModuleCxxSpecJSI_getNumberWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumberWithAlias(rt, args[0].asNumber()); +} + +NativeNumberTurboModuleCxxSpecJSI::NativeNumberTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeNumberTurboModuleCxxSpecJSI_getNumber}; + methodMap_[\\"getNumberWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeNumberTurboModuleCxxSpecJSI_getNumberWithAlias}; +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getGenericObject(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObjectReadOnly(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getGenericObjectReadOnly(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObjectWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getGenericObjectWithAlias(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_difficultObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->difficultObject(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} + +NativeObjectTurboModuleCxxSpecJSI::NativeObjectTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getGenericObject\\"] = MethodMetadata {1, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObject}; + methodMap_[\\"getGenericObjectReadOnly\\"] = MethodMetadata {1, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObjectReadOnly}; + methodMap_[\\"getGenericObjectWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getGenericObjectWithAlias}; + methodMap_[\\"difficultObject\\"] = MethodMetadata {1, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_difficultObject}; + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeObjectTurboModuleCxxSpecJSI_getConstants}; +} +static jsi::Value __hostFunction_NativeOptionalObjectTurboModuleCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} + +NativeOptionalObjectTurboModuleCxxSpecJSI::NativeOptionalObjectTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeOptionalObjectTurboModuleCxxSpecJSI_getConstants}; +} +static jsi::Value __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getSomeObj(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getSomeObj(rt); +} +static jsi::Value __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getPartialSomeObj(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getPartialSomeObj(rt); +} +static jsi::Value __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getSomeObjFromPartialSomeObj(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getSomeObjFromPartialSomeObj(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getPartialPartial(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getPartialPartial(rt, args[0].asObject(rt), args[1].asObject(rt)); +} + +NativePartialAnnotationTurboModuleCxxSpecJSI::NativePartialAnnotationTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"NativePartialAnnotationTurboModule\\", jsInvoker) { + methodMap_[\\"getSomeObj\\"] = MethodMetadata {0, __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getSomeObj}; + methodMap_[\\"getPartialSomeObj\\"] = MethodMetadata {0, __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getPartialSomeObj}; + methodMap_[\\"getSomeObjFromPartialSomeObj\\"] = MethodMetadata {1, __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getSomeObjFromPartialSomeObj}; + methodMap_[\\"getPartialPartial\\"] = MethodMetadata {2, __hostFunction_NativePartialAnnotationTurboModuleCxxSpecJSI_getPartialPartial}; +} +static jsi::Value __hostFunction_NativePromiseTurboModuleCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromise(rt, args[0].asBool()); +} +static jsi::Value __hostFunction_NativePromiseTurboModuleCxxSpecJSI_getValueWithPromiseWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromiseWithAlias(rt, args[0].asString(rt)); +} + +NativePromiseTurboModuleCxxSpecJSI::NativePromiseTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativePromiseTurboModuleCxxSpecJSI_getValueWithPromise}; + methodMap_[\\"getValueWithPromiseWithAlias\\"] = MethodMetadata {1, __hostFunction_NativePromiseTurboModuleCxxSpecJSI_getValueWithPromiseWithAlias}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBool(rt, args[0].asBool()); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumber(rt, args[0].asNumber()); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getString(rt, args[0].asString(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArray(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObject(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObjectShape(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getAlias(rt, args[0].asObject(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getRootTag(rt, args[0].getNumber()); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValue(rt, args[0].asNumber(), args[1].asString(rt), args[2].asObject(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromise(rt, args[0].asBool()); +} + +NativeSampleTurboModuleCxxSpecJSI::NativeSampleTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBool(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumber(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getString(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArray(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObject(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObjectShape(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getAlias(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getRootTag(rt, args[0].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValue(rt, args[0].asObject(rt).asArray(rt), args[1].asObject(rt).asArray(rt), args[2].asObject(rt).asArray(rt)); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromise(rt, args[0].asObject(rt).asArray(rt)); +} + +NativeSampleTurboModuleArraysCxxSpecJSI::NativeSampleTurboModuleArraysCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleArrays\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleArraysCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getBool(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getNumber(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getString(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asString(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getArray(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asArray(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getObject(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getObjectShape(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getAlias(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getRootTag(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].getNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValue(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber()), args[1].isNull() || args[1].isUndefined() ? std::nullopt : std::make_optional(args[1].asString(rt)), args[2].isNull() || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, args[0].asObject(rt).asFunction(rt)); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValueWithPromise(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} + +NativeSampleTurboModuleNullableCxxSpecJSI::NativeSampleTurboModuleNullableCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleNullable\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getBool(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getNumber(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getString(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asString(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getArray(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asArray(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getObject(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObjectShape(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getAlias(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getRootTag(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].getNumber())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValue(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber()), count < 1 || args[1].isNull() || args[1].isUndefined() ? std::nullopt : std::make_optional(args[1].asString(rt)), count < 2 || args[2].isNull() || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asObject(rt))); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asFunction(rt))); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + auto result = static_cast(&turboModule)->getValueWithPromise(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); + return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); +} + +NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI::NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleNullableAndOptional\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getConstants(rt); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_voidFunc(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->voidFunc(rt); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getBool(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getBool(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getNumber(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getNumber(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber())); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getString(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asString(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getArray(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asArray(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getObject(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObject(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getObjectShape(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getObjectShape(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getAlias(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getRootTag(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getRootTag(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].getNumber())); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValue(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValue(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asNumber()), count < 1 || args[1].isNull() || args[1].isUndefined() ? std::nullopt : std::make_optional(args[1].asString(rt)), count < 2 || args[2].isNull() || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asObject(rt))); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValueWithCallback(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_cast(&turboModule)->getValueWithCallback(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).asFunction(rt))); + return jsi::Value::undefined(); +} +static jsi::Value __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValueWithPromise(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getValueWithPromise(rt, count < 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asBool())); +} + +NativeSampleTurboModuleOptionalCxxSpecJSI::NativeSampleTurboModuleOptionalCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleOptional\\", jsInvoker) { + methodMap_[\\"getConstants\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getConstants}; + methodMap_[\\"voidFunc\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_voidFunc}; + methodMap_[\\"getBool\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getBool}; + methodMap_[\\"getNumber\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getNumber}; + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getString}; + methodMap_[\\"getArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getArray}; + methodMap_[\\"getObject\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getObject}; + methodMap_[\\"getObjectShape\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getObjectShape}; + methodMap_[\\"getAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getAlias}; + methodMap_[\\"getRootTag\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getRootTag}; + methodMap_[\\"getValue\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValue}; + methodMap_[\\"getValueWithCallback\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValueWithCallback}; + methodMap_[\\"getValueWithPromise\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleOptionalCxxSpecJSI_getValueWithPromise}; +} +static jsi::Value __hostFunction_NativeStringTurboModuleCxxSpecJSI_getString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getString(rt, args[0].asString(rt)); +} +static jsi::Value __hostFunction_NativeStringTurboModuleCxxSpecJSI_getStringWithAlias(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getStringWithAlias(rt, args[0].asString(rt)); +} + +NativeStringTurboModuleCxxSpecJSI::NativeStringTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker) { + methodMap_[\\"getString\\"] = MethodMetadata {1, __hostFunction_NativeStringTurboModuleCxxSpecJSI_getString}; + methodMap_[\\"getStringWithAlias\\"] = MethodMetadata {1, __hostFunction_NativeStringTurboModuleCxxSpecJSI_getStringWithAlias}; +} + + +} // namespace react +} // namespace facebook +" +`; diff --git a/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleH-test.js.snap b/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleH-test.js.snap new file mode 100644 index 00000000000000..20b3a737fbee08 --- /dev/null +++ b/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleH-test.js.snap @@ -0,0 +1,3601 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenerateModuleH can generate a header file NativeModule specs 1`] = ` +"/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateModuleH.js + */ + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +class JSI_EXPORT NativeArrayTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeArrayTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Array getArray(jsi::Runtime &rt, jsi::Array a) = 0; + virtual jsi::Array getReadOnlyArray(jsi::Runtime &rt, jsi::Array a) = 0; + virtual jsi::Array getArrayWithAlias(jsi::Runtime &rt, jsi::Array a, jsi::Array b) = 0; + +}; + +template +class JSI_EXPORT NativeArrayTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeArrayTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeArrayTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeArrayTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Array getArray(jsi::Runtime &rt, jsi::Array a) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArray, jsInvoker_, instance_, std::move(a)); + } + jsi::Array getReadOnlyArray(jsi::Runtime &rt, jsi::Array a) override { + static_assert( + bridging::getParameterCount(&T::getReadOnlyArray) == 2, + \\"Expected getReadOnlyArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getReadOnlyArray, jsInvoker_, instance_, std::move(a)); + } + jsi::Array getArrayWithAlias(jsi::Runtime &rt, jsi::Array a, jsi::Array b) override { + static_assert( + bridging::getParameterCount(&T::getArrayWithAlias) == 3, + \\"Expected getArrayWithAlias(...) to have 3 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArrayWithAlias, jsInvoker_, instance_, std::move(a), std::move(b)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeBooleanTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeBooleanTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual bool getBoolean(jsi::Runtime &rt, bool arg) = 0; + virtual bool getBooleanWithAlias(jsi::Runtime &rt, bool arg) = 0; + +}; + +template +class JSI_EXPORT NativeBooleanTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeBooleanTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeBooleanTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeBooleanTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + bool getBoolean(jsi::Runtime &rt, bool arg) override { + static_assert( + bridging::getParameterCount(&T::getBoolean) == 2, + \\"Expected getBoolean(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBoolean, jsInvoker_, instance_, std::move(arg)); + } + bool getBooleanWithAlias(jsi::Runtime &rt, bool arg) override { + static_assert( + bridging::getParameterCount(&T::getBooleanWithAlias) == 2, + \\"Expected getBooleanWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBooleanWithAlias, jsInvoker_, instance_, std::move(arg)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeCallbackTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeCallbackTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) = 0; + virtual void getValueWithCallbackWithAlias(jsi::Runtime &rt, jsi::Function c) = 0; + +}; + +template +class JSI_EXPORT NativeCallbackTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeCallbackTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeCallbackTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeCallbackTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + void getValueWithCallbackWithAlias(jsi::Runtime &rt, jsi::Function c) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallbackWithAlias) == 2, + \\"Expected getValueWithCallbackWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallbackWithAlias, jsInvoker_, instance_, std::move(c)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - NativeEnumTurboModuleBaseStateType + +template +struct NativeEnumTurboModuleBaseStateType { + P0 state; + bool operator==(const NativeEnumTurboModuleBaseStateType &other) const { + return state == other.state; + } +}; + +template +struct NativeEnumTurboModuleBaseStateTypeBridging { + static NativeEnumTurboModuleBaseStateType fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + NativeEnumTurboModuleBaseStateType result{ + bridging::fromJs(rt, value.getProperty(rt, \\"state\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String stateToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const NativeEnumTurboModuleBaseStateType &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"state\\", bridging::toJs(rt, value.state, jsInvoker)); + return result; + } + }; + + +#pragma mark - NativeEnumTurboModuleBaseStateTypeWithEnums + +template +struct NativeEnumTurboModuleBaseStateTypeWithEnums { + P0 state; + P1 regular; + P2 str; + P3 num; + P4 fraction; + bool operator==(const NativeEnumTurboModuleBaseStateTypeWithEnums &other) const { + return state == other.state && regular == other.regular && str == other.str && num == other.num && fraction == other.fraction; + } +}; + +template +struct NativeEnumTurboModuleBaseStateTypeWithEnumsBridging { + static NativeEnumTurboModuleBaseStateTypeWithEnums fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + NativeEnumTurboModuleBaseStateTypeWithEnums result{ + bridging::fromJs(rt, value.getProperty(rt, \\"state\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"regular\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"str\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"num\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"fraction\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String stateToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } + static jsi::String regularToJs(jsi::Runtime &rt, P1 value) { + return bridging::toJs(rt, value); + } + static jsi::String strToJs(jsi::Runtime &rt, P2 value) { + return bridging::toJs(rt, value); + } + static double numToJs(jsi::Runtime &rt, P3 value) { + return bridging::toJs(rt, value); + } + static double fractionToJs(jsi::Runtime &rt, P4 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const NativeEnumTurboModuleBaseStateTypeWithEnums &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"state\\", bridging::toJs(rt, value.state, jsInvoker)); + result.setProperty(rt, \\"regular\\", bridging::toJs(rt, value.regular, jsInvoker)); + result.setProperty(rt, \\"str\\", bridging::toJs(rt, value.str, jsInvoker)); + result.setProperty(rt, \\"num\\", bridging::toJs(rt, value.num, jsInvoker)); + result.setProperty(rt, \\"fraction\\", bridging::toJs(rt, value.fraction, jsInvoker)); + return result; + } + }; + +class JSI_EXPORT NativeEnumTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeEnumTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::String getStatusRegular(jsi::Runtime &rt, jsi::Object statusProp) = 0; + virtual jsi::String getStatusStr(jsi::Runtime &rt, jsi::Object statusProp) = 0; + virtual double getStatusNum(jsi::Runtime &rt, jsi::Object statusProp) = 0; + virtual double getStatusFraction(jsi::Runtime &rt, jsi::Object statusProp) = 0; + virtual jsi::Object getStateType(jsi::Runtime &rt, jsi::String a, jsi::String b, double c, double d) = 0; + virtual jsi::Object getStateTypeWithEnums(jsi::Runtime &rt, jsi::Object paramOfTypeWithEnums) = 0; + +}; + +template +class JSI_EXPORT NativeEnumTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeEnumTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"NativeEnumTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeEnumTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeEnumTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::String getStatusRegular(jsi::Runtime &rt, jsi::Object statusProp) override { + static_assert( + bridging::getParameterCount(&T::getStatusRegular) == 2, + \\"Expected getStatusRegular(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStatusRegular, jsInvoker_, instance_, std::move(statusProp)); + } + jsi::String getStatusStr(jsi::Runtime &rt, jsi::Object statusProp) override { + static_assert( + bridging::getParameterCount(&T::getStatusStr) == 2, + \\"Expected getStatusStr(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStatusStr, jsInvoker_, instance_, std::move(statusProp)); + } + double getStatusNum(jsi::Runtime &rt, jsi::Object statusProp) override { + static_assert( + bridging::getParameterCount(&T::getStatusNum) == 2, + \\"Expected getStatusNum(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStatusNum, jsInvoker_, instance_, std::move(statusProp)); + } + double getStatusFraction(jsi::Runtime &rt, jsi::Object statusProp) override { + static_assert( + bridging::getParameterCount(&T::getStatusFraction) == 2, + \\"Expected getStatusFraction(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStatusFraction, jsInvoker_, instance_, std::move(statusProp)); + } + jsi::Object getStateType(jsi::Runtime &rt, jsi::String a, jsi::String b, double c, double d) override { + static_assert( + bridging::getParameterCount(&T::getStateType) == 5, + \\"Expected getStateType(...) to have 5 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStateType, jsInvoker_, instance_, std::move(a), std::move(b), std::move(c), std::move(d)); + } + jsi::Object getStateTypeWithEnums(jsi::Runtime &rt, jsi::Object paramOfTypeWithEnums) override { + static_assert( + bridging::getParameterCount(&T::getStateTypeWithEnums) == 2, + \\"Expected getStateTypeWithEnums(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStateTypeWithEnums, jsInvoker_, instance_, std::move(paramOfTypeWithEnums)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeNullableTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeNullableTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual std::optional getBool(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getNumber(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getString(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getArray(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getObject(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getValueWithPromise(jsi::Runtime &rt) = 0; + +}; + +template +class JSI_EXPORT NativeNullableTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeNullableTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeNullableTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeNullableTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + std::optional getBool(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getBool, jsInvoker_, instance_, std::move(a)); + } + std::optional getNumber(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getNumber, jsInvoker_, instance_, std::move(a)); + } + std::optional getString(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getString, jsInvoker_, instance_, std::move(a)); + } + std::optional getArray(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getArray, jsInvoker_, instance_, std::move(a)); + } + std::optional getObject(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getObject, jsInvoker_, instance_, std::move(a)); + } + std::optional getValueWithPromise(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 1, + \\"Expected getValueWithPromise(...) to have 1 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValueWithPromise, jsInvoker_, instance_); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeNumberTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeNumberTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual double getNumber(jsi::Runtime &rt, double arg) = 0; + virtual double getNumberWithAlias(jsi::Runtime &rt, double arg) = 0; + +}; + +template +class JSI_EXPORT NativeNumberTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeNumberTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeNumberTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeNumberTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + double getNumber(jsi::Runtime &rt, double arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + double getNumberWithAlias(jsi::Runtime &rt, double arg) override { + static_assert( + bridging::getParameterCount(&T::getNumberWithAlias) == 2, + \\"Expected getNumberWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumberWithAlias, jsInvoker_, instance_, std::move(arg)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeObjectTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeObjectTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getGenericObject(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object getGenericObjectReadOnly(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object getGenericObjectWithAlias(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object difficultObject(jsi::Runtime &rt, jsi::Object A) = 0; + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + +}; + +template +class JSI_EXPORT NativeObjectTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeObjectTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeObjectTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeObjectTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getGenericObject(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getGenericObject) == 2, + \\"Expected getGenericObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getGenericObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getGenericObjectReadOnly(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getGenericObjectReadOnly) == 2, + \\"Expected getGenericObjectReadOnly(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getGenericObjectReadOnly, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getGenericObjectWithAlias(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getGenericObjectWithAlias) == 2, + \\"Expected getGenericObjectWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getGenericObjectWithAlias, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object difficultObject(jsi::Runtime &rt, jsi::Object A) override { + static_assert( + bridging::getParameterCount(&T::difficultObject) == 2, + \\"Expected difficultObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::difficultObject, jsInvoker_, instance_, std::move(A)); + } + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeOptionalObjectTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeOptionalObjectTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + +}; + +template +class JSI_EXPORT NativeOptionalObjectTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeOptionalObjectTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeOptionalObjectTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeOptionalObjectTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - NativePartialAnnotationTurboModuleBaseSomeObj + +template +struct NativePartialAnnotationTurboModuleBaseSomeObj { + P0 a; + P1 b; + bool operator==(const NativePartialAnnotationTurboModuleBaseSomeObj &other) const { + return a == other.a && b == other.b; + } +}; + +template +struct NativePartialAnnotationTurboModuleBaseSomeObjBridging { + static NativePartialAnnotationTurboModuleBaseSomeObj fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + NativePartialAnnotationTurboModuleBaseSomeObj result{ + bridging::fromJs(rt, value.getProperty(rt, \\"a\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"b\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String aToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } + static bool bToJs(jsi::Runtime &rt, P1 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const NativePartialAnnotationTurboModuleBaseSomeObj &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"a\\", bridging::toJs(rt, value.a, jsInvoker)); + if (value.b) { + result.setProperty(rt, \\"b\\", bridging::toJs(rt, value.b.value(), jsInvoker)); + } + return result; + } + }; + +class JSI_EXPORT NativePartialAnnotationTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativePartialAnnotationTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getSomeObj(jsi::Runtime &rt) = 0; + virtual jsi::Object getPartialSomeObj(jsi::Runtime &rt) = 0; + virtual jsi::Object getSomeObjFromPartialSomeObj(jsi::Runtime &rt, jsi::Object value) = 0; + virtual jsi::Object getPartialPartial(jsi::Runtime &rt, jsi::Object value1, jsi::Object value2) = 0; + +}; + +template +class JSI_EXPORT NativePartialAnnotationTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativePartialAnnotationTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"NativePartialAnnotationTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativePartialAnnotationTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativePartialAnnotationTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getSomeObj(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getSomeObj) == 1, + \\"Expected getSomeObj(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getSomeObj, jsInvoker_, instance_); + } + jsi::Object getPartialSomeObj(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getPartialSomeObj) == 1, + \\"Expected getPartialSomeObj(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getPartialSomeObj, jsInvoker_, instance_); + } + jsi::Object getSomeObjFromPartialSomeObj(jsi::Runtime &rt, jsi::Object value) override { + static_assert( + bridging::getParameterCount(&T::getSomeObjFromPartialSomeObj) == 2, + \\"Expected getSomeObjFromPartialSomeObj(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getSomeObjFromPartialSomeObj, jsInvoker_, instance_, std::move(value)); + } + jsi::Object getPartialPartial(jsi::Runtime &rt, jsi::Object value1, jsi::Object value2) override { + static_assert( + bridging::getParameterCount(&T::getPartialPartial) == 3, + \\"Expected getPartialPartial(...) to have 3 parameters\\"); + + return bridging::callFromJs( + rt, &T::getPartialPartial, jsInvoker_, instance_, std::move(value1), std::move(value2)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativePromiseTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativePromiseTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Value getValueWithPromise(jsi::Runtime &rt, bool error) = 0; + virtual jsi::Value getValueWithPromiseWithAlias(jsi::Runtime &rt, jsi::String arg) = 0; + +}; + +template +class JSI_EXPORT NativePromiseTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativePromiseTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativePromiseTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativePromiseTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Value getValueWithPromise(jsi::Runtime &rt, bool error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + jsi::Value getValueWithPromiseWithAlias(jsi::Runtime &rt, jsi::String arg) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromiseWithAlias) == 2, + \\"Expected getValueWithPromiseWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromiseWithAlias, jsInvoker_, instance_, std::move(arg)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleBaseAnimal + +template +struct SampleTurboModuleBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleBaseAnimalBridging { + static SampleTurboModuleBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name, jsInvoker)); + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual bool getBool(jsi::Runtime &rt, bool arg) = 0; + virtual double getNumber(jsi::Runtime &rt, double arg) = 0; + virtual jsi::String getString(jsi::Runtime &rt, jsi::String arg) = 0; + virtual jsi::Array getArray(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Object getObject(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object getObjectShape(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object getAlias(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual double getRootTag(jsi::Runtime &rt, double arg) = 0; + virtual jsi::Object getValue(jsi::Runtime &rt, double x, jsi::String getValuegetValuegetValuegetValuegetValuey, jsi::Object z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) = 0; + virtual jsi::Value getValueWithPromise(jsi::Runtime &rt, bool error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + bool getBool(jsi::Runtime &rt, bool arg) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBool, jsInvoker_, instance_, std::move(arg)); + } + double getNumber(jsi::Runtime &rt, double arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + jsi::String getString(jsi::Runtime &rt, jsi::String arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getArray(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObject(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObjectShape(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getAlias(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + double getRootTag(jsi::Runtime &rt, double arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getValue(jsi::Runtime &rt, double x, jsi::String getValuegetValuegetValuegetValuegetValuey, jsi::Object z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(getValuegetValuegetValuegetValuegetValuey), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + jsi::Value getValueWithPromise(jsi::Runtime &rt, bool error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleArraysBaseAnimal + +template +struct SampleTurboModuleArraysBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleArraysBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleArraysBaseAnimalBridging { + static SampleTurboModuleArraysBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleArraysBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleArraysBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name, jsInvoker)); + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleArraysCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleArraysCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual jsi::Array getBool(jsi::Runtime &rt, jsi::Array id) = 0; + virtual jsi::Array getNumber(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getString(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getArray(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getObject(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getObjectShape(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getAlias(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getRootTag(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getValue(jsi::Runtime &rt, jsi::Array x, jsi::Array y, jsi::Array z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) = 0; + virtual jsi::Value getValueWithPromise(jsi::Runtime &rt, jsi::Array error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleArraysCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleArraysCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleArrays\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleArraysCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleArraysCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + jsi::Array getBool(jsi::Runtime &rt, jsi::Array id) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBool, jsInvoker_, instance_, std::move(id)); + } + jsi::Array getNumber(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getString(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getArray(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getObject(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getObjectShape(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getAlias(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getRootTag(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getValue(jsi::Runtime &rt, jsi::Array x, jsi::Array y, jsi::Array z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(y), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + jsi::Value getValueWithPromise(jsi::Runtime &rt, jsi::Array error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleNullableBaseAnimal + +template +struct SampleTurboModuleNullableBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleNullableBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleNullableBaseAnimalBridging { + static SampleTurboModuleNullableBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleNullableBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static std::optional nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleNullableBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name, jsInvoker)); + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleNullableCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleNullableCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual std::optional getBool(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getNumber(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getString(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getArray(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getObject(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getObjectShape(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getAlias(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getRootTag(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) = 0; + virtual std::optional getValueWithPromise(jsi::Runtime &rt, std::optional error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleNullableCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleNullableCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleNullable\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleNullableCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleNullableCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + std::optional getBool(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getBool, jsInvoker_, instance_, std::move(arg)); + } + std::optional getNumber(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + std::optional getString(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + std::optional getArray(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + std::optional getObject(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + std::optional getObjectShape(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + std::optional getAlias(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + std::optional getRootTag(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + std::optional getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(y), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + std::optional getValueWithPromise(jsi::Runtime &rt, std::optional error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleNullableAndOptionalBaseAnimal + +template +struct SampleTurboModuleNullableAndOptionalBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleNullableAndOptionalBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleNullableAndOptionalBaseAnimalBridging { + static SampleTurboModuleNullableAndOptionalBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleNullableAndOptionalBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static std::optional nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleNullableAndOptionalBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + if (value.name) { + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name.value(), jsInvoker)); + } + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual std::optional getBool(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getNumber(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getString(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getArray(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getObject(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getObjectShape(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getAlias(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getRootTag(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, std::optional callback) = 0; + virtual std::optional getValueWithPromise(jsi::Runtime &rt, std::optional error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleNullableAndOptionalCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleNullableAndOptionalCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleNullableAndOptional\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + std::optional getBool(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getBool, jsInvoker_, instance_, std::move(arg)); + } + std::optional getNumber(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + std::optional getString(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + std::optional getArray(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + std::optional getObject(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObjectShape(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + std::optional getAlias(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + std::optional getRootTag(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + std::optional getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(y), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, std::optional callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + std::optional getValueWithPromise(jsi::Runtime &rt, std::optional error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleOptionalBaseAnimal + +template +struct SampleTurboModuleOptionalBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleOptionalBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleOptionalBaseAnimalBridging { + static SampleTurboModuleOptionalBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleOptionalBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleOptionalBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + if (value.name) { + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name.value(), jsInvoker)); + } + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleOptionalCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleOptionalCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual bool getBool(jsi::Runtime &rt, std::optional arg) = 0; + virtual double getNumber(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::String getString(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Array getArray(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getObject(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getObjectShape(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getAlias(jsi::Runtime &rt, std::optional arg) = 0; + virtual double getRootTag(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, std::optional callback) = 0; + virtual jsi::Value getValueWithPromise(jsi::Runtime &rt, std::optional error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleOptionalCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleOptionalCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleOptional\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleOptionalCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleOptionalCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + bool getBool(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBool, jsInvoker_, instance_, std::move(arg)); + } + double getNumber(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + jsi::String getString(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getArray(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObject(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObjectShape(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getAlias(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + double getRootTag(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(y), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, std::optional callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + jsi::Value getValueWithPromise(jsi::Runtime &rt, std::optional error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeStringTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeStringTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::String getString(jsi::Runtime &rt, jsi::String arg) = 0; + virtual jsi::String getStringWithAlias(jsi::Runtime &rt, jsi::String arg) = 0; + +}; + +template +class JSI_EXPORT NativeStringTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeStringTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeStringTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeStringTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::String getString(jsi::Runtime &rt, jsi::String arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + jsi::String getStringWithAlias(jsi::Runtime &rt, jsi::String arg) override { + static_assert( + bridging::getParameterCount(&T::getStringWithAlias) == 2, + \\"Expected getStringWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStringWithAlias, jsInvoker_, instance_, std::move(arg)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +} // namespace react +} // namespace facebook +" +`; + +exports[`GenerateModuleH can generate a header file NativeModule specs with assume nonnull enabled 1`] = ` +"/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateModuleH.js + */ + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +class JSI_EXPORT NativeArrayTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeArrayTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Array getArray(jsi::Runtime &rt, jsi::Array a) = 0; + virtual jsi::Array getReadOnlyArray(jsi::Runtime &rt, jsi::Array a) = 0; + virtual jsi::Array getArrayWithAlias(jsi::Runtime &rt, jsi::Array a, jsi::Array b) = 0; + +}; + +template +class JSI_EXPORT NativeArrayTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeArrayTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeArrayTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeArrayTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Array getArray(jsi::Runtime &rt, jsi::Array a) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArray, jsInvoker_, instance_, std::move(a)); + } + jsi::Array getReadOnlyArray(jsi::Runtime &rt, jsi::Array a) override { + static_assert( + bridging::getParameterCount(&T::getReadOnlyArray) == 2, + \\"Expected getReadOnlyArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getReadOnlyArray, jsInvoker_, instance_, std::move(a)); + } + jsi::Array getArrayWithAlias(jsi::Runtime &rt, jsi::Array a, jsi::Array b) override { + static_assert( + bridging::getParameterCount(&T::getArrayWithAlias) == 3, + \\"Expected getArrayWithAlias(...) to have 3 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArrayWithAlias, jsInvoker_, instance_, std::move(a), std::move(b)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeBooleanTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeBooleanTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual bool getBoolean(jsi::Runtime &rt, bool arg) = 0; + virtual bool getBooleanWithAlias(jsi::Runtime &rt, bool arg) = 0; + +}; + +template +class JSI_EXPORT NativeBooleanTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeBooleanTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeBooleanTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeBooleanTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + bool getBoolean(jsi::Runtime &rt, bool arg) override { + static_assert( + bridging::getParameterCount(&T::getBoolean) == 2, + \\"Expected getBoolean(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBoolean, jsInvoker_, instance_, std::move(arg)); + } + bool getBooleanWithAlias(jsi::Runtime &rt, bool arg) override { + static_assert( + bridging::getParameterCount(&T::getBooleanWithAlias) == 2, + \\"Expected getBooleanWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBooleanWithAlias, jsInvoker_, instance_, std::move(arg)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeCallbackTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeCallbackTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) = 0; + virtual void getValueWithCallbackWithAlias(jsi::Runtime &rt, jsi::Function c) = 0; + +}; + +template +class JSI_EXPORT NativeCallbackTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeCallbackTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeCallbackTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeCallbackTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + void getValueWithCallbackWithAlias(jsi::Runtime &rt, jsi::Function c) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallbackWithAlias) == 2, + \\"Expected getValueWithCallbackWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallbackWithAlias, jsInvoker_, instance_, std::move(c)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - NativeEnumTurboModuleBaseStateType + +template +struct NativeEnumTurboModuleBaseStateType { + P0 state; + bool operator==(const NativeEnumTurboModuleBaseStateType &other) const { + return state == other.state; + } +}; + +template +struct NativeEnumTurboModuleBaseStateTypeBridging { + static NativeEnumTurboModuleBaseStateType fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + NativeEnumTurboModuleBaseStateType result{ + bridging::fromJs(rt, value.getProperty(rt, \\"state\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String stateToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const NativeEnumTurboModuleBaseStateType &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"state\\", bridging::toJs(rt, value.state, jsInvoker)); + return result; + } + }; + + +#pragma mark - NativeEnumTurboModuleBaseStateTypeWithEnums + +template +struct NativeEnumTurboModuleBaseStateTypeWithEnums { + P0 state; + P1 regular; + P2 str; + P3 num; + P4 fraction; + bool operator==(const NativeEnumTurboModuleBaseStateTypeWithEnums &other) const { + return state == other.state && regular == other.regular && str == other.str && num == other.num && fraction == other.fraction; + } +}; + +template +struct NativeEnumTurboModuleBaseStateTypeWithEnumsBridging { + static NativeEnumTurboModuleBaseStateTypeWithEnums fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + NativeEnumTurboModuleBaseStateTypeWithEnums result{ + bridging::fromJs(rt, value.getProperty(rt, \\"state\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"regular\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"str\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"num\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"fraction\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String stateToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } + static jsi::String regularToJs(jsi::Runtime &rt, P1 value) { + return bridging::toJs(rt, value); + } + static jsi::String strToJs(jsi::Runtime &rt, P2 value) { + return bridging::toJs(rt, value); + } + static double numToJs(jsi::Runtime &rt, P3 value) { + return bridging::toJs(rt, value); + } + static double fractionToJs(jsi::Runtime &rt, P4 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const NativeEnumTurboModuleBaseStateTypeWithEnums &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"state\\", bridging::toJs(rt, value.state, jsInvoker)); + result.setProperty(rt, \\"regular\\", bridging::toJs(rt, value.regular, jsInvoker)); + result.setProperty(rt, \\"str\\", bridging::toJs(rt, value.str, jsInvoker)); + result.setProperty(rt, \\"num\\", bridging::toJs(rt, value.num, jsInvoker)); + result.setProperty(rt, \\"fraction\\", bridging::toJs(rt, value.fraction, jsInvoker)); + return result; + } + }; + +class JSI_EXPORT NativeEnumTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeEnumTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::String getStatusRegular(jsi::Runtime &rt, jsi::Object statusProp) = 0; + virtual jsi::String getStatusStr(jsi::Runtime &rt, jsi::Object statusProp) = 0; + virtual double getStatusNum(jsi::Runtime &rt, jsi::Object statusProp) = 0; + virtual double getStatusFraction(jsi::Runtime &rt, jsi::Object statusProp) = 0; + virtual jsi::Object getStateType(jsi::Runtime &rt, jsi::String a, jsi::String b, double c, double d) = 0; + virtual jsi::Object getStateTypeWithEnums(jsi::Runtime &rt, jsi::Object paramOfTypeWithEnums) = 0; + +}; + +template +class JSI_EXPORT NativeEnumTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeEnumTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"NativeEnumTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeEnumTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeEnumTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::String getStatusRegular(jsi::Runtime &rt, jsi::Object statusProp) override { + static_assert( + bridging::getParameterCount(&T::getStatusRegular) == 2, + \\"Expected getStatusRegular(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStatusRegular, jsInvoker_, instance_, std::move(statusProp)); + } + jsi::String getStatusStr(jsi::Runtime &rt, jsi::Object statusProp) override { + static_assert( + bridging::getParameterCount(&T::getStatusStr) == 2, + \\"Expected getStatusStr(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStatusStr, jsInvoker_, instance_, std::move(statusProp)); + } + double getStatusNum(jsi::Runtime &rt, jsi::Object statusProp) override { + static_assert( + bridging::getParameterCount(&T::getStatusNum) == 2, + \\"Expected getStatusNum(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStatusNum, jsInvoker_, instance_, std::move(statusProp)); + } + double getStatusFraction(jsi::Runtime &rt, jsi::Object statusProp) override { + static_assert( + bridging::getParameterCount(&T::getStatusFraction) == 2, + \\"Expected getStatusFraction(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStatusFraction, jsInvoker_, instance_, std::move(statusProp)); + } + jsi::Object getStateType(jsi::Runtime &rt, jsi::String a, jsi::String b, double c, double d) override { + static_assert( + bridging::getParameterCount(&T::getStateType) == 5, + \\"Expected getStateType(...) to have 5 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStateType, jsInvoker_, instance_, std::move(a), std::move(b), std::move(c), std::move(d)); + } + jsi::Object getStateTypeWithEnums(jsi::Runtime &rt, jsi::Object paramOfTypeWithEnums) override { + static_assert( + bridging::getParameterCount(&T::getStateTypeWithEnums) == 2, + \\"Expected getStateTypeWithEnums(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStateTypeWithEnums, jsInvoker_, instance_, std::move(paramOfTypeWithEnums)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeNullableTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeNullableTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual std::optional getBool(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getNumber(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getString(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getArray(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getObject(jsi::Runtime &rt, std::optional a) = 0; + virtual std::optional getValueWithPromise(jsi::Runtime &rt) = 0; + +}; + +template +class JSI_EXPORT NativeNullableTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeNullableTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeNullableTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeNullableTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + std::optional getBool(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getBool, jsInvoker_, instance_, std::move(a)); + } + std::optional getNumber(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getNumber, jsInvoker_, instance_, std::move(a)); + } + std::optional getString(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getString, jsInvoker_, instance_, std::move(a)); + } + std::optional getArray(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getArray, jsInvoker_, instance_, std::move(a)); + } + std::optional getObject(jsi::Runtime &rt, std::optional a) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getObject, jsInvoker_, instance_, std::move(a)); + } + std::optional getValueWithPromise(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 1, + \\"Expected getValueWithPromise(...) to have 1 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValueWithPromise, jsInvoker_, instance_); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeNumberTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeNumberTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual double getNumber(jsi::Runtime &rt, double arg) = 0; + virtual double getNumberWithAlias(jsi::Runtime &rt, double arg) = 0; + +}; + +template +class JSI_EXPORT NativeNumberTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeNumberTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeNumberTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeNumberTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + double getNumber(jsi::Runtime &rt, double arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + double getNumberWithAlias(jsi::Runtime &rt, double arg) override { + static_assert( + bridging::getParameterCount(&T::getNumberWithAlias) == 2, + \\"Expected getNumberWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumberWithAlias, jsInvoker_, instance_, std::move(arg)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeObjectTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeObjectTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getGenericObject(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object getGenericObjectReadOnly(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object getGenericObjectWithAlias(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object difficultObject(jsi::Runtime &rt, jsi::Object A) = 0; + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + +}; + +template +class JSI_EXPORT NativeObjectTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeObjectTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeObjectTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeObjectTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getGenericObject(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getGenericObject) == 2, + \\"Expected getGenericObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getGenericObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getGenericObjectReadOnly(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getGenericObjectReadOnly) == 2, + \\"Expected getGenericObjectReadOnly(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getGenericObjectReadOnly, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getGenericObjectWithAlias(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getGenericObjectWithAlias) == 2, + \\"Expected getGenericObjectWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getGenericObjectWithAlias, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object difficultObject(jsi::Runtime &rt, jsi::Object A) override { + static_assert( + bridging::getParameterCount(&T::difficultObject) == 2, + \\"Expected difficultObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::difficultObject, jsInvoker_, instance_, std::move(A)); + } + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeOptionalObjectTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeOptionalObjectTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + +}; + +template +class JSI_EXPORT NativeOptionalObjectTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeOptionalObjectTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeOptionalObjectTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeOptionalObjectTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - NativePartialAnnotationTurboModuleBaseSomeObj + +template +struct NativePartialAnnotationTurboModuleBaseSomeObj { + P0 a; + P1 b; + bool operator==(const NativePartialAnnotationTurboModuleBaseSomeObj &other) const { + return a == other.a && b == other.b; + } +}; + +template +struct NativePartialAnnotationTurboModuleBaseSomeObjBridging { + static NativePartialAnnotationTurboModuleBaseSomeObj fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + NativePartialAnnotationTurboModuleBaseSomeObj result{ + bridging::fromJs(rt, value.getProperty(rt, \\"a\\"), jsInvoker), + bridging::fromJs(rt, value.getProperty(rt, \\"b\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String aToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } + static bool bToJs(jsi::Runtime &rt, P1 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const NativePartialAnnotationTurboModuleBaseSomeObj &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"a\\", bridging::toJs(rt, value.a, jsInvoker)); + if (value.b) { + result.setProperty(rt, \\"b\\", bridging::toJs(rt, value.b.value(), jsInvoker)); + } + return result; + } + }; + +class JSI_EXPORT NativePartialAnnotationTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativePartialAnnotationTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getSomeObj(jsi::Runtime &rt) = 0; + virtual jsi::Object getPartialSomeObj(jsi::Runtime &rt) = 0; + virtual jsi::Object getSomeObjFromPartialSomeObj(jsi::Runtime &rt, jsi::Object value) = 0; + virtual jsi::Object getPartialPartial(jsi::Runtime &rt, jsi::Object value1, jsi::Object value2) = 0; + +}; + +template +class JSI_EXPORT NativePartialAnnotationTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativePartialAnnotationTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"NativePartialAnnotationTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativePartialAnnotationTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativePartialAnnotationTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getSomeObj(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getSomeObj) == 1, + \\"Expected getSomeObj(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getSomeObj, jsInvoker_, instance_); + } + jsi::Object getPartialSomeObj(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getPartialSomeObj) == 1, + \\"Expected getPartialSomeObj(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getPartialSomeObj, jsInvoker_, instance_); + } + jsi::Object getSomeObjFromPartialSomeObj(jsi::Runtime &rt, jsi::Object value) override { + static_assert( + bridging::getParameterCount(&T::getSomeObjFromPartialSomeObj) == 2, + \\"Expected getSomeObjFromPartialSomeObj(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getSomeObjFromPartialSomeObj, jsInvoker_, instance_, std::move(value)); + } + jsi::Object getPartialPartial(jsi::Runtime &rt, jsi::Object value1, jsi::Object value2) override { + static_assert( + bridging::getParameterCount(&T::getPartialPartial) == 3, + \\"Expected getPartialPartial(...) to have 3 parameters\\"); + + return bridging::callFromJs( + rt, &T::getPartialPartial, jsInvoker_, instance_, std::move(value1), std::move(value2)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativePromiseTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativePromiseTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Value getValueWithPromise(jsi::Runtime &rt, bool error) = 0; + virtual jsi::Value getValueWithPromiseWithAlias(jsi::Runtime &rt, jsi::String arg) = 0; + +}; + +template +class JSI_EXPORT NativePromiseTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativePromiseTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativePromiseTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativePromiseTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Value getValueWithPromise(jsi::Runtime &rt, bool error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + jsi::Value getValueWithPromiseWithAlias(jsi::Runtime &rt, jsi::String arg) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromiseWithAlias) == 2, + \\"Expected getValueWithPromiseWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromiseWithAlias, jsInvoker_, instance_, std::move(arg)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleBaseAnimal + +template +struct SampleTurboModuleBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleBaseAnimalBridging { + static SampleTurboModuleBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name, jsInvoker)); + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual bool getBool(jsi::Runtime &rt, bool arg) = 0; + virtual double getNumber(jsi::Runtime &rt, double arg) = 0; + virtual jsi::String getString(jsi::Runtime &rt, jsi::String arg) = 0; + virtual jsi::Array getArray(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Object getObject(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object getObjectShape(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual jsi::Object getAlias(jsi::Runtime &rt, jsi::Object arg) = 0; + virtual double getRootTag(jsi::Runtime &rt, double arg) = 0; + virtual jsi::Object getValue(jsi::Runtime &rt, double x, jsi::String getValuegetValuegetValuegetValuegetValuey, jsi::Object z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) = 0; + virtual jsi::Value getValueWithPromise(jsi::Runtime &rt, bool error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + bool getBool(jsi::Runtime &rt, bool arg) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBool, jsInvoker_, instance_, std::move(arg)); + } + double getNumber(jsi::Runtime &rt, double arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + jsi::String getString(jsi::Runtime &rt, jsi::String arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getArray(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObject(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObjectShape(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getAlias(jsi::Runtime &rt, jsi::Object arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + double getRootTag(jsi::Runtime &rt, double arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getValue(jsi::Runtime &rt, double x, jsi::String getValuegetValuegetValuegetValuegetValuey, jsi::Object z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(getValuegetValuegetValuegetValuegetValuey), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + jsi::Value getValueWithPromise(jsi::Runtime &rt, bool error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleArraysBaseAnimal + +template +struct SampleTurboModuleArraysBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleArraysBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleArraysBaseAnimalBridging { + static SampleTurboModuleArraysBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleArraysBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleArraysBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name, jsInvoker)); + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleArraysCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleArraysCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual jsi::Array getBool(jsi::Runtime &rt, jsi::Array id) = 0; + virtual jsi::Array getNumber(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getString(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getArray(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getObject(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getObjectShape(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getAlias(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getRootTag(jsi::Runtime &rt, jsi::Array arg) = 0; + virtual jsi::Array getValue(jsi::Runtime &rt, jsi::Array x, jsi::Array y, jsi::Array z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) = 0; + virtual jsi::Value getValueWithPromise(jsi::Runtime &rt, jsi::Array error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleArraysCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleArraysCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleArrays\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleArraysCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleArraysCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + jsi::Array getBool(jsi::Runtime &rt, jsi::Array id) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBool, jsInvoker_, instance_, std::move(id)); + } + jsi::Array getNumber(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getString(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getArray(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getObject(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getObjectShape(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getAlias(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getRootTag(jsi::Runtime &rt, jsi::Array arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getValue(jsi::Runtime &rt, jsi::Array x, jsi::Array y, jsi::Array z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(y), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + jsi::Value getValueWithPromise(jsi::Runtime &rt, jsi::Array error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleNullableBaseAnimal + +template +struct SampleTurboModuleNullableBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleNullableBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleNullableBaseAnimalBridging { + static SampleTurboModuleNullableBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleNullableBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static std::optional nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleNullableBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name, jsInvoker)); + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleNullableCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleNullableCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual std::optional getBool(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getNumber(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getString(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getArray(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getObject(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getObjectShape(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getAlias(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getRootTag(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) = 0; + virtual std::optional getValueWithPromise(jsi::Runtime &rt, std::optional error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleNullableCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleNullableCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleNullable\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleNullableCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleNullableCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + std::optional getBool(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getBool, jsInvoker_, instance_, std::move(arg)); + } + std::optional getNumber(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + std::optional getString(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + std::optional getArray(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + std::optional getObject(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + std::optional getObjectShape(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + std::optional getAlias(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + std::optional getRootTag(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + std::optional getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(y), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, jsi::Function callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + std::optional getValueWithPromise(jsi::Runtime &rt, std::optional error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleNullableAndOptionalBaseAnimal + +template +struct SampleTurboModuleNullableAndOptionalBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleNullableAndOptionalBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleNullableAndOptionalBaseAnimalBridging { + static SampleTurboModuleNullableAndOptionalBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleNullableAndOptionalBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static std::optional nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleNullableAndOptionalBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + if (value.name) { + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name.value(), jsInvoker)); + } + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual std::optional getBool(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getNumber(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getString(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getArray(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getObject(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getObjectShape(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getAlias(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getRootTag(jsi::Runtime &rt, std::optional arg) = 0; + virtual std::optional getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, std::optional callback) = 0; + virtual std::optional getValueWithPromise(jsi::Runtime &rt, std::optional error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleNullableAndOptionalCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleNullableAndOptionalCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleNullableAndOptional\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleNullableAndOptionalCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + std::optional getBool(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getBool, jsInvoker_, instance_, std::move(arg)); + } + std::optional getNumber(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + std::optional getString(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + std::optional getArray(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + std::optional getObject(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObjectShape(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + std::optional getAlias(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + std::optional getRootTag(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + std::optional getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(y), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, std::optional callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + std::optional getValueWithPromise(jsi::Runtime &rt, std::optional error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs>( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +#pragma mark - SampleTurboModuleOptionalBaseAnimal + +template +struct SampleTurboModuleOptionalBaseAnimal { + P0 name; + bool operator==(const SampleTurboModuleOptionalBaseAnimal &other) const { + return name == other.name; + } +}; + +template +struct SampleTurboModuleOptionalBaseAnimalBridging { + static SampleTurboModuleOptionalBaseAnimal fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + SampleTurboModuleOptionalBaseAnimal result{ + bridging::fromJs(rt, value.getProperty(rt, \\"name\\"), jsInvoker)}; + return result; + } + +#ifdef DEBUG + static jsi::String nameToJs(jsi::Runtime &rt, P0 value) { + return bridging::toJs(rt, value); + } +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const SampleTurboModuleOptionalBaseAnimal &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); + if (value.name) { + result.setProperty(rt, \\"name\\", bridging::toJs(rt, value.name.value(), jsInvoker)); + } + return result; + } + }; + +class JSI_EXPORT NativeSampleTurboModuleOptionalCxxSpecJSI : public TurboModule { +protected: + NativeSampleTurboModuleOptionalCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::Object getConstants(jsi::Runtime &rt) = 0; + virtual void voidFunc(jsi::Runtime &rt) = 0; + virtual bool getBool(jsi::Runtime &rt, std::optional arg) = 0; + virtual double getNumber(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::String getString(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Array getArray(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getObject(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getObjectShape(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getAlias(jsi::Runtime &rt, std::optional arg) = 0; + virtual double getRootTag(jsi::Runtime &rt, std::optional arg) = 0; + virtual jsi::Object getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) = 0; + virtual void getValueWithCallback(jsi::Runtime &rt, std::optional callback) = 0; + virtual jsi::Value getValueWithPromise(jsi::Runtime &rt, std::optional error) = 0; + +}; + +template +class JSI_EXPORT NativeSampleTurboModuleOptionalCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeSampleTurboModuleOptionalCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModuleOptional\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeSampleTurboModuleOptionalCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeSampleTurboModuleOptionalCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::Object getConstants(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::getConstants) == 1, + \\"Expected getConstants(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::getConstants, jsInvoker_, instance_); + } + void voidFunc(jsi::Runtime &rt) override { + static_assert( + bridging::getParameterCount(&T::voidFunc) == 1, + \\"Expected voidFunc(...) to have 1 parameters\\"); + + return bridging::callFromJs( + rt, &T::voidFunc, jsInvoker_, instance_); + } + bool getBool(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getBool) == 2, + \\"Expected getBool(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getBool, jsInvoker_, instance_, std::move(arg)); + } + double getNumber(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getNumber) == 2, + \\"Expected getNumber(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getNumber, jsInvoker_, instance_, std::move(arg)); + } + jsi::String getString(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + jsi::Array getArray(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getArray) == 2, + \\"Expected getArray(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getArray, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObject(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObject) == 2, + \\"Expected getObject(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObject, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getObjectShape(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getObjectShape) == 2, + \\"Expected getObjectShape(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getObjectShape, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getAlias(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getAlias) == 2, + \\"Expected getAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getAlias, jsInvoker_, instance_, std::move(arg)); + } + double getRootTag(jsi::Runtime &rt, std::optional arg) override { + static_assert( + bridging::getParameterCount(&T::getRootTag) == 2, + \\"Expected getRootTag(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getRootTag, jsInvoker_, instance_, std::move(arg)); + } + jsi::Object getValue(jsi::Runtime &rt, std::optional x, std::optional y, std::optional z) override { + static_assert( + bridging::getParameterCount(&T::getValue) == 4, + \\"Expected getValue(...) to have 4 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValue, jsInvoker_, instance_, std::move(x), std::move(y), std::move(z)); + } + void getValueWithCallback(jsi::Runtime &rt, std::optional callback) override { + static_assert( + bridging::getParameterCount(&T::getValueWithCallback) == 2, + \\"Expected getValueWithCallback(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithCallback, jsInvoker_, instance_, std::move(callback)); + } + jsi::Value getValueWithPromise(jsi::Runtime &rt, std::optional error) override { + static_assert( + bridging::getParameterCount(&T::getValueWithPromise) == 2, + \\"Expected getValueWithPromise(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getValueWithPromise, jsInvoker_, instance_, std::move(error)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +class JSI_EXPORT NativeStringTurboModuleCxxSpecJSI : public TurboModule { +protected: + NativeStringTurboModuleCxxSpecJSI(std::shared_ptr jsInvoker); + +public: + virtual jsi::String getString(jsi::Runtime &rt, jsi::String arg) = 0; + virtual jsi::String getStringWithAlias(jsi::Runtime &rt, jsi::String arg) = 0; + +}; + +template +class JSI_EXPORT NativeStringTurboModuleCxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + +protected: + NativeStringTurboModuleCxxSpec(std::shared_ptr jsInvoker) + : TurboModule(\\"SampleTurboModule\\", jsInvoker), + delegate_(static_cast(this), jsInvoker) {} + +private: + class Delegate : public NativeStringTurboModuleCxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + NativeStringTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {} + + jsi::String getString(jsi::Runtime &rt, jsi::String arg) override { + static_assert( + bridging::getParameterCount(&T::getString) == 2, + \\"Expected getString(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getString, jsInvoker_, instance_, std::move(arg)); + } + jsi::String getStringWithAlias(jsi::Runtime &rt, jsi::String arg) override { + static_assert( + bridging::getParameterCount(&T::getStringWithAlias) == 2, + \\"Expected getStringWithAlias(...) to have 2 parameters\\"); + + return bridging::callFromJs( + rt, &T::getStringWithAlias, jsInvoker_, instance_, std::move(arg)); + } + + private: + T *instance_; + }; + + Delegate delegate_; +}; + +} // namespace react +} // namespace facebook +" +`; diff --git a/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap b/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap index 5a2dd6a2343779..a1238aa502e17b 100644 --- a/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap +++ b/packages/react-native-codegen/e2e/__tests__/modules/__snapshots__/GenerateModuleObjCpp-test.js.snap @@ -84,6 +84,64 @@ namespace facebook { }; } // namespace react } // namespace facebook +namespace JS { + namespace NativeEnumTurboModule { + struct StateType { + NSString *state() const; + + StateType(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (NativeEnumTurboModule_StateType) ++ (RCTManagedPointer *)JS_NativeEnumTurboModule_StateType:(id)json; +@end +namespace JS { + namespace NativeEnumTurboModule { + struct StateTypeWithEnums { + NSString *state() const; + NSString *regular() const; + NSString *str() const; + double num() const; + double fraction() const; + + StateTypeWithEnums(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (NativeEnumTurboModule_StateTypeWithEnums) ++ (RCTManagedPointer *)JS_NativeEnumTurboModule_StateTypeWithEnums:(id)json; +@end +@protocol NativeEnumTurboModuleSpec + +- (NSString *)getStatusRegular:(JS::NativeEnumTurboModule::StateType &)statusProp; +- (NSString *)getStatusStr:(JS::NativeEnumTurboModule::StateType &)statusProp; +- (NSNumber *)getStatusNum:(JS::NativeEnumTurboModule::StateType &)statusProp; +- (NSNumber *)getStatusFraction:(JS::NativeEnumTurboModule::StateType &)statusProp; +- (NSDictionary *)getStateType:(NSString *)a + b:(NSString *)b + c:(double)c + d:(double)d; +- (NSDictionary *)getStateTypeWithEnums:(JS::NativeEnumTurboModule::StateTypeWithEnums &)paramOfTypeWithEnums; + +@end +namespace facebook { + namespace react { + /** + * ObjC++ class for module 'NativeEnumTurboModule' + */ + class JSI_EXPORT NativeEnumTurboModuleSpecJSI : public ObjCTurboModule { + public: + NativeEnumTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms); + }; + } // namespace react +} // namespace facebook @protocol NativeNullableTurboModuleSpec @@ -999,6 +1057,36 @@ namespace facebook { +inline NSString *JS::NativeEnumTurboModule::StateType::state() const +{ + id const p = _v[@\\"state\\"]; + return RCTBridgingToString(p); +} +inline NSString *JS::NativeEnumTurboModule::StateTypeWithEnums::state() const +{ + id const p = _v[@\\"state\\"]; + return RCTBridgingToString(p); +} +inline NSString *JS::NativeEnumTurboModule::StateTypeWithEnums::regular() const +{ + id const p = _v[@\\"regular\\"]; + return RCTBridgingToString(p); +} +inline NSString *JS::NativeEnumTurboModule::StateTypeWithEnums::str() const +{ + id const p = _v[@\\"str\\"]; + return RCTBridgingToString(p); +} +inline double JS::NativeEnumTurboModule::StateTypeWithEnums::num() const +{ + id const p = _v[@\\"num\\"]; + return RCTBridgingToDouble(p); +} +inline double JS::NativeEnumTurboModule::StateTypeWithEnums::fraction() const +{ + id const p = _v[@\\"fraction\\"]; + return RCTBridgingToDouble(p); +} inline bool JS::NativeObjectTurboModule::SpecDifficultObjectAE::D() const @@ -1374,6 +1462,64 @@ namespace facebook { }; } // namespace react } // namespace facebook +namespace JS { + namespace NativeEnumTurboModule { + struct StateType { + NSString *state() const; + + StateType(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (NativeEnumTurboModule_StateType) ++ (RCTManagedPointer *)JS_NativeEnumTurboModule_StateType:(id)json; +@end +namespace JS { + namespace NativeEnumTurboModule { + struct StateTypeWithEnums { + NSString *state() const; + NSString *regular() const; + NSString *str() const; + double num() const; + double fraction() const; + + StateTypeWithEnums(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (NativeEnumTurboModule_StateTypeWithEnums) ++ (RCTManagedPointer *)JS_NativeEnumTurboModule_StateTypeWithEnums:(id)json; +@end +@protocol NativeEnumTurboModuleSpec + +- (NSString *)getStatusRegular:(JS::NativeEnumTurboModule::StateType &)statusProp; +- (NSString *)getStatusStr:(JS::NativeEnumTurboModule::StateType &)statusProp; +- (NSNumber *)getStatusNum:(JS::NativeEnumTurboModule::StateType &)statusProp; +- (NSNumber *)getStatusFraction:(JS::NativeEnumTurboModule::StateType &)statusProp; +- (NSDictionary *)getStateType:(NSString *)a + b:(NSString *)b + c:(double)c + d:(double)d; +- (NSDictionary *)getStateTypeWithEnums:(JS::NativeEnumTurboModule::StateTypeWithEnums &)paramOfTypeWithEnums; + +@end +namespace facebook { + namespace react { + /** + * ObjC++ class for module 'NativeEnumTurboModule' + */ + class JSI_EXPORT NativeEnumTurboModuleSpecJSI : public ObjCTurboModule { + public: + NativeEnumTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms); + }; + } // namespace react +} // namespace facebook @protocol NativeNullableTurboModuleSpec @@ -2289,6 +2435,36 @@ namespace facebook { +inline NSString *JS::NativeEnumTurboModule::StateType::state() const +{ + id const p = _v[@\\"state\\"]; + return RCTBridgingToString(p); +} +inline NSString *JS::NativeEnumTurboModule::StateTypeWithEnums::state() const +{ + id const p = _v[@\\"state\\"]; + return RCTBridgingToString(p); +} +inline NSString *JS::NativeEnumTurboModule::StateTypeWithEnums::regular() const +{ + id const p = _v[@\\"regular\\"]; + return RCTBridgingToString(p); +} +inline NSString *JS::NativeEnumTurboModule::StateTypeWithEnums::str() const +{ + id const p = _v[@\\"str\\"]; + return RCTBridgingToString(p); +} +inline double JS::NativeEnumTurboModule::StateTypeWithEnums::num() const +{ + id const p = _v[@\\"num\\"]; + return RCTBridgingToDouble(p); +} +inline double JS::NativeEnumTurboModule::StateTypeWithEnums::fraction() const +{ + id const p = _v[@\\"fraction\\"]; + return RCTBridgingToDouble(p); +} inline bool JS::NativeObjectTurboModule::SpecDifficultObjectAE::D() const @@ -2671,6 +2847,68 @@ namespace facebook { } } // namespace react } // namespace facebook +@implementation RCTCxxConvert (NativeEnumTurboModule_StateType) ++ (RCTManagedPointer *)JS_NativeEnumTurboModule_StateType:(id)json +{ + return facebook::react::managedPointer(json); +} +@end +@implementation RCTCxxConvert (NativeEnumTurboModule_StateTypeWithEnums) ++ (RCTManagedPointer *)JS_NativeEnumTurboModule_StateTypeWithEnums:(id)json +{ + return facebook::react::managedPointer(json); +} +@end +namespace facebook { + namespace react { + + static facebook::jsi::Value __hostFunction_NativeEnumTurboModuleSpecJSI_getStatusRegular(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, StringKind, \\"getStatusRegular\\", @selector(getStatusRegular:), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeEnumTurboModuleSpecJSI_getStatusStr(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, StringKind, \\"getStatusStr\\", @selector(getStatusStr:), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeEnumTurboModuleSpecJSI_getStatusNum(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, NumberKind, \\"getStatusNum\\", @selector(getStatusNum:), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeEnumTurboModuleSpecJSI_getStatusFraction(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, NumberKind, \\"getStatusFraction\\", @selector(getStatusFraction:), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeEnumTurboModuleSpecJSI_getStateType(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, ObjectKind, \\"getStateType\\", @selector(getStateType:b:c:d:), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeEnumTurboModuleSpecJSI_getStateTypeWithEnums(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, ObjectKind, \\"getStateTypeWithEnums\\", @selector(getStateTypeWithEnums:), args, count); + } + + NativeEnumTurboModuleSpecJSI::NativeEnumTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms) + : ObjCTurboModule(params) { + + methodMap_[\\"getStatusRegular\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleSpecJSI_getStatusRegular}; + setMethodArgConversionSelector(@\\"getStatusRegular\\", 0, @\\"JS_NativeEnumTurboModule_StateType:\\"); + + methodMap_[\\"getStatusStr\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleSpecJSI_getStatusStr}; + setMethodArgConversionSelector(@\\"getStatusStr\\", 0, @\\"JS_NativeEnumTurboModule_StateType:\\"); + + methodMap_[\\"getStatusNum\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleSpecJSI_getStatusNum}; + setMethodArgConversionSelector(@\\"getStatusNum\\", 0, @\\"JS_NativeEnumTurboModule_StateType:\\"); + + methodMap_[\\"getStatusFraction\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleSpecJSI_getStatusFraction}; + setMethodArgConversionSelector(@\\"getStatusFraction\\", 0, @\\"JS_NativeEnumTurboModule_StateType:\\"); + + methodMap_[\\"getStateType\\"] = MethodMetadata {4, __hostFunction_NativeEnumTurboModuleSpecJSI_getStateType}; + + + methodMap_[\\"getStateTypeWithEnums\\"] = MethodMetadata {1, __hostFunction_NativeEnumTurboModuleSpecJSI_getStateTypeWithEnums}; + setMethodArgConversionSelector(@\\"getStateTypeWithEnums\\", 0, @\\"JS_NativeEnumTurboModule_StateTypeWithEnums:\\"); + } + } // namespace react +} // namespace facebook namespace facebook { namespace react { From 53c9786b71a5b688fca32a3e1d7819d19851836d Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Fri, 3 Feb 2023 03:03:41 -0800 Subject: [PATCH 35/65] remove 'EnumDeclaration' as a type expected to throw error in module generators Summary: Handling of `EnumDeclaration` was introduced in D38967241 (https://github.com/facebook/react-native/commit/745f3ee8c571560406629bc7af3cf4914ef1b211) so it is no longer a type expected to fail generators. Changelog: [Internal] remove 'EnumDeclaration' as a type expected to throw error in module generators Reviewed By: cipolleschi Differential Revision: D42917947 fbshipit-source-id: 16fcb915ccd42c613ca4d30b815d6365681f5fa1 --- .../src/generators/modules/GenerateModuleJavaSpec.js | 6 +++--- .../src/generators/modules/GenerateModuleJniCpp.js | 6 +++--- .../modules/GenerateModuleObjCpp/serializeMethod.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index aff908ec83cd7c..8497bb68a84111 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -184,7 +184,7 @@ function translateFunctionParamToJavaType( imports.add('com.facebook.react.bridge.Callback'); return wrapNullable('Callback'); default: - (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); + (realTypeAnnotation.type: 'MixedTypeAnnotation'); throw new Error(createErrorMessage(realTypeAnnotation.type)); } } @@ -272,7 +272,7 @@ function translateFunctionReturnTypeToJavaType( imports.add('com.facebook.react.bridge.WritableArray'); return wrapNullable('WritableArray'); default: - (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); + (realTypeAnnotation.type: 'MixedTypeAnnotation'); throw new Error(createErrorMessage(realTypeAnnotation.type)); } } @@ -346,7 +346,7 @@ function getFalsyReturnStatementFromReturnType( case 'ArrayTypeAnnotation': return 'return null;'; default: - (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); + (realTypeAnnotation.type: 'MixedTypeAnnotation'); throw new Error(createErrorMessage(realTypeAnnotation.type)); } } diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js index 1c825918fbb55a..ad23221bc4ce56 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -193,7 +193,7 @@ function translateReturnTypeToKind( case 'ArrayTypeAnnotation': return 'ArrayKind'; default: - (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); + (realTypeAnnotation.type: 'MixedTypeAnnotation'); throw new Error( `Unknown prop type for returning value, found: ${realTypeAnnotation.type}"`, ); @@ -272,7 +272,7 @@ function translateParamTypeToJniType( case 'FunctionTypeAnnotation': return 'Lcom/facebook/react/bridge/Callback;'; default: - (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); + (realTypeAnnotation.type: 'MixedTypeAnnotation'); throw new Error( `Unknown prop type for method arg, found: ${realTypeAnnotation.type}"`, ); @@ -348,7 +348,7 @@ function translateReturnTypeToJniType( case 'ArrayTypeAnnotation': return 'Lcom/facebook/react/bridge/WritableArray;'; default: - (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); + (realTypeAnnotation.type: 'MixedTypeAnnotation'); throw new Error( `Unknown prop type for method return type, found: ${realTypeAnnotation.type}"`, ); diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js index 2894de5ccf262d..32a13879c9443d 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js @@ -375,7 +375,7 @@ function getReturnObjCType( case 'GenericObjectTypeAnnotation': return wrapIntoNullableIfNeeded('NSDictionary *'); default: - (typeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); + (typeAnnotation.type: 'MixedTypeAnnotation'); throw new Error( `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, ); @@ -439,7 +439,7 @@ function getReturnJSType( ); } default: - (typeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); + (typeAnnotation.type: 'MixedTypeAnnotation'); throw new Error( `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, ); From 39bdb34178702d3b25d1980713c9034cd6dc6a98 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Fri, 3 Feb 2023 03:03:41 -0800 Subject: [PATCH 36/65] Parse and collect enum members in turbo module specs (#36044) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36044 > **Notice**: This commit does not change any behaviour. Parse and collect enum members in module specs instead of the current behaviour of parsing enum members as 'string' / 'number' and ignoring the member names. This commit would allow us to generate native Enums corresponding to the schema's enums. Prior to this commit, enum params were parsed as: ``` { 'name': 'qualityParam', 'optional': false, 'typeAnnotation': { 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } }, ``` The name of the enum type and the members of the enum type were ignored. After this commit, parsed modules would hold a new object member called `enumMap` that would look like this: ``` 'enumMap': { 'QualityEnum': { 'name': 'QualityEnum', 'type': 'EnumDeclarationWithMembers', 'memberType': 'StringTypeAnnotation', 'members': [ { 'name': 'SD', 'value': 'sd' }, { 'name': 'HD', 'value': 'hd' } ] }, // ... } ``` And enum params would be exported as: ``` { 'name': 'qualityParam', 'optional': false, 'typeAnnotation': { 'name': 'NativeModuleEnumTypeAnnotation', 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } }, ``` Combining the two new outputs would allow us to generate Native enums in the Native generators. Changelog: [Internal] Parse and collect enum members in turbo module specs Reviewed By: cipolleschi Differential Revision: D42778258 fbshipit-source-id: 56479342e085bc4e13c5a3e12b265b140e49893c --- .../react-native-codegen/src/CodegenSchema.js | 26 ++- .../generators/__test_fixtures__/fixtures.js | 1 + .../modules/__test_fixtures__/fixtures.js | 10 + .../parsers/__tests__/parsers-commons-test.js | 5 + .../__tests__/parsers-primitives-test.js | 113 +++++++++++- .../modules/__test_fixtures__/failures.js | 56 ++++++ .../module-parser-snapshot-test.js.snap | 167 +++++++++++++++++ .../src/parsers/flow/modules/index.js | 44 ++++- .../src/parsers/flow/parser.js | 68 ++++++- .../src/parsers/flow/utils.js | 59 +++--- .../src/parsers/parser.js | 32 ++-- .../src/parsers/parserMock.js | 47 ++++- .../src/parsers/parsers-commons.js | 49 +---- .../src/parsers/parsers-primitives.js | 80 +++++++- .../modules/__test_fixtures__/failures.js | 54 ++++++ ...script-module-parser-snapshot-test.js.snap | 171 ++++++++++++++++++ .../src/parsers/typescript/modules/index.js | 46 ++++- .../src/parsers/typescript/parser.js | 65 +++++-- .../src/parsers/typescript/utils.js | 71 +++++--- .../react-native-codegen/src/parsers/utils.js | 5 +- 20 files changed, 999 insertions(+), 170 deletions(-) diff --git a/packages/react-native-codegen/src/CodegenSchema.js b/packages/react-native-codegen/src/CodegenSchema.js index ab0ff056e79613..17497531ed0803 100644 --- a/packages/react-native-codegen/src/CodegenSchema.js +++ b/packages/react-native-codegen/src/CodegenSchema.js @@ -223,6 +223,7 @@ export type NullableTypeAnnotation<+T: NativeModuleTypeAnnotation> = $ReadOnly<{ export type NativeModuleSchema = $ReadOnly<{ type: 'NativeModule', aliasMap: NativeModuleAliasMap, + enumMap: NativeModuleEnumMap, spec: NativeModuleSpec, moduleName: string, // Use for modules that are not used on other platforms. @@ -239,6 +240,10 @@ export type NativeModulePropertyShape = NamedShape< Nullable, >; +export type NativeModuleEnumMap = $ReadOnly<{ + [enumName: string]: NativeModuleEnumDeclarationWithMembers, +}>; + export type NativeModuleAliasMap = $ReadOnly<{ [aliasName: string]: NativeModuleObjectTypeAnnotation, }>; @@ -287,11 +292,30 @@ export type NativeModuleBooleanTypeAnnotation = $ReadOnly<{ type: 'BooleanTypeAnnotation', }>; +export type NativeModuleEnumMembers = $ReadOnlyArray< + $ReadOnly<{ + name: string, + value: string, + }>, +>; + +export type NativeModuleEnumMemberType = + | 'NumberTypeAnnotation' + | 'StringTypeAnnotation'; + export type NativeModuleEnumDeclaration = $ReadOnly<{ + name: string, type: 'EnumDeclaration', - memberType: 'NumberTypeAnnotation' | 'StringTypeAnnotation', + memberType: NativeModuleEnumMemberType, }>; +export type NativeModuleEnumDeclarationWithMembers = { + name: string, + type: 'EnumDeclarationWithMembers', + memberType: NativeModuleEnumMemberType, + members: NativeModuleEnumMembers, +}; + export type NativeModuleGenericObjectTypeAnnotation = $ReadOnly<{ type: 'GenericObjectTypeAnnotation', }>; diff --git a/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js index 2a60dc45f888a4..8678249f5a8207 100644 --- a/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js @@ -42,6 +42,7 @@ const SCHEMA_WITH_TM_AND_FC: SchemaType = { NativeCalculator: { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: { properties: [ { diff --git a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js index 0f838c2d836abd..5764facb3bc32e 100644 --- a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js @@ -17,6 +17,7 @@ const EMPTY_NATIVE_MODULES: SchemaType = { NativeSampleTurboModule: { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: { properties: [], }, @@ -30,6 +31,7 @@ const SIMPLE_NATIVE_MODULES: SchemaType = { NativeSampleTurboModule: { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: { properties: [ { @@ -342,6 +344,7 @@ const TWO_MODULES_DIFFERENT_FILES: SchemaType = { NativeSampleTurboModule: { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: { properties: [ { @@ -362,6 +365,7 @@ const TWO_MODULES_DIFFERENT_FILES: SchemaType = { NativeSampleTurboModule2: { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: { properties: [ { @@ -399,6 +403,7 @@ const COMPLEX_OBJECTS: SchemaType = { NativeSampleTurboModule: { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: { properties: [ { @@ -854,6 +859,7 @@ const NATIVE_MODULES_WITH_TYPE_ALIASES: SchemaType = { ], }, }, + enumMap: {}, spec: { properties: [ { @@ -1138,6 +1144,7 @@ const REAL_MODULE_EXAMPLE: SchemaType = { ], }, }, + enumMap: {}, spec: { properties: [ { @@ -1333,6 +1340,7 @@ const REAL_MODULE_EXAMPLE: SchemaType = { type: 'ObjectTypeAnnotation', }, }, + enumMap: {}, spec: { properties: [ { @@ -1508,6 +1516,7 @@ const CXX_ONLY_NATIVE_MODULES: SchemaType = { ], }, }, + enumMap: {}, spec: { properties: [ { @@ -1648,6 +1657,7 @@ const SAMPLE_WITH_UPPERCASE_NAME: SchemaType = { modules: { NativeSampleTurboModule: { type: 'NativeModule', + enumMap: {}, aliasMap: {}, spec: { properties: [], diff --git a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js index bd97c0f757a406..95ef4306ebee73 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/parsers-commons-test.js @@ -277,6 +277,7 @@ describe('parseObjectProperty', () => { const moduleName = 'testModuleName'; const types = {['wrongName']: 'wrongType'}; const aliasMap = {}; + const enumMap = {}; const tryParse = () => null; const cxxOnly = false; const nullable = true; @@ -305,6 +306,7 @@ describe('parseObjectProperty', () => { moduleName, types, aliasMap, + enumMap, tryParse, cxxOnly, nullable, @@ -338,6 +340,7 @@ describe('parseObjectProperty', () => { moduleName, types, aliasMap, + enumMap, tryParse, cxxOnly, nullable, @@ -375,6 +378,7 @@ describe('buildSchemaFromConfigType', () => { const moduleSchemaMock = { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: {properties: []}, moduleName: '', }; @@ -743,6 +747,7 @@ describe('buildSchema', () => { fileName: { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: { properties: [ { diff --git a/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js b/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js index 92f391d3bcb24f..66b7e3af66ce15 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/parsers-primitives-test.js @@ -29,6 +29,7 @@ const { emitStringish, emitMixed, typeAliasResolution, + typeEnumResolution, } = require('../parsers-primitives.js'); const {MockedParser} = require('../parserMock'); const {emitUnion} = require('../parsers-primitives'); @@ -264,14 +265,14 @@ describe('typeAliasResolution', () => { ], }; - describe('when typeAliasResolutionStatus is successful', () => { - const typeAliasResolutionStatus = {successful: true, aliasName: 'Foo'}; + describe('when typeResolution is successful', () => { + const typeResolution = {successful: true, type: 'alias', name: 'Foo'}; describe('when nullable is true', () => { it('returns nullable TypeAliasTypeAnnotation and map it in aliasMap', () => { const aliasMap = {}; const result = typeAliasResolution( - typeAliasResolutionStatus, + typeResolution, objectTypeAnnotation, aliasMap, true, @@ -292,7 +293,7 @@ describe('typeAliasResolution', () => { it('returns non nullable TypeAliasTypeAnnotation and map it in aliasMap', () => { const aliasMap = {}; const result = typeAliasResolution( - typeAliasResolutionStatus, + typeResolution, objectTypeAnnotation, aliasMap, false, @@ -307,14 +308,14 @@ describe('typeAliasResolution', () => { }); }); - describe('when typeAliasResolutionStatus is not successful', () => { - const typeAliasResolutionStatus = {successful: false}; + describe('when typeResolution is not successful', () => { + const typeResolution = {successful: false}; describe('when nullable is true', () => { it('returns nullable ObjectTypeAnnotation', () => { const aliasMap = {}; const result = typeAliasResolution( - typeAliasResolutionStatus, + typeResolution, objectTypeAnnotation, aliasMap, true, @@ -332,7 +333,7 @@ describe('typeAliasResolution', () => { it('returns non nullable ObjectTypeAnnotation', () => { const aliasMap = {}; const result = typeAliasResolution( - typeAliasResolutionStatus, + typeResolution, objectTypeAnnotation, aliasMap, false, @@ -345,6 +346,98 @@ describe('typeAliasResolution', () => { }); }); +describe('typeEnumResolution', () => { + describe('when typeResolution is successful', () => { + describe('when nullable is true', () => { + it('returns nullable EnumDeclaration and map it in enumMap', () => { + const enumMap = {}; + const mockTypeAnnotation = {type: 'StringTypeAnnotation'}; + + const result = typeEnumResolution( + mockTypeAnnotation, + {successful: true, type: 'enum', name: 'Foo'}, + true /* nullable */, + 'SomeModule' /* name */, + 'Flow', + enumMap, + parser, + ); + + expect(enumMap).toEqual({ + Foo: { + type: 'EnumDeclarationWithMembers', + name: 'Foo', + memberType: 'StringTypeAnnotation', + members: [ + { + name: 'Hello', + value: 'hello', + }, + { + name: 'Goodbye', + value: 'goodbye', + }, + ], + }, + }); + + expect(result).toEqual({ + type: 'NullableTypeAnnotation', + typeAnnotation: { + name: 'Foo', + type: 'EnumDeclaration', + memberType: 'StringTypeAnnotation', + }, + }); + }); + }); + + describe('when nullable is false', () => { + it('returns non nullable TypeAliasTypeAnnotation and map it in aliasMap', () => { + const enumMap = {}; + const mockTypeAnnotation = {type: 'NumberTypeAnnotation'}; + + const result = typeEnumResolution( + mockTypeAnnotation, + {successful: true, type: 'enum', name: 'Foo'}, + true /* nullable */, + 'SomeModule' /* name */, + 'Flow', + enumMap, + parser, + ); + + expect(enumMap).toEqual({ + Foo: { + type: 'EnumDeclarationWithMembers', + name: 'Foo', + memberType: 'NumberTypeAnnotation', + members: [ + { + name: 'On', + value: '1', + }, + { + name: 'Off', + value: '0', + }, + ], + }, + }); + + expect(result).toEqual({ + type: 'NullableTypeAnnotation', + typeAnnotation: { + name: 'Foo', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }); + }); + }); + }); +}); + describe('emitPromise', () => { const moduleName = 'testModuleName'; @@ -362,6 +455,8 @@ describe('emitPromise', () => { {}, /* aliasMap: {...NativeModuleAliasMap} */ {}, + /* enumMap: {...NativeModuleEnumMap} */ + {}, /* tryParse: ParserErrorCapturer */ // $FlowFixMe[missing-local-annot] function (_: () => T) { @@ -991,6 +1086,8 @@ describe('emitArrayType', () => { {}, /* aliasMap: {...NativeModuleAliasMap} */ {}, + /* enumMap: {...NativeModuleEnumMap} */ + {}, /* cxxOnly: boolean */ false, nullable, diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js index 8024079b0fb683..8734095f43f2d4 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js @@ -211,6 +211,60 @@ export interface Spec2 extends TurboModule { `; +const EMPTY_ENUM_NATIVE_MODULE = ` +/** + * 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 {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export enum SomeEnum { +} + +export interface Spec extends TurboModule { + +getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing('EmptyEnumNativeModule'); +`; + +const MIXED_VALUES_ENUM_NATIVE_MODULE = ` +/** + * 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 {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export enum SomeEnum { + NUM = 1, + STR = 'str', +} + +export interface Spec extends TurboModule { + +getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing('MixedValuesEnumNativeModule'); +`; + module.exports = { NATIVE_MODULES_WITH_READ_ONLY_OBJECT_NO_TYPE_FOR_CONTENT, NATIVE_MODULES_WITH_UNNAMED_PARAMS, @@ -220,4 +274,6 @@ module.exports = { TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT, NATIVE_MODULES_WITH_NOT_ONLY_METHODS, TWO_NATIVE_EXTENDING_TURBO_MODULE, + EMPTY_ENUM_NATIVE_MODULE, + MIXED_VALUES_ENUM_NATIVE_MODULE, }; diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap index ce01f78c2627f5..6de97cbbc6b47a 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap @@ -1,5 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`RN Codegen Flow Parser Fails with error message EMPTY_ENUM_NATIVE_MODULE 1`] = `"Module NativeSampleTurboModule: Failed parsing the enum SomeEnum in NativeSampleTurboModule with the error: Enums should have at least one member and member values can not be mixed- they all must be either blank, number, or string values."`; + +exports[`RN Codegen Flow Parser Fails with error message MIXED_VALUES_ENUM_NATIVE_MODULE 1`] = `"Module NativeSampleTurboModule: Failed parsing the enum SomeEnum in NativeSampleTurboModule with the error: Enums should have at least one member and member values can not be mixed- they all must be either blank, number, or string values."`; + exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT 1`] = `"Module NativeSampleTurboModule: Generic 'Array' must have type parameters."`; exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM 1`] = `"Module NativeSampleTurboModule: Generic 'Array' must have type parameters."`; @@ -22,6 +26,7 @@ exports[`RN Codegen Flow Parser can generate fixture ANDROID_ONLY_NATIVE_MODULE 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [] }, @@ -40,6 +45,72 @@ exports[`RN Codegen Flow Parser can generate fixture CXX_ONLY_NATIVE_MODULE 1`] 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': { + 'Quality': { + 'name': 'Quality', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'StringTypeAnnotation', + 'members': [ + { + 'name': 'SD', + 'value': 'SD' + }, + { + 'name': 'HD', + 'value': 'HD' + } + ] + }, + 'Resolution': { + 'name': 'Resolution', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'NumberTypeAnnotation', + 'members': [ + { + 'name': 'Low', + 'value': 720 + }, + { + 'name': 'High', + 'value': 1080 + } + ] + }, + 'Floppy': { + 'name': 'Floppy', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'NumberTypeAnnotation', + 'members': [ + { + 'name': 'LowDensity', + 'value': 0.72 + }, + { + 'name': 'HighDensity', + 'value': 1.44 + } + ] + }, + 'StringOptions': { + 'name': 'StringOptions', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'StringTypeAnnotation', + 'members': [ + { + 'name': 'One', + 'value': 'one' + }, + { + 'name': 'Two', + 'value': 'two' + }, + { + 'name': 'Three', + 'value': 'three' + } + ] + } + }, 'spec': { 'properties': [ { @@ -89,6 +160,7 @@ exports[`RN Codegen Flow Parser can generate fixture CXX_ONLY_NATIVE_MODULE 1`] 'name': 'quality', 'optional': false, 'typeAnnotation': { + 'name': 'Quality', 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } @@ -97,6 +169,7 @@ exports[`RN Codegen Flow Parser can generate fixture CXX_ONLY_NATIVE_MODULE 1`] 'name': 'resolution', 'optional': true, 'typeAnnotation': { + 'name': 'Resolution', 'type': 'EnumDeclaration', 'memberType': 'NumberTypeAnnotation' } @@ -105,6 +178,7 @@ exports[`RN Codegen Flow Parser can generate fixture CXX_ONLY_NATIVE_MODULE 1`] 'name': 'floppy', 'optional': false, 'typeAnnotation': { + 'name': 'Floppy', 'type': 'EnumDeclaration', 'memberType': 'NumberTypeAnnotation' } @@ -113,6 +187,7 @@ exports[`RN Codegen Flow Parser can generate fixture CXX_ONLY_NATIVE_MODULE 1`] 'name': 'stringOptions', 'optional': false, 'typeAnnotation': { + 'name': 'StringOptions', 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } @@ -221,6 +296,7 @@ exports[`RN Codegen Flow Parser can generate fixture EMPTY_NATIVE_MODULE 1`] = ` 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [] }, @@ -236,6 +312,72 @@ exports[`RN Codegen Flow Parser can generate fixture IOS_ONLY_NATIVE_MODULE 1`] 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': { + 'Quality': { + 'name': 'Quality', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'StringTypeAnnotation', + 'members': [ + { + 'name': 'SD', + 'value': 'SD' + }, + { + 'name': 'HD', + 'value': 'HD' + } + ] + }, + 'Resolution': { + 'name': 'Resolution', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'NumberTypeAnnotation', + 'members': [ + { + 'name': 'Low', + 'value': 720 + }, + { + 'name': 'High', + 'value': 1080 + } + ] + }, + 'Floppy': { + 'name': 'Floppy', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'NumberTypeAnnotation', + 'members': [ + { + 'name': 'LowDensity', + 'value': 0.72 + }, + { + 'name': 'HighDensity', + 'value': 1.44 + } + ] + }, + 'StringOptions': { + 'name': 'StringOptions', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'StringTypeAnnotation', + 'members': [ + { + 'name': 'One', + 'value': 'one' + }, + { + 'name': 'Two', + 'value': 'two' + }, + { + 'name': 'Three', + 'value': 'three' + } + ] + } + }, 'spec': { 'properties': [ { @@ -251,6 +393,7 @@ exports[`RN Codegen Flow Parser can generate fixture IOS_ONLY_NATIVE_MODULE 1`] 'name': 'quality', 'optional': false, 'typeAnnotation': { + 'name': 'Quality', 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } @@ -259,6 +402,7 @@ exports[`RN Codegen Flow Parser can generate fixture IOS_ONLY_NATIVE_MODULE 1`] 'name': 'resolution', 'optional': true, 'typeAnnotation': { + 'name': 'Resolution', 'type': 'EnumDeclaration', 'memberType': 'NumberTypeAnnotation' } @@ -267,6 +411,7 @@ exports[`RN Codegen Flow Parser can generate fixture IOS_ONLY_NATIVE_MODULE 1`] 'name': 'floppy', 'optional': false, 'typeAnnotation': { + 'name': 'Floppy', 'type': 'EnumDeclaration', 'memberType': 'NumberTypeAnnotation' } @@ -275,6 +420,7 @@ exports[`RN Codegen Flow Parser can generate fixture IOS_ONLY_NATIVE_MODULE 1`] 'name': 'stringOptions', 'optional': false, 'typeAnnotation': { + 'name': 'StringOptions', 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } @@ -333,6 +479,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ALIASES ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -496,6 +643,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ARRAY_WI 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -537,6 +685,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ARRAY_WI 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -572,6 +721,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_BASIC_AR 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -638,6 +788,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_BASIC_PA 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -730,6 +881,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_CALLBACK 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -790,6 +942,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_COMPLEX_ 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -849,6 +1002,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_COMPLEX_ 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1061,6 +1215,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_COMPLEX_ 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1165,6 +1320,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_FLOAT_AN 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1253,6 +1409,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_NESTED_A ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1310,6 +1467,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_NULLABLE 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1361,6 +1519,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_OBJECT_W ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1457,6 +1616,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_PARTIALS ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1568,6 +1728,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_PARTIALS ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1656,6 +1817,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_PROMISE ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1715,6 +1877,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ROOT_TAG 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1752,6 +1915,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_SIMPLE_O 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1787,6 +1951,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_UNION 1` 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1848,6 +2013,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_UNSAFE_O 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1921,6 +2087,7 @@ exports[`RN Codegen Flow Parser can generate fixture PROMISE_WITH_COMMONLY_USED_ ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { diff --git a/packages/react-native-codegen/src/parsers/flow/modules/index.js b/packages/react-native-codegen/src/parsers/flow/modules/index.js index d8fba8e465eff2..6fa83c5bf96f79 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/index.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/index.js @@ -13,6 +13,7 @@ import type { NamedShape, NativeModuleAliasMap, + NativeModuleEnumMap, NativeModuleBaseTypeAnnotation, NativeModuleTypeAnnotation, NativeModulePropertyShape, @@ -30,7 +31,6 @@ const { wrapNullable, assertGenericTypeAnnotationHasExactlyOneTypeParameter, parseObjectProperty, - translateDefault, buildPropertySchema, } = require('../../parsers-commons'); const { @@ -51,12 +51,13 @@ const { emitMixed, emitUnion, typeAliasResolution, - translateArrayTypeAnnotation, + typeEnumResolution, } = require('../../parsers-primitives'); const { UnsupportedTypeAnnotationParserError, IncorrectModuleRegistryCallArgumentTypeParserError, + UnsupportedGenericParserError, } = require('../../errors'); const { @@ -80,11 +81,12 @@ function translateTypeAnnotation( flowTypeAnnotation: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, parser: Parser, ): Nullable { - const {nullable, typeAnnotation, typeAliasResolutionStatus} = + const {nullable, typeAnnotation, typeResolutionStatus} = resolveTypeAnnotation(flowTypeAnnotation, types); switch (typeAnnotation.type) { @@ -101,6 +103,7 @@ function translateTypeAnnotation( nullable, types, aliasMap, + enumMap, tryParse, cxxOnly, translateTypeAnnotation, @@ -114,6 +117,7 @@ function translateTypeAnnotation( parser, types, aliasMap, + enumMap, cxxOnly, nullable, translateTypeAnnotation, @@ -132,6 +136,7 @@ function translateTypeAnnotation( typeAnnotation.typeParameters.params[0], types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -181,6 +186,7 @@ function translateTypeAnnotation( prop.value, types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -191,11 +197,9 @@ function translateTypeAnnotation( return emitObject(nullable, properties); } default: { - return translateDefault( + throw new UnsupportedGenericParserError( hasteModuleName, typeAnnotation, - types, - nullable, parser, ); } @@ -216,6 +220,7 @@ function translateTypeAnnotation( propertyType, types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -240,6 +245,7 @@ function translateTypeAnnotation( hasteModuleName, types, aliasMap, + enumMap, tryParse, cxxOnly, nullable, @@ -253,7 +259,7 @@ function translateTypeAnnotation( }; return typeAliasResolution( - typeAliasResolutionStatus, + typeResolutionStatus, objectTypeAnnotation, aliasMap, nullable, @@ -278,6 +284,7 @@ function translateTypeAnnotation( typeAnnotation, types, aliasMap, + enumMap, tryParse, cxxOnly, translateTypeAnnotation, @@ -301,6 +308,18 @@ function translateTypeAnnotation( return emitGenericObject(nullable); } } + case 'EnumStringBody': + case 'EnumNumberBody': { + return typeEnumResolution( + typeAnnotation, + typeResolutionStatus, + nullable, + hasteModuleName, + language, + enumMap, + parser, + ); + } default: { throw new UnsupportedTypeAnnotationParserError( hasteModuleName, @@ -430,16 +449,20 @@ function buildModuleSchema( .filter(property => property.type === 'ObjectTypeProperty') .map(property => { const aliasMap: {...NativeModuleAliasMap} = {}; + const enumMap: {...NativeModuleEnumMap} = {}; return tryParse(() => ({ aliasMap: aliasMap, + enumMap: enumMap, propertyShape: buildPropertySchema( hasteModuleName, property, types, aliasMap, + enumMap, tryParse, cxxOnly, resolveTypeAnnotation, @@ -450,9 +473,13 @@ function buildModuleSchema( }) .filter(Boolean) .reduce( - (moduleSchema: NativeModuleSchema, {aliasMap, propertyShape}) => ({ + ( + moduleSchema: NativeModuleSchema, + {aliasMap, enumMap, propertyShape}, + ) => ({ type: 'NativeModule', aliasMap: {...moduleSchema.aliasMap, ...aliasMap}, + enumMap: {...moduleSchema.enumMap, ...enumMap}, spec: { properties: [...moduleSchema.spec.properties, propertyShape], }, @@ -462,6 +489,7 @@ function buildModuleSchema( { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: {properties: []}, moduleName, excludedPlatforms: diff --git a/packages/react-native-codegen/src/parsers/flow/parser.js b/packages/react-native-codegen/src/parsers/flow/parser.js index 561ae77a3f19fd..bdcaf3b9eca91a 100644 --- a/packages/react-native-codegen/src/parsers/flow/parser.js +++ b/packages/react-native-codegen/src/parsers/flow/parser.js @@ -16,6 +16,8 @@ import type { NamedShape, Nullable, NativeModuleParamTypeAnnotation, + NativeModuleEnumMemberType, + NativeModuleEnumMembers, } from '../../CodegenSchema'; import type {ParserType} from '../errors'; import type {Parser} from '../parser'; @@ -54,16 +56,6 @@ class FlowParser implements Parser { return property.key.name; } - getMaybeEnumMemberType(maybeEnumDeclaration: $FlowFixMe): string { - return maybeEnumDeclaration.body.type - .replace('EnumNumberBody', 'NumberTypeAnnotation') - .replace('EnumStringBody', 'StringTypeAnnotation'); - } - - isEnumDeclaration(maybeEnumDeclaration: $FlowFixMe): boolean { - return maybeEnumDeclaration.type === 'EnumDeclaration'; - } - language(): ParserType { return 'Flow'; } @@ -148,6 +140,62 @@ class FlowParser implements Parser { ): $FlowFixMe { return functionTypeAnnotation.returnType; } + + parseEnumMembersType(typeAnnotation: $FlowFixMe): NativeModuleEnumMemberType { + const enumMembersType: ?NativeModuleEnumMemberType = + typeAnnotation.type === 'EnumStringBody' + ? 'StringTypeAnnotation' + : typeAnnotation.type === 'EnumNumberBody' + ? 'NumberTypeAnnotation' + : null; + if (!enumMembersType) { + throw new Error( + `Unknown enum type annotation type. Got: ${typeAnnotation.type}. Expected: EnumStringBody or EnumNumberBody.`, + ); + } + return enumMembersType; + } + + validateEnumMembersSupported( + typeAnnotation: $FlowFixMe, + enumMembersType: NativeModuleEnumMemberType, + ): void { + if (!typeAnnotation.members || typeAnnotation.members.length === 0) { + // passing mixed members to flow would result in a flow error + // if the tool is launched ignoring that error, the enum would appear like not having enums + throw new Error( + 'Enums should have at least one member and member values can not be mixed- they all must be either blank, number, or string values.', + ); + } + + typeAnnotation.members.forEach(member => { + if ( + enumMembersType === 'StringTypeAnnotation' && + (!member.init || typeof member.init.value === 'string') + ) { + return; + } + + if ( + enumMembersType === 'NumberTypeAnnotation' && + member.init && + typeof member.init.value === 'number' + ) { + return; + } + + throw new Error( + 'Enums can not be mixed- they all must be either blank, number, or string values.', + ); + }); + } + + parseEnumMembers(typeAnnotation: $FlowFixMe): NativeModuleEnumMembers { + return typeAnnotation.members.map(member => ({ + name: member.id.name, + value: member.init?.value ?? member.id.name, + })); + } } module.exports = { diff --git a/packages/react-native-codegen/src/parsers/flow/utils.js b/packages/react-native-codegen/src/parsers/flow/utils.js index f172d8ec4c8283..d8939913389f0f 100644 --- a/packages/react-native-codegen/src/parsers/flow/utils.js +++ b/packages/react-native-codegen/src/parsers/flow/utils.js @@ -10,7 +10,7 @@ 'use strict'; -import type {TypeAliasResolutionStatus, TypeDeclarationMap} from '../utils'; +import type {TypeResolutionStatus, TypeDeclarationMap} from '../utils'; /** * This FlowFixMe is supposed to refer to an InterfaceDeclaration or TypeAlias @@ -61,7 +61,7 @@ function resolveTypeAnnotation( ): { nullable: boolean, typeAnnotation: $FlowFixMe, - typeAliasResolutionStatus: TypeAliasResolutionStatus, + typeResolutionStatus: TypeResolutionStatus, } { invariant( typeAnnotation != null, @@ -70,7 +70,7 @@ function resolveTypeAnnotation( let node = typeAnnotation; let nullable = false; - let typeAliasResolutionStatus: TypeAliasResolutionStatus = { + let typeResolutionStatus: TypeResolutionStatus = { successful: false, }; @@ -78,34 +78,49 @@ function resolveTypeAnnotation( if (node.type === 'NullableTypeAnnotation') { nullable = true; node = node.typeAnnotation; - } else if (node.type === 'GenericTypeAnnotation') { - typeAliasResolutionStatus = { - successful: true, - aliasName: node.id.name, - }; - const resolvedTypeAnnotation = types[node.id.name]; - if ( - resolvedTypeAnnotation == null || - resolvedTypeAnnotation.type === 'EnumDeclaration' - ) { - break; - } + continue; + } - invariant( - resolvedTypeAnnotation.type === 'TypeAlias', - `GenericTypeAnnotation '${node.id.name}' must resolve to a TypeAlias. Instead, it resolved to a '${resolvedTypeAnnotation.type}'`, - ); + if (node.type !== 'GenericTypeAnnotation') { + break; + } - node = resolvedTypeAnnotation.right; - } else { + const resolvedTypeAnnotation = types[node.id.name]; + if (resolvedTypeAnnotation == null) { break; } + + switch (resolvedTypeAnnotation.type) { + case 'TypeAlias': { + typeResolutionStatus = { + successful: true, + type: 'alias', + name: node.id.name, + }; + node = resolvedTypeAnnotation.right; + break; + } + case 'EnumDeclaration': { + typeResolutionStatus = { + successful: true, + type: 'enum', + name: node.id.name, + }; + node = resolvedTypeAnnotation.body; + break; + } + default: { + throw new TypeError( + `A non GenericTypeAnnotation must be a type declaration ('TypeAlias') or enum ('EnumDeclaration'). Instead, got the unsupported ${resolvedTypeAnnotation.type}.`, + ); + } + } } return { nullable: nullable, typeAnnotation: node, - typeAliasResolutionStatus, + typeResolutionStatus, }; } diff --git a/packages/react-native-codegen/src/parsers/parser.js b/packages/react-native-codegen/src/parsers/parser.js index 9f73635e6eae65..52755f81fcbd96 100644 --- a/packages/react-native-codegen/src/parsers/parser.js +++ b/packages/react-native-codegen/src/parsers/parser.js @@ -16,6 +16,8 @@ import type { NamedShape, Nullable, NativeModuleParamTypeAnnotation, + NativeModuleEnumMemberType, + NativeModuleEnumMembers, } from '../CodegenSchema'; import type {ParserType} from './errors'; @@ -41,18 +43,6 @@ export interface Parser { * @throws if property does not contain a property declaration. */ getKeyName(property: $FlowFixMe, hasteModuleName: string): string; - /** - * Given a type declaration, it possibly returns the name of the Enum type. - * @parameter maybeEnumDeclaration: an object possibly containing an Enum declaration. - * @returns: the name of the Enum type. - */ - getMaybeEnumMemberType(maybeEnumDeclaration: $FlowFixMe): string; - /** - * Given a type declaration, it returns a boolean specifying if is an Enum declaration. - * @parameter maybeEnumDeclaration: an object possibly containing an Enum declaration. - * @returns: a boolean specifying if is an Enum declaration. - */ - isEnumDeclaration(maybeEnumDeclaration: $FlowFixMe): boolean; /** * @returns: the Parser language. */ @@ -144,4 +134,22 @@ export interface Parser { getFunctionTypeAnnotationReturnType( functionTypeAnnotation: $FlowFixMe, ): $FlowFixMe; + + /** + * Calculates an enum's members type + */ + parseEnumMembersType(typeAnnotation: $FlowFixMe): NativeModuleEnumMemberType; + + /** + * Throws if enum mebers are not supported + */ + validateEnumMembersSupported( + typeAnnotation: $FlowFixMe, + enumMembersType: NativeModuleEnumMemberType, + ): void; + + /** + * Calculates enum's members + */ + parseEnumMembers(typeAnnotation: $FlowFixMe): NativeModuleEnumMembers; } diff --git a/packages/react-native-codegen/src/parsers/parserMock.js b/packages/react-native-codegen/src/parsers/parserMock.js index c3caff7aaacd79..475920a0983825 100644 --- a/packages/react-native-codegen/src/parsers/parserMock.js +++ b/packages/react-native-codegen/src/parsers/parserMock.js @@ -18,6 +18,8 @@ import type { NamedShape, Nullable, NativeModuleParamTypeAnnotation, + NativeModuleEnumMemberType, + NativeModuleEnumMembers, } from '../CodegenSchema'; // $FlowFixMe[untyped-import] there's no flowtype flow-parser @@ -61,16 +63,6 @@ export class MockedParser implements Parser { return property.key.name; } - getMaybeEnumMemberType(maybeEnumDeclaration: $FlowFixMe): string { - return maybeEnumDeclaration.body.type - .replace('EnumNumberBody', 'NumberTypeAnnotation') - .replace('EnumStringBody', 'StringTypeAnnotation'); - } - - isEnumDeclaration(maybeEnumDeclaration: $FlowFixMe): boolean { - return maybeEnumDeclaration.type === 'EnumDeclaration'; - } - language(): ParserType { return 'Flow'; } @@ -132,4 +124,39 @@ export class MockedParser implements Parser { ): $FlowFixMe { return functionTypeAnnotation.returnType; } + + parseEnumMembersType(typeAnnotation: $FlowFixMe): NativeModuleEnumMemberType { + return typeAnnotation.type; + } + + validateEnumMembersSupported( + typeAnnotation: $FlowFixMe, + enumMembersType: NativeModuleEnumMemberType, + ): void { + return; + } + + parseEnumMembers(typeAnnotation: $FlowFixMe): NativeModuleEnumMembers { + return typeAnnotation.type === 'StringTypeAnnotation' + ? [ + { + name: 'Hello', + value: 'hello', + }, + { + name: 'Goodbye', + value: 'goodbye', + }, + ] + : [ + { + name: 'On', + value: '1', + }, + { + name: 'Off', + value: '0', + }, + ]; + } } diff --git a/packages/react-native-codegen/src/parsers/parsers-commons.js b/packages/react-native-codegen/src/parsers/parsers-commons.js index 9f0310107b833a..2509d384da96e8 100644 --- a/packages/react-native-codegen/src/parsers/parsers-commons.js +++ b/packages/react-native-codegen/src/parsers/parsers-commons.js @@ -15,7 +15,6 @@ import type { NamedShape, NativeModuleAliasMap, NativeModuleBaseTypeAnnotation, - NativeModuleEnumDeclaration, NativeModuleSchema, NativeModuleTypeAnnotation, NativeModuleFunctionTypeAnnotation, @@ -45,12 +44,11 @@ const { const { MissingTypeParameterGenericParserError, MoreThanOneTypeParameterGenericParserError, - UnsupportedEnumDeclarationParserError, - UnsupportedGenericParserError, UnnamedFunctionParamParserError, } = require('./errors'); const invariant = require('invariant'); +import type {NativeModuleEnumMap} from '../CodegenSchema'; function wrapModuleSchema( nativeModuleSchema: NativeModuleSchema, @@ -135,6 +133,7 @@ function parseObjectProperty( hasteModuleName: string, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, nullable: boolean, @@ -157,6 +156,7 @@ function parseObjectProperty( languageTypeAnnotation, types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -183,43 +183,6 @@ function parseObjectProperty( }; } -function translateDefault( - hasteModuleName: string, - typeAnnotation: $FlowFixMe, - types: TypeDeclarationMap, - nullable: boolean, - parser: Parser, -): Nullable { - const maybeEnumDeclaration = - types[parser.nameForGenericTypeAnnotation(typeAnnotation)]; - - if (maybeEnumDeclaration && parser.isEnumDeclaration(maybeEnumDeclaration)) { - const memberType = parser.getMaybeEnumMemberType(maybeEnumDeclaration); - - if ( - memberType === 'NumberTypeAnnotation' || - memberType === 'StringTypeAnnotation' - ) { - return wrapNullable(nullable, { - type: 'EnumDeclaration', - memberType: memberType, - }); - } else { - throw new UnsupportedEnumDeclarationParserError( - hasteModuleName, - typeAnnotation, - memberType, - ); - } - } - - throw new UnsupportedGenericParserError( - hasteModuleName, - typeAnnotation, - parser, - ); -} - function translateFunctionTypeAnnotation( hasteModuleName: string, // TODO(T108222691): Use flow-types for @babel/parser @@ -227,6 +190,7 @@ function translateFunctionTypeAnnotation( functionTypeAnnotation: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, translateTypeAnnotation: $FlowFixMe, @@ -252,6 +216,7 @@ function translateFunctionTypeAnnotation( parser.getParameterTypeAnnotation(param), types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -292,6 +257,7 @@ function translateFunctionTypeAnnotation( parser.getFunctionTypeAnnotationReturnType(functionTypeAnnotation), types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -326,6 +292,7 @@ function buildPropertySchema( property: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, resolveTypeAnnotation: $FlowFixMe, @@ -363,6 +330,7 @@ function buildPropertySchema( value, types, aliasMap, + enumMap, tryParse, cxxOnly, translateTypeAnnotation, @@ -471,7 +439,6 @@ module.exports = { assertGenericTypeAnnotationHasExactlyOneTypeParameter, isObjectProperty, parseObjectProperty, - translateDefault, translateFunctionTypeAnnotation, buildPropertySchema, buildSchemaFromConfigType, diff --git a/packages/react-native-codegen/src/parsers/parsers-primitives.js b/packages/react-native-codegen/src/parsers/parsers-primitives.js index 23a7495b1654af..9b225fbc85c901 100644 --- a/packages/react-native-codegen/src/parsers/parsers-primitives.js +++ b/packages/react-native-codegen/src/parsers/parsers-primitives.js @@ -16,6 +16,7 @@ import type { DoubleTypeAnnotation, Int32TypeAnnotation, NativeModuleAliasMap, + NativeModuleEnumMap, NativeModuleBaseTypeAnnotation, NativeModuleTypeAnnotation, NativeModuleFloatTypeAnnotation, @@ -31,15 +32,22 @@ import type { StringTypeAnnotation, VoidTypeAnnotation, NativeModuleObjectTypeAnnotation, + NativeModuleEnumDeclaration, } from '../CodegenSchema'; +import type {ParserType} from './errors'; import type {Parser} from './parser'; import type { ParserErrorCapturer, - TypeAliasResolutionStatus, + TypeResolutionStatus, TypeDeclarationMap, } from './utils'; -const {UnsupportedUnionTypeAnnotationParserError} = require('./errors'); +const { + UnsupportedUnionTypeAnnotationParserError, + UnsupportedTypeAnnotationParserError, + ParserError, +} = require('./errors'); + const { throwIfArrayElementTypeAnnotationIsUnsupported, } = require('./error-utils'); @@ -102,6 +110,7 @@ function emitFunction( typeAnnotation: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, translateTypeAnnotation: $FlowFixMe, @@ -113,6 +122,7 @@ function emitFunction( typeAnnotation, types, aliasMap, + enumMap, tryParse, cxxOnly, translateTypeAnnotation, @@ -136,7 +146,7 @@ function emitString(nullable: boolean): Nullable { } function typeAliasResolution( - typeAliasResolutionStatus: TypeAliasResolutionStatus, + typeResolution: TypeResolutionStatus, objectTypeAnnotation: ObjectTypeAnnotation< Nullable, >, @@ -145,14 +155,14 @@ function typeAliasResolution( ): | Nullable | Nullable>> { - if (!typeAliasResolutionStatus.successful) { + if (!typeResolution.successful) { return wrapNullable(nullable, objectTypeAnnotation); } /** * All aliases RHS are required. */ - aliasMap[typeAliasResolutionStatus.aliasName] = objectTypeAnnotation; + aliasMap[typeResolution.name] = objectTypeAnnotation; /** * Nullability of type aliases is transitive. @@ -185,7 +195,58 @@ function typeAliasResolution( */ return wrapNullable(nullable, { type: 'TypeAliasTypeAnnotation', - name: typeAliasResolutionStatus.aliasName, + name: typeResolution.name, + }); +} + +function typeEnumResolution( + typeAnnotation: $FlowFixMe, + typeResolution: TypeResolutionStatus, + nullable: boolean, + hasteModuleName: string, + language: ParserType, + enumMap: {...NativeModuleEnumMap}, + parser: Parser, +): Nullable { + if (!typeResolution.successful || typeResolution.type !== 'enum') { + throw new UnsupportedTypeAnnotationParserError( + hasteModuleName, + typeAnnotation, + language, + ); + } + + const enumName = typeResolution.name; + + const enumMemberType = parser.parseEnumMembersType(typeAnnotation); + + try { + parser.validateEnumMembersSupported(typeAnnotation, enumMemberType); + } catch (e) { + if (e instanceof Error) { + throw new ParserError( + hasteModuleName, + typeAnnotation, + `Failed parsing the enum ${enumName} in ${hasteModuleName} with the error: ${e.message}`, + ); + } else { + throw e; + } + } + + const enumMembers = parser.parseEnumMembers(typeAnnotation); + + enumMap[enumName] = { + name: enumName, + type: 'EnumDeclarationWithMembers', + memberType: enumMemberType, + members: enumMembers, + }; + + return wrapNullable(nullable, { + name: enumName, + type: 'EnumDeclaration', + memberType: enumMemberType, }); } @@ -196,6 +257,7 @@ function emitPromise( nullable: boolean, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, translateTypeAnnotation: $FlowFixMe, @@ -223,6 +285,7 @@ function emitPromise( typeAnnotation.typeParameters.params[0], types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -291,6 +354,7 @@ function translateArrayTypeAnnotation( hasteModuleName: string, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, cxxOnly: boolean, arrayType: 'Array' | 'ReadonlyArray', elementType: $FlowFixMe, @@ -310,6 +374,7 @@ function translateArrayTypeAnnotation( elementType, types, aliasMap, + enumMap, /** * TODO(T72031674): Ensure that all ParsingErrors that are thrown * while parsing the array element don't get captured and collected. @@ -349,6 +414,7 @@ function emitArrayType( parser: Parser, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, cxxOnly: boolean, nullable: boolean, translateTypeAnnotation: $FlowFixMe, @@ -363,6 +429,7 @@ function emitArrayType( hasteModuleName, types, aliasMap, + enumMap, cxxOnly, typeAnnotation.type, typeAnnotation.typeParameters.params[0], @@ -390,5 +457,6 @@ module.exports = { emitMixed, emitUnion, typeAliasResolution, + typeEnumResolution, translateArrayTypeAnnotation, }; diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js index f25e8b3d10347a..a8d08e8395ff9a 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js @@ -156,6 +156,58 @@ export interface Spec2 extends TurboModule { } `; +const EMPTY_ENUM_NATIVE_MODULE = ` +/** + * 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 {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum SomeEnum { +} + +export interface Spec extends TurboModule { + readonly getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing( + 'EmptyEnumNativeModule', +); +`; + +const MIXED_VALUES_ENUM_NATIVE_MODULE = ` +/** + * 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 {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum SomeEnum { + NUM = 1, + STR = 'str', +} + +export interface Spec extends TurboModule { + readonly getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing( + 'MixedValuesEnumNativeModule', +); +`; + module.exports = { NATIVE_MODULES_WITH_UNNAMED_PARAMS, NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE, @@ -164,4 +216,6 @@ module.exports = { TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT, NATIVE_MODULES_WITH_NOT_ONLY_METHODS, TWO_NATIVE_EXTENDING_TURBO_MODULE, + EMPTY_ENUM_NATIVE_MODULE, + MIXED_VALUES_ENUM_NATIVE_MODULE, }; diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap index 60f9fbaba5a0b0..eed47982636b24 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap @@ -1,5 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`RN Codegen TypeScript Parser Fails with error message EMPTY_ENUM_NATIVE_MODULE 1`] = `"Module NativeSampleTurboModule: Failed parsing the enum SomeEnum in NativeSampleTurboModule with the error: Enums should have at least one member."`; + +exports[`RN Codegen TypeScript Parser Fails with error message MIXED_VALUES_ENUM_NATIVE_MODULE 1`] = `"Module NativeSampleTurboModule: Failed parsing the enum SomeEnum in NativeSampleTurboModule with the error: Enum values can not be mixed. They all must be either blank, number, or string values."`; + exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT 1`] = `"Module NativeSampleTurboModule: Generic 'Array' must have type parameters."`; exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM 1`] = `"Module NativeSampleTurboModule: Generic 'Array' must have type parameters."`; @@ -20,6 +24,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture ANDROID_ONLY_NATIVE_M 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [] }, @@ -38,6 +43,72 @@ exports[`RN Codegen TypeScript Parser can generate fixture CXX_ONLY_NATIVE_MODUL 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': { + 'Quality': { + 'name': 'Quality', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'StringTypeAnnotation', + 'members': [ + { + 'name': 'SD', + 'value': 'SD' + }, + { + 'name': 'HD', + 'value': 'HD' + } + ] + }, + 'Resolution': { + 'name': 'Resolution', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'NumberTypeAnnotation', + 'members': [ + { + 'name': 'Low', + 'value': 720 + }, + { + 'name': 'High', + 'value': 1080 + } + ] + }, + 'Floppy': { + 'name': 'Floppy', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'NumberTypeAnnotation', + 'members': [ + { + 'name': 'LowDensity', + 'value': 0.72 + }, + { + 'name': 'HighDensity', + 'value': 1.44 + } + ] + }, + 'StringOptions': { + 'name': 'StringOptions', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'StringTypeAnnotation', + 'members': [ + { + 'name': 'One', + 'value': 'one' + }, + { + 'name': 'Two', + 'value': 'two' + }, + { + 'name': 'Three', + 'value': 'three' + } + ] + } + }, 'spec': { 'properties': [ { @@ -87,6 +158,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture CXX_ONLY_NATIVE_MODUL 'name': 'quality', 'optional': false, 'typeAnnotation': { + 'name': 'Quality', 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } @@ -95,6 +167,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture CXX_ONLY_NATIVE_MODUL 'name': 'resolution', 'optional': true, 'typeAnnotation': { + 'name': 'Resolution', 'type': 'EnumDeclaration', 'memberType': 'NumberTypeAnnotation' } @@ -103,6 +176,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture CXX_ONLY_NATIVE_MODUL 'name': 'floppy', 'optional': false, 'typeAnnotation': { + 'name': 'Floppy', 'type': 'EnumDeclaration', 'memberType': 'NumberTypeAnnotation' } @@ -111,6 +185,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture CXX_ONLY_NATIVE_MODUL 'name': 'stringOptions', 'optional': false, 'typeAnnotation': { + 'name': 'StringOptions', 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } @@ -219,6 +294,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture EMPTY_NATIVE_MODULE 1 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [] }, @@ -234,6 +310,72 @@ exports[`RN Codegen TypeScript Parser can generate fixture IOS_ONLY_NATIVE_MODUL 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': { + 'Quality': { + 'name': 'Quality', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'StringTypeAnnotation', + 'members': [ + { + 'name': 'SD', + 'value': 'SD' + }, + { + 'name': 'HD', + 'value': 'HD' + } + ] + }, + 'Resolution': { + 'name': 'Resolution', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'NumberTypeAnnotation', + 'members': [ + { + 'name': 'Low', + 'value': 720 + }, + { + 'name': 'High', + 'value': 1080 + } + ] + }, + 'Floppy': { + 'name': 'Floppy', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'NumberTypeAnnotation', + 'members': [ + { + 'name': 'LowDensity', + 'value': 0.72 + }, + { + 'name': 'HighDensity', + 'value': 1.44 + } + ] + }, + 'StringOptions': { + 'name': 'StringOptions', + 'type': 'EnumDeclarationWithMembers', + 'memberType': 'StringTypeAnnotation', + 'members': [ + { + 'name': 'One', + 'value': 'one' + }, + { + 'name': 'Two', + 'value': 'two' + }, + { + 'name': 'Three', + 'value': 'three' + } + ] + } + }, 'spec': { 'properties': [ { @@ -249,6 +391,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture IOS_ONLY_NATIVE_MODUL 'name': 'quality', 'optional': false, 'typeAnnotation': { + 'name': 'Quality', 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } @@ -257,6 +400,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture IOS_ONLY_NATIVE_MODUL 'name': 'resolution', 'optional': true, 'typeAnnotation': { + 'name': 'Resolution', 'type': 'EnumDeclaration', 'memberType': 'NumberTypeAnnotation' } @@ -265,6 +409,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture IOS_ONLY_NATIVE_MODUL 'name': 'floppy', 'optional': false, 'typeAnnotation': { + 'name': 'Floppy', 'type': 'EnumDeclaration', 'memberType': 'NumberTypeAnnotation' } @@ -273,6 +418,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture IOS_ONLY_NATIVE_MODUL 'name': 'stringOptions', 'optional': false, 'typeAnnotation': { + 'name': 'StringOptions', 'type': 'EnumDeclaration', 'memberType': 'StringTypeAnnotation' } @@ -331,6 +477,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AL ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -494,6 +641,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -535,6 +683,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -570,6 +719,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -611,6 +761,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -646,6 +797,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BA 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -712,6 +864,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BA 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -778,6 +931,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_BA 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -870,6 +1024,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CA 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -930,6 +1085,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CO 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -989,6 +1145,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CO 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1048,6 +1205,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CO 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1260,6 +1418,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_CO 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1364,6 +1523,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_FL 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1452,6 +1612,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_NE ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1613,6 +1774,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_NE ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1670,6 +1832,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_NU 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1721,6 +1884,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_OB ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1817,6 +1981,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_PA ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -1928,6 +2093,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_PA ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -2016,6 +2182,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_PR ] } }, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -2075,6 +2242,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_RO 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -2112,6 +2280,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_SI 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -2147,6 +2316,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_UN 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { @@ -2208,6 +2378,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_UN 'NativeSampleTurboModule': { 'type': 'NativeModule', 'aliasMap': {}, + 'enumMap': {}, 'spec': { 'properties': [ { diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/index.js b/packages/react-native-codegen/src/parsers/typescript/modules/index.js index 0d9c431ef2a22a..18c9ddf13492af 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -13,6 +13,7 @@ import type { NamedShape, NativeModuleAliasMap, + NativeModuleEnumMap, NativeModuleBaseTypeAnnotation, NativeModulePropertyShape, NativeModuleTypeAnnotation, @@ -29,9 +30,9 @@ const {resolveTypeAnnotation, getTypes} = require('../utils'); const { parseObjectProperty, - translateDefault, buildPropertySchema, } = require('../../parsers-commons'); +const {typeEnumResolution} = require('../../parsers-primitives'); const { emitArrayType, @@ -81,11 +82,12 @@ function translateTypeAnnotation( typeScriptTypeAnnotation: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, parser: Parser, ): Nullable { - const {nullable, typeAnnotation, typeAliasResolutionStatus} = + const {nullable, typeAnnotation, typeResolutionStatus} = resolveTypeAnnotation(typeScriptTypeAnnotation, types); switch (typeAnnotation.type) { @@ -94,6 +96,7 @@ function translateTypeAnnotation( hasteModuleName, types, aliasMap, + enumMap, cxxOnly, 'Array', typeAnnotation.elementType, @@ -111,6 +114,7 @@ function translateTypeAnnotation( hasteModuleName, types, aliasMap, + enumMap, cxxOnly, 'ReadonlyArray', typeAnnotation.typeAnnotation.elementType, @@ -139,6 +143,7 @@ function translateTypeAnnotation( nullable, types, aliasMap, + enumMap, tryParse, cxxOnly, translateTypeAnnotation, @@ -152,6 +157,7 @@ function translateTypeAnnotation( parser, types, aliasMap, + enumMap, cxxOnly, nullable, translateTypeAnnotation, @@ -199,6 +205,7 @@ function translateTypeAnnotation( member.typeAnnotation.typeAnnotation, types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -210,11 +217,9 @@ function translateTypeAnnotation( return emitObject(nullable, properties); } default: { - return translateDefault( + throw new UnsupportedGenericParserError( hasteModuleName, typeAnnotation, - types, - nullable, parser, ); } @@ -234,6 +239,7 @@ function translateTypeAnnotation( }, types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -255,6 +261,7 @@ function translateTypeAnnotation( hasteModuleName, types, aliasMap, + enumMap, tryParse, cxxOnly, nullable, @@ -275,7 +282,7 @@ function translateTypeAnnotation( } return typeAliasResolution( - typeAliasResolutionStatus, + typeResolutionStatus, objectTypeAnnotation, aliasMap, nullable, @@ -296,6 +303,7 @@ function translateTypeAnnotation( propertyType, types, aliasMap, + enumMap, tryParse, cxxOnly, parser, @@ -317,6 +325,7 @@ function translateTypeAnnotation( hasteModuleName, types, aliasMap, + enumMap, tryParse, cxxOnly, nullable, @@ -330,12 +339,23 @@ function translateTypeAnnotation( }; return typeAliasResolution( - typeAliasResolutionStatus, + typeResolutionStatus, objectTypeAnnotation, aliasMap, nullable, ); } + case 'TSEnumDeclaration': { + return typeEnumResolution( + typeAnnotation, + typeResolutionStatus, + nullable, + hasteModuleName, + language, + enumMap, + parser, + ); + } case 'TSBooleanKeyword': { return emitBoolean(nullable); } @@ -355,6 +375,7 @@ function translateTypeAnnotation( typeAnnotation, types, aliasMap, + enumMap, tryParse, cxxOnly, translateTypeAnnotation, @@ -503,17 +524,21 @@ function buildModuleSchema( ) .map(property => { const aliasMap: {...NativeModuleAliasMap} = {}; + const enumMap: {...NativeModuleEnumMap} = {}; return tryParse(() => ({ aliasMap: aliasMap, + enumMap: enumMap, propertyShape: buildPropertySchema( hasteModuleName, property, types, aliasMap, + enumMap, tryParse, cxxOnly, resolveTypeAnnotation, @@ -524,10 +549,14 @@ function buildModuleSchema( }) .filter(Boolean) .reduce( - (moduleSchema: NativeModuleSchema, {aliasMap, propertyShape}) => { + ( + moduleSchema: NativeModuleSchema, + {aliasMap, enumMap, propertyShape}, + ) => { return { type: 'NativeModule', aliasMap: {...moduleSchema.aliasMap, ...aliasMap}, + enumMap: {...moduleSchema.enumMap, ...enumMap}, spec: { properties: [...moduleSchema.spec.properties, propertyShape], }, @@ -538,6 +567,7 @@ function buildModuleSchema( { type: 'NativeModule', aliasMap: {}, + enumMap: {}, spec: {properties: []}, moduleName: moduleName, excludedPlatforms: diff --git a/packages/react-native-codegen/src/parsers/typescript/parser.js b/packages/react-native-codegen/src/parsers/typescript/parser.js index 40b4af14d8ed95..9b40a7da55b70a 100644 --- a/packages/react-native-codegen/src/parsers/typescript/parser.js +++ b/packages/react-native-codegen/src/parsers/typescript/parser.js @@ -16,6 +16,8 @@ import type { NamedShape, Nullable, NativeModuleParamTypeAnnotation, + NativeModuleEnumMembers, + NativeModuleEnumMemberType, } from '../../CodegenSchema'; import type {ParserType} from '../errors'; import type {Parser} from '../parser'; @@ -54,20 +56,6 @@ class TypeScriptParser implements Parser { return property.key.name; } - getMaybeEnumMemberType(maybeEnumDeclaration: $FlowFixMe): string { - if (maybeEnumDeclaration.members[0].initializer) { - return maybeEnumDeclaration.members[0].initializer.type - .replace('NumericLiteral', 'NumberTypeAnnotation') - .replace('StringLiteral', 'StringTypeAnnotation'); - } - - return 'StringTypeAnnotation'; - } - - isEnumDeclaration(maybeEnumDeclaration: $FlowFixMe): boolean { - return maybeEnumDeclaration.type === 'TSEnumDeclaration'; - } - language(): ParserType { return 'TypeScript'; } @@ -155,6 +143,55 @@ class TypeScriptParser implements Parser { ): $FlowFixMe { return functionTypeAnnotation.typeAnnotation.typeAnnotation; } + + parseEnumMembersType(typeAnnotation: $FlowFixMe): NativeModuleEnumMemberType { + const enumInitializer = typeAnnotation.members[0]?.initializer; + const enumMembersType: ?NativeModuleEnumMemberType = + !enumInitializer || enumInitializer.type === 'StringLiteral' + ? 'StringTypeAnnotation' + : enumInitializer.type === 'NumericLiteral' + ? 'NumberTypeAnnotation' + : null; + if (!enumMembersType) { + throw new Error( + 'Enum values must be either blank, number, or string values.', + ); + } + return enumMembersType; + } + + validateEnumMembersSupported( + typeAnnotation: $FlowFixMe, + enumMembersType: NativeModuleEnumMemberType, + ): void { + if (!typeAnnotation.members || typeAnnotation.members.length === 0) { + throw new Error('Enums should have at least one member.'); + } + + const enumInitializerType = + enumMembersType === 'StringTypeAnnotation' + ? 'StringLiteral' + : enumMembersType === 'NumberTypeAnnotation' + ? 'NumericLiteral' + : null; + + typeAnnotation.members.forEach(member => { + if ( + (member.initializer?.type ?? 'StringLiteral') !== enumInitializerType + ) { + throw new Error( + 'Enum values can not be mixed. They all must be either blank, number, or string values.', + ); + } + }); + } + + parseEnumMembers(typeAnnotation: $FlowFixMe): NativeModuleEnumMembers { + return typeAnnotation.members.map(member => ({ + name: member.id.name, + value: member.initializer?.value ?? member.id.name, + })); + } } module.exports = { TypeScriptParser, diff --git a/packages/react-native-codegen/src/parsers/typescript/utils.js b/packages/react-native-codegen/src/parsers/typescript/utils.js index 725b74968ae6cc..ae72b030f640c7 100644 --- a/packages/react-native-codegen/src/parsers/typescript/utils.js +++ b/packages/react-native-codegen/src/parsers/typescript/utils.js @@ -10,7 +10,7 @@ 'use strict'; -import type {TypeAliasResolutionStatus, TypeDeclarationMap} from '../utils'; +import type {TypeResolutionStatus, TypeDeclarationMap} from '../utils'; const {parseTopLevelType} = require('./parseTopLevelType'); @@ -56,7 +56,7 @@ function resolveTypeAnnotation( ): { nullable: boolean, typeAnnotation: $FlowFixMe, - typeAliasResolutionStatus: TypeAliasResolutionStatus, + typeResolutionStatus: TypeResolutionStatus, } { invariant( typeAnnotation != null, @@ -68,7 +68,7 @@ function resolveTypeAnnotation( ? typeAnnotation.typeAnnotation : typeAnnotation; let nullable = false; - let typeAliasResolutionStatus: TypeAliasResolutionStatus = { + let typeResolutionStatus: TypeResolutionStatus = { successful: false, }; @@ -77,40 +77,55 @@ function resolveTypeAnnotation( nullable = nullable || topLevelType.optional; node = topLevelType.type; - if (node.type === 'TSTypeReference') { - typeAliasResolutionStatus = { - successful: true, - aliasName: node.typeName.name, - }; - const resolvedTypeAnnotation = types[node.typeName.name]; - if ( - resolvedTypeAnnotation == null || - resolvedTypeAnnotation.type === 'TSEnumDeclaration' - ) { + if (node.type !== 'TSTypeReference') { + break; + } + + const resolvedTypeAnnotation = types[node.typeName.name]; + if (resolvedTypeAnnotation == null) { + break; + } + + switch (resolvedTypeAnnotation.type) { + case 'TSTypeAliasDeclaration': { + typeResolutionStatus = { + successful: true, + type: 'alias', + name: node.typeName.name, + }; + node = resolvedTypeAnnotation.typeAnnotation; break; } - - switch (resolvedTypeAnnotation.type) { - case 'TSTypeAliasDeclaration': - node = resolvedTypeAnnotation.typeAnnotation; - break; - case 'TSInterfaceDeclaration': - node = resolvedTypeAnnotation; - break; - default: - throw new Error( - `GenericTypeAnnotation '${node.typeName.name}' must resolve to a TSTypeAliasDeclaration or a TSInterfaceDeclaration. Instead, it resolved to a '${resolvedTypeAnnotation.type}'`, - ); + case 'TSInterfaceDeclaration': { + typeResolutionStatus = { + successful: true, + type: 'alias', + name: node.typeName.name, + }; + node = resolvedTypeAnnotation; + break; + } + case 'TSEnumDeclaration': { + typeResolutionStatus = { + successful: true, + type: 'enum', + name: node.typeName.name, + }; + node = resolvedTypeAnnotation; + break; + } + default: { + throw new TypeError( + `A non GenericTypeAnnotation must be a type declaration ('TSTypeAliasDeclaration'), an interface ('TSInterfaceDeclaration'), or enum ('TSEnumDeclaration'). Instead, got the unsupported ${resolvedTypeAnnotation.type}.`, + ); } - } else { - break; } } return { nullable: nullable, typeAnnotation: node, - typeAliasResolutionStatus, + typeResolutionStatus, }; } diff --git a/packages/react-native-codegen/src/parsers/utils.js b/packages/react-native-codegen/src/parsers/utils.js index f3ad6483e06eaa..299bddef91d143 100644 --- a/packages/react-native-codegen/src/parsers/utils.js +++ b/packages/react-native-codegen/src/parsers/utils.js @@ -16,10 +16,11 @@ const path = require('path'); export type TypeDeclarationMap = {[declarationName: string]: $FlowFixMe}; -export type TypeAliasResolutionStatus = +export type TypeResolutionStatus = | $ReadOnly<{ + type: 'alias' | 'enum', successful: true, - aliasName: string, + name: string, }> | $ReadOnly<{ successful: false, From 82a7968d0990a71008679253b4972d500a88f0b2 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Fri, 3 Feb 2023 05:54:47 -0800 Subject: [PATCH 37/65] Remove duplication of default field values in iterator-based prop parsing (#36051) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36051 [Changelog][Internal] This has been on my backlog for some time, submitting the diff to get it out of the way. It makes the macro-based code in the "iterator-based property parsing" branch somewhat less horrible and less error prone (by removing duplication vs the default values in the class declaration). Reviewed By: sammy-SC Differential Revision: D42990595 fbshipit-source-id: e4b91160c6e09d3d1eab2ba70a58d390243bb335 --- .../renderer/components/image/ImageProps.cpp | 16 +-- .../components/scrollview/ScrollViewProps.cpp | 70 ++++++------ .../components/text/ParagraphProps.cpp | 6 +- .../AndroidTextInputProps.cpp | 104 +++++++++--------- .../components/view/AccessibilityProps.cpp | 37 ++++--- .../renderer/components/view/ViewProps.cpp | 48 ++++---- .../components/view/YogaStylableProps.cpp | 70 +++++------- ReactCommon/react/renderer/core/PropsMacros.h | 10 +- 8 files changed, 179 insertions(+), 182 deletions(-) diff --git a/ReactCommon/react/renderer/components/image/ImageProps.cpp b/ReactCommon/react/renderer/components/image/ImageProps.cpp index 7feac6300f8b17..5598746faa9f85 100644 --- a/ReactCommon/react/renderer/components/image/ImageProps.cpp +++ b/ReactCommon/react/renderer/components/image/ImageProps.cpp @@ -87,14 +87,16 @@ void ImageProps::setProp( // reuse the same values. ViewProps::setProp(context, hash, propName, value); + static auto defaults = ImageProps{}; + switch (hash) { - RAW_SET_PROP_SWITCH_CASE(sources, "source", {}); - RAW_SET_PROP_SWITCH_CASE(defaultSources, "defaultSource", {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(resizeMode, ImageResizeMode::Stretch); - RAW_SET_PROP_SWITCH_CASE_BASIC(blurRadius, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(capInsets, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(tintColor, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(internal_analyticTag, {}); + RAW_SET_PROP_SWITCH_CASE(sources, "source"); + RAW_SET_PROP_SWITCH_CASE(defaultSources, "defaultSource"); + RAW_SET_PROP_SWITCH_CASE_BASIC(resizeMode); + RAW_SET_PROP_SWITCH_CASE_BASIC(blurRadius); + RAW_SET_PROP_SWITCH_CASE_BASIC(capInsets); + RAW_SET_PROP_SWITCH_CASE_BASIC(tintColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(internal_analyticTag); } } diff --git a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp index 4eff278fdfc1f7..4f0d53929c9602 100644 --- a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp +++ b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp @@ -322,42 +322,42 @@ void ScrollViewProps::setProp( // reuse the same values. ViewProps::setProp(context, hash, propName, value); + static auto defaults = ScrollViewProps{}; + switch (hash) { - RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceHorizontal, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceVertical, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(bounces, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(bouncesZoom, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(canCancelContentTouches, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(centerContent, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(automaticallyAdjustContentInsets, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC( - automaticallyAdjustsScrollIndicatorInsets, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(decelerationRate, (Float)0.998); - RAW_SET_PROP_SWITCH_CASE_BASIC(directionalLockEnabled, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(indicatorStyle, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(keyboardDismissMode, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(maximumZoomScale, (Float)1.0); - RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale, (Float)1.0); - RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(pagingEnabled, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(pinchGestureEnabled, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(scrollsToTop, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(showsHorizontalScrollIndicator, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(showsVerticalScrollIndicator, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEventThrottle, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(zoomScale, (Float)1.0); - RAW_SET_PROP_SWITCH_CASE_BASIC(contentInset, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(contentOffset, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(scrollIndicatorInsets, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(snapToInterval, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(snapToAlignment, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(disableIntervalMomentum, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(snapToOffsets, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(snapToStart, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(snapToEnd, true); - RAW_SET_PROP_SWITCH_CASE_BASIC( - contentInsetAdjustmentBehavior, ContentInsetAdjustmentBehavior::Never); - RAW_SET_PROP_SWITCH_CASE_BASIC(scrollToOverflowEnabled, {}); + RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceHorizontal); + RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceVertical); + RAW_SET_PROP_SWITCH_CASE_BASIC(bounces); + RAW_SET_PROP_SWITCH_CASE_BASIC(bouncesZoom); + RAW_SET_PROP_SWITCH_CASE_BASIC(canCancelContentTouches); + RAW_SET_PROP_SWITCH_CASE_BASIC(centerContent); + RAW_SET_PROP_SWITCH_CASE_BASIC(automaticallyAdjustContentInsets); + RAW_SET_PROP_SWITCH_CASE_BASIC(automaticallyAdjustsScrollIndicatorInsets); + RAW_SET_PROP_SWITCH_CASE_BASIC(decelerationRate); + RAW_SET_PROP_SWITCH_CASE_BASIC(directionalLockEnabled); + RAW_SET_PROP_SWITCH_CASE_BASIC(indicatorStyle); + RAW_SET_PROP_SWITCH_CASE_BASIC(keyboardDismissMode); + RAW_SET_PROP_SWITCH_CASE_BASIC(maximumZoomScale); + RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale); + RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled); + RAW_SET_PROP_SWITCH_CASE_BASIC(pagingEnabled); + RAW_SET_PROP_SWITCH_CASE_BASIC(pinchGestureEnabled); + RAW_SET_PROP_SWITCH_CASE_BASIC(scrollsToTop); + RAW_SET_PROP_SWITCH_CASE_BASIC(showsHorizontalScrollIndicator); + RAW_SET_PROP_SWITCH_CASE_BASIC(showsVerticalScrollIndicator); + RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEventThrottle); + RAW_SET_PROP_SWITCH_CASE_BASIC(zoomScale); + RAW_SET_PROP_SWITCH_CASE_BASIC(contentInset); + RAW_SET_PROP_SWITCH_CASE_BASIC(contentOffset); + RAW_SET_PROP_SWITCH_CASE_BASIC(scrollIndicatorInsets); + RAW_SET_PROP_SWITCH_CASE_BASIC(snapToInterval); + RAW_SET_PROP_SWITCH_CASE_BASIC(snapToAlignment); + RAW_SET_PROP_SWITCH_CASE_BASIC(disableIntervalMomentum); + RAW_SET_PROP_SWITCH_CASE_BASIC(snapToOffsets); + RAW_SET_PROP_SWITCH_CASE_BASIC(snapToStart); + RAW_SET_PROP_SWITCH_CASE_BASIC(snapToEnd); + RAW_SET_PROP_SWITCH_CASE_BASIC(contentInsetAdjustmentBehavior); + RAW_SET_PROP_SWITCH_CASE_BASIC(scrollToOverflowEnabled); } } diff --git a/ReactCommon/react/renderer/components/text/ParagraphProps.cpp b/ReactCommon/react/renderer/components/text/ParagraphProps.cpp index c666e5e2d86ad4..242567dc4b6e63 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphProps.cpp +++ b/ReactCommon/react/renderer/components/text/ParagraphProps.cpp @@ -66,6 +66,8 @@ void ParagraphProps::setProp( ViewProps::setProp(context, hash, propName, value); BaseTextProps::setProp(context, hash, propName, value); + static auto defaults = ParagraphProps{}; + // ParagraphAttributes has its own switch statement - to keep all // of these fields together, and because there are some collisions between // propnames parsed here and outside of ParagraphAttributes. @@ -119,8 +121,8 @@ void ParagraphProps::setProp( } switch (hash) { - RAW_SET_PROP_SWITCH_CASE_BASIC(isSelectable, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(onTextLayout, {}); + RAW_SET_PROP_SWITCH_CASE_BASIC(isSelectable); + RAW_SET_PROP_SWITCH_CASE_BASIC(onTextLayout); } /* diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp index c8eaf160ba1ca3..8c0900480c4c54 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp @@ -269,6 +269,8 @@ void AndroidTextInputProps::setProp( ViewProps::setProp(context, hash, propName, value); BaseTextProps::setProp(context, hash, propName, value); + static auto defaults = AndroidTextInputProps{}; + // ParagraphAttributes has its own switch statement - to keep all // of these fields together, and because there are some collisions between // propnames parsed here and outside of ParagraphAttributes. For example, @@ -323,55 +325,59 @@ void AndroidTextInputProps::setProp( } switch (hash) { - RAW_SET_PROP_SWITCH_CASE_BASIC(autoComplete, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyLabel, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(numberOfLines, 0); - RAW_SET_PROP_SWITCH_CASE_BASIC(disableFullscreenUI, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(textBreakStrategy, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(underlineColorAndroid, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(inlineImageLeft, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(inlineImagePadding, 0); - RAW_SET_PROP_SWITCH_CASE_BASIC(importantForAutofill, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(showSoftInputOnFocus, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(autoCapitalize, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(autoCorrect, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(autoFocus, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(allowFontScaling, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(maxFontSizeMultiplier, (Float)0.0); - RAW_SET_PROP_SWITCH_CASE_BASIC(editable, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(keyboardType, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyType, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(maxLength, 0); - RAW_SET_PROP_SWITCH_CASE_BASIC(multiline, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(placeholder, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(placeholderTextColor, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(secureTextEntry, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(selectionColor, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(selection, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(this->value, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(defaultValue, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(selectTextOnFocus, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(submitBehavior, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(caretHidden, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(contextMenuHidden, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(textShadowColor, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(textShadowRadius, (Float)0.0); - RAW_SET_PROP_SWITCH_CASE_BASIC(textDecorationLine, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(fontStyle, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(textShadowOffset, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(lineHeight, (Float)0.0); - RAW_SET_PROP_SWITCH_CASE_BASIC(textTransform, {}); - // RAW_SET_PROP_SWITCH_CASE_BASIC(color, {0}); // currently not being parsed - RAW_SET_PROP_SWITCH_CASE_BASIC(letterSpacing, (Float)0.0); - RAW_SET_PROP_SWITCH_CASE_BASIC(fontSize, (Float)0.0); - RAW_SET_PROP_SWITCH_CASE_BASIC(textAlign, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(includeFontPadding, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(fontWeight, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(fontFamily, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(textAlignVertical, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(cursorColor, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(mostRecentEventCount, 0); - RAW_SET_PROP_SWITCH_CASE_BASIC(text, {}); + RAW_SET_PROP_SWITCH_CASE_BASIC(autoComplete); + RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyLabel); + RAW_SET_PROP_SWITCH_CASE_BASIC(numberOfLines); + RAW_SET_PROP_SWITCH_CASE_BASIC(disableFullscreenUI); + RAW_SET_PROP_SWITCH_CASE_BASIC(textBreakStrategy); + RAW_SET_PROP_SWITCH_CASE_BASIC(underlineColorAndroid); + RAW_SET_PROP_SWITCH_CASE_BASIC(inlineImageLeft); + RAW_SET_PROP_SWITCH_CASE_BASIC(inlineImagePadding); + RAW_SET_PROP_SWITCH_CASE_BASIC(importantForAutofill); + RAW_SET_PROP_SWITCH_CASE_BASIC(showSoftInputOnFocus); + RAW_SET_PROP_SWITCH_CASE_BASIC(autoCapitalize); + RAW_SET_PROP_SWITCH_CASE_BASIC(autoCorrect); + RAW_SET_PROP_SWITCH_CASE_BASIC(autoFocus); + RAW_SET_PROP_SWITCH_CASE_BASIC(allowFontScaling); + RAW_SET_PROP_SWITCH_CASE_BASIC(maxFontSizeMultiplier); + RAW_SET_PROP_SWITCH_CASE_BASIC(editable); + RAW_SET_PROP_SWITCH_CASE_BASIC(keyboardType); + RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyType); + RAW_SET_PROP_SWITCH_CASE_BASIC(maxLength); + RAW_SET_PROP_SWITCH_CASE_BASIC(multiline); + RAW_SET_PROP_SWITCH_CASE_BASIC(placeholder); + RAW_SET_PROP_SWITCH_CASE_BASIC(placeholderTextColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(secureTextEntry); + RAW_SET_PROP_SWITCH_CASE_BASIC(selectionColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(selection); + RAW_SET_PROP_SWITCH_CASE_BASIC(defaultValue); + RAW_SET_PROP_SWITCH_CASE_BASIC(selectTextOnFocus); + RAW_SET_PROP_SWITCH_CASE_BASIC(submitBehavior); + RAW_SET_PROP_SWITCH_CASE_BASIC(caretHidden); + RAW_SET_PROP_SWITCH_CASE_BASIC(contextMenuHidden); + RAW_SET_PROP_SWITCH_CASE_BASIC(textShadowColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(textShadowRadius); + RAW_SET_PROP_SWITCH_CASE_BASIC(textDecorationLine); + RAW_SET_PROP_SWITCH_CASE_BASIC(fontStyle); + RAW_SET_PROP_SWITCH_CASE_BASIC(textShadowOffset); + RAW_SET_PROP_SWITCH_CASE_BASIC(lineHeight); + RAW_SET_PROP_SWITCH_CASE_BASIC(textTransform); + // RAW_SET_PROP_SWITCH_CASE_BASIC(color); + RAW_SET_PROP_SWITCH_CASE_BASIC(letterSpacing); + RAW_SET_PROP_SWITCH_CASE_BASIC(fontSize); + RAW_SET_PROP_SWITCH_CASE_BASIC(textAlign); + RAW_SET_PROP_SWITCH_CASE_BASIC(includeFontPadding); + RAW_SET_PROP_SWITCH_CASE_BASIC(fontWeight); + RAW_SET_PROP_SWITCH_CASE_BASIC(fontFamily); + RAW_SET_PROP_SWITCH_CASE_BASIC(textAlignVertical); + RAW_SET_PROP_SWITCH_CASE_BASIC(cursorColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(mostRecentEventCount); + RAW_SET_PROP_SWITCH_CASE_BASIC(text); + + case CONSTEXPR_RAW_PROPS_KEY_HASH("value"): { + fromRawValue(context, value, this->value, {}); + return; + } // Paddings are not parsed at this level of the component (they're parsed in // ViewProps) but we do need to know if they're present or not. See diff --git a/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp b/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp index 8ee8640c189478..2e87d5d34f082a 100644 --- a/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp +++ b/ReactCommon/react/renderer/components/view/AccessibilityProps.cpp @@ -208,25 +208,26 @@ void AccessibilityProps::setProp( RawPropsPropNameHash hash, const char * /*propName*/, RawValue const &value) { + static auto defaults = AccessibilityProps{}; + switch (hash) { - RAW_SET_PROP_SWITCH_CASE_BASIC(accessible, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityState, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLabel, std::string{""}); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLabelledBy, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityHint, std::string{""}); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLanguage, std::string{""}); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityValue, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityActions, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityViewIsModal, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityElementsHidden, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityIgnoresInvertColors, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityTap, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityMagicTap, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityEscape, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityAction, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC( - importantForAccessibility, ImportantForAccessibility::Auto); - RAW_SET_PROP_SWITCH_CASE(testId, "testID", std::string{""}); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessible); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityState); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLabel); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLabelledBy); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityHint); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityLanguage); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityValue); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityActions); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityViewIsModal); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityElementsHidden); + RAW_SET_PROP_SWITCH_CASE_BASIC(accessibilityIgnoresInvertColors); + RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityTap); + RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityMagicTap); + RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityEscape); + RAW_SET_PROP_SWITCH_CASE_BASIC(onAccessibilityAction); + RAW_SET_PROP_SWITCH_CASE_BASIC(importantForAccessibility); + RAW_SET_PROP_SWITCH_CASE(testId, "testID"); case CONSTEXPR_RAW_PROPS_KEY_HASH("accessibilityRole"): { AccessibilityTraits traits = AccessibilityTraits::None; std::string roleString; diff --git a/ReactCommon/react/renderer/components/view/ViewProps.cpp b/ReactCommon/react/renderer/components/view/ViewProps.cpp index f8294e40b70fcf..f97a0560f61dd5 100644 --- a/ReactCommon/react/renderer/components/view/ViewProps.cpp +++ b/ReactCommon/react/renderer/components/view/ViewProps.cpp @@ -291,23 +291,25 @@ void ViewProps::setProp( YogaStylableProps::setProp(context, hash, propName, value); AccessibilityProps::setProp(context, hash, propName, value); + static auto defaults = ViewProps{}; + switch (hash) { - RAW_SET_PROP_SWITCH_CASE_BASIC(opacity, (Float)1.0); - RAW_SET_PROP_SWITCH_CASE_BASIC(foregroundColor, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(backgroundColor, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(shadowColor, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOffset, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOpacity, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(shadowRadius, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(transform, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(backfaceVisibility, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(shouldRasterize, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(zIndex, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(pointerEvents, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(hitSlop, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(onLayout, {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable, true); - RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews, false); + RAW_SET_PROP_SWITCH_CASE_BASIC(opacity); + RAW_SET_PROP_SWITCH_CASE_BASIC(foregroundColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(backgroundColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(shadowColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOffset); + RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOpacity); + RAW_SET_PROP_SWITCH_CASE_BASIC(shadowRadius); + RAW_SET_PROP_SWITCH_CASE_BASIC(transform); + RAW_SET_PROP_SWITCH_CASE_BASIC(backfaceVisibility); + RAW_SET_PROP_SWITCH_CASE_BASIC(shouldRasterize); + RAW_SET_PROP_SWITCH_CASE_BASIC(zIndex); + RAW_SET_PROP_SWITCH_CASE_BASIC(pointerEvents); + RAW_SET_PROP_SWITCH_CASE_BASIC(hitSlop); + RAW_SET_PROP_SWITCH_CASE_BASIC(onLayout); + RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable); + RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews); // events field VIEW_EVENT_CASE(PointerEnter); VIEW_EVENT_CASE(PointerEnterCapture); @@ -335,13 +337,13 @@ void ViewProps::setProp( VIEW_EVENT_CASE(TouchEnd); VIEW_EVENT_CASE(TouchCancel); #ifdef ANDROID - RAW_SET_PROP_SWITCH_CASE_BASIC(elevation, {}); - RAW_SET_PROP_SWITCH_CASE(nativeBackground, "nativeBackgroundAndroid", {}); - RAW_SET_PROP_SWITCH_CASE(nativeForeground, "nativeForegroundAndroid", {}); - RAW_SET_PROP_SWITCH_CASE_BASIC(focusable, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(hasTVPreferredFocus, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(needsOffscreenAlphaCompositing, false); - RAW_SET_PROP_SWITCH_CASE_BASIC(renderToHardwareTextureAndroid, false); + RAW_SET_PROP_SWITCH_CASE_BASIC(elevation); + RAW_SET_PROP_SWITCH_CASE(nativeBackground, "nativeBackgroundAndroid"); + RAW_SET_PROP_SWITCH_CASE(nativeForeground, "nativeForegroundAndroid"); + RAW_SET_PROP_SWITCH_CASE_BASIC(focusable); + RAW_SET_PROP_SWITCH_CASE_BASIC(hasTVPreferredFocus); + RAW_SET_PROP_SWITCH_CASE_BASIC(needsOffscreenAlphaCompositing); + RAW_SET_PROP_SWITCH_CASE_BASIC(renderToHardwareTextureAndroid); #endif // BorderRadii SET_CASCADED_RECTANGLE_CORNERS(borderRadii, "border", "Radius", value); diff --git a/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp b/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp index 17e342dd6ee14c..724a33ab4a3b80 100644 --- a/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp +++ b/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp @@ -48,10 +48,10 @@ static inline T const getFieldValue( return defaultValue; } -#define REBUILD_FIELD_SWITCH_CASE2(field, fieldName) \ - case CONSTEXPR_RAW_PROPS_KEY_HASH(fieldName): { \ - yogaStyle.field() = getFieldValue(context, value, defaults.field()); \ - return; \ +#define REBUILD_FIELD_SWITCH_CASE2(field, fieldName) \ + case CONSTEXPR_RAW_PROPS_KEY_HASH(fieldName): { \ + yogaStyle.field() = getFieldValue(context, value, ygDefaults.field()); \ + return; \ } // @lint-ignore CLANGTIDY cppcoreguidelines-macro-usage @@ -61,7 +61,7 @@ static inline T const getFieldValue( #define REBUILD_YG_FIELD_SWITCH_CASE_INDEXED(field, index, fieldName) \ case CONSTEXPR_RAW_PROPS_KEY_HASH(fieldName): { \ yogaStyle.field()[index] = \ - getFieldValue(context, value, defaults.field()[index]); \ + getFieldValue(context, value, ygDefaults.field()[index]); \ return; \ } @@ -104,7 +104,7 @@ void YogaStylableProps::setProp( RawPropsPropNameHash hash, const char *propName, RawValue const &value) { - static const auto defaults = YGStyle{}; + static const auto ygDefaults = YGStyle{}; Props::setProp(context, hash, propName, value); @@ -133,44 +133,28 @@ void YogaStylableProps::setProp( REBUILD_FIELD_YG_EDGES(padding, "padding", ""); REBUILD_FIELD_YG_EDGES(border, "border", "Width"); + static const auto defaults = YogaStylableProps{}; + // Aliases - RAW_SET_PROP_SWITCH_CASE(inset, "inset", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - insetBlock, "insetBlock", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - insetBlockEnd, "insetBlockEnd", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - insetBlockStart, "insetBlockStart", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - insetInline, "insetInline", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - insetInlineEnd, "insetInlineEnd", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - insetInlineStart, "insetInlineStart", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - marginInline, "marginInline", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - marginInlineStart, "marginInlineStart", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - marginInlineEnd, "marginInlineEnd", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - marginBlock, "marginBlock", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - marginBlockStart, "marginBlockStart", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - marginBlockEnd, "marginBlockEnd", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - paddingInline, "paddingInline", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - paddingInlineStart, "paddingInlineStart", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - paddingInlineEnd, "paddingInlineEnd", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - paddingBlock, "paddingBlock", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - paddingBlockStart, "paddingBlockStart", CompactValue::ofUndefined()); - RAW_SET_PROP_SWITCH_CASE( - paddingBlockEnd, "paddingBlockEnd", CompactValue::ofUndefined()); + RAW_SET_PROP_SWITCH_CASE(inset, "inset"); + RAW_SET_PROP_SWITCH_CASE(insetBlock, "insetBlock"); + RAW_SET_PROP_SWITCH_CASE(insetBlockEnd, "insetBlockEnd"); + RAW_SET_PROP_SWITCH_CASE(insetBlockStart, "insetBlockStart"); + RAW_SET_PROP_SWITCH_CASE(insetInline, "insetInline"); + RAW_SET_PROP_SWITCH_CASE(insetInlineEnd, "insetInlineEnd"); + RAW_SET_PROP_SWITCH_CASE(insetInlineStart, "insetInlineStart"); + RAW_SET_PROP_SWITCH_CASE(marginInline, "marginInline"); + RAW_SET_PROP_SWITCH_CASE(marginInlineStart, "marginInlineStart"); + RAW_SET_PROP_SWITCH_CASE(marginInlineEnd, "marginInlineEnd"); + RAW_SET_PROP_SWITCH_CASE(marginBlock, "marginBlock"); + RAW_SET_PROP_SWITCH_CASE(marginBlockStart, "marginBlockStart"); + RAW_SET_PROP_SWITCH_CASE(marginBlockEnd, "marginBlockEnd"); + RAW_SET_PROP_SWITCH_CASE(paddingInline, "paddingInline"); + RAW_SET_PROP_SWITCH_CASE(paddingInlineStart, "paddingInlineStart"); + RAW_SET_PROP_SWITCH_CASE(paddingInlineEnd, "paddingInlineEnd"); + RAW_SET_PROP_SWITCH_CASE(paddingBlock, "paddingBlock"); + RAW_SET_PROP_SWITCH_CASE(paddingBlockStart, "paddingBlockStart"); + RAW_SET_PROP_SWITCH_CASE(paddingBlockEnd, "paddingBlockEnd"); } } diff --git a/ReactCommon/react/renderer/core/PropsMacros.h b/ReactCommon/react/renderer/core/PropsMacros.h index 6fa0bd9a90ca48..a3d07018fb95da 100644 --- a/ReactCommon/react/renderer/core/PropsMacros.h +++ b/ReactCommon/react/renderer/core/PropsMacros.h @@ -31,15 +31,15 @@ // Convenience for building setProps switch statements. // This injects `fromRawValue` into source; each file that uses // this macro must import the proper, respective headers required. -#define RAW_SET_PROP_SWITCH_CASE(field, jsPropName, defaultValue) \ - case CONSTEXPR_RAW_PROPS_KEY_HASH(jsPropName): \ - fromRawValue(context, value, field, defaultValue); \ +#define RAW_SET_PROP_SWITCH_CASE(field, jsPropName) \ + case CONSTEXPR_RAW_PROPS_KEY_HASH(jsPropName): \ + fromRawValue(context, value, field, defaults.field); \ return; // Convenience for building setProps switch statements where the field name is // the same as the string identifier -#define RAW_SET_PROP_SWITCH_CASE_BASIC(field, defaultValue) \ - RAW_SET_PROP_SWITCH_CASE(field, #field, defaultValue) +#define RAW_SET_PROP_SWITCH_CASE_BASIC(field) \ + RAW_SET_PROP_SWITCH_CASE(field, #field) #define CASE_STATEMENT_SET_FIELD_VALUE_INDEXED( \ struct, field, fieldNameString, value) \ From 7fedd7577a249b1dd4f51b5b4a03858fd09cb7ef Mon Sep 17 00:00:00 2001 From: rj1 Date: Fri, 3 Feb 2023 08:12:29 -0800 Subject: [PATCH 38/65] fixed typo in interface.js (#36049) Summary: fixed a typo in interface.js ## Changelog [GENERAL] [FIXED] - Fixed a typo in interface.js Pull Request resolved: https://github.com/facebook/react-native/pull/36049 Test Plan: simple typo fix - no testing required Reviewed By: cortinico Differential Revision: D42996797 Pulled By: cipolleschi fbshipit-source-id: 8a5a5f28174f874c530549743019c281732d2da4 --- interface.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface.js b/interface.js index 291c81952e340b..8052785f9b5ffa 100644 --- a/interface.js +++ b/interface.js @@ -11,7 +11,7 @@ 'use strict'; // NOTE: Hmm... I don't think declaring variables within this module actually -// accomplishes anything besides documenting that these globals are exepcted to +// accomplishes anything besides documenting that these globals are expected to // exist. So I think the correct "fix" to this lint warning is to delete this // entire file. But in lieu of doing that... no harm for now in keeping this // file around, even if it is only for documentation purposes. ¯\_(ツ)_/¯ From 11ece22fc6955d169def9ef9f2809c24bc457ba8 Mon Sep 17 00:00:00 2001 From: Evan Yeung Date: Fri, 3 Feb 2023 14:25:53 -0800 Subject: [PATCH 39/65] Deploy 0.199.1 to xplat Summary: Changelog: [Internal] Reviewed By: SamChou19815 Differential Revision: D42982746 fbshipit-source-id: b45980db2d7204eee9bee9918eafdd810d147579 --- .flowconfig | 2 +- .flowconfig.android | 2 +- package.json | 2 +- repo-config/package.json | 2 +- yarn.lock | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.flowconfig b/.flowconfig index 747437031e30a1..132a3aeeda51d6 100644 --- a/.flowconfig +++ b/.flowconfig @@ -76,4 +76,4 @@ untyped-import untyped-type-import [version] -^0.198.2 +^0.199.1 diff --git a/.flowconfig.android b/.flowconfig.android index 2f4a143d1677d3..406d98a2231d5a 100644 --- a/.flowconfig.android +++ b/.flowconfig.android @@ -76,4 +76,4 @@ untyped-import untyped-type-import [version] -^0.198.2 +^0.199.1 diff --git a/package.json b/package.json index d8a95a8941f325..109d1dd68e389f 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "ws": "^6.2.2" }, "devDependencies": { - "flow-bin": "^0.198.2", + "flow-bin": "^0.199.1", "hermes-eslint": "0.8.0", "mock-fs": "^5.1.4", "react": "18.2.0", diff --git a/repo-config/package.json b/repo-config/package.json index 5377cf8187ccb2..82002581f821e4 100644 --- a/repo-config/package.json +++ b/repo-config/package.json @@ -38,7 +38,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native": "^4.0.0", "eslint-plugin-relay": "^1.8.3", - "flow-bin": "^0.198.2", + "flow-bin": "^0.199.1", "inquirer": "^7.1.0", "jest": "^29.2.1", "jest-junit": "^10.0.0", diff --git a/yarn.lock b/yarn.lock index b5f311bbfedace..640749212e5e5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4399,10 +4399,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== -flow-bin@^0.198.2: - version "0.198.2" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.198.2.tgz#e601121a411b4ec5889cc3567d2706369cc510a9" - integrity sha512-PML2zhAZVd8EgzDqS9oSJF+OxUtJ+YLEdVVsVO7d1BwnrXCgiVrLjxiQP8/hur820grzC51Xb2CI3J+ohH/bMg== +flow-bin@^0.199.1: + version "0.199.1" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.199.1.tgz#678eac2303fa898227f4d103264b6ce49f4430c1" + integrity sha512-Ic0Mp9iZ2exbH0mNj/XhzUWPZa9JylHb6uQARZnnYCTRwumOpjNOP0qwyRTltWrbCpfHjnWngNO9VLaVKHz2aQ== flow-parser@0.*, flow-parser@^0.185.0: version "0.185.0" From 7f2dd1d49cc3c0bf5e24fdb37f6457151c1f06c4 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Sun, 5 Feb 2023 06:49:08 -0800 Subject: [PATCH 40/65] Pull out CGContext early in UIImage+Diff (#35940) Summary: This is a small refactor designed to make future merges for React Native macOS easier. It was found while merging React Native 0.71 into React Native macOS. In React Native macOS, we ifdef iOS out API's and replace them with macOS APIs, reusing as much code as possible. `CGContext` is a type that is available on both iOS and macOS, but `UIGraphicsImageRendererContext` is not. A simple refactor makes it easier to reuse this code in React Native macOS without affecting iOS :) Resolves https://github.com/microsoft/react-native-macos/issues/1676 ## Changelog Reviewed By: NickGerleman Differential Revision: D42934757 Pulled By: skinsshark fbshipit-source-id: cb5622a79523bccbdfbc15470baf84422f635b33 --- Libraries/Lists/VirtualizedList.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index 9d1d336eccc762..2fad4637e6c57f 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -1822,10 +1822,7 @@ export default class VirtualizedList extends StateSafePureComponent< 'Tried to get frame for out of range index ' + index, ); const item = getItem(data, index); - const frame = - item != null - ? this._frames[this._keyExtractor(item, index, props)] - : undefined; + const frame = this._frames[this._keyExtractor(item, index, props)]; if (!frame || frame.index !== index) { if (getItemLayout) { /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment From 34604fa8fda27776ca247fc2fba525f843d6fb13 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 6 Feb 2023 06:55:52 -0800 Subject: [PATCH 42/65] Back out "Restore scroll position when scroll view is hidden and shown" Summary: Backing out D42815359 (https://github.com/facebook/react-native/commit/47903d0c62de9399a5cd0993a59e4af0bdea4f0c) as it caused [this](https://fb.workplace.com/groups/rn.support/permalink/24250950937860193/) ## Changelog: [iOS][Changed] - Backout to prevent issue Reviewed By: sammy-SC Differential Revision: D43041078 fbshipit-source-id: 844764af15c37baee55c927501b8b293878df284 --- .../ScrollView/RCTScrollViewComponentView.mm | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index ae41b9cb8dfd2b..8c95cdbc0b9e18 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -25,12 +25,6 @@ using namespace facebook::react; -struct PendingOffset { - bool isPending; - CGPoint offset; - CGPoint lastOffset; -}; - static CGFloat const kClippingLeeway = 44.0; static UIScrollViewKeyboardDismissMode RCTUIKeyboardDismissModeFromProps(ScrollViewProps const &props) @@ -105,8 +99,6 @@ @implementation RCTScrollViewComponentView { BOOL _shouldUpdateContentInsetAdjustmentBehavior; CGPoint _contentOffsetWhenClipped; - - PendingOffset _pendingOffset; } + (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view @@ -181,12 +173,6 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics _containerView.transform = transform; _scrollView.transform = transform; } - - // If there is a pending offset, apply it - if (_pendingOffset.isPending) { - [self scrollTo:_pendingOffset.offset.x y:_pendingOffset.offset.y animated:false]; - _pendingOffset.isPending = false; - } } - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps @@ -435,13 +421,6 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView [self _updateStateWithContentOffset]; } - // If the view is hidden, then set as pending offset. Apply it later on - // updateLayoutMetrics. - if (_scrollView.window == nil && !_pendingOffset.isPending) { - _pendingOffset.offset = _pendingOffset.lastOffset; - _pendingOffset.isPending = true; - } - NSTimeInterval now = CACurrentMediaTime(); if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) { _lastScrollEventDispatchTime = now; @@ -452,8 +431,6 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag); } - _pendingOffset.lastOffset = _scrollView.contentOffset; - [self _remountChildrenIfNeeded]; } From 98009ad94b92320307f2721ee39dbeb9152c0a58 Mon Sep 17 00:00:00 2001 From: Julien Brayere Date: Mon, 6 Feb 2023 08:05:59 -0800 Subject: [PATCH 43/65] fix: fix virtualizedList scrollToEnd for 0 items (#36067) Summary: Fixes https://github.com/facebook/react-native/issues/36066 ## Changelog [GENERAL] [FIXED] - VirtualizedList scrollToEnd with no data Pull Request resolved: https://github.com/facebook/react-native/pull/36067 Test Plan: Run `yarn test VirtualizedList-test` Reviewed By: jacdebug Differential Revision: D43041763 Pulled By: javache fbshipit-source-id: d4d5e871284708a89bf9911d82e9aa97d7625aca --- Libraries/Lists/VirtualizedList.js | 3 +++ Libraries/Lists/__tests__/VirtualizedList-test.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index 2fad4637e6c57f..efce2bed49bfcd 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -167,6 +167,9 @@ export default class VirtualizedList extends StateSafePureComponent< scrollToEnd(params?: ?{animated?: ?boolean, ...}) { const animated = params ? params.animated : true; const veryLast = this.props.getItemCount(this.props.data) - 1; + if (veryLast < 0) { + return; + } const frame = this.__getFrameMetricsApprox(veryLast, this.props); const offset = Math.max( 0, diff --git a/Libraries/Lists/__tests__/VirtualizedList-test.js b/Libraries/Lists/__tests__/VirtualizedList-test.js index e05f68ee8708aa..45858695abbe8b 100644 --- a/Libraries/Lists/__tests__/VirtualizedList-test.js +++ b/Libraries/Lists/__tests__/VirtualizedList-test.js @@ -137,6 +137,20 @@ describe('VirtualizedList', () => { expect(component).toMatchSnapshot(); }); + it('scrollToEnd works with null list', () => { + const listRef = React.createRef(null); + ReactTestRenderer.create( + } + getItem={(data, index) => data[index]} + getItemCount={data => 0} + ref={listRef} + />, + ); + listRef.current.scrollToEnd(); + }); + it('renders empty list with empty component', () => { const component = ReactTestRenderer.create( Date: Mon, 6 Feb 2023 08:18:54 -0800 Subject: [PATCH 44/65] ci: Add `Type: Expo` label action (#36069) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36069 This PR adds a github action to auto closes an issue labelled with `Type: Expo`. Changelog: [Internal][Added] - Support `Type: Expo` label action Reviewed By: cortinico Differential Revision: D43041449 fbshipit-source-id: 8ac29487b172acd5798c8c36f5ef89cf60e69d04 --- .github/workflows/on-issue-labeled.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/on-issue-labeled.yml b/.github/workflows/on-issue-labeled.yml index 8207432ec8cd30..ae55455cfd1316 100644 --- a/.github/workflows/on-issue-labeled.yml +++ b/.github/workflows/on-issue-labeled.yml @@ -182,3 +182,22 @@ jobs: repo: context.repo.repo, labels: ['Needs: Author Feedback'] }) + type-expo: + runs-on: ubuntu-latest + if: "${{ contains(github.event.label.name, 'Type: Expo') }}" + steps: + - uses: actions/github-script@v6 + with: + script: | + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `It looks like your issue is related to Expo and not React Native core. Please open your issue on an [Expo's repository](https://github.com/expo/expo/issues/new). If you are able to create a repro that showcases that this issue is also happening in React Native vanilla, we will be happy to re-open.`, + }) + await github.rest.issues.update({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + state: "closed", + }) From 2e3dbe9c2fbff52448e2d5a7c1e4c96b1016cf25 Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Dall'Agnol Date: Mon, 6 Feb 2023 13:39:13 -0800 Subject: [PATCH 45/65] feat: Move virtualized lists to @react-native/virtualized-lists (#35406) Summary: This PR moves `VirtualizedList`, `VirtualizedSectionList`, and its files to a separate package called `react-native/virtualized-lists` located under `packages/virtualized-lists` as proposed on https://github.com/facebook/react-native/issues/35263 ## Changelog [General] [Changed] - Move virtualized lists to react-native/virtualized-lists package Pull Request resolved: https://github.com/facebook/react-native/pull/35406 Test Plan: 1. Open the RNTester app and navigate to `FlatList` or `SectionList` page 2. Test virtualized lists through the many sections https://user-images.githubusercontent.com/11707729/202878843-2b1322f5-cfee-484e-aaf3-d8d4dc0b96cc.mov Reviewed By: cipolleschi Differential Revision: D41745930 Pulled By: hoxyq fbshipit-source-id: d3d33896801fd69448c6893b86fd5c2363144fd0 --- Libraries/Inspector/NetworkOverlay.js | 2 +- Libraries/Lists/FlatList.d.ts | 2 +- Libraries/Lists/FlatList.js | 11 +- Libraries/Lists/FlatList.js.flow | 9 +- Libraries/Lists/SectionList.d.ts | 2 +- Libraries/Lists/SectionList.js | 6 +- Libraries/Lists/SectionListModern.js | 6 +- Libraries/Lists/ViewabilityHelper.js | 351 +-- Libraries/Lists/VirtualizedList.js | 1949 +--------------- Libraries/Lists/VirtualizedSectionList.js | 609 +---- Libraries/Modal/Modal.js | 2 +- Libraries/Utilities/ReactNativeTestTools.js | 2 +- index.js | 2 +- package.json | 1 + .../js/examples/FlatList/FlatList-basic.js | 2 +- .../js/examples/FlatList/FlatList-nested.js | 4 +- .../FlatList-onViewableItemsChanged.js | 2 +- .../Interaction/Batchinator.js | 2 +- .../Interaction/__tests__/Batchinator-test.js | 4 - .../Lists/CellRenderMask.js | 0 .../Lists/ChildListCollection.js | 0 .../Lists/FillRateHelper.js | 0 .../Lists/StateSafePureComponent.js | 0 .../Lists/ViewabilityHelper.js | 360 +++ .../Lists/VirtualizeUtils.js | 0 .../Lists/VirtualizedList.d.ts | 12 +- .../Lists/VirtualizedList.js | 1955 +++++++++++++++++ .../Lists/VirtualizedListCellRenderer.js | 10 +- .../Lists/VirtualizedListContext.js | 0 .../Lists/VirtualizedListProps.js | 4 +- .../Lists/VirtualizedSectionList.js | 617 ++++++ .../Lists/__tests__/CellRenderMask-test.js | 0 .../Lists/__tests__/FillRateHelper-test.js | 0 .../Lists/__tests__/ViewabilityHelper-test.js | 0 .../Lists/__tests__/VirtualizeUtils-test.js | 0 .../Lists/__tests__/VirtualizedList-test.js | 0 .../__tests__/VirtualizedSectionList-test.js | 0 .../VirtualizedList-test.js.snap | 0 .../VirtualizedSectionList-test.js.snap | 0 .../Utilities/__tests__/clamp-test.js | 0 .../virtualized-lists}/Utilities/clamp.js | 0 .../virtualized-lists/Utilities/infoLog.js | 20 + packages/virtualized-lists/index.d.ts | 10 + packages/virtualized-lists/index.js | 51 + packages/virtualized-lists/package.json | 18 + types/index.d.ts | 2 +- 46 files changed, 3100 insertions(+), 2927 deletions(-) rename {Libraries => packages/virtualized-lists}/Interaction/Batchinator.js (97%) rename {Libraries => packages/virtualized-lists}/Interaction/__tests__/Batchinator-test.js (95%) rename {Libraries => packages/virtualized-lists}/Lists/CellRenderMask.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/ChildListCollection.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/FillRateHelper.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/StateSafePureComponent.js (100%) create mode 100644 packages/virtualized-lists/Lists/ViewabilityHelper.js rename {Libraries => packages/virtualized-lists}/Lists/VirtualizeUtils.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/VirtualizedList.d.ts (97%) create mode 100644 packages/virtualized-lists/Lists/VirtualizedList.js rename {Libraries => packages/virtualized-lists}/Lists/VirtualizedListCellRenderer.js (96%) rename {Libraries => packages/virtualized-lists}/Lists/VirtualizedListContext.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/VirtualizedListProps.js (98%) create mode 100644 packages/virtualized-lists/Lists/VirtualizedSectionList.js rename {Libraries => packages/virtualized-lists}/Lists/__tests__/CellRenderMask-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/FillRateHelper-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/ViewabilityHelper-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/VirtualizeUtils-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/VirtualizedList-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/VirtualizedSectionList-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap (100%) rename {Libraries => packages/virtualized-lists}/Utilities/__tests__/clamp-test.js (100%) rename {Libraries => packages/virtualized-lists}/Utilities/clamp.js (100%) create mode 100644 packages/virtualized-lists/Utilities/infoLog.js create mode 100644 packages/virtualized-lists/index.d.ts create mode 100644 packages/virtualized-lists/index.js create mode 100644 packages/virtualized-lists/package.json diff --git a/Libraries/Inspector/NetworkOverlay.js b/Libraries/Inspector/NetworkOverlay.js index 169318cea27f28..c6d1ec33907e36 100644 --- a/Libraries/Inspector/NetworkOverlay.js +++ b/Libraries/Inspector/NetworkOverlay.js @@ -10,7 +10,7 @@ 'use strict'; -import type {RenderItemProps} from '../Lists/VirtualizedList'; +import type {RenderItemProps} from '@react-native/virtualized-lists'; const ScrollView = require('../Components/ScrollView/ScrollView'); const TouchableHighlight = require('../Components/Touchable/TouchableHighlight'); diff --git a/Libraries/Lists/FlatList.d.ts b/Libraries/Lists/FlatList.d.ts index 344d5671359c45..6ac7f57fdcd843 100644 --- a/Libraries/Lists/FlatList.d.ts +++ b/Libraries/Lists/FlatList.d.ts @@ -12,7 +12,7 @@ import type { ListRenderItem, ViewToken, VirtualizedListProps, -} from './VirtualizedList'; +} from '@react-native/virtualized-lists'; import type {ScrollViewComponent} from '../Components/ScrollView/ScrollView'; import {StyleProp} from '../StyleSheet/StyleSheet'; import {ViewStyle} from '../StyleSheet/StyleSheetTypes'; diff --git a/Libraries/Lists/FlatList.js b/Libraries/Lists/FlatList.js index 56748eaf75240d..40372bc70dd311 100644 --- a/Libraries/Lists/FlatList.js +++ b/Libraries/Lists/FlatList.js @@ -11,14 +11,17 @@ import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; import type { + RenderItemProps, + RenderItemType, ViewabilityConfigCallbackPair, ViewToken, -} from './ViewabilityHelper'; -import type {RenderItemProps, RenderItemType} from './VirtualizedList'; +} from '@react-native/virtualized-lists'; import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import VirtualizedList from './VirtualizedList'; -import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; +import { + VirtualizedList, + keyExtractor as defaultKeyExtractor, +} from '@react-native/virtualized-lists'; import memoizeOne from 'memoize-one'; const View = require('../Components/View/View'); diff --git a/Libraries/Lists/FlatList.js.flow b/Libraries/Lists/FlatList.js.flow index 10a7c9073257bd..304a1006182186 100644 --- a/Libraries/Lists/FlatList.js.flow +++ b/Libraries/Lists/FlatList.js.flow @@ -14,8 +14,13 @@ const View = require('../Components/View/View'); import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; -import type {RenderItemType} from './VirtualizedList'; -import typeof VirtualizedList from './VirtualizedList'; +import type { + RenderItemType, + RenderItemProps, + ViewToken, + ViewabilityConfigCallbackPair, +} from '@react-native/virtualized-lists'; +import {typeof VirtualizedList} from '@react-native/virtualized-lists'; type RequiredProps = {| /** diff --git a/Libraries/Lists/SectionList.d.ts b/Libraries/Lists/SectionList.d.ts index ae1b10df46a10d..7ff5bbb0a3cbe1 100644 --- a/Libraries/Lists/SectionList.d.ts +++ b/Libraries/Lists/SectionList.d.ts @@ -11,7 +11,7 @@ import type * as React from 'react'; import type { ListRenderItemInfo, VirtualizedListWithoutRenderItemProps, -} from './VirtualizedList'; +} from '@react-native/virtualized-lists'; import type { ScrollView, ScrollViewProps, diff --git a/Libraries/Lists/SectionList.js b/Libraries/Lists/SectionList.js index d452ee2b7f6419..0f199487b92a23 100644 --- a/Libraries/Lists/SectionList.js +++ b/Libraries/Lists/SectionList.js @@ -12,13 +12,13 @@ import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type { - Props as VirtualizedSectionListProps, ScrollToLocationParamsType, SectionBase as _SectionBase, -} from './VirtualizedSectionList'; + VirtualizedSectionListProps, +} from '@react-native/virtualized-lists'; import Platform from '../Utilities/Platform'; -import VirtualizedSectionList from './VirtualizedSectionList'; +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; import * as React from 'react'; type Item = any; diff --git a/Libraries/Lists/SectionListModern.js b/Libraries/Lists/SectionListModern.js index c7856e13d9a666..d9676f106f8cfc 100644 --- a/Libraries/Lists/SectionListModern.js +++ b/Libraries/Lists/SectionListModern.js @@ -12,14 +12,14 @@ import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type { - Props as VirtualizedSectionListProps, ScrollToLocationParamsType, SectionBase as _SectionBase, -} from './VirtualizedSectionList'; + VirtualizedSectionListProps, +} from '@react-native/virtualized-lists'; import type {AbstractComponent, Element, ElementRef} from 'react'; import Platform from '../Utilities/Platform'; -import VirtualizedSectionList from './VirtualizedSectionList'; +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; import React, {forwardRef, useImperativeHandle, useRef} from 'react'; type Item = any; diff --git a/Libraries/Lists/ViewabilityHelper.js b/Libraries/Lists/ViewabilityHelper.js index 33a9811825affd..9a0a5aa694fb96 100644 --- a/Libraries/Lists/ViewabilityHelper.js +++ b/Libraries/Lists/ViewabilityHelper.js @@ -10,351 +10,14 @@ 'use strict'; -import type {FrameMetricProps} from './VirtualizedListProps'; +export type { + ViewToken, + ViewabilityConfigCallbackPair, +} from '@react-native/virtualized-lists'; -const invariant = require('invariant'); +import {typeof ViewabilityHelper as ViewabilityHelperType} from '@react-native/virtualized-lists'; -export type ViewToken = { - item: any, - key: string, - index: ?number, - isViewable: boolean, - section?: any, - ... -}; - -export type ViewabilityConfigCallbackPair = { - viewabilityConfig: ViewabilityConfig, - onViewableItemsChanged: (info: { - viewableItems: Array, - changed: Array, - ... - }) => void, - ... -}; - -export type ViewabilityConfig = {| - /** - * Minimum amount of time (in milliseconds) that an item must be physically viewable before the - * viewability callback will be fired. A high number means that scrolling through content without - * stopping will not mark the content as viewable. - */ - minimumViewTime?: number, - - /** - * Percent of viewport that must be covered for a partially occluded item to count as - * "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means - * that a single pixel in the viewport makes the item viewable, and a value of 100 means that - * an item must be either entirely visible or cover the entire viewport to count as viewable. - */ - viewAreaCoveragePercentThreshold?: number, - - /** - * Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible, - * rather than the fraction of the viewable area it covers. - */ - itemVisiblePercentThreshold?: number, - - /** - * Nothing is considered viewable until the user scrolls or `recordInteraction` is called after - * render. - */ - waitForInteraction?: boolean, -|}; - -/** - * A Utility class for calculating viewable items based on current metrics like scroll position and - * layout. - * - * An item is said to be in a "viewable" state when any of the following - * is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction` - * is true): - * - * - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item - * visible in the view area >= `itemVisiblePercentThreshold`. - * - Entirely visible on screen - */ -class ViewabilityHelper { - _config: ViewabilityConfig; - _hasInteracted: boolean = false; - _timers: Set = new Set(); - _viewableIndices: Array = []; - _viewableItems: Map = new Map(); - - constructor( - config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0}, - ) { - this._config = config; - } - - /** - * Cleanup, e.g. on unmount. Clears any pending timers. - */ - dispose() { - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To see - * the error delete this comment and run Flow. */ - this._timers.forEach(clearTimeout); - } - - /** - * Determines which items are viewable based on the current metrics and config. - */ - computeViewableItems( - props: FrameMetricProps, - scrollOffset: number, - viewportHeight: number, - getFrameMetrics: ( - index: number, - props: FrameMetricProps, - ) => ?{ - length: number, - offset: number, - ... - }, - // Optional optimization to reduce the scan size - renderRange?: { - first: number, - last: number, - ... - }, - ): Array { - const itemCount = props.getItemCount(props.data); - const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} = - this._config; - const viewAreaMode = viewAreaCoveragePercentThreshold != null; - const viewablePercentThreshold = viewAreaMode - ? viewAreaCoveragePercentThreshold - : itemVisiblePercentThreshold; - invariant( - viewablePercentThreshold != null && - (itemVisiblePercentThreshold != null) !== - (viewAreaCoveragePercentThreshold != null), - 'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold', - ); - const viewableIndices = []; - if (itemCount === 0) { - return viewableIndices; - } - let firstVisible = -1; - const {first, last} = renderRange || {first: 0, last: itemCount - 1}; - if (last >= itemCount) { - console.warn( - 'Invalid render range computing viewability ' + - JSON.stringify({renderRange, itemCount}), - ); - return []; - } - for (let idx = first; idx <= last; idx++) { - const metrics = getFrameMetrics(idx, props); - if (!metrics) { - continue; - } - const top = metrics.offset - scrollOffset; - const bottom = top + metrics.length; - if (top < viewportHeight && bottom > 0) { - firstVisible = idx; - if ( - _isViewable( - viewAreaMode, - viewablePercentThreshold, - top, - bottom, - viewportHeight, - metrics.length, - ) - ) { - viewableIndices.push(idx); - } - } else if (firstVisible >= 0) { - break; - } - } - return viewableIndices; - } - - /** - * Figures out which items are viewable and how that has changed from before and calls - * `onViewableItemsChanged` as appropriate. - */ - onUpdate( - props: FrameMetricProps, - scrollOffset: number, - viewportHeight: number, - getFrameMetrics: ( - index: number, - props: FrameMetricProps, - ) => ?{ - length: number, - offset: number, - ... - }, - createViewToken: ( - index: number, - isViewable: boolean, - props: FrameMetricProps, - ) => ViewToken, - onViewableItemsChanged: ({ - viewableItems: Array, - changed: Array, - ... - }) => void, - // Optional optimization to reduce the scan size - renderRange?: { - first: number, - last: number, - ... - }, - ): void { - const itemCount = props.getItemCount(props.data); - if ( - (this._config.waitForInteraction && !this._hasInteracted) || - itemCount === 0 || - !getFrameMetrics(0, props) - ) { - return; - } - let viewableIndices: Array = []; - if (itemCount) { - viewableIndices = this.computeViewableItems( - props, - scrollOffset, - viewportHeight, - getFrameMetrics, - renderRange, - ); - } - if ( - this._viewableIndices.length === viewableIndices.length && - this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) - ) { - // We might get a lot of scroll events where visibility doesn't change and we don't want to do - // extra work in those cases. - return; - } - this._viewableIndices = viewableIndices; - if (this._config.minimumViewTime) { - const handle: TimeoutID = setTimeout(() => { - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To - * see the error delete this comment and run Flow. */ - this._timers.delete(handle); - this._onUpdateSync( - props, - viewableIndices, - onViewableItemsChanged, - createViewToken, - ); - }, this._config.minimumViewTime); - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To see - * the error delete this comment and run Flow. */ - this._timers.add(handle); - } else { - this._onUpdateSync( - props, - viewableIndices, - onViewableItemsChanged, - createViewToken, - ); - } - } - - /** - * clean-up cached _viewableIndices to evaluate changed items on next update - */ - resetViewableIndices() { - this._viewableIndices = []; - } - - /** - * Records that an interaction has happened even if there has been no scroll. - */ - recordInteraction() { - this._hasInteracted = true; - } - - _onUpdateSync( - props: FrameMetricProps, - viewableIndicesToCheck: Array, - onViewableItemsChanged: ({ - changed: Array, - viewableItems: Array, - ... - }) => void, - createViewToken: ( - index: number, - isViewable: boolean, - props: FrameMetricProps, - ) => ViewToken, - ) { - // Filter out indices that have gone out of view since this call was scheduled. - viewableIndicesToCheck = viewableIndicesToCheck.filter(ii => - this._viewableIndices.includes(ii), - ); - const prevItems = this._viewableItems; - const nextItems = new Map( - viewableIndicesToCheck.map(ii => { - const viewable = createViewToken(ii, true, props); - return [viewable.key, viewable]; - }), - ); - - const changed = []; - for (const [key, viewable] of nextItems) { - if (!prevItems.has(key)) { - changed.push(viewable); - } - } - for (const [key, viewable] of prevItems) { - if (!nextItems.has(key)) { - changed.push({...viewable, isViewable: false}); - } - } - if (changed.length > 0) { - this._viewableItems = nextItems; - onViewableItemsChanged({ - viewableItems: Array.from(nextItems.values()), - changed, - viewabilityConfig: this._config, - }); - } - } -} - -function _isViewable( - viewAreaMode: boolean, - viewablePercentThreshold: number, - top: number, - bottom: number, - viewportHeight: number, - itemLength: number, -): boolean { - if (_isEntirelyVisible(top, bottom, viewportHeight)) { - return true; - } else { - const pixels = _getPixelsVisible(top, bottom, viewportHeight); - const percent = - 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength); - return percent >= viewablePercentThreshold; - } -} - -function _getPixelsVisible( - top: number, - bottom: number, - viewportHeight: number, -): number { - const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); - return Math.max(0, visibleHeight); -} - -function _isEntirelyVisible( - top: number, - bottom: number, - viewportHeight: number, -): boolean { - return top >= 0 && bottom <= viewportHeight && bottom > top; -} +const ViewabilityHelper: ViewabilityHelperType = + require('@react-native/virtualized-lists').ViewabilityHelper; module.exports = ViewabilityHelper; diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index efce2bed49bfcd..2488b1e5e37f57 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -8,1945 +8,16 @@ * @format */ -import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; -import type {LayoutEvent, ScrollEvent} from '../Types/CoreEventTypes'; -import type {ViewToken} from './ViewabilityHelper'; -import type { - FrameMetricProps, - Item, - Props, - RenderItemProps, - RenderItemType, - Separators, -} from './VirtualizedListProps'; - -import RefreshControl from '../Components/RefreshControl/RefreshControl'; -import ScrollView from '../Components/ScrollView/ScrollView'; -import View from '../Components/View/View'; -import Batchinator from '../Interaction/Batchinator'; -import {findNodeHandle} from '../ReactNative/RendererProxy'; -import flattenStyle from '../StyleSheet/flattenStyle'; -import StyleSheet from '../StyleSheet/StyleSheet'; -import clamp from '../Utilities/clamp'; -import infoLog from '../Utilities/infoLog'; -import {CellRenderMask} from './CellRenderMask'; -import ChildListCollection from './ChildListCollection'; -import FillRateHelper from './FillRateHelper'; -import StateSafePureComponent from './StateSafePureComponent'; -import ViewabilityHelper from './ViewabilityHelper'; -import CellRenderer from './VirtualizedListCellRenderer'; -import { - VirtualizedListCellContextProvider, - VirtualizedListContext, - VirtualizedListContextProvider, -} from './VirtualizedListContext.js'; -import { - computeWindowedRenderLimits, - keyExtractor as defaultKeyExtractor, -} from './VirtualizeUtils'; -import invariant from 'invariant'; -import * as React from 'react'; - -export type {RenderItemProps, RenderItemType, Separators}; - -const ON_EDGE_REACHED_EPSILON = 0.001; - -let _usedIndexForKey = false; -let _keylessItemComponentName: string = ''; - -type ViewabilityHelperCallbackTuple = { - viewabilityHelper: ViewabilityHelper, - onViewableItemsChanged: (info: { - viewableItems: Array, - changed: Array, - ... - }) => void, - ... -}; - -type State = { - renderMask: CellRenderMask, - cellsAroundViewport: {first: number, last: number}, -}; - -/** - * Default Props Helper Functions - * Use the following helper functions for default values - */ - -// horizontalOrDefault(this.props.horizontal) -function horizontalOrDefault(horizontal: ?boolean) { - return horizontal ?? false; -} - -// initialNumToRenderOrDefault(this.props.initialNumToRenderOrDefault) -function initialNumToRenderOrDefault(initialNumToRender: ?number) { - return initialNumToRender ?? 10; -} - -// maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch) -function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) { - return maxToRenderPerBatch ?? 10; -} - -// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold) -function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) { - return onStartReachedThreshold ?? 2; -} - -// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold) -function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) { - return onEndReachedThreshold ?? 2; -} - -// getScrollingThreshold(visibleLength, onEndReachedThreshold) -function getScrollingThreshold(threshold: number, visibleLength: number) { - return (threshold * visibleLength) / 2; -} - -// scrollEventThrottleOrDefault(this.props.scrollEventThrottle) -function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) { - return scrollEventThrottle ?? 50; -} - -// windowSizeOrDefault(this.props.windowSize) -function windowSizeOrDefault(windowSize: ?number) { - return windowSize ?? 21; -} - -function findLastWhere( - arr: $ReadOnlyArray, - predicate: (element: T) => boolean, -): T | null { - for (let i = arr.length - 1; i >= 0; i--) { - if (predicate(arr[i])) { - return arr[i]; - } - } - - return null; -} - -/** - * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) - * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better - * documented. In general, this should only really be used if you need more flexibility than - * `FlatList` provides, e.g. for use with immutable data instead of plain arrays. - * - * Virtualization massively improves memory consumption and performance of large lists by - * maintaining a finite render window of active items and replacing all items outside of the render - * window with appropriately sized blank space. The window adapts to scrolling behavior, and items - * are rendered incrementally with low-pri (after any running interactions) if they are far from the - * visible area, or with hi-pri otherwise to minimize the potential of seeing blank space. - * - * Some caveats: - * - * - Internal state is not preserved when content scrolls out of the render window. Make sure all - * your data is captured in the item data or external stores like Flux, Redux, or Relay. - * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- - * equal. Make sure that everything your `renderItem` function depends on is passed as a prop - * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on - * changes. This includes the `data` prop and parent component state. - * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously - * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see - * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, - * and we are working on improving it behind the scenes. - * - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key. - * Alternatively, you can provide a custom `keyExtractor` prop. - * - As an effort to remove defaultProps, use helper functions when referencing certain props - * - */ -export default class VirtualizedList extends StateSafePureComponent< - Props, - State, -> { - static contextType: typeof VirtualizedListContext = VirtualizedListContext; - - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params?: ?{animated?: ?boolean, ...}) { - const animated = params ? params.animated : true; - const veryLast = this.props.getItemCount(this.props.data) - 1; - if (veryLast < 0) { - return; - } - const frame = this.__getFrameMetricsApprox(veryLast, this.props); - const offset = Math.max( - 0, - frame.offset + - frame.length + - this._footerLength - - this._scrollMetrics.visibleLength, - ); - - if (this._scrollRef == null) { - return; - } - - if (this._scrollRef.scrollTo == null) { - console.warn( - 'No scrollTo method provided. This may be because you have two nested ' + - 'VirtualizedLists with the same orientation, or because you are ' + - 'using a custom component that does not implement scrollTo.', - ); - return; - } - - this._scrollRef.scrollTo( - horizontalOrDefault(this.props.horizontal) - ? {x: offset, animated} - : {y: offset, animated}, - ); - } - - // scrollToIndex may be janky without getItemLayout prop - scrollToIndex(params: { - animated?: ?boolean, - index: number, - viewOffset?: number, - viewPosition?: number, - ... - }): $FlowFixMe { - const { - data, - horizontal, - getItemCount, - getItemLayout, - onScrollToIndexFailed, - } = this.props; - const {animated, index, viewOffset, viewPosition} = params; - invariant( - index >= 0, - `scrollToIndex out of range: requested index ${index} but minimum is 0`, - ); - invariant( - getItemCount(data) >= 1, - `scrollToIndex out of range: item length ${getItemCount( - data, - )} but minimum is 1`, - ); - invariant( - index < getItemCount(data), - `scrollToIndex out of range: requested index ${index} is out of 0 to ${ - getItemCount(data) - 1 - }`, - ); - if (!getItemLayout && index > this._highestMeasuredFrameIndex) { - invariant( - !!onScrollToIndexFailed, - 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' + - 'otherwise there is no way to know the location of offscreen indices or handle failures.', - ); - onScrollToIndexFailed({ - averageItemLength: this._averageCellLength, - highestMeasuredFrameIndex: this._highestMeasuredFrameIndex, - index, - }); - return; - } - const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props); - const offset = - Math.max( - 0, - this._getOffsetApprox(index, this.props) - - (viewPosition || 0) * - (this._scrollMetrics.visibleLength - frame.length), - ) - (viewOffset || 0); - - if (this._scrollRef == null) { - return; - } - - if (this._scrollRef.scrollTo == null) { - console.warn( - 'No scrollTo method provided. This may be because you have two nested ' + - 'VirtualizedLists with the same orientation, or because you are ' + - 'using a custom component that does not implement scrollTo.', - ); - return; - } - - this._scrollRef.scrollTo( - horizontal ? {x: offset, animated} : {y: offset, animated}, - ); - } - - // scrollToItem may be janky without getItemLayout prop. Required linear scan through items - - // use scrollToIndex instead if possible. - scrollToItem(params: { - animated?: ?boolean, - item: Item, - viewOffset?: number, - viewPosition?: number, - ... - }) { - const {item} = params; - const {data, getItem, getItemCount} = this.props; - const itemCount = getItemCount(data); - for (let index = 0; index < itemCount; index++) { - if (getItem(data, index) === item) { - this.scrollToIndex({...params, index}); - break; - } - } - } - - /** - * Scroll to a specific content pixel offset in the list. - * - * Param `offset` expects the offset to scroll to. - * In case of `horizontal` is true, the offset is the x-value, - * in any other case the offset is the y-value. - * - * Param `animated` (`true` by default) defines whether the list - * should do an animation while scrolling. - */ - scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { - const {animated, offset} = params; - - if (this._scrollRef == null) { - return; - } - - if (this._scrollRef.scrollTo == null) { - console.warn( - 'No scrollTo method provided. This may be because you have two nested ' + - 'VirtualizedLists with the same orientation, or because you are ' + - 'using a custom component that does not implement scrollTo.', - ); - return; - } - - this._scrollRef.scrollTo( - horizontalOrDefault(this.props.horizontal) - ? {x: offset, animated} - : {y: offset, animated}, - ); - } - - recordInteraction() { - this._nestedChildLists.forEach(childList => { - childList.recordInteraction(); - }); - this._viewabilityTuples.forEach(t => { - t.viewabilityHelper.recordInteraction(); - }); - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - } - - flashScrollIndicators() { - if (this._scrollRef == null) { - return; - } - - this._scrollRef.flashScrollIndicators(); - } - - /** - * Provides a handle to the underlying scroll responder. - * Note that `this._scrollRef` might not be a `ScrollView`, so we - * need to check that it responds to `getScrollResponder` before calling it. - */ - getScrollResponder(): ?ScrollResponderType { - if (this._scrollRef && this._scrollRef.getScrollResponder) { - return this._scrollRef.getScrollResponder(); - } - } - - getScrollableNode(): ?number { - if (this._scrollRef && this._scrollRef.getScrollableNode) { - return this._scrollRef.getScrollableNode(); - } else { - return findNodeHandle(this._scrollRef); - } - } - - getScrollRef(): - | ?React.ElementRef - | ?React.ElementRef { - if (this._scrollRef && this._scrollRef.getScrollRef) { - return this._scrollRef.getScrollRef(); - } else { - return this._scrollRef; - } - } - - setNativeProps(props: Object) { - if (this._scrollRef) { - this._scrollRef.setNativeProps(props); - } - } - - _getCellKey(): string { - return this.context?.cellKey || 'rootList'; - } - - // $FlowFixMe[missing-local-annot] - _getScrollMetrics = () => { - return this._scrollMetrics; - }; - - hasMore(): boolean { - return this._hasMore; - } - - // $FlowFixMe[missing-local-annot] - _getOutermostParentListRef = () => { - if (this._isNestedWithSameOrientation()) { - return this.context.getOutermostParentListRef(); - } else { - return this; - } - }; - - _registerAsNestedChild = (childList: { - cellKey: string, - ref: React.ElementRef, - }): void => { - this._nestedChildLists.add(childList.ref, childList.cellKey); - if (this._hasInteracted) { - childList.ref.recordInteraction(); - } - }; - - _unregisterAsNestedChild = (childList: { - ref: React.ElementRef, - }): void => { - this._nestedChildLists.remove(childList.ref); - }; - - state: State; - - constructor(props: Props) { - super(props); - invariant( - // $FlowFixMe[prop-missing] - !props.onScroll || !props.onScroll.__isNative, - 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + - 'to support native onScroll events with useNativeDriver', - ); - invariant( - windowSizeOrDefault(props.windowSize) > 0, - 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.', - ); - - invariant( - props.getItemCount, - 'VirtualizedList: The "getItemCount" prop must be provided', - ); - - this._fillRateHelper = new FillRateHelper(this._getFrameMetrics); - this._updateCellsToRenderBatcher = new Batchinator( - this._updateCellsToRender, - this.props.updateCellsBatchingPeriod ?? 50, - ); - - if (this.props.viewabilityConfigCallbackPairs) { - this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map( - pair => ({ - viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig), - onViewableItemsChanged: pair.onViewableItemsChanged, - }), - ); - } else { - const {onViewableItemsChanged, viewabilityConfig} = this.props; - if (onViewableItemsChanged) { - this._viewabilityTuples.push({ - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged, - }); - } - } - - invariant( - !this.context, - 'Unexpectedly saw VirtualizedListContext available in ctor', - ); - - const initialRenderRegion = VirtualizedList._initialRenderRegion(props); - - this.state = { - cellsAroundViewport: initialRenderRegion, - renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), - }; - } - - static _createRenderMask( - props: Props, - cellsAroundViewport: {first: number, last: number}, - additionalRegions?: ?$ReadOnlyArray<{first: number, last: number}>, - ): CellRenderMask { - const itemCount = props.getItemCount(props.data); - - invariant( - cellsAroundViewport.first >= 0 && - cellsAroundViewport.last >= cellsAroundViewport.first - 1 && - cellsAroundViewport.last < itemCount, - `Invalid cells around viewport "[${cellsAroundViewport.first}, ${cellsAroundViewport.last}]" was passed to VirtualizedList._createRenderMask`, - ); - - const renderMask = new CellRenderMask(itemCount); - - if (itemCount > 0) { - const allRegions = [cellsAroundViewport, ...(additionalRegions ?? [])]; - for (const region of allRegions) { - renderMask.addCells(region); - } - - // The initially rendered cells are retained as part of the - // "scroll-to-top" optimization - if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) { - const initialRegion = VirtualizedList._initialRenderRegion(props); - renderMask.addCells(initialRegion); - } - - // The layout coordinates of sticker headers may be off-screen while the - // actual header is on-screen. Keep the most recent before the viewport - // rendered, even if its layout coordinates are not in viewport. - const stickyIndicesSet = new Set(props.stickyHeaderIndices); - VirtualizedList._ensureClosestStickyHeader( - props, - stickyIndicesSet, - renderMask, - cellsAroundViewport.first, - ); - } - - return renderMask; - } - - static _initialRenderRegion(props: Props): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); - const scrollIndex = Math.floor(Math.max(0, props.initialScrollIndex ?? 0)); - - return { - first: scrollIndex, - last: - Math.min( - itemCount, - scrollIndex + initialNumToRenderOrDefault(props.initialNumToRender), - ) - 1, - }; - } - - static _ensureClosestStickyHeader( - props: Props, - stickyIndicesSet: Set, - renderMask: CellRenderMask, - cellIdx: number, - ) { - const stickyOffset = props.ListHeaderComponent ? 1 : 0; - - for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) { - if (stickyIndicesSet.has(itemIdx + stickyOffset)) { - renderMask.addCells({first: itemIdx, last: itemIdx}); - break; - } - } - } - - _adjustCellsAroundViewport( - props: Props, - cellsAroundViewport: {first: number, last: number}, - ): {first: number, last: number} { - const {data, getItemCount} = props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( - props.onEndReachedThreshold, - ); - this._updateViewableItems(props, cellsAroundViewport); - - const {contentLength, offset, visibleLength} = this._scrollMetrics; - const distanceFromEnd = contentLength - visibleLength - offset; - - // Wait until the scroll view metrics have been set up. And until then, - // we will trust the initialNumToRender suggestion - if (visibleLength <= 0 || contentLength <= 0) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; - } - - let newCellsAroundViewport: {first: number, last: number}; - if (props.disableVirtualization) { - const renderAhead = - distanceFromEnd < onEndReachedThreshold * visibleLength - ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) - : 0; - - newCellsAroundViewport = { - first: 0, - last: Math.min( - cellsAroundViewport.last + renderAhead, - getItemCount(data) - 1, - ), - }; - } else { - // If we have a non-zero initialScrollIndex and run this before we've scrolled, - // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. - // So let's wait until we've scrolled the view to the right place. And until then, - // we will trust the initialScrollIndex suggestion. - - // Thus, we want to recalculate the windowed render limits if any of the following hold: - // - initialScrollIndex is undefined or is 0 - // - initialScrollIndex > 0 AND scrolling is complete - // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case - // where the list is shorter than the visible area) - if ( - props.initialScrollIndex && - !this._scrollMetrics.offset && - Math.abs(distanceFromEnd) >= Number.EPSILON - ) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; - } - - newCellsAroundViewport = computeWindowedRenderLimits( - props, - maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), - windowSizeOrDefault(props.windowSize), - cellsAroundViewport, - this.__getFrameMetricsApprox, - this._scrollMetrics, - ); - invariant( - newCellsAroundViewport.last < getItemCount(data), - 'computeWindowedRenderLimits() should return range in-bounds', - ); - } - - if (this._nestedChildLists.size() > 0) { - // If some cell in the new state has a child list in it, we should only render - // up through that item, so that we give that list a chance to render. - // Otherwise there's churn from multiple child lists mounting and un-mounting - // their items. - - // Will this prevent rendering if the nested list doesn't realize the end? - const childIdx = this._findFirstChildWithMore( - newCellsAroundViewport.first, - newCellsAroundViewport.last, - ); - - newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last; - } - - return newCellsAroundViewport; - } - - _findFirstChildWithMore(first: number, last: number): number | null { - for (let ii = first; ii <= last; ii++) { - const cellKeyForIndex = this._indicesToKeys.get(ii); - if ( - cellKeyForIndex != null && - this._nestedChildLists.anyInCell(cellKeyForIndex, childList => - childList.hasMore(), - ) - ) { - return ii; - } - } - - return null; - } +'use strict'; - componentDidMount() { - if (this._isNestedWithSameOrientation()) { - this.context.registerAsNestedChild({ - ref: this, - cellKey: this.context.cellKey, - }); - } - } +import {typeof VirtualizedList as VirtualizedListType} from '@react-native/virtualized-lists'; - componentWillUnmount() { - if (this._isNestedWithSameOrientation()) { - this.context.unregisterAsNestedChild({ref: this}); - } - this._updateCellsToRenderBatcher.dispose({abort: true}); - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.dispose(); - }); - this._fillRateHelper.deactivateAndFlush(); - } +const VirtualizedList: VirtualizedListType = + require('@react-native/virtualized-lists').VirtualizedList; - static getDerivedStateFromProps(newProps: Props, prevState: State): State { - // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make - // sure we're rendering a reasonable range here. - const itemCount = newProps.getItemCount(newProps.data); - if (itemCount === prevState.renderMask.numCells()) { - return prevState; - } - - const constrainedCells = VirtualizedList._constrainToItemCount( - prevState.cellsAroundViewport, - newProps, - ); - - return { - cellsAroundViewport: constrainedCells, - renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), - }; - } - - _pushCells( - cells: Array, - stickyHeaderIndices: Array, - stickyIndicesFromProps: Set, - first: number, - last: number, - inversionStyle: ViewStyleProp, - ) { - const { - CellRendererComponent, - ItemSeparatorComponent, - ListHeaderComponent, - ListItemComponent, - data, - debug, - getItem, - getItemCount, - getItemLayout, - horizontal, - renderItem, - } = this.props; - const stickyOffset = ListHeaderComponent ? 1 : 0; - const end = getItemCount(data) - 1; - let prevCellKey; - last = Math.min(end, last); - for (let ii = first; ii <= last; ii++) { - const item = getItem(data, ii); - const key = this._keyExtractor(item, ii, this.props); - this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { - stickyHeaderIndices.push(cells.length); - } - cells.push( - this._onCellFocusCapture(key)} - onUnmount={this._onCellUnmount} - ref={ref => { - this._cellRefs[key] = ref; - }} - renderItem={renderItem} - />, - ); - prevCellKey = key; - } - } - - static _constrainToItemCount( - cells: {first: number, last: number}, - props: Props, - ): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); - const last = Math.min(itemCount - 1, cells.last); - - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - props.maxToRenderPerBatch, - ); - - return { - first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), - last, - }; - } - - _onUpdateSeparators = (keys: Array, newProps: Object) => { - keys.forEach(key => { - const ref = key != null && this._cellRefs[key]; - ref && ref.updateSeparatorProps(newProps); - }); - }; - - _isNestedWithSameOrientation(): boolean { - const nestedContext = this.context; - return !!( - nestedContext && - !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal) - ); - } - - _getSpacerKey = (isVertical: boolean): string => - isVertical ? 'height' : 'width'; - - _keyExtractor( - item: Item, - index: number, - props: { - keyExtractor?: ?(item: Item, index: number) => string, - ... - }, - // $FlowFixMe[missing-local-annot] - ) { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } - - const key = defaultKeyExtractor(item, index); - if (key === String(index)) { - _usedIndexForKey = true; - if (item.type && item.type.displayName) { - _keylessItemComponentName = item.type.displayName; - } - } - return key; - } - - render(): React.Node { - if (__DEV__) { - // $FlowFixMe[underconstrained-implicit-instantiation] - const flatStyles = flattenStyle(this.props.contentContainerStyle); - if (flatStyles != null && flatStyles.flexWrap === 'wrap') { - console.warn( - '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + - 'Consider using `numColumns` with `FlatList` instead.', - ); - } - } - const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = - this.props; - const {data, horizontal} = this.props; - const inversionStyle = this.props.inverted - ? horizontalOrDefault(this.props.horizontal) - ? styles.horizontallyInverted - : styles.verticallyInverted - : null; - const cells: Array = []; - const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); - const stickyHeaderIndices = []; - - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { - stickyHeaderIndices.push(0); - } - const element = React.isValidElement(ListHeaderComponent) ? ( - ListHeaderComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - ); - cells.push( - - - { - // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors - element - } - - , - ); - } - - // 2a. Add a cell for ListEmptyComponent if applicable - const itemCount = this.props.getItemCount(data); - if (itemCount === 0 && ListEmptyComponent) { - const element: React.Element = ((React.isValidElement( - ListEmptyComponent, - ) ? ( - ListEmptyComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - )): any); - cells.push( - - {React.cloneElement(element, { - onLayout: (event: LayoutEvent) => { - this._onLayoutEmpty(event); - if (element.props.onLayout) { - element.props.onLayout(event); - } - }, - style: StyleSheet.compose(inversionStyle, element.props.style), - })} - , - ); - } - - // 2b. Add cells and spacers for each item - if (itemCount > 0) { - _usedIndexForKey = false; - _keylessItemComponentName = ''; - const spacerKey = this._getSpacerKey(!horizontal); - - const renderRegions = this.state.renderMask.enumerateRegions(); - const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); - - for (const section of renderRegions) { - if (section.isSpacer) { - // Legacy behavior is to avoid spacers when virtualization is - // disabled (including head spacers on initial render). - if (this.props.disableVirtualization) { - continue; - } - - // Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to - // prevent the user for hyperscrolling into un-measured area because otherwise content will - // likely jump around as it renders in above the viewport. - const isLastSpacer = section === lastSpacer; - const constrainToMeasured = isLastSpacer && !this.props.getItemLayout; - const last = constrainToMeasured - ? clamp( - section.first - 1, - section.last, - this._highestMeasuredFrameIndex, - ) - : section.last; - - const firstMetrics = this.__getFrameMetricsApprox( - section.first, - this.props, - ); - const lastMetrics = this.__getFrameMetricsApprox(last, this.props); - const spacerSize = - lastMetrics.offset + lastMetrics.length - firstMetrics.offset; - cells.push( - , - ); - } else { - this._pushCells( - cells, - stickyHeaderIndices, - stickyIndicesFromProps, - section.first, - section.last, - inversionStyle, - ); - } - } - - if (!this._hasWarned.keys && _usedIndexForKey) { - console.warn( - 'VirtualizedList: missing keys for items, make sure to specify a key or id property on each ' + - 'item or provide a custom keyExtractor.', - _keylessItemComponentName, - ); - this._hasWarned.keys = true; - } - } - - // 3. Add cell for ListFooterComponent - if (ListFooterComponent) { - const element = React.isValidElement(ListFooterComponent) ? ( - ListFooterComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - ); - cells.push( - - - { - // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors - element - } - - , - ); - } - - // 4. Render the ScrollView - const scrollProps = { - ...this.props, - onContentSizeChange: this._onContentSizeChange, - onLayout: this._onLayout, - onScroll: this._onScroll, - onScrollBeginDrag: this._onScrollBeginDrag, - onScrollEndDrag: this._onScrollEndDrag, - onMomentumScrollBegin: this._onMomentumScrollBegin, - onMomentumScrollEnd: this._onMomentumScrollEnd, - scrollEventThrottle: scrollEventThrottleOrDefault( - this.props.scrollEventThrottle, - ), // TODO: Android support - invertStickyHeaders: - this.props.invertStickyHeaders !== undefined - ? this.props.invertStickyHeaders - : this.props.inverted, - stickyHeaderIndices, - style: inversionStyle - ? [inversionStyle, this.props.style] - : this.props.style, - }; - - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; - - const innerRet = ( - - {React.cloneElement( - ( - this.props.renderScrollComponent || - this._defaultRenderScrollComponent - )(scrollProps), - { - ref: this._captureScrollRef, - }, - cells, - )} - - ); - let ret: React.Node = innerRet; - if (__DEV__) { - ret = ( - - {scrollContext => { - if ( - scrollContext != null && - !scrollContext.horizontal === - !horizontalOrDefault(this.props.horizontal) && - !this._hasWarned.nesting && - this.context == null && - this.props.scrollEnabled !== false - ) { - // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170 - console.error( - 'VirtualizedLists should never be nested inside plain ScrollViews with the same ' + - 'orientation because it can break windowing and other functionality - use another ' + - 'VirtualizedList-backed container instead.', - ); - this._hasWarned.nesting = true; - } - return innerRet; - }} - - ); - } - if (this.props.debug) { - return ( - - {ret} - {this._renderDebugOverlay()} - - ); - } else { - return ret; - } - } - - componentDidUpdate(prevProps: Props) { - const {data, extraData} = this.props; - if (data !== prevProps.data || extraData !== prevProps.extraData) { - // clear the viewableIndices cache to also trigger - // the onViewableItemsChanged callback with the new data - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.resetViewableIndices(); - }); - } - // The `this._hiPriInProgress` is guaranteeing a hiPri cell update will only happen - // once per fiber update. The `_scheduleCellsToRenderUpdate` will set it to true - // if a hiPri update needs to perform. If `componentDidUpdate` is triggered with - // `this._hiPriInProgress=true`, means it's triggered by the hiPri update. The - // `_scheduleCellsToRenderUpdate` will check this condition and not perform - // another hiPri update. - const hiPriInProgress = this._hiPriInProgress; - this._scheduleCellsToRenderUpdate(); - // Make sure setting `this._hiPriInProgress` back to false after `componentDidUpdate` - // is triggered with `this._hiPriInProgress = true` - if (hiPriInProgress) { - this._hiPriInProgress = false; - } - } - - _averageCellLength = 0; - _cellRefs: {[string]: null | CellRenderer} = {}; - _fillRateHelper: FillRateHelper; - _frames: { - [string]: { - inLayout?: boolean, - index: number, - length: number, - offset: number, - }, - } = {}; - _footerLength = 0; - // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex - _hasTriggeredInitialScrollToIndex = false; - _hasInteracted = false; - _hasMore = false; - _hasWarned: {[string]: boolean} = {}; - _headerLength = 0; - _hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update - _highestMeasuredFrameIndex = 0; - _indicesToKeys: Map = new Map(); - _lastFocusedCellKey: ?string = null; - _nestedChildLists: ChildListCollection = - new ChildListCollection(); - _offsetFromParentVirtualizedList: number = 0; - _prevParentOffset: number = 0; - // $FlowFixMe[missing-local-annot] - _scrollMetrics = { - contentLength: 0, - dOffset: 0, - dt: 10, - offset: 0, - timestamp: 0, - velocity: 0, - visibleLength: 0, - zoomScale: 1, - }; - _scrollRef: ?React.ElementRef = null; - _sentStartForContentLength = 0; - _sentEndForContentLength = 0; - _totalCellLength = 0; - _totalCellsMeasured = 0; - _updateCellsToRenderBatcher: Batchinator; - _viewabilityTuples: Array = []; - - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ - _captureScrollRef = ref => { - this._scrollRef = ref; - }; - - _computeBlankness() { - this._fillRateHelper.computeBlankness( - this.props, - this.state.cellsAroundViewport, - this._scrollMetrics, - ); - } - - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; - } else if (onRefresh) { - invariant( - typeof props.refreshing === 'boolean', - '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + - JSON.stringify(props.refreshing ?? 'undefined') + - '`', - ); - return ( - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] - - ) : ( - props.refreshControl - ) - } - /> - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] - return ; - } - }; - - _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => { - const layout = e.nativeEvent.layout; - const next = { - offset: this._selectOffset(layout), - length: this._selectLength(layout), - index, - inLayout: true, - }; - const curr = this._frames[cellKey]; - if ( - !curr || - next.offset !== curr.offset || - next.length !== curr.length || - index !== curr.index - ) { - this._totalCellLength += next.length - (curr ? curr.length : 0); - this._totalCellsMeasured += curr ? 0 : 1; - this._averageCellLength = - this._totalCellLength / this._totalCellsMeasured; - this._frames[cellKey] = next; - this._highestMeasuredFrameIndex = Math.max( - this._highestMeasuredFrameIndex, - index, - ); - this._scheduleCellsToRenderUpdate(); - } else { - this._frames[cellKey].inLayout = true; - } - - this._triggerRemeasureForChildListsInCell(cellKey); - - this._computeBlankness(); - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - }; - - _onCellFocusCapture(cellKey: string) { - this._lastFocusedCellKey = cellKey; - const renderMask = VirtualizedList._createRenderMask( - this.props, - this.state.cellsAroundViewport, - this._getNonViewportRenderRegions(this.props), - ); - - this.setState(state => { - if (!renderMask.equals(state.renderMask)) { - return {renderMask}; - } - return null; - }); - } - - _onCellUnmount = (cellKey: string) => { - const curr = this._frames[cellKey]; - if (curr) { - this._frames[cellKey] = {...curr, inLayout: false}; - } - }; - - _triggerRemeasureForChildListsInCell(cellKey: string): void { - this._nestedChildLists.forEachInCell(cellKey, childList => { - childList.measureLayoutRelativeToContainingList(); - }); - } - - measureLayoutRelativeToContainingList(): void { - // TODO (T35574538): findNodeHandle sometimes crashes with "Unable to find - // node on an unmounted component" during scrolling - try { - if (!this._scrollRef) { - return; - } - // We are assuming that getOutermostParentListRef().getScrollRef() - // is a non-null reference to a ScrollView - this._scrollRef.measureLayout( - this.context.getOutermostParentListRef().getScrollRef(), - (x, y, width, height) => { - this._offsetFromParentVirtualizedList = this._selectOffset({x, y}); - this._scrollMetrics.contentLength = this._selectLength({ - width, - height, - }); - const scrollMetrics = this._convertParentScrollMetrics( - this.context.getScrollMetrics(), - ); - - const metricsChanged = - this._scrollMetrics.visibleLength !== scrollMetrics.visibleLength || - this._scrollMetrics.offset !== scrollMetrics.offset; - - if (metricsChanged) { - this._scrollMetrics.visibleLength = scrollMetrics.visibleLength; - this._scrollMetrics.offset = scrollMetrics.offset; - - // If metrics of the scrollView changed, then we triggered remeasure for child list - // to ensure VirtualizedList has the right information. - this._nestedChildLists.forEach(childList => { - childList.measureLayoutRelativeToContainingList(); - }); - } - }, - error => { - console.warn( - "VirtualizedList: Encountered an error while measuring a list's" + - ' offset from its containing VirtualizedList.', - ); - }, - ); - } catch (error) { - console.warn( - 'measureLayoutRelativeToContainingList threw an error', - error.stack, - ); - } - } - - _onLayout = (e: LayoutEvent) => { - if (this._isNestedWithSameOrientation()) { - // Need to adjust our scroll metrics to be relative to our containing - // VirtualizedList before we can make claims about list item viewability - this.measureLayoutRelativeToContainingList(); - } else { - this._scrollMetrics.visibleLength = this._selectLength( - e.nativeEvent.layout, - ); - } - this.props.onLayout && this.props.onLayout(e); - this._scheduleCellsToRenderUpdate(); - this._maybeCallOnEdgeReached(); - }; - - _onLayoutEmpty = (e: LayoutEvent) => { - this.props.onLayout && this.props.onLayout(e); - }; - - _getFooterCellKey(): string { - return this._getCellKey() + '-footer'; - } - - _onLayoutFooter = (e: LayoutEvent) => { - this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()); - this._footerLength = this._selectLength(e.nativeEvent.layout); - }; - - _onLayoutHeader = (e: LayoutEvent) => { - this._headerLength = this._selectLength(e.nativeEvent.layout); - }; - - // $FlowFixMe[missing-local-annot] - _renderDebugOverlay() { - const normalize = - this._scrollMetrics.visibleLength / - (this._scrollMetrics.contentLength || 1); - const framesInLayout = []; - const itemCount = this.props.getItemCount(this.props.data); - for (let ii = 0; ii < itemCount; ii++) { - const frame = this.__getFrameMetricsApprox(ii, this.props); - /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { - framesInLayout.push(frame); - } - } - const windowTop = this.__getFrameMetricsApprox( - this.state.cellsAroundViewport.first, - this.props, - ).offset; - const frameLast = this.__getFrameMetricsApprox( - this.state.cellsAroundViewport.last, - this.props, - ); - const windowLen = frameLast.offset + frameLast.length - windowTop; - const visTop = this._scrollMetrics.offset; - const visLen = this._scrollMetrics.visibleLength; - - return ( - - {framesInLayout.map((f, ii) => ( - - ))} - - - - ); - } - - _selectLength( - metrics: $ReadOnly<{ - height: number, - width: number, - ... - }>, - ): number { - return !horizontalOrDefault(this.props.horizontal) - ? metrics.height - : metrics.width; - } - - _selectOffset( - metrics: $ReadOnly<{ - x: number, - y: number, - ... - }>, - ): number { - return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x; - } - - _maybeCallOnEdgeReached() { - const { - data, - getItemCount, - onStartReached, - onStartReachedThreshold, - onEndReached, - onEndReachedThreshold, - initialScrollIndex, - } = this.props; - const {contentLength, visibleLength, offset} = this._scrollMetrics; - let distanceFromStart = offset; - let distanceFromEnd = contentLength - visibleLength - offset; - - // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0 - // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus - // be at the edge of the list with a distance approximating 0 but not quite there. - if (distanceFromStart < ON_EDGE_REACHED_EPSILON) { - distanceFromStart = 0; - } - if (distanceFromEnd < ON_EDGE_REACHED_EPSILON) { - distanceFromEnd = 0; - } - - // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px - // when oERT is not present (different from 2 viewports used elsewhere) - const DEFAULT_THRESHOLD_PX = 2; - - const startThreshold = - onStartReachedThreshold != null - ? onStartReachedThreshold * visibleLength - : DEFAULT_THRESHOLD_PX; - const endThreshold = - onEndReachedThreshold != null - ? onEndReachedThreshold * visibleLength - : DEFAULT_THRESHOLD_PX; - const isWithinStartThreshold = distanceFromStart <= startThreshold; - const isWithinEndThreshold = distanceFromEnd <= endThreshold; - - // First check if the user just scrolled within the end threshold - // and call onEndReached only once for a given content length, - // and only if onStartReached is not being executed - if ( - onEndReached && - this.state.cellsAroundViewport.last === getItemCount(data) - 1 && - isWithinEndThreshold && - this._scrollMetrics.contentLength !== this._sentEndForContentLength - ) { - this._sentEndForContentLength = this._scrollMetrics.contentLength; - onEndReached({distanceFromEnd}); - } - - // Next check if the user just scrolled within the start threshold - // and call onStartReached only once for a given content length, - // and only if onEndReached is not being executed - else if ( - onStartReached != null && - this.state.cellsAroundViewport.first === 0 && - isWithinStartThreshold && - this._scrollMetrics.contentLength !== this._sentStartForContentLength - ) { - // On initial mount when using initialScrollIndex the offset will be 0 initially - // and will trigger an unexpected onStartReached. To avoid this we can use - // timestamp to differentiate between the initial scroll metrics and when we actually - // received the first scroll event. - if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { - this._sentStartForContentLength = this._scrollMetrics.contentLength; - onStartReached({distanceFromStart}); - } - } - - // If the user scrolls away from the start or end and back again, - // cause onStartReached or onEndReached to be triggered again - else { - this._sentStartForContentLength = isWithinStartThreshold - ? this._sentStartForContentLength - : 0; - this._sentEndForContentLength = isWithinEndThreshold - ? this._sentEndForContentLength - : 0; - } - } - - _onContentSizeChange = (width: number, height: number) => { - if ( - width > 0 && - height > 0 && - this.props.initialScrollIndex != null && - this.props.initialScrollIndex > 0 && - !this._hasTriggeredInitialScrollToIndex - ) { - if (this.props.contentOffset == null) { - this.scrollToIndex({ - animated: false, - index: this.props.initialScrollIndex, - }); - } - this._hasTriggeredInitialScrollToIndex = true; - } - if (this.props.onContentSizeChange) { - this.props.onContentSizeChange(width, height); - } - this._scrollMetrics.contentLength = this._selectLength({height, width}); - this._scheduleCellsToRenderUpdate(); - this._maybeCallOnEdgeReached(); - }; - - /* Translates metrics from a scroll event in a parent VirtualizedList into - * coordinates relative to the child list. - */ - _convertParentScrollMetrics = (metrics: { - visibleLength: number, - offset: number, - ... - }): $FlowFixMe => { - // Offset of the top of the nested list relative to the top of its parent's viewport - const offset = metrics.offset - this._offsetFromParentVirtualizedList; - // Child's visible length is the same as its parent's - const visibleLength = metrics.visibleLength; - const dOffset = offset - this._scrollMetrics.offset; - const contentLength = this._scrollMetrics.contentLength; - - return { - visibleLength, - contentLength, - offset, - dOffset, - }; - }; - - _onScroll = (e: Object) => { - this._nestedChildLists.forEach(childList => { - childList._onScroll(e); - }); - if (this.props.onScroll) { - this.props.onScroll(e); - } - const timestamp = e.timeStamp; - let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement); - let contentLength = this._selectLength(e.nativeEvent.contentSize); - let offset = this._selectOffset(e.nativeEvent.contentOffset); - let dOffset = offset - this._scrollMetrics.offset; - - if (this._isNestedWithSameOrientation()) { - if (this._scrollMetrics.contentLength === 0) { - // Ignore scroll events until onLayout has been called and we - // know our offset from our offset from our parent - return; - } - ({visibleLength, contentLength, offset, dOffset} = - this._convertParentScrollMetrics({ - visibleLength, - offset, - })); - } - - const dt = this._scrollMetrics.timestamp - ? Math.max(1, timestamp - this._scrollMetrics.timestamp) - : 1; - const velocity = dOffset / dt; - - if ( - dt > 500 && - this._scrollMetrics.dt > 500 && - contentLength > 5 * visibleLength && - !this._hasWarned.perf - ) { - infoLog( - 'VirtualizedList: You have a large list that is slow to update - make sure your ' + - 'renderItem function renders components that follow React performance best practices ' + - 'like PureComponent, shouldComponentUpdate, etc.', - {dt, prevDt: this._scrollMetrics.dt, contentLength}, - ); - this._hasWarned.perf = true; - } - - // For invalid negative values (w/ RTL), set this to 1. - const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale; - this._scrollMetrics = { - contentLength, - dt, - dOffset, - offset, - timestamp, - velocity, - visibleLength, - zoomScale, - }; - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; - } - this._maybeCallOnEdgeReached(); - if (velocity !== 0) { - this._fillRateHelper.activate(); - } - this._computeBlankness(); - this._scheduleCellsToRenderUpdate(); - }; - - _scheduleCellsToRenderUpdate() { - const {first, last} = this.state.cellsAroundViewport; - const {offset, visibleLength, velocity} = this._scrollMetrics; - const itemCount = this.props.getItemCount(this.props.data); - let hiPri = false; - const onStartReachedThreshold = onStartReachedThresholdOrDefault( - this.props.onStartReachedThreshold, - ); - const onEndReachedThreshold = onEndReachedThresholdOrDefault( - this.props.onEndReachedThreshold, - ); - // Mark as high priority if we're close to the start of the first item - // But only if there are items before the first rendered item - if (first > 0) { - const distTop = - offset - this.__getFrameMetricsApprox(first, this.props).offset; - hiPri = - distTop < 0 || - (velocity < -2 && - distTop < - getScrollingThreshold(onStartReachedThreshold, visibleLength)); - } - // Mark as high priority if we're close to the end of the last item - // But only if there are items after the last rendered item - if (!hiPri && last >= 0 && last < itemCount - 1) { - const distBottom = - this.__getFrameMetricsApprox(last, this.props).offset - - (offset + visibleLength); - hiPri = - distBottom < 0 || - (velocity > 2 && - distBottom < - getScrollingThreshold(onEndReachedThreshold, visibleLength)); - } - // Only trigger high-priority updates if we've actually rendered cells, - // and with that size estimate, accurately compute how many cells we should render. - // Otherwise, it would just render as many cells as it can (of zero dimension), - // each time through attempting to render more (limited by maxToRenderPerBatch), - // starving the renderer from actually laying out the objects and computing _averageCellLength. - // If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate - // We shouldn't do another hipri cellToRenderUpdate - if ( - hiPri && - (this._averageCellLength || this.props.getItemLayout) && - !this._hiPriInProgress - ) { - this._hiPriInProgress = true; - // Don't worry about interactions when scrolling quickly; focus on filling content as fast - // as possible. - this._updateCellsToRenderBatcher.dispose({abort: true}); - this._updateCellsToRender(); - return; - } else { - this._updateCellsToRenderBatcher.schedule(); - } - } - - _onScrollBeginDrag = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onScrollBeginDrag(e); - }); - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.recordInteraction(); - }); - this._hasInteracted = true; - this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); - }; - - _onScrollEndDrag = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onScrollEndDrag(e); - }); - const {velocity} = e.nativeEvent; - if (velocity) { - this._scrollMetrics.velocity = this._selectOffset(velocity); - } - this._computeBlankness(); - this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); - }; - - _onMomentumScrollBegin = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onMomentumScrollBegin(e); - }); - this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); - }; - - _onMomentumScrollEnd = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onMomentumScrollEnd(e); - }); - this._scrollMetrics.velocity = 0; - this._computeBlankness(); - this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); - }; - - _updateCellsToRender = () => { - this.setState((state, props) => { - const cellsAroundViewport = this._adjustCellsAroundViewport( - props, - state.cellsAroundViewport, - ); - const renderMask = VirtualizedList._createRenderMask( - props, - cellsAroundViewport, - this._getNonViewportRenderRegions(props), - ); - - if ( - cellsAroundViewport.first === state.cellsAroundViewport.first && - cellsAroundViewport.last === state.cellsAroundViewport.last && - renderMask.equals(state.renderMask) - ) { - return null; - } - - return {cellsAroundViewport, renderMask}; - }); - }; - - _createViewToken = ( - index: number, - isViewable: boolean, - props: FrameMetricProps, - // $FlowFixMe[missing-local-annot] - ) => { - const {data, getItem} = props; - const item = getItem(data, index); - return { - index, - item, - key: this._keyExtractor(item, index, props), - isViewable, - }; - }; - - /** - * Gets an approximate offset to an item at a given index. Supports - * fractional indices. - */ - _getOffsetApprox = (index: number, props: FrameMetricProps): number => { - if (Number.isInteger(index)) { - return this.__getFrameMetricsApprox(index, props).offset; - } else { - const frameMetrics = this.__getFrameMetricsApprox( - Math.floor(index), - props, - ); - const remainder = index - Math.floor(index); - return frameMetrics.offset + remainder * frameMetrics.length; - } - }; - - __getFrameMetricsApprox: ( - index: number, - props: FrameMetricProps, - ) => { - length: number, - offset: number, - ... - } = (index, props) => { - const frame = this._getFrameMetrics(index, props); - if (frame && frame.index === index) { - // check for invalid frames due to row re-ordering - return frame; - } else { - const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); - invariant( - !getItemLayout, - 'Should not have to estimate frames when a measurement metrics function is provided', - ); - return { - length: this._averageCellLength, - offset: this._averageCellLength * index, - }; - } - }; - - _getFrameMetrics = ( - index: number, - props: FrameMetricProps, - ): ?{ - length: number, - offset: number, - index: number, - inLayout?: boolean, - ... - } => { - const {data, getItem, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); - const item = getItem(data, index); - const frame = this._frames[this._keyExtractor(item, index, props)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.63 was deployed. To see the error - * delete this comment and run Flow. */ - return getItemLayout(data, index); - } - } - return frame; - }; - - _getNonViewportRenderRegions = ( - props: FrameMetricProps, - ): $ReadOnlyArray<{ - first: number, - last: number, - }> => { - // Keep a viewport's worth of content around the last focused cell to allow - // random navigation around it without any blanking. E.g. tabbing from one - // focused item out of viewport to another. - if ( - !(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey]) - ) { - return []; - } - - const lastFocusedCellRenderer = this._cellRefs[this._lastFocusedCellKey]; - const focusedCellIndex = lastFocusedCellRenderer.props.index; - const itemCount = props.getItemCount(props.data); - - // The cell may have been unmounted and have a stale index - if ( - focusedCellIndex >= itemCount || - this._indicesToKeys.get(focusedCellIndex) !== this._lastFocusedCellKey - ) { - return []; - } - - let first = focusedCellIndex; - let heightOfCellsBeforeFocused = 0; - for ( - let i = first - 1; - i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength; - i-- - ) { - first--; - heightOfCellsBeforeFocused += this.__getFrameMetricsApprox( - i, - props, - ).length; - } - - let last = focusedCellIndex; - let heightOfCellsAfterFocused = 0; - for ( - let i = last + 1; - i < itemCount && - heightOfCellsAfterFocused < this._scrollMetrics.visibleLength; - i++ - ) { - last++; - heightOfCellsAfterFocused += this.__getFrameMetricsApprox( - i, - props, - ).length; - } - - return [{first, last}]; - }; - - _updateViewableItems( - props: FrameMetricProps, - cellsAroundViewport: {first: number, last: number}, - ) { - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate( - props, - this._scrollMetrics.offset, - this._scrollMetrics.visibleLength, - this._getFrameMetrics, - this._createViewToken, - tuple.onViewableItemsChanged, - cellsAroundViewport, - ); - }); - } -} - -const styles = StyleSheet.create({ - verticallyInverted: { - transform: [{scaleY: -1}], - }, - horizontallyInverted: { - transform: [{scaleX: -1}], - }, - debug: { - flex: 1, - }, - debugOverlayBase: { - position: 'absolute', - top: 0, - right: 0, - }, - debugOverlay: { - bottom: 0, - width: 20, - borderColor: 'blue', - borderWidth: 1, - }, - debugOverlayFrame: { - left: 0, - backgroundColor: 'orange', - }, - debugOverlayFrameLast: { - left: 0, - borderColor: 'green', - borderWidth: 2, - }, - debugOverlayFrameVis: { - left: 0, - borderColor: 'red', - borderWidth: 2, - }, -}); +export type { + RenderItemProps, + RenderItemType, + Separators, +} from '@react-native/virtualized-lists'; +module.exports = VirtualizedList; diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index e4bf2fe58a0c44..90c187bc659bbe 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -8,610 +8,11 @@ * @format */ -import type {ViewToken} from './ViewabilityHelper'; +'use strict'; -import View from '../Components/View/View'; -import VirtualizedList from './VirtualizedList'; -import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; -import invariant from 'invariant'; -import * as React from 'react'; +import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@react-native/virtualized-lists'; -type Item = any; +const VirtualizedSectionList: VirtualizedSectionListType = + require('@react-native/virtualized-lists').VirtualizedSectionList; -export type SectionBase = { - /** - * The data for rendering items in this section. - */ - data: $ReadOnlyArray, - /** - * Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections, - * the array index will be used by default. - */ - key?: string, - // Optional props will override list-wide props just for this section. - renderItem?: ?(info: { - item: SectionItemT, - index: number, - section: SectionBase, - separators: { - highlight: () => void, - unhighlight: () => void, - updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, - ... - }, - ... - }) => null | React.Element, - ItemSeparatorComponent?: ?React.ComponentType, - keyExtractor?: (item: SectionItemT, index?: ?number) => string, - ... -}; - -type RequiredProps> = {| - sections: $ReadOnlyArray, -|}; - -type OptionalProps> = {| - /** - * Default renderer for every item in every section. - */ - renderItem?: (info: { - item: Item, - index: number, - section: SectionT, - separators: { - highlight: () => void, - unhighlight: () => void, - updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, - ... - }, - ... - }) => null | React.Element, - /** - * Rendered at the top of each section. These stick to the top of the `ScrollView` by default on - * iOS. See `stickySectionHeadersEnabled`. - */ - renderSectionHeader?: ?(info: { - section: SectionT, - ... - }) => null | React.Element, - /** - * Rendered at the bottom of each section. - */ - renderSectionFooter?: ?(info: { - section: SectionT, - ... - }) => null | React.Element, - /** - * Rendered at the top and bottom of each section (note this is different from - * `ItemSeparatorComponent` which is only rendered between items). These are intended to separate - * sections from the headers above and below and typically have the same highlight response as - * `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`, - * and any custom props from `separators.updateProps`. - */ - SectionSeparatorComponent?: ?React.ComponentType, - /** - * Makes section headers stick to the top of the screen until the next one pushes it off. Only - * enabled by default on iOS because that is the platform standard there. - */ - stickySectionHeadersEnabled?: boolean, - onEndReached?: ?({distanceFromEnd: number, ...}) => void, -|}; - -type VirtualizedListProps = React.ElementConfig; - -export type Props = {| - ...RequiredProps, - ...OptionalProps, - ...$Diff< - VirtualizedListProps, - { - renderItem: $PropertyType, - data: $PropertyType, - ... - }, - >, -|}; -export type ScrollToLocationParamsType = {| - animated?: ?boolean, - itemIndex: number, - sectionIndex: number, - viewOffset?: number, - viewPosition?: number, -|}; - -type State = {childProps: VirtualizedListProps, ...}; - -/** - * Right now this just flattens everything into one list and uses VirtualizedList under the - * hood. The only operation that might not scale well is concatting the data arrays of all the - * sections when new props are received, which should be plenty fast for up to ~10,000 items. - */ -class VirtualizedSectionList< - SectionT: SectionBase, -> extends React.PureComponent, State> { - scrollToLocation(params: ScrollToLocationParamsType) { - let index = params.itemIndex; - for (let i = 0; i < params.sectionIndex; i++) { - index += this.props.getItemCount(this.props.sections[i].data) + 2; - } - let viewOffset = params.viewOffset || 0; - if (this._listRef == null) { - return; - } - if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) { - const frame = this._listRef.__getFrameMetricsApprox( - index - params.itemIndex, - this._listRef.props, - ); - viewOffset += frame.length; - } - const toIndexParams = { - ...params, - viewOffset, - index, - }; - // $FlowFixMe[incompatible-use] - this._listRef.scrollToIndex(toIndexParams); - } - - getListRef(): ?React.ElementRef { - return this._listRef; - } - - render(): React.Node { - const { - ItemSeparatorComponent, // don't pass through, rendered with renderItem - SectionSeparatorComponent, - renderItem: _renderItem, - renderSectionFooter, - renderSectionHeader, - sections: _sections, - stickySectionHeadersEnabled, - ...passThroughProps - } = this.props; - - const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0; - - const stickyHeaderIndices = this.props.stickySectionHeadersEnabled - ? ([]: Array) - : undefined; - - let itemCount = 0; - for (const section of this.props.sections) { - // Track the section header indices - if (stickyHeaderIndices != null) { - stickyHeaderIndices.push(itemCount + listHeaderOffset); - } - - // Add two for the section header and footer. - itemCount += 2; - itemCount += this.props.getItemCount(section.data); - } - const renderItem = this._renderItem(itemCount); - - return ( - - this._getItem(this.props, sections, index) - } - getItemCount={() => itemCount} - onViewableItemsChanged={ - this.props.onViewableItemsChanged - ? this._onViewableItemsChanged - : undefined - } - ref={this._captureRef} - /> - ); - } - - _getItem( - props: Props, - sections: ?$ReadOnlyArray, - index: number, - ): ?Item { - if (!sections) { - return null; - } - let itemIdx = index - 1; - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - const sectionData = section.data; - const itemCount = props.getItemCount(sectionData); - if (itemIdx === -1 || itemIdx === itemCount) { - // We intend for there to be overflow by one on both ends of the list. - // This will be for headers and footers. When returning a header or footer - // item the section itself is the item. - return section; - } else if (itemIdx < itemCount) { - // If we are in the bounds of the list's data then return the item. - return props.getItem(sectionData, itemIdx); - } else { - itemIdx -= itemCount + 2; // Add two for the header and footer - } - } - return null; - } - - // $FlowFixMe[missing-local-annot] - _keyExtractor = (item: Item, index: number) => { - const info = this._subExtractor(index); - return (info && info.key) || String(index); - }; - - _subExtractor(index: number): ?{ - section: SectionT, - // Key of the section or combined key for section + item - key: string, - // Relative index within the section - index: ?number, - // True if this is the section header - header?: ?boolean, - leadingItem?: ?Item, - leadingSection?: ?SectionT, - trailingItem?: ?Item, - trailingSection?: ?SectionT, - ... - } { - let itemIndex = index; - const {getItem, getItemCount, keyExtractor, sections} = this.props; - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - const sectionData = section.data; - const key = section.key || String(i); - itemIndex -= 1; // The section adds an item for the header - if (itemIndex >= getItemCount(sectionData) + 1) { - itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer. - } else if (itemIndex === -1) { - return { - section, - key: key + ':header', - index: null, - header: true, - trailingSection: sections[i + 1], - }; - } else if (itemIndex === getItemCount(sectionData)) { - return { - section, - key: key + ':footer', - index: null, - header: false, - trailingSection: sections[i + 1], - }; - } else { - const extractor = - section.keyExtractor || keyExtractor || defaultKeyExtractor; - return { - section, - key: - key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex), - index: itemIndex, - leadingItem: getItem(sectionData, itemIndex - 1), - leadingSection: sections[i - 1], - trailingItem: getItem(sectionData, itemIndex + 1), - trailingSection: sections[i + 1], - }; - } - } - } - - _convertViewable = (viewable: ViewToken): ?ViewToken => { - invariant(viewable.index != null, 'Received a broken ViewToken'); - const info = this._subExtractor(viewable.index); - if (!info) { - return null; - } - const keyExtractorWithNullableIndex = info.section.keyExtractor; - const keyExtractorWithNonNullableIndex = - this.props.keyExtractor || defaultKeyExtractor; - const key = - keyExtractorWithNullableIndex != null - ? keyExtractorWithNullableIndex(viewable.item, info.index) - : keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0); - - return { - ...viewable, - index: info.index, - key, - section: info.section, - }; - }; - - _onViewableItemsChanged = ({ - viewableItems, - changed, - }: { - viewableItems: Array, - changed: Array, - ... - }) => { - const onViewableItemsChanged = this.props.onViewableItemsChanged; - if (onViewableItemsChanged != null) { - onViewableItemsChanged({ - viewableItems: viewableItems - .map(this._convertViewable, this) - .filter(Boolean), - changed: changed.map(this._convertViewable, this).filter(Boolean), - }); - } - }; - - _renderItem = - (listItemCount: number): $FlowFixMe => - // eslint-disable-next-line react/no-unstable-nested-components - ({item, index}: {item: Item, index: number, ...}) => { - const info = this._subExtractor(index); - if (!info) { - return null; - } - const infoIndex = info.index; - if (infoIndex == null) { - const {section} = info; - if (info.header === true) { - const {renderSectionHeader} = this.props; - return renderSectionHeader ? renderSectionHeader({section}) : null; - } else { - const {renderSectionFooter} = this.props; - return renderSectionFooter ? renderSectionFooter({section}) : null; - } - } else { - const renderItem = info.section.renderItem || this.props.renderItem; - const SeparatorComponent = this._getSeparatorComponent( - index, - info, - listItemCount, - ); - invariant(renderItem, 'no renderItem!'); - return ( - - ); - } - }; - - _updatePropsFor = (cellKey: string, value: any) => { - const updateProps = this._updatePropsMap[cellKey]; - if (updateProps != null) { - updateProps(value); - } - }; - - _updateHighlightFor = (cellKey: string, value: boolean) => { - const updateHighlight = this._updateHighlightMap[cellKey]; - if (updateHighlight != null) { - updateHighlight(value); - } - }; - - _setUpdateHighlightFor = ( - cellKey: string, - updateHighlightFn: ?(boolean) => void, - ) => { - if (updateHighlightFn != null) { - this._updateHighlightMap[cellKey] = updateHighlightFn; - } else { - // $FlowFixMe[prop-missing] - delete this._updateHighlightFor[cellKey]; - } - }; - - _setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => { - if (updatePropsFn != null) { - this._updatePropsMap[cellKey] = updatePropsFn; - } else { - delete this._updatePropsMap[cellKey]; - } - }; - - _getSeparatorComponent( - index: number, - info?: ?Object, - listItemCount: number, - ): ?React.ComponentType { - info = info || this._subExtractor(index); - if (!info) { - return null; - } - const ItemSeparatorComponent = - info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; - const {SectionSeparatorComponent} = this.props; - const isLastItemInList = index === listItemCount - 1; - const isLastItemInSection = - info.index === this.props.getItemCount(info.section.data) - 1; - if (SectionSeparatorComponent && isLastItemInSection) { - return SectionSeparatorComponent; - } - if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { - return ItemSeparatorComponent; - } - return null; - } - - _updateHighlightMap: {[string]: (boolean) => void} = {}; - _updatePropsMap: {[string]: void | (boolean => void)} = {}; - _listRef: ?React.ElementRef; - _captureRef = (ref: null | React$ElementRef>) => { - this._listRef = ref; - }; -} - -type ItemWithSeparatorCommonProps = $ReadOnly<{| - leadingItem: ?Item, - leadingSection: ?Object, - section: Object, - trailingItem: ?Item, - trailingSection: ?Object, -|}>; - -type ItemWithSeparatorProps = $ReadOnly<{| - ...ItemWithSeparatorCommonProps, - LeadingSeparatorComponent: ?React.ComponentType, - SeparatorComponent: ?React.ComponentType, - cellKey: string, - index: number, - item: Item, - setSelfHighlightCallback: ( - cellKey: string, - updateFn: ?(boolean) => void, - ) => void, - setSelfUpdatePropsCallback: ( - cellKey: string, - updateFn: ?(boolean) => void, - ) => void, - prevCellKey?: ?string, - updateHighlightFor: (prevCellKey: string, value: boolean) => void, - updatePropsFor: (prevCellKey: string, value: Object) => void, - renderItem: Function, - inverted: boolean, -|}>; - -function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node { - const { - LeadingSeparatorComponent, - // this is the trailing separator and is associated with this item - SeparatorComponent, - cellKey, - prevCellKey, - setSelfHighlightCallback, - updateHighlightFor, - setSelfUpdatePropsCallback, - updatePropsFor, - item, - index, - section, - inverted, - } = props; - - const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] = - React.useState(false); - - const [separatorHighlighted, setSeparatorHighlighted] = React.useState(false); - - const [leadingSeparatorProps, setLeadingSeparatorProps] = React.useState({ - leadingItem: props.leadingItem, - leadingSection: props.leadingSection, - section: props.section, - trailingItem: props.item, - trailingSection: props.trailingSection, - }); - const [separatorProps, setSeparatorProps] = React.useState({ - leadingItem: props.item, - leadingSection: props.leadingSection, - section: props.section, - trailingItem: props.trailingItem, - trailingSection: props.trailingSection, - }); - - React.useEffect(() => { - setSelfHighlightCallback(cellKey, setSeparatorHighlighted); - // $FlowFixMe[incompatible-call] - setSelfUpdatePropsCallback(cellKey, setSeparatorProps); - - return () => { - setSelfUpdatePropsCallback(cellKey, null); - setSelfHighlightCallback(cellKey, null); - }; - }, [ - cellKey, - setSelfHighlightCallback, - setSeparatorProps, - setSelfUpdatePropsCallback, - ]); - - const separators = { - highlight: () => { - setLeadingSeparatorHighlighted(true); - setSeparatorHighlighted(true); - if (prevCellKey != null) { - updateHighlightFor(prevCellKey, true); - } - }, - unhighlight: () => { - setLeadingSeparatorHighlighted(false); - setSeparatorHighlighted(false); - if (prevCellKey != null) { - updateHighlightFor(prevCellKey, false); - } - }, - updateProps: ( - select: 'leading' | 'trailing', - newProps: $Shape, - ) => { - if (select === 'leading') { - if (LeadingSeparatorComponent != null) { - setLeadingSeparatorProps({...leadingSeparatorProps, ...newProps}); - } else if (prevCellKey != null) { - // update the previous item's separator - updatePropsFor(prevCellKey, {...leadingSeparatorProps, ...newProps}); - } - } else if (select === 'trailing' && SeparatorComponent != null) { - setSeparatorProps({...separatorProps, ...newProps}); - } - }, - }; - const element = props.renderItem({ - item, - index, - section, - separators, - }); - const leadingSeparator = LeadingSeparatorComponent != null && ( - - ); - const separator = SeparatorComponent != null && ( - - ); - return leadingSeparator || separator ? ( - - {inverted === false ? leadingSeparator : separator} - {element} - {inverted === false ? separator : leadingSeparator} - - ) : ( - element - ); -} - -/* $FlowFixMe[class-object-subtyping] added when improving typing for this - * parameters */ -// $FlowFixMe[method-unbinding] -module.exports = (VirtualizedSectionList: React.AbstractComponent< - React.ElementConfig, - $ReadOnly<{ - getListRef: () => ?React.ElementRef, - scrollToLocation: (params: ScrollToLocationParamsType) => void, - ... - }>, ->); +module.exports = VirtualizedSectionList; diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index 46e9e714d29262..9750d2e5be31d3 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -13,11 +13,11 @@ import type {RootTag} from '../ReactNative/RootTag'; import type {DirectEventHandler} from '../Types/CodegenTypes'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; -import {VirtualizedListContextResetter} from '../Lists/VirtualizedListContext.js'; import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import ModalInjection from './ModalInjection'; import NativeModalManager from './NativeModalManager'; import RCTModalHostView from './RCTModalHostViewNativeComponent'; +import {VirtualizedListContextResetter} from '@react-native/virtualized-lists'; const ScrollView = require('../Components/ScrollView/ScrollView'); const View = require('../Components/View/View'); diff --git a/Libraries/Utilities/ReactNativeTestTools.js b/Libraries/Utilities/ReactNativeTestTools.js index 5dc2221528dcee..8671575054137c 100644 --- a/Libraries/Utilities/ReactNativeTestTools.js +++ b/Libraries/Utilities/ReactNativeTestTools.js @@ -15,8 +15,8 @@ import type {ReactTestRenderer as ReactTestRendererType} from 'react-test-render const Switch = require('../Components/Switch/Switch').default; const TextInput = require('../Components/TextInput/TextInput'); const View = require('../Components/View/View'); -const VirtualizedList = require('../Lists/VirtualizedList').default; const Text = require('../Text/Text'); +const {VirtualizedList} = require('@react-native/virtualized-lists'); const React = require('react'); const ShallowRenderer = require('react-shallow-renderer'); const ReactTestRenderer = require('react-test-renderer'); diff --git a/index.js b/index.js index 0b6e97c0522640..7149c6463b52fa 100644 --- a/index.js +++ b/index.js @@ -191,7 +191,7 @@ module.exports = { return require('./Libraries/Components/View/View'); }, get VirtualizedList(): VirtualizedList { - return require('./Libraries/Lists/VirtualizedList').default; + return require('./Libraries/Lists/VirtualizedList'); }, get VirtualizedSectionList(): VirtualizedSectionList { return require('./Libraries/Lists/VirtualizedSectionList'); diff --git a/package.json b/package.json index 109d1dd68e389f..225346a4a1d73d 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "@react-native/gradle-plugin": "^0.72.2", "@react-native/js-polyfills": "^0.72.0", "@react-native/normalize-colors": "^0.72.0", + "@react-native/virtualized-lists": "0.72.0", "abort-controller": "^3.0.0", "anser": "^1.4.9", "base64-js": "^1.1.2", diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js index 25c511e570a3eb..97f991265de3ba 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js @@ -12,7 +12,7 @@ import type {AnimatedComponentType} from 'react-native/Libraries/Animated/createAnimatedComponent'; import typeof FlatListType from 'react-native/Libraries/Lists/FlatList'; -import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedListProps'; +import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import * as React from 'react'; diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-nested.js b/packages/rn-tester/js/examples/FlatList/FlatList-nested.js index bb53258da9c82c..cd1f96ded9d3af 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-nested.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-nested.js @@ -9,8 +9,8 @@ */ 'use strict'; -import type {ViewToken} from '../../../../../Libraries/Lists/ViewabilityHelper'; -import type {RenderItemProps} from '../../../../../Libraries/Lists/VirtualizedListProps'; +import type {ViewToken} from 'react-native/Libraries/Lists/ViewabilityHelper'; +import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import RNTesterPage from '../../components/RNTesterPage'; diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js b/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js index 76e29fac105fe1..f65111e16596ba 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js @@ -10,7 +10,7 @@ 'use strict'; -import type {ViewToken} from '../../../../../Libraries/Lists/ViewabilityHelper'; +import type {ViewToken} from 'react-native/Libraries/Lists/ViewabilityHelper'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import BaseFlatListExample from './BaseFlatListExample'; diff --git a/Libraries/Interaction/Batchinator.js b/packages/virtualized-lists/Interaction/Batchinator.js similarity index 97% rename from Libraries/Interaction/Batchinator.js rename to packages/virtualized-lists/Interaction/Batchinator.js index 2ca2d7986d1a78..4fbc1931ca5708 100644 --- a/Libraries/Interaction/Batchinator.js +++ b/packages/virtualized-lists/Interaction/Batchinator.js @@ -10,7 +10,7 @@ 'use strict'; -const InteractionManager = require('./InteractionManager'); +const {InteractionManager} = require('react-native'); /** * A simple class for batching up invocations of a low-pri callback. A timeout is set to run the diff --git a/Libraries/Interaction/__tests__/Batchinator-test.js b/packages/virtualized-lists/Interaction/__tests__/Batchinator-test.js similarity index 95% rename from Libraries/Interaction/__tests__/Batchinator-test.js rename to packages/virtualized-lists/Interaction/__tests__/Batchinator-test.js index e8261b3515e23f..b680e98c507d00 100644 --- a/Libraries/Interaction/__tests__/Batchinator-test.js +++ b/packages/virtualized-lists/Interaction/__tests__/Batchinator-test.js @@ -10,10 +10,6 @@ 'use strict'; -jest - .mock('../../vendor/core/ErrorUtils') - .mock('../../BatchedBridge/BatchedBridge'); - function expectToBeCalledOnce(fn) { expect(fn.mock.calls.length).toBe(1); } diff --git a/Libraries/Lists/CellRenderMask.js b/packages/virtualized-lists/Lists/CellRenderMask.js similarity index 100% rename from Libraries/Lists/CellRenderMask.js rename to packages/virtualized-lists/Lists/CellRenderMask.js diff --git a/Libraries/Lists/ChildListCollection.js b/packages/virtualized-lists/Lists/ChildListCollection.js similarity index 100% rename from Libraries/Lists/ChildListCollection.js rename to packages/virtualized-lists/Lists/ChildListCollection.js diff --git a/Libraries/Lists/FillRateHelper.js b/packages/virtualized-lists/Lists/FillRateHelper.js similarity index 100% rename from Libraries/Lists/FillRateHelper.js rename to packages/virtualized-lists/Lists/FillRateHelper.js diff --git a/Libraries/Lists/StateSafePureComponent.js b/packages/virtualized-lists/Lists/StateSafePureComponent.js similarity index 100% rename from Libraries/Lists/StateSafePureComponent.js rename to packages/virtualized-lists/Lists/StateSafePureComponent.js diff --git a/packages/virtualized-lists/Lists/ViewabilityHelper.js b/packages/virtualized-lists/Lists/ViewabilityHelper.js new file mode 100644 index 00000000000000..33a9811825affd --- /dev/null +++ b/packages/virtualized-lists/Lists/ViewabilityHelper.js @@ -0,0 +1,360 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import type {FrameMetricProps} from './VirtualizedListProps'; + +const invariant = require('invariant'); + +export type ViewToken = { + item: any, + key: string, + index: ?number, + isViewable: boolean, + section?: any, + ... +}; + +export type ViewabilityConfigCallbackPair = { + viewabilityConfig: ViewabilityConfig, + onViewableItemsChanged: (info: { + viewableItems: Array, + changed: Array, + ... + }) => void, + ... +}; + +export type ViewabilityConfig = {| + /** + * Minimum amount of time (in milliseconds) that an item must be physically viewable before the + * viewability callback will be fired. A high number means that scrolling through content without + * stopping will not mark the content as viewable. + */ + minimumViewTime?: number, + + /** + * Percent of viewport that must be covered for a partially occluded item to count as + * "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means + * that a single pixel in the viewport makes the item viewable, and a value of 100 means that + * an item must be either entirely visible or cover the entire viewport to count as viewable. + */ + viewAreaCoveragePercentThreshold?: number, + + /** + * Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible, + * rather than the fraction of the viewable area it covers. + */ + itemVisiblePercentThreshold?: number, + + /** + * Nothing is considered viewable until the user scrolls or `recordInteraction` is called after + * render. + */ + waitForInteraction?: boolean, +|}; + +/** + * A Utility class for calculating viewable items based on current metrics like scroll position and + * layout. + * + * An item is said to be in a "viewable" state when any of the following + * is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction` + * is true): + * + * - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item + * visible in the view area >= `itemVisiblePercentThreshold`. + * - Entirely visible on screen + */ +class ViewabilityHelper { + _config: ViewabilityConfig; + _hasInteracted: boolean = false; + _timers: Set = new Set(); + _viewableIndices: Array = []; + _viewableItems: Map = new Map(); + + constructor( + config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0}, + ) { + this._config = config; + } + + /** + * Cleanup, e.g. on unmount. Clears any pending timers. + */ + dispose() { + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To see + * the error delete this comment and run Flow. */ + this._timers.forEach(clearTimeout); + } + + /** + * Determines which items are viewable based on the current metrics and config. + */ + computeViewableItems( + props: FrameMetricProps, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: ( + index: number, + props: FrameMetricProps, + ) => ?{ + length: number, + offset: number, + ... + }, + // Optional optimization to reduce the scan size + renderRange?: { + first: number, + last: number, + ... + }, + ): Array { + const itemCount = props.getItemCount(props.data); + const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} = + this._config; + const viewAreaMode = viewAreaCoveragePercentThreshold != null; + const viewablePercentThreshold = viewAreaMode + ? viewAreaCoveragePercentThreshold + : itemVisiblePercentThreshold; + invariant( + viewablePercentThreshold != null && + (itemVisiblePercentThreshold != null) !== + (viewAreaCoveragePercentThreshold != null), + 'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold', + ); + const viewableIndices = []; + if (itemCount === 0) { + return viewableIndices; + } + let firstVisible = -1; + const {first, last} = renderRange || {first: 0, last: itemCount - 1}; + if (last >= itemCount) { + console.warn( + 'Invalid render range computing viewability ' + + JSON.stringify({renderRange, itemCount}), + ); + return []; + } + for (let idx = first; idx <= last; idx++) { + const metrics = getFrameMetrics(idx, props); + if (!metrics) { + continue; + } + const top = metrics.offset - scrollOffset; + const bottom = top + metrics.length; + if (top < viewportHeight && bottom > 0) { + firstVisible = idx; + if ( + _isViewable( + viewAreaMode, + viewablePercentThreshold, + top, + bottom, + viewportHeight, + metrics.length, + ) + ) { + viewableIndices.push(idx); + } + } else if (firstVisible >= 0) { + break; + } + } + return viewableIndices; + } + + /** + * Figures out which items are viewable and how that has changed from before and calls + * `onViewableItemsChanged` as appropriate. + */ + onUpdate( + props: FrameMetricProps, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: ( + index: number, + props: FrameMetricProps, + ) => ?{ + length: number, + offset: number, + ... + }, + createViewToken: ( + index: number, + isViewable: boolean, + props: FrameMetricProps, + ) => ViewToken, + onViewableItemsChanged: ({ + viewableItems: Array, + changed: Array, + ... + }) => void, + // Optional optimization to reduce the scan size + renderRange?: { + first: number, + last: number, + ... + }, + ): void { + const itemCount = props.getItemCount(props.data); + if ( + (this._config.waitForInteraction && !this._hasInteracted) || + itemCount === 0 || + !getFrameMetrics(0, props) + ) { + return; + } + let viewableIndices: Array = []; + if (itemCount) { + viewableIndices = this.computeViewableItems( + props, + scrollOffset, + viewportHeight, + getFrameMetrics, + renderRange, + ); + } + if ( + this._viewableIndices.length === viewableIndices.length && + this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) + ) { + // We might get a lot of scroll events where visibility doesn't change and we don't want to do + // extra work in those cases. + return; + } + this._viewableIndices = viewableIndices; + if (this._config.minimumViewTime) { + const handle: TimeoutID = setTimeout(() => { + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To + * see the error delete this comment and run Flow. */ + this._timers.delete(handle); + this._onUpdateSync( + props, + viewableIndices, + onViewableItemsChanged, + createViewToken, + ); + }, this._config.minimumViewTime); + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To see + * the error delete this comment and run Flow. */ + this._timers.add(handle); + } else { + this._onUpdateSync( + props, + viewableIndices, + onViewableItemsChanged, + createViewToken, + ); + } + } + + /** + * clean-up cached _viewableIndices to evaluate changed items on next update + */ + resetViewableIndices() { + this._viewableIndices = []; + } + + /** + * Records that an interaction has happened even if there has been no scroll. + */ + recordInteraction() { + this._hasInteracted = true; + } + + _onUpdateSync( + props: FrameMetricProps, + viewableIndicesToCheck: Array, + onViewableItemsChanged: ({ + changed: Array, + viewableItems: Array, + ... + }) => void, + createViewToken: ( + index: number, + isViewable: boolean, + props: FrameMetricProps, + ) => ViewToken, + ) { + // Filter out indices that have gone out of view since this call was scheduled. + viewableIndicesToCheck = viewableIndicesToCheck.filter(ii => + this._viewableIndices.includes(ii), + ); + const prevItems = this._viewableItems; + const nextItems = new Map( + viewableIndicesToCheck.map(ii => { + const viewable = createViewToken(ii, true, props); + return [viewable.key, viewable]; + }), + ); + + const changed = []; + for (const [key, viewable] of nextItems) { + if (!prevItems.has(key)) { + changed.push(viewable); + } + } + for (const [key, viewable] of prevItems) { + if (!nextItems.has(key)) { + changed.push({...viewable, isViewable: false}); + } + } + if (changed.length > 0) { + this._viewableItems = nextItems; + onViewableItemsChanged({ + viewableItems: Array.from(nextItems.values()), + changed, + viewabilityConfig: this._config, + }); + } + } +} + +function _isViewable( + viewAreaMode: boolean, + viewablePercentThreshold: number, + top: number, + bottom: number, + viewportHeight: number, + itemLength: number, +): boolean { + if (_isEntirelyVisible(top, bottom, viewportHeight)) { + return true; + } else { + const pixels = _getPixelsVisible(top, bottom, viewportHeight); + const percent = + 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength); + return percent >= viewablePercentThreshold; + } +} + +function _getPixelsVisible( + top: number, + bottom: number, + viewportHeight: number, +): number { + const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); + return Math.max(0, visibleHeight); +} + +function _isEntirelyVisible( + top: number, + bottom: number, + viewportHeight: number, +): boolean { + return top >= 0 && bottom <= viewportHeight && bottom > top; +} + +module.exports = ViewabilityHelper; diff --git a/Libraries/Lists/VirtualizeUtils.js b/packages/virtualized-lists/Lists/VirtualizeUtils.js similarity index 100% rename from Libraries/Lists/VirtualizeUtils.js rename to packages/virtualized-lists/Lists/VirtualizeUtils.js diff --git a/Libraries/Lists/VirtualizedList.d.ts b/packages/virtualized-lists/Lists/VirtualizedList.d.ts similarity index 97% rename from Libraries/Lists/VirtualizedList.d.ts rename to packages/virtualized-lists/Lists/VirtualizedList.d.ts index d874dab4d10271..a2702d9101e107 100644 --- a/Libraries/Lists/VirtualizedList.d.ts +++ b/packages/virtualized-lists/Lists/VirtualizedList.d.ts @@ -8,15 +8,15 @@ */ import type * as React from 'react'; -import type {LayoutChangeEvent} from '../../types'; -import {StyleProp} from '../StyleSheet/StyleSheet'; -import {ViewStyle} from '../StyleSheet/StyleSheetTypes'; import type { + StyleProp, + ViewStyle, + ScrollViewProps, + LayoutChangeEvent, + View, ScrollResponderMixin, ScrollView, - ScrollViewProps, -} from '../Components/ScrollView/ScrollView'; -import type {View} from '../Components/View/View'; +} from 'react-native'; export interface ViewToken { item: any; diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js new file mode 100644 index 00000000000000..b93878c5658295 --- /dev/null +++ b/packages/virtualized-lists/Lists/VirtualizedList.js @@ -0,0 +1,1955 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +import type {ScrollResponderType} from 'react-native/Libraries/Components/ScrollView/ScrollView'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import type { + LayoutEvent, + ScrollEvent, +} from 'react-native/Libraries/Types/CoreEventTypes'; +import type {ViewToken} from './ViewabilityHelper'; +import type { + FrameMetricProps, + Item, + Props, + RenderItemProps, + RenderItemType, + Separators, +} from './VirtualizedListProps'; + +import { + RefreshControl, + ScrollView, + View, + StyleSheet, + findNodeHandle, +} from 'react-native'; +import Batchinator from '../Interaction/Batchinator'; +import clamp from '../Utilities/clamp'; +import infoLog from '../Utilities/infoLog'; +import {CellRenderMask} from './CellRenderMask'; +import ChildListCollection from './ChildListCollection'; +import FillRateHelper from './FillRateHelper'; +import StateSafePureComponent from './StateSafePureComponent'; +import ViewabilityHelper from './ViewabilityHelper'; +import CellRenderer from './VirtualizedListCellRenderer'; +import { + VirtualizedListCellContextProvider, + VirtualizedListContext, + VirtualizedListContextProvider, +} from './VirtualizedListContext.js'; +import { + computeWindowedRenderLimits, + keyExtractor as defaultKeyExtractor, +} from './VirtualizeUtils'; +import invariant from 'invariant'; +import * as React from 'react'; + +export type {RenderItemProps, RenderItemType, Separators}; + +const ON_EDGE_REACHED_EPSILON = 0.001; + +let _usedIndexForKey = false; +let _keylessItemComponentName: string = ''; + +type ViewabilityHelperCallbackTuple = { + viewabilityHelper: ViewabilityHelper, + onViewableItemsChanged: (info: { + viewableItems: Array, + changed: Array, + ... + }) => void, + ... +}; + +type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, +}; + +/** + * Default Props Helper Functions + * Use the following helper functions for default values + */ + +// horizontalOrDefault(this.props.horizontal) +function horizontalOrDefault(horizontal: ?boolean) { + return horizontal ?? false; +} + +// initialNumToRenderOrDefault(this.props.initialNumToRenderOrDefault) +function initialNumToRenderOrDefault(initialNumToRender: ?number) { + return initialNumToRender ?? 10; +} + +// maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch) +function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) { + return maxToRenderPerBatch ?? 10; +} + +// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold) +function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) { + return onStartReachedThreshold ?? 2; +} + +// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold) +function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) { + return onEndReachedThreshold ?? 2; +} + +// getScrollingThreshold(visibleLength, onEndReachedThreshold) +function getScrollingThreshold(threshold: number, visibleLength: number) { + return (threshold * visibleLength) / 2; +} + +// scrollEventThrottleOrDefault(this.props.scrollEventThrottle) +function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) { + return scrollEventThrottle ?? 50; +} + +// windowSizeOrDefault(this.props.windowSize) +function windowSizeOrDefault(windowSize: ?number) { + return windowSize ?? 21; +} + +function findLastWhere( + arr: $ReadOnlyArray, + predicate: (element: T) => boolean, +): T | null { + for (let i = arr.length - 1; i >= 0; i--) { + if (predicate(arr[i])) { + return arr[i]; + } + } + + return null; +} + +/** + * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) + * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better + * documented. In general, this should only really be used if you need more flexibility than + * `FlatList` provides, e.g. for use with immutable data instead of plain arrays. + * + * Virtualization massively improves memory consumption and performance of large lists by + * maintaining a finite render window of active items and replacing all items outside of the render + * window with appropriately sized blank space. The window adapts to scrolling behavior, and items + * are rendered incrementally with low-pri (after any running interactions) if they are far from the + * visible area, or with hi-pri otherwise to minimize the potential of seeing blank space. + * + * Some caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * - As an effort to remove defaultProps, use helper functions when referencing certain props + * + */ +class VirtualizedList extends StateSafePureComponent { + static contextType: typeof VirtualizedListContext = VirtualizedListContext; + + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { + const animated = params ? params.animated : true; + const veryLast = this.props.getItemCount(this.props.data) - 1; + if (veryLast < 0) { + return; + } + const frame = this.__getFrameMetricsApprox(veryLast, this.props); + const offset = Math.max( + 0, + frame.offset + + frame.length + + this._footerLength - + this._scrollMetrics.visibleLength, + ); + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + + this._scrollRef.scrollTo( + horizontalOrDefault(this.props.horizontal) + ? {x: offset, animated} + : {y: offset, animated}, + ); + } + + // scrollToIndex may be janky without getItemLayout prop + scrollToIndex(params: { + animated?: ?boolean, + index: number, + viewOffset?: number, + viewPosition?: number, + ... + }): $FlowFixMe { + const { + data, + horizontal, + getItemCount, + getItemLayout, + onScrollToIndexFailed, + } = this.props; + const {animated, index, viewOffset, viewPosition} = params; + invariant( + index >= 0, + `scrollToIndex out of range: requested index ${index} but minimum is 0`, + ); + invariant( + getItemCount(data) >= 1, + `scrollToIndex out of range: item length ${getItemCount( + data, + )} but minimum is 1`, + ); + invariant( + index < getItemCount(data), + `scrollToIndex out of range: requested index ${index} is out of 0 to ${ + getItemCount(data) - 1 + }`, + ); + if (!getItemLayout && index > this._highestMeasuredFrameIndex) { + invariant( + !!onScrollToIndexFailed, + 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' + + 'otherwise there is no way to know the location of offscreen indices or handle failures.', + ); + onScrollToIndexFailed({ + averageItemLength: this._averageCellLength, + highestMeasuredFrameIndex: this._highestMeasuredFrameIndex, + index, + }); + return; + } + const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props); + const offset = + Math.max( + 0, + this._getOffsetApprox(index, this.props) - + (viewPosition || 0) * + (this._scrollMetrics.visibleLength - frame.length), + ) - (viewOffset || 0); + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + + this._scrollRef.scrollTo( + horizontal ? {x: offset, animated} : {y: offset, animated}, + ); + } + + // scrollToItem may be janky without getItemLayout prop. Required linear scan through items - + // use scrollToIndex instead if possible. + scrollToItem(params: { + animated?: ?boolean, + item: Item, + viewOffset?: number, + viewPosition?: number, + ... + }) { + const {item} = params; + const {data, getItem, getItemCount} = this.props; + const itemCount = getItemCount(data); + for (let index = 0; index < itemCount; index++) { + if (getItem(data, index) === item) { + this.scrollToIndex({...params, index}); + break; + } + } + } + + /** + * Scroll to a specific content pixel offset in the list. + * + * Param `offset` expects the offset to scroll to. + * In case of `horizontal` is true, the offset is the x-value, + * in any other case the offset is the y-value. + * + * Param `animated` (`true` by default) defines whether the list + * should do an animation while scrolling. + */ + scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { + const {animated, offset} = params; + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + + this._scrollRef.scrollTo( + horizontalOrDefault(this.props.horizontal) + ? {x: offset, animated} + : {y: offset, animated}, + ); + } + + recordInteraction() { + this._nestedChildLists.forEach(childList => { + childList.recordInteraction(); + }); + this._viewabilityTuples.forEach(t => { + t.viewabilityHelper.recordInteraction(); + }); + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + } + + flashScrollIndicators() { + if (this._scrollRef == null) { + return; + } + + this._scrollRef.flashScrollIndicators(); + } + + /** + * Provides a handle to the underlying scroll responder. + * Note that `this._scrollRef` might not be a `ScrollView`, so we + * need to check that it responds to `getScrollResponder` before calling it. + */ + getScrollResponder(): ?ScrollResponderType { + if (this._scrollRef && this._scrollRef.getScrollResponder) { + return this._scrollRef.getScrollResponder(); + } + } + + getScrollableNode(): ?number { + if (this._scrollRef && this._scrollRef.getScrollableNode) { + return this._scrollRef.getScrollableNode(); + } else { + return findNodeHandle(this._scrollRef); + } + } + + getScrollRef(): + | ?React.ElementRef + | ?React.ElementRef { + if (this._scrollRef && this._scrollRef.getScrollRef) { + return this._scrollRef.getScrollRef(); + } else { + return this._scrollRef; + } + } + + setNativeProps(props: Object) { + if (this._scrollRef) { + this._scrollRef.setNativeProps(props); + } + } + + _getCellKey(): string { + return this.context?.cellKey || 'rootList'; + } + + // $FlowFixMe[missing-local-annot] + _getScrollMetrics = () => { + return this._scrollMetrics; + }; + + hasMore(): boolean { + return this._hasMore; + } + + // $FlowFixMe[missing-local-annot] + _getOutermostParentListRef = () => { + if (this._isNestedWithSameOrientation()) { + return this.context.getOutermostParentListRef(); + } else { + return this; + } + }; + + _registerAsNestedChild = (childList: { + cellKey: string, + ref: React.ElementRef, + }): void => { + this._nestedChildLists.add(childList.ref, childList.cellKey); + if (this._hasInteracted) { + childList.ref.recordInteraction(); + } + }; + + _unregisterAsNestedChild = (childList: { + ref: React.ElementRef, + }): void => { + this._nestedChildLists.remove(childList.ref); + }; + + state: State; + + constructor(props: Props) { + super(props); + invariant( + // $FlowFixMe[prop-missing] + !props.onScroll || !props.onScroll.__isNative, + 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + + 'to support native onScroll events with useNativeDriver', + ); + invariant( + windowSizeOrDefault(props.windowSize) > 0, + 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.', + ); + + invariant( + props.getItemCount, + 'VirtualizedList: The "getItemCount" prop must be provided', + ); + + this._fillRateHelper = new FillRateHelper(this._getFrameMetrics); + this._updateCellsToRenderBatcher = new Batchinator( + this._updateCellsToRender, + this.props.updateCellsBatchingPeriod ?? 50, + ); + + if (this.props.viewabilityConfigCallbackPairs) { + this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map( + pair => ({ + viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig), + onViewableItemsChanged: pair.onViewableItemsChanged, + }), + ); + } else { + const {onViewableItemsChanged, viewabilityConfig} = this.props; + if (onViewableItemsChanged) { + this._viewabilityTuples.push({ + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged, + }); + } + } + + invariant( + !this.context, + 'Unexpectedly saw VirtualizedListContext available in ctor', + ); + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); + + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), + }; + } + + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, + additionalRegions?: ?$ReadOnlyArray<{first: number, last: number}>, + ): CellRenderMask { + const itemCount = props.getItemCount(props.data); + + invariant( + cellsAroundViewport.first >= 0 && + cellsAroundViewport.last >= cellsAroundViewport.first - 1 && + cellsAroundViewport.last < itemCount, + `Invalid cells around viewport "[${cellsAroundViewport.first}, ${cellsAroundViewport.last}]" was passed to VirtualizedList._createRenderMask`, + ); + + const renderMask = new CellRenderMask(itemCount); + + if (itemCount > 0) { + const allRegions = [cellsAroundViewport, ...(additionalRegions ?? [])]; + for (const region of allRegions) { + renderMask.addCells(region); + } + + // The initially rendered cells are retained as part of the + // "scroll-to-top" optimization + if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) { + const initialRegion = VirtualizedList._initialRenderRegion(props); + renderMask.addCells(initialRegion); + } + + // The layout coordinates of sticker headers may be off-screen while the + // actual header is on-screen. Keep the most recent before the viewport + // rendered, even if its layout coordinates are not in viewport. + const stickyIndicesSet = new Set(props.stickyHeaderIndices); + VirtualizedList._ensureClosestStickyHeader( + props, + stickyIndicesSet, + renderMask, + cellsAroundViewport.first, + ); + } + + return renderMask; + } + + static _initialRenderRegion(props: Props): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); + const scrollIndex = Math.floor(Math.max(0, props.initialScrollIndex ?? 0)); + + return { + first: scrollIndex, + last: + Math.min( + itemCount, + scrollIndex + initialNumToRenderOrDefault(props.initialNumToRender), + ) - 1, + }; + } + + static _ensureClosestStickyHeader( + props: Props, + stickyIndicesSet: Set, + renderMask: CellRenderMask, + cellIdx: number, + ) { + const stickyOffset = props.ListHeaderComponent ? 1 : 0; + + for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) { + if (stickyIndicesSet.has(itemIdx + stickyOffset)) { + renderMask.addCells({first: itemIdx, last: itemIdx}); + break; + } + } + } + + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( + props.onEndReachedThreshold, + ); + this._updateViewableItems(props, cellsAroundViewport); + + const {contentLength, offset, visibleLength} = this._scrollMetrics; + const distanceFromEnd = contentLength - visibleLength - offset; + + // Wait until the scroll view metrics have been set up. And until then, + // we will trust the initialNumToRender suggestion + if (visibleLength <= 0 || contentLength <= 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; + } + + let newCellsAroundViewport: {first: number, last: number}; + if (props.disableVirtualization) { + const renderAhead = + distanceFromEnd < onEndReachedThreshold * visibleLength + ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) + : 0; + + newCellsAroundViewport = { + first: 0, + last: Math.min( + cellsAroundViewport.last + renderAhead, + getItemCount(data) - 1, + ), + }; + } else { + // If we have a non-zero initialScrollIndex and run this before we've scrolled, + // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. + // So let's wait until we've scrolled the view to the right place. And until then, + // we will trust the initialScrollIndex suggestion. + + // Thus, we want to recalculate the windowed render limits if any of the following hold: + // - initialScrollIndex is undefined or is 0 + // - initialScrollIndex > 0 AND scrolling is complete + // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case + // where the list is shorter than the visible area) + if ( + props.initialScrollIndex && + !this._scrollMetrics.offset && + Math.abs(distanceFromEnd) >= Number.EPSILON + ) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; + } + + newCellsAroundViewport = computeWindowedRenderLimits( + props, + maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), + windowSizeOrDefault(props.windowSize), + cellsAroundViewport, + this.__getFrameMetricsApprox, + this._scrollMetrics, + ); + invariant( + newCellsAroundViewport.last < getItemCount(data), + 'computeWindowedRenderLimits() should return range in-bounds', + ); + } + + if (this._nestedChildLists.size() > 0) { + // If some cell in the new state has a child list in it, we should only render + // up through that item, so that we give that list a chance to render. + // Otherwise there's churn from multiple child lists mounting and un-mounting + // their items. + + // Will this prevent rendering if the nested list doesn't realize the end? + const childIdx = this._findFirstChildWithMore( + newCellsAroundViewport.first, + newCellsAroundViewport.last, + ); + + newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last; + } + + return newCellsAroundViewport; + } + + _findFirstChildWithMore(first: number, last: number): number | null { + for (let ii = first; ii <= last; ii++) { + const cellKeyForIndex = this._indicesToKeys.get(ii); + if ( + cellKeyForIndex != null && + this._nestedChildLists.anyInCell(cellKeyForIndex, childList => + childList.hasMore(), + ) + ) { + return ii; + } + } + + return null; + } + + componentDidMount() { + if (this._isNestedWithSameOrientation()) { + this.context.registerAsNestedChild({ + ref: this, + cellKey: this.context.cellKey, + }); + } + } + + componentWillUnmount() { + if (this._isNestedWithSameOrientation()) { + this.context.unregisterAsNestedChild({ref: this}); + } + this._updateCellsToRenderBatcher.dispose({abort: true}); + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.dispose(); + }); + this._fillRateHelper.deactivateAndFlush(); + } + + static getDerivedStateFromProps(newProps: Props, prevState: State): State { + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + const itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } + + const constrainedCells = VirtualizedList._constrainToItemCount( + prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), + }; + } + + _pushCells( + cells: Array, + stickyHeaderIndices: Array, + stickyIndicesFromProps: Set, + first: number, + last: number, + inversionStyle: ViewStyleProp, + ) { + const { + CellRendererComponent, + ItemSeparatorComponent, + ListHeaderComponent, + ListItemComponent, + data, + debug, + getItem, + getItemCount, + getItemLayout, + horizontal, + renderItem, + } = this.props; + const stickyOffset = ListHeaderComponent ? 1 : 0; + const end = getItemCount(data) - 1; + let prevCellKey; + last = Math.min(end, last); + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); + const key = this._keyExtractor(item, ii, this.props); + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + stickyHeaderIndices.push(cells.length); + } + cells.push( + this._onCellFocusCapture(key)} + onUnmount={this._onCellUnmount} + ref={ref => { + this._cellRefs[key] = ref; + }} + renderItem={renderItem} + />, + ); + prevCellKey = key; + } + } + + static _constrainToItemCount( + cells: {first: number, last: number}, + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); + const last = Math.min(itemCount - 1, cells.last); + + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); + + return { + first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), + last, + }; + } + + _onUpdateSeparators = (keys: Array, newProps: Object) => { + keys.forEach(key => { + const ref = key != null && this._cellRefs[key]; + ref && ref.updateSeparatorProps(newProps); + }); + }; + + _isNestedWithSameOrientation(): boolean { + const nestedContext = this.context; + return !!( + nestedContext && + !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal) + ); + } + + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + + _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, + // $FlowFixMe[missing-local-annot] + ) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } + + const key = defaultKeyExtractor(item, index); + if (key === String(index)) { + _usedIndexForKey = true; + if (item.type && item.type.displayName) { + _keylessItemComponentName = item.type.displayName; + } + } + return key; + } + + render(): React.Node { + if (__DEV__) { + // $FlowFixMe[underconstrained-implicit-instantiation] + const flatStyles = StyleSheet.flatten(this.props.contentContainerStyle); + if (flatStyles != null && flatStyles.flexWrap === 'wrap') { + console.warn( + '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + + 'Consider using `numColumns` with `FlatList` instead.', + ); + } + } + const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = + this.props; + const {data, horizontal} = this.props; + const inversionStyle = this.props.inverted + ? horizontalOrDefault(this.props.horizontal) + ? styles.horizontallyInverted + : styles.verticallyInverted + : null; + const cells: Array = []; + const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); + const stickyHeaderIndices = []; + + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { + stickyHeaderIndices.push(0); + } + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + ); + cells.push( + + + { + // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors + element + } + + , + ); + } + + // 2a. Add a cell for ListEmptyComponent if applicable + const itemCount = this.props.getItemCount(data); + if (itemCount === 0 && ListEmptyComponent) { + const element: React.Element = ((React.isValidElement( + ListEmptyComponent, + ) ? ( + ListEmptyComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + )): any); + cells.push( + + {React.cloneElement(element, { + onLayout: (event: LayoutEvent) => { + this._onLayoutEmpty(event); + if (element.props.onLayout) { + element.props.onLayout(event); + } + }, + style: StyleSheet.compose(inversionStyle, element.props.style), + })} + , + ); + } + + // 2b. Add cells and spacers for each item + if (itemCount > 0) { + _usedIndexForKey = false; + _keylessItemComponentName = ''; + const spacerKey = this._getSpacerKey(!horizontal); + + const renderRegions = this.state.renderMask.enumerateRegions(); + const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); + + for (const section of renderRegions) { + if (section.isSpacer) { + // Legacy behavior is to avoid spacers when virtualization is + // disabled (including head spacers on initial render). + if (this.props.disableVirtualization) { + continue; + } + + // Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to + // prevent the user for hyperscrolling into un-measured area because otherwise content will + // likely jump around as it renders in above the viewport. + const isLastSpacer = section === lastSpacer; + const constrainToMeasured = isLastSpacer && !this.props.getItemLayout; + const last = constrainToMeasured + ? clamp( + section.first - 1, + section.last, + this._highestMeasuredFrameIndex, + ) + : section.last; + + const firstMetrics = this.__getFrameMetricsApprox( + section.first, + this.props, + ); + const lastMetrics = this.__getFrameMetricsApprox(last, this.props); + const spacerSize = + lastMetrics.offset + lastMetrics.length - firstMetrics.offset; + cells.push( + , + ); + } else { + this._pushCells( + cells, + stickyHeaderIndices, + stickyIndicesFromProps, + section.first, + section.last, + inversionStyle, + ); + } + } + + if (!this._hasWarned.keys && _usedIndexForKey) { + console.warn( + 'VirtualizedList: missing keys for items, make sure to specify a key or id property on each ' + + 'item or provide a custom keyExtractor.', + _keylessItemComponentName, + ); + this._hasWarned.keys = true; + } + } + + // 3. Add cell for ListFooterComponent + if (ListFooterComponent) { + const element = React.isValidElement(ListFooterComponent) ? ( + ListFooterComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + ); + cells.push( + + + { + // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors + element + } + + , + ); + } + + // 4. Render the ScrollView + const scrollProps = { + ...this.props, + onContentSizeChange: this._onContentSizeChange, + onLayout: this._onLayout, + onScroll: this._onScroll, + onScrollBeginDrag: this._onScrollBeginDrag, + onScrollEndDrag: this._onScrollEndDrag, + onMomentumScrollBegin: this._onMomentumScrollBegin, + onMomentumScrollEnd: this._onMomentumScrollEnd, + scrollEventThrottle: scrollEventThrottleOrDefault( + this.props.scrollEventThrottle, + ), // TODO: Android support + invertStickyHeaders: + this.props.invertStickyHeaders !== undefined + ? this.props.invertStickyHeaders + : this.props.inverted, + stickyHeaderIndices, + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, + }; + + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + + const innerRet = ( + + {React.cloneElement( + ( + this.props.renderScrollComponent || + this._defaultRenderScrollComponent + )(scrollProps), + { + ref: this._captureScrollRef, + }, + cells, + )} + + ); + let ret: React.Node = innerRet; + if (__DEV__) { + ret = ( + + {scrollContext => { + if ( + scrollContext != null && + !scrollContext.horizontal === + !horizontalOrDefault(this.props.horizontal) && + !this._hasWarned.nesting && + this.context == null && + this.props.scrollEnabled !== false + ) { + // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170 + console.error( + 'VirtualizedLists should never be nested inside plain ScrollViews with the same ' + + 'orientation because it can break windowing and other functionality - use another ' + + 'VirtualizedList-backed container instead.', + ); + this._hasWarned.nesting = true; + } + return innerRet; + }} + + ); + } + if (this.props.debug) { + return ( + + {ret} + {this._renderDebugOverlay()} + + ); + } else { + return ret; + } + } + + componentDidUpdate(prevProps: Props) { + const {data, extraData} = this.props; + if (data !== prevProps.data || extraData !== prevProps.extraData) { + // clear the viewableIndices cache to also trigger + // the onViewableItemsChanged callback with the new data + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.resetViewableIndices(); + }); + } + // The `this._hiPriInProgress` is guaranteeing a hiPri cell update will only happen + // once per fiber update. The `_scheduleCellsToRenderUpdate` will set it to true + // if a hiPri update needs to perform. If `componentDidUpdate` is triggered with + // `this._hiPriInProgress=true`, means it's triggered by the hiPri update. The + // `_scheduleCellsToRenderUpdate` will check this condition and not perform + // another hiPri update. + const hiPriInProgress = this._hiPriInProgress; + this._scheduleCellsToRenderUpdate(); + // Make sure setting `this._hiPriInProgress` back to false after `componentDidUpdate` + // is triggered with `this._hiPriInProgress = true` + if (hiPriInProgress) { + this._hiPriInProgress = false; + } + } + + _averageCellLength = 0; + _cellRefs: {[string]: null | CellRenderer} = {}; + _fillRateHelper: FillRateHelper; + _frames: { + [string]: { + inLayout?: boolean, + index: number, + length: number, + offset: number, + }, + } = {}; + _footerLength = 0; + // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex + _hasTriggeredInitialScrollToIndex = false; + _hasInteracted = false; + _hasMore = false; + _hasWarned: {[string]: boolean} = {}; + _headerLength = 0; + _hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update + _highestMeasuredFrameIndex = 0; + _indicesToKeys: Map = new Map(); + _lastFocusedCellKey: ?string = null; + _nestedChildLists: ChildListCollection = + new ChildListCollection(); + _offsetFromParentVirtualizedList: number = 0; + _prevParentOffset: number = 0; + // $FlowFixMe[missing-local-annot] + _scrollMetrics = { + contentLength: 0, + dOffset: 0, + dt: 10, + offset: 0, + timestamp: 0, + velocity: 0, + visibleLength: 0, + zoomScale: 1, + }; + _scrollRef: ?React.ElementRef = null; + _sentStartForContentLength = 0; + _sentEndForContentLength = 0; + _totalCellLength = 0; + _totalCellsMeasured = 0; + _updateCellsToRenderBatcher: Batchinator; + _viewabilityTuples: Array = []; + + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + _captureScrollRef = ref => { + this._scrollRef = ref; + }; + + _computeBlankness() { + this._fillRateHelper.computeBlankness( + this.props, + this.state.cellsAroundViewport, + this._scrollMetrics, + ); + } + + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; + } else if (onRefresh) { + invariant( + typeof props.refreshing === 'boolean', + '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + + JSON.stringify(props.refreshing ?? 'undefined') + + '`', + ); + return ( + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] + + ) : ( + props.refreshControl + ) + } + /> + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] + return ; + } + }; + + _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => { + const layout = e.nativeEvent.layout; + const next = { + offset: this._selectOffset(layout), + length: this._selectLength(layout), + index, + inLayout: true, + }; + const curr = this._frames[cellKey]; + if ( + !curr || + next.offset !== curr.offset || + next.length !== curr.length || + index !== curr.index + ) { + this._totalCellLength += next.length - (curr ? curr.length : 0); + this._totalCellsMeasured += curr ? 0 : 1; + this._averageCellLength = + this._totalCellLength / this._totalCellsMeasured; + this._frames[cellKey] = next; + this._highestMeasuredFrameIndex = Math.max( + this._highestMeasuredFrameIndex, + index, + ); + this._scheduleCellsToRenderUpdate(); + } else { + this._frames[cellKey].inLayout = true; + } + + this._triggerRemeasureForChildListsInCell(cellKey); + + this._computeBlankness(); + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + }; + + _onCellFocusCapture(cellKey: string) { + this._lastFocusedCellKey = cellKey; + const renderMask = VirtualizedList._createRenderMask( + this.props, + this.state.cellsAroundViewport, + this._getNonViewportRenderRegions(this.props), + ); + + this.setState(state => { + if (!renderMask.equals(state.renderMask)) { + return {renderMask}; + } + return null; + }); + } + + _onCellUnmount = (cellKey: string) => { + const curr = this._frames[cellKey]; + if (curr) { + this._frames[cellKey] = {...curr, inLayout: false}; + } + }; + + _triggerRemeasureForChildListsInCell(cellKey: string): void { + this._nestedChildLists.forEachInCell(cellKey, childList => { + childList.measureLayoutRelativeToContainingList(); + }); + } + + measureLayoutRelativeToContainingList(): void { + // TODO (T35574538): findNodeHandle sometimes crashes with "Unable to find + // node on an unmounted component" during scrolling + try { + if (!this._scrollRef) { + return; + } + // We are assuming that getOutermostParentListRef().getScrollRef() + // is a non-null reference to a ScrollView + this._scrollRef.measureLayout( + this.context.getOutermostParentListRef().getScrollRef(), + (x, y, width, height) => { + this._offsetFromParentVirtualizedList = this._selectOffset({x, y}); + this._scrollMetrics.contentLength = this._selectLength({ + width, + height, + }); + const scrollMetrics = this._convertParentScrollMetrics( + this.context.getScrollMetrics(), + ); + + const metricsChanged = + this._scrollMetrics.visibleLength !== scrollMetrics.visibleLength || + this._scrollMetrics.offset !== scrollMetrics.offset; + + if (metricsChanged) { + this._scrollMetrics.visibleLength = scrollMetrics.visibleLength; + this._scrollMetrics.offset = scrollMetrics.offset; + + // If metrics of the scrollView changed, then we triggered remeasure for child list + // to ensure VirtualizedList has the right information. + this._nestedChildLists.forEach(childList => { + childList.measureLayoutRelativeToContainingList(); + }); + } + }, + error => { + console.warn( + "VirtualizedList: Encountered an error while measuring a list's" + + ' offset from its containing VirtualizedList.', + ); + }, + ); + } catch (error) { + console.warn( + 'measureLayoutRelativeToContainingList threw an error', + error.stack, + ); + } + } + + _onLayout = (e: LayoutEvent) => { + if (this._isNestedWithSameOrientation()) { + // Need to adjust our scroll metrics to be relative to our containing + // VirtualizedList before we can make claims about list item viewability + this.measureLayoutRelativeToContainingList(); + } else { + this._scrollMetrics.visibleLength = this._selectLength( + e.nativeEvent.layout, + ); + } + this.props.onLayout && this.props.onLayout(e); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEdgeReached(); + }; + + _onLayoutEmpty = (e: LayoutEvent) => { + this.props.onLayout && this.props.onLayout(e); + }; + + _getFooterCellKey(): string { + return this._getCellKey() + '-footer'; + } + + _onLayoutFooter = (e: LayoutEvent) => { + this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()); + this._footerLength = this._selectLength(e.nativeEvent.layout); + }; + + _onLayoutHeader = (e: LayoutEvent) => { + this._headerLength = this._selectLength(e.nativeEvent.layout); + }; + + // $FlowFixMe[missing-local-annot] + _renderDebugOverlay() { + const normalize = + this._scrollMetrics.visibleLength / + (this._scrollMetrics.contentLength || 1); + const framesInLayout = []; + const itemCount = this.props.getItemCount(this.props.data); + for (let ii = 0; ii < itemCount; ii++) { + const frame = this.__getFrameMetricsApprox(ii, this.props); + /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { + framesInLayout.push(frame); + } + } + const windowTop = this.__getFrameMetricsApprox( + this.state.cellsAroundViewport.first, + this.props, + ).offset; + const frameLast = this.__getFrameMetricsApprox( + this.state.cellsAroundViewport.last, + this.props, + ); + const windowLen = frameLast.offset + frameLast.length - windowTop; + const visTop = this._scrollMetrics.offset; + const visLen = this._scrollMetrics.visibleLength; + + return ( + + {framesInLayout.map((f, ii) => ( + + ))} + + + + ); + } + + _selectLength( + metrics: $ReadOnly<{ + height: number, + width: number, + ... + }>, + ): number { + return !horizontalOrDefault(this.props.horizontal) + ? metrics.height + : metrics.width; + } + + _selectOffset( + metrics: $ReadOnly<{ + x: number, + y: number, + ... + }>, + ): number { + return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x; + } + + _maybeCallOnEdgeReached() { + const { + data, + getItemCount, + onStartReached, + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, + initialScrollIndex, + } = this.props; + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; + + // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0 + // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus + // be at the edge of the list with a distance approximating 0 but not quite there. + if (distanceFromStart < ON_EDGE_REACHED_EPSILON) { + distanceFromStart = 0; + } + if (distanceFromEnd < ON_EDGE_REACHED_EPSILON) { + distanceFromEnd = 0; + } + + // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px + // when oERT is not present (different from 2 viewports used elsewhere) + const DEFAULT_THRESHOLD_PX = 2; + + const startThreshold = + onStartReachedThreshold != null + ? onStartReachedThreshold * visibleLength + : DEFAULT_THRESHOLD_PX; + const endThreshold = + onEndReachedThreshold != null + ? onEndReachedThreshold * visibleLength + : DEFAULT_THRESHOLD_PX; + const isWithinStartThreshold = distanceFromStart <= startThreshold; + const isWithinEndThreshold = distanceFromEnd <= endThreshold; + + // First check if the user just scrolled within the end threshold + // and call onEndReached only once for a given content length, + // and only if onStartReached is not being executed + if ( + onEndReached && + this.state.cellsAroundViewport.last === getItemCount(data) - 1 && + isWithinEndThreshold && + this._scrollMetrics.contentLength !== this._sentEndForContentLength + ) { + this._sentEndForContentLength = this._scrollMetrics.contentLength; + onEndReached({distanceFromEnd}); + } + + // Next check if the user just scrolled within the start threshold + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if ( + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { + // On initial mount when using initialScrollIndex the offset will be 0 initially + // and will trigger an unexpected onStartReached. To avoid this we can use + // timestamp to differentiate between the initial scroll metrics and when we actually + // received the first scroll event. + if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { + this._sentStartForContentLength = this._scrollMetrics.contentLength; + onStartReached({distanceFromStart}); + } + } + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + else { + this._sentStartForContentLength = isWithinStartThreshold + ? this._sentStartForContentLength + : 0; + this._sentEndForContentLength = isWithinEndThreshold + ? this._sentEndForContentLength + : 0; + } + } + + _onContentSizeChange = (width: number, height: number) => { + if ( + width > 0 && + height > 0 && + this.props.initialScrollIndex != null && + this.props.initialScrollIndex > 0 && + !this._hasTriggeredInitialScrollToIndex + ) { + if (this.props.contentOffset == null) { + this.scrollToIndex({ + animated: false, + index: this.props.initialScrollIndex, + }); + } + this._hasTriggeredInitialScrollToIndex = true; + } + if (this.props.onContentSizeChange) { + this.props.onContentSizeChange(width, height); + } + this._scrollMetrics.contentLength = this._selectLength({height, width}); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEdgeReached(); + }; + + /* Translates metrics from a scroll event in a parent VirtualizedList into + * coordinates relative to the child list. + */ + _convertParentScrollMetrics = (metrics: { + visibleLength: number, + offset: number, + ... + }): $FlowFixMe => { + // Offset of the top of the nested list relative to the top of its parent's viewport + const offset = metrics.offset - this._offsetFromParentVirtualizedList; + // Child's visible length is the same as its parent's + const visibleLength = metrics.visibleLength; + const dOffset = offset - this._scrollMetrics.offset; + const contentLength = this._scrollMetrics.contentLength; + + return { + visibleLength, + contentLength, + offset, + dOffset, + }; + }; + + _onScroll = (e: Object) => { + this._nestedChildLists.forEach(childList => { + childList._onScroll(e); + }); + if (this.props.onScroll) { + this.props.onScroll(e); + } + const timestamp = e.timeStamp; + let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement); + let contentLength = this._selectLength(e.nativeEvent.contentSize); + let offset = this._selectOffset(e.nativeEvent.contentOffset); + let dOffset = offset - this._scrollMetrics.offset; + + if (this._isNestedWithSameOrientation()) { + if (this._scrollMetrics.contentLength === 0) { + // Ignore scroll events until onLayout has been called and we + // know our offset from our offset from our parent + return; + } + ({visibleLength, contentLength, offset, dOffset} = + this._convertParentScrollMetrics({ + visibleLength, + offset, + })); + } + + const dt = this._scrollMetrics.timestamp + ? Math.max(1, timestamp - this._scrollMetrics.timestamp) + : 1; + const velocity = dOffset / dt; + + if ( + dt > 500 && + this._scrollMetrics.dt > 500 && + contentLength > 5 * visibleLength && + !this._hasWarned.perf + ) { + infoLog( + 'VirtualizedList: You have a large list that is slow to update - make sure your ' + + 'renderItem function renders components that follow React performance best practices ' + + 'like PureComponent, shouldComponentUpdate, etc.', + {dt, prevDt: this._scrollMetrics.dt, contentLength}, + ); + this._hasWarned.perf = true; + } + + // For invalid negative values (w/ RTL), set this to 1. + const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale; + this._scrollMetrics = { + contentLength, + dt, + dOffset, + offset, + timestamp, + velocity, + visibleLength, + zoomScale, + }; + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; + } + this._maybeCallOnEdgeReached(); + if (velocity !== 0) { + this._fillRateHelper.activate(); + } + this._computeBlankness(); + this._scheduleCellsToRenderUpdate(); + }; + + _scheduleCellsToRenderUpdate() { + const {first, last} = this.state.cellsAroundViewport; + const {offset, visibleLength, velocity} = this._scrollMetrics; + const itemCount = this.props.getItemCount(this.props.data); + let hiPri = false; + const onStartReachedThreshold = onStartReachedThresholdOrDefault( + this.props.onStartReachedThreshold, + ); + const onEndReachedThreshold = onEndReachedThresholdOrDefault( + this.props.onEndReachedThreshold, + ); + // Mark as high priority if we're close to the start of the first item + // But only if there are items before the first rendered item + if (first > 0) { + const distTop = + offset - this.__getFrameMetricsApprox(first, this.props).offset; + hiPri = + distTop < 0 || + (velocity < -2 && + distTop < + getScrollingThreshold(onStartReachedThreshold, visibleLength)); + } + // Mark as high priority if we're close to the end of the last item + // But only if there are items after the last rendered item + if (!hiPri && last >= 0 && last < itemCount - 1) { + const distBottom = + this.__getFrameMetricsApprox(last, this.props).offset - + (offset + visibleLength); + hiPri = + distBottom < 0 || + (velocity > 2 && + distBottom < + getScrollingThreshold(onEndReachedThreshold, visibleLength)); + } + // Only trigger high-priority updates if we've actually rendered cells, + // and with that size estimate, accurately compute how many cells we should render. + // Otherwise, it would just render as many cells as it can (of zero dimension), + // each time through attempting to render more (limited by maxToRenderPerBatch), + // starving the renderer from actually laying out the objects and computing _averageCellLength. + // If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate + // We shouldn't do another hipri cellToRenderUpdate + if ( + hiPri && + (this._averageCellLength || this.props.getItemLayout) && + !this._hiPriInProgress + ) { + this._hiPriInProgress = true; + // Don't worry about interactions when scrolling quickly; focus on filling content as fast + // as possible. + this._updateCellsToRenderBatcher.dispose({abort: true}); + this._updateCellsToRender(); + return; + } else { + this._updateCellsToRenderBatcher.schedule(); + } + } + + _onScrollBeginDrag = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onScrollBeginDrag(e); + }); + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.recordInteraction(); + }); + this._hasInteracted = true; + this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); + }; + + _onScrollEndDrag = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onScrollEndDrag(e); + }); + const {velocity} = e.nativeEvent; + if (velocity) { + this._scrollMetrics.velocity = this._selectOffset(velocity); + } + this._computeBlankness(); + this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); + }; + + _onMomentumScrollBegin = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onMomentumScrollBegin(e); + }); + this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); + }; + + _onMomentumScrollEnd = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onMomentumScrollEnd(e); + }); + this._scrollMetrics.velocity = 0; + this._computeBlankness(); + this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); + }; + + _updateCellsToRender = () => { + this.setState((state, props) => { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, + ); + const renderMask = VirtualizedList._createRenderMask( + props, + cellsAroundViewport, + this._getNonViewportRenderRegions(props), + ); + + if ( + cellsAroundViewport.first === state.cellsAroundViewport.first && + cellsAroundViewport.last === state.cellsAroundViewport.last && + renderMask.equals(state.renderMask) + ) { + return null; + } + + return {cellsAroundViewport, renderMask}; + }); + }; + + _createViewToken = ( + index: number, + isViewable: boolean, + props: FrameMetricProps, + // $FlowFixMe[missing-local-annot] + ) => { + const {data, getItem} = props; + const item = getItem(data, index); + return { + index, + item, + key: this._keyExtractor(item, index, props), + isViewable, + }; + }; + + /** + * Gets an approximate offset to an item at a given index. Supports + * fractional indices. + */ + _getOffsetApprox = (index: number, props: FrameMetricProps): number => { + if (Number.isInteger(index)) { + return this.__getFrameMetricsApprox(index, props).offset; + } else { + const frameMetrics = this.__getFrameMetricsApprox( + Math.floor(index), + props, + ); + const remainder = index - Math.floor(index); + return frameMetrics.offset + remainder * frameMetrics.length; + } + }; + + __getFrameMetricsApprox: ( + index: number, + props: FrameMetricProps, + ) => { + length: number, + offset: number, + ... + } = (index, props) => { + const frame = this._getFrameMetrics(index, props); + if (frame && frame.index === index) { + // check for invalid frames due to row re-ordering + return frame; + } else { + const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); + invariant( + !getItemLayout, + 'Should not have to estimate frames when a measurement metrics function is provided', + ); + return { + length: this._averageCellLength, + offset: this._averageCellLength * index, + }; + } + }; + + _getFrameMetrics = ( + index: number, + props: FrameMetricProps, + ): ?{ + length: number, + offset: number, + index: number, + inLayout?: boolean, + ... + } => { + const {data, getItem, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); + const item = getItem(data, index); + const frame = this._frames[this._keyExtractor(item, index, props)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.63 was deployed. To see the error + * delete this comment and run Flow. */ + return getItemLayout(data, index); + } + } + return frame; + }; + + _getNonViewportRenderRegions = ( + props: FrameMetricProps, + ): $ReadOnlyArray<{ + first: number, + last: number, + }> => { + // Keep a viewport's worth of content around the last focused cell to allow + // random navigation around it without any blanking. E.g. tabbing from one + // focused item out of viewport to another. + if ( + !(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey]) + ) { + return []; + } + + const lastFocusedCellRenderer = this._cellRefs[this._lastFocusedCellKey]; + const focusedCellIndex = lastFocusedCellRenderer.props.index; + const itemCount = props.getItemCount(props.data); + + // The cell may have been unmounted and have a stale index + if ( + focusedCellIndex >= itemCount || + this._indicesToKeys.get(focusedCellIndex) !== this._lastFocusedCellKey + ) { + return []; + } + + let first = focusedCellIndex; + let heightOfCellsBeforeFocused = 0; + for ( + let i = first - 1; + i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength; + i-- + ) { + first--; + heightOfCellsBeforeFocused += this.__getFrameMetricsApprox( + i, + props, + ).length; + } + + let last = focusedCellIndex; + let heightOfCellsAfterFocused = 0; + for ( + let i = last + 1; + i < itemCount && + heightOfCellsAfterFocused < this._scrollMetrics.visibleLength; + i++ + ) { + last++; + heightOfCellsAfterFocused += this.__getFrameMetricsApprox( + i, + props, + ).length; + } + + return [{first, last}]; + }; + + _updateViewableItems( + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, + this._scrollMetrics.offset, + this._scrollMetrics.visibleLength, + this._getFrameMetrics, + this._createViewToken, + tuple.onViewableItemsChanged, + cellsAroundViewport, + ); + }); + } +} + +const styles = StyleSheet.create({ + verticallyInverted: { + transform: [{scaleY: -1}], + }, + horizontallyInverted: { + transform: [{scaleX: -1}], + }, + debug: { + flex: 1, + }, + debugOverlayBase: { + position: 'absolute', + top: 0, + right: 0, + }, + debugOverlay: { + bottom: 0, + width: 20, + borderColor: 'blue', + borderWidth: 1, + }, + debugOverlayFrame: { + left: 0, + backgroundColor: 'orange', + }, + debugOverlayFrameLast: { + left: 0, + borderColor: 'green', + borderWidth: 2, + }, + debugOverlayFrameVis: { + left: 0, + borderColor: 'red', + borderWidth: 2, + }, +}); + +module.exports = VirtualizedList; diff --git a/Libraries/Lists/VirtualizedListCellRenderer.js b/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js similarity index 96% rename from Libraries/Lists/VirtualizedListCellRenderer.js rename to packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js index b16900d63b742b..0064f788b8a9c4 100644 --- a/Libraries/Lists/VirtualizedListCellRenderer.js +++ b/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js @@ -8,13 +8,15 @@ * @format */ -import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; -import type {FocusEvent, LayoutEvent} from '../Types/CoreEventTypes'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import type { + FocusEvent, + LayoutEvent, +} from 'react-native/Libraries/Types/CoreEventTypes'; import type FillRateHelper from './FillRateHelper'; import type {RenderItemType} from './VirtualizedListProps'; -import View from '../Components/View/View'; -import StyleSheet from '../StyleSheet/StyleSheet'; +import {View, StyleSheet} from 'react-native'; import {VirtualizedListCellContextProvider} from './VirtualizedListContext.js'; import invariant from 'invariant'; import * as React from 'react'; diff --git a/Libraries/Lists/VirtualizedListContext.js b/packages/virtualized-lists/Lists/VirtualizedListContext.js similarity index 100% rename from Libraries/Lists/VirtualizedListContext.js rename to packages/virtualized-lists/Lists/VirtualizedListContext.js diff --git a/Libraries/Lists/VirtualizedListProps.js b/packages/virtualized-lists/Lists/VirtualizedListProps.js similarity index 98% rename from Libraries/Lists/VirtualizedListProps.js rename to packages/virtualized-lists/Lists/VirtualizedListProps.js index f4d497b1d467a8..bfc59673270a57 100644 --- a/Libraries/Lists/VirtualizedListProps.js +++ b/packages/virtualized-lists/Lists/VirtualizedListProps.js @@ -8,8 +8,8 @@ * @format */ -import typeof ScrollView from '../Components/ScrollView/ScrollView'; -import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; +import {typeof ScrollView} from 'react-native'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type { ViewabilityConfig, ViewabilityConfigCallbackPair, diff --git a/packages/virtualized-lists/Lists/VirtualizedSectionList.js b/packages/virtualized-lists/Lists/VirtualizedSectionList.js new file mode 100644 index 00000000000000..61519bca4dc866 --- /dev/null +++ b/packages/virtualized-lists/Lists/VirtualizedSectionList.js @@ -0,0 +1,617 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +import type {ViewToken} from './ViewabilityHelper'; + +import {View} from 'react-native'; +import VirtualizedList from './VirtualizedList'; +import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; +import invariant from 'invariant'; +import * as React from 'react'; + +type Item = any; + +export type SectionBase = { + /** + * The data for rendering items in this section. + */ + data: $ReadOnlyArray, + /** + * Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections, + * the array index will be used by default. + */ + key?: string, + // Optional props will override list-wide props just for this section. + renderItem?: ?(info: { + item: SectionItemT, + index: number, + section: SectionBase, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... + }, + ... + }) => null | React.Element, + ItemSeparatorComponent?: ?React.ComponentType, + keyExtractor?: (item: SectionItemT, index?: ?number) => string, + ... +}; + +type RequiredProps> = {| + sections: $ReadOnlyArray, +|}; + +type OptionalProps> = {| + /** + * Default renderer for every item in every section. + */ + renderItem?: (info: { + item: Item, + index: number, + section: SectionT, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... + }, + ... + }) => null | React.Element, + /** + * Rendered at the top of each section. These stick to the top of the `ScrollView` by default on + * iOS. See `stickySectionHeadersEnabled`. + */ + renderSectionHeader?: ?(info: { + section: SectionT, + ... + }) => null | React.Element, + /** + * Rendered at the bottom of each section. + */ + renderSectionFooter?: ?(info: { + section: SectionT, + ... + }) => null | React.Element, + /** + * Rendered at the top and bottom of each section (note this is different from + * `ItemSeparatorComponent` which is only rendered between items). These are intended to separate + * sections from the headers above and below and typically have the same highlight response as + * `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`, + * and any custom props from `separators.updateProps`. + */ + SectionSeparatorComponent?: ?React.ComponentType, + /** + * Makes section headers stick to the top of the screen until the next one pushes it off. Only + * enabled by default on iOS because that is the platform standard there. + */ + stickySectionHeadersEnabled?: boolean, + onEndReached?: ?({distanceFromEnd: number, ...}) => void, +|}; + +type VirtualizedListProps = React.ElementConfig; + +export type Props = {| + ...RequiredProps, + ...OptionalProps, + ...$Diff< + VirtualizedListProps, + { + renderItem: $PropertyType, + data: $PropertyType, + ... + }, + >, +|}; +export type ScrollToLocationParamsType = {| + animated?: ?boolean, + itemIndex: number, + sectionIndex: number, + viewOffset?: number, + viewPosition?: number, +|}; + +type State = {childProps: VirtualizedListProps, ...}; + +/** + * Right now this just flattens everything into one list and uses VirtualizedList under the + * hood. The only operation that might not scale well is concatting the data arrays of all the + * sections when new props are received, which should be plenty fast for up to ~10,000 items. + */ +class VirtualizedSectionList< + SectionT: SectionBase, +> extends React.PureComponent, State> { + scrollToLocation(params: ScrollToLocationParamsType) { + let index = params.itemIndex; + for (let i = 0; i < params.sectionIndex; i++) { + index += this.props.getItemCount(this.props.sections[i].data) + 2; + } + let viewOffset = params.viewOffset || 0; + if (this._listRef == null) { + return; + } + if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) { + const frame = this._listRef.__getFrameMetricsApprox( + index - params.itemIndex, + this._listRef.props, + ); + viewOffset += frame.length; + } + const toIndexParams = { + ...params, + viewOffset, + index, + }; + // $FlowFixMe[incompatible-use] + this._listRef.scrollToIndex(toIndexParams); + } + + getListRef(): ?React.ElementRef { + return this._listRef; + } + + render(): React.Node { + const { + ItemSeparatorComponent, // don't pass through, rendered with renderItem + SectionSeparatorComponent, + renderItem: _renderItem, + renderSectionFooter, + renderSectionHeader, + sections: _sections, + stickySectionHeadersEnabled, + ...passThroughProps + } = this.props; + + const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0; + + const stickyHeaderIndices = this.props.stickySectionHeadersEnabled + ? ([]: Array) + : undefined; + + let itemCount = 0; + for (const section of this.props.sections) { + // Track the section header indices + if (stickyHeaderIndices != null) { + stickyHeaderIndices.push(itemCount + listHeaderOffset); + } + + // Add two for the section header and footer. + itemCount += 2; + itemCount += this.props.getItemCount(section.data); + } + const renderItem = this._renderItem(itemCount); + + return ( + + this._getItem(this.props, sections, index) + } + getItemCount={() => itemCount} + onViewableItemsChanged={ + this.props.onViewableItemsChanged + ? this._onViewableItemsChanged + : undefined + } + ref={this._captureRef} + /> + ); + } + + _getItem( + props: Props, + sections: ?$ReadOnlyArray, + index: number, + ): ?Item { + if (!sections) { + return null; + } + let itemIdx = index - 1; + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + const sectionData = section.data; + const itemCount = props.getItemCount(sectionData); + if (itemIdx === -1 || itemIdx === itemCount) { + // We intend for there to be overflow by one on both ends of the list. + // This will be for headers and footers. When returning a header or footer + // item the section itself is the item. + return section; + } else if (itemIdx < itemCount) { + // If we are in the bounds of the list's data then return the item. + return props.getItem(sectionData, itemIdx); + } else { + itemIdx -= itemCount + 2; // Add two for the header and footer + } + } + return null; + } + + // $FlowFixMe[missing-local-annot] + _keyExtractor = (item: Item, index: number) => { + const info = this._subExtractor(index); + return (info && info.key) || String(index); + }; + + _subExtractor(index: number): ?{ + section: SectionT, + // Key of the section or combined key for section + item + key: string, + // Relative index within the section + index: ?number, + // True if this is the section header + header?: ?boolean, + leadingItem?: ?Item, + leadingSection?: ?SectionT, + trailingItem?: ?Item, + trailingSection?: ?SectionT, + ... + } { + let itemIndex = index; + const {getItem, getItemCount, keyExtractor, sections} = this.props; + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + const sectionData = section.data; + const key = section.key || String(i); + itemIndex -= 1; // The section adds an item for the header + if (itemIndex >= getItemCount(sectionData) + 1) { + itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer. + } else if (itemIndex === -1) { + return { + section, + key: key + ':header', + index: null, + header: true, + trailingSection: sections[i + 1], + }; + } else if (itemIndex === getItemCount(sectionData)) { + return { + section, + key: key + ':footer', + index: null, + header: false, + trailingSection: sections[i + 1], + }; + } else { + const extractor = + section.keyExtractor || keyExtractor || defaultKeyExtractor; + return { + section, + key: + key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex), + index: itemIndex, + leadingItem: getItem(sectionData, itemIndex - 1), + leadingSection: sections[i - 1], + trailingItem: getItem(sectionData, itemIndex + 1), + trailingSection: sections[i + 1], + }; + } + } + } + + _convertViewable = (viewable: ViewToken): ?ViewToken => { + invariant(viewable.index != null, 'Received a broken ViewToken'); + const info = this._subExtractor(viewable.index); + if (!info) { + return null; + } + const keyExtractorWithNullableIndex = info.section.keyExtractor; + const keyExtractorWithNonNullableIndex = + this.props.keyExtractor || defaultKeyExtractor; + const key = + keyExtractorWithNullableIndex != null + ? keyExtractorWithNullableIndex(viewable.item, info.index) + : keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0); + + return { + ...viewable, + index: info.index, + key, + section: info.section, + }; + }; + + _onViewableItemsChanged = ({ + viewableItems, + changed, + }: { + viewableItems: Array, + changed: Array, + ... + }) => { + const onViewableItemsChanged = this.props.onViewableItemsChanged; + if (onViewableItemsChanged != null) { + onViewableItemsChanged({ + viewableItems: viewableItems + .map(this._convertViewable, this) + .filter(Boolean), + changed: changed.map(this._convertViewable, this).filter(Boolean), + }); + } + }; + + _renderItem = + (listItemCount: number): $FlowFixMe => + // eslint-disable-next-line react/no-unstable-nested-components + ({item, index}: {item: Item, index: number, ...}) => { + const info = this._subExtractor(index); + if (!info) { + return null; + } + const infoIndex = info.index; + if (infoIndex == null) { + const {section} = info; + if (info.header === true) { + const {renderSectionHeader} = this.props; + return renderSectionHeader ? renderSectionHeader({section}) : null; + } else { + const {renderSectionFooter} = this.props; + return renderSectionFooter ? renderSectionFooter({section}) : null; + } + } else { + const renderItem = info.section.renderItem || this.props.renderItem; + const SeparatorComponent = this._getSeparatorComponent( + index, + info, + listItemCount, + ); + invariant(renderItem, 'no renderItem!'); + return ( + + ); + } + }; + + _updatePropsFor = (cellKey: string, value: any) => { + const updateProps = this._updatePropsMap[cellKey]; + if (updateProps != null) { + updateProps(value); + } + }; + + _updateHighlightFor = (cellKey: string, value: boolean) => { + const updateHighlight = this._updateHighlightMap[cellKey]; + if (updateHighlight != null) { + updateHighlight(value); + } + }; + + _setUpdateHighlightFor = ( + cellKey: string, + updateHighlightFn: ?(boolean) => void, + ) => { + if (updateHighlightFn != null) { + this._updateHighlightMap[cellKey] = updateHighlightFn; + } else { + // $FlowFixMe[prop-missing] + delete this._updateHighlightFor[cellKey]; + } + }; + + _setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => { + if (updatePropsFn != null) { + this._updatePropsMap[cellKey] = updatePropsFn; + } else { + delete this._updatePropsMap[cellKey]; + } + }; + + _getSeparatorComponent( + index: number, + info?: ?Object, + listItemCount: number, + ): ?React.ComponentType { + info = info || this._subExtractor(index); + if (!info) { + return null; + } + const ItemSeparatorComponent = + info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; + const {SectionSeparatorComponent} = this.props; + const isLastItemInList = index === listItemCount - 1; + const isLastItemInSection = + info.index === this.props.getItemCount(info.section.data) - 1; + if (SectionSeparatorComponent && isLastItemInSection) { + return SectionSeparatorComponent; + } + if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { + return ItemSeparatorComponent; + } + return null; + } + + _updateHighlightMap: {[string]: (boolean) => void} = {}; + _updatePropsMap: {[string]: void | (boolean => void)} = {}; + _listRef: ?React.ElementRef; + _captureRef = (ref: null | React$ElementRef>) => { + this._listRef = ref; + }; +} + +type ItemWithSeparatorCommonProps = $ReadOnly<{| + leadingItem: ?Item, + leadingSection: ?Object, + section: Object, + trailingItem: ?Item, + trailingSection: ?Object, +|}>; + +type ItemWithSeparatorProps = $ReadOnly<{| + ...ItemWithSeparatorCommonProps, + LeadingSeparatorComponent: ?React.ComponentType, + SeparatorComponent: ?React.ComponentType, + cellKey: string, + index: number, + item: Item, + setSelfHighlightCallback: ( + cellKey: string, + updateFn: ?(boolean) => void, + ) => void, + setSelfUpdatePropsCallback: ( + cellKey: string, + updateFn: ?(boolean) => void, + ) => void, + prevCellKey?: ?string, + updateHighlightFor: (prevCellKey: string, value: boolean) => void, + updatePropsFor: (prevCellKey: string, value: Object) => void, + renderItem: Function, + inverted: boolean, +|}>; + +function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node { + const { + LeadingSeparatorComponent, + // this is the trailing separator and is associated with this item + SeparatorComponent, + cellKey, + prevCellKey, + setSelfHighlightCallback, + updateHighlightFor, + setSelfUpdatePropsCallback, + updatePropsFor, + item, + index, + section, + inverted, + } = props; + + const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] = + React.useState(false); + + const [separatorHighlighted, setSeparatorHighlighted] = React.useState(false); + + const [leadingSeparatorProps, setLeadingSeparatorProps] = React.useState({ + leadingItem: props.leadingItem, + leadingSection: props.leadingSection, + section: props.section, + trailingItem: props.item, + trailingSection: props.trailingSection, + }); + const [separatorProps, setSeparatorProps] = React.useState({ + leadingItem: props.item, + leadingSection: props.leadingSection, + section: props.section, + trailingItem: props.trailingItem, + trailingSection: props.trailingSection, + }); + + React.useEffect(() => { + setSelfHighlightCallback(cellKey, setSeparatorHighlighted); + // $FlowFixMe[incompatible-call] + setSelfUpdatePropsCallback(cellKey, setSeparatorProps); + + return () => { + setSelfUpdatePropsCallback(cellKey, null); + setSelfHighlightCallback(cellKey, null); + }; + }, [ + cellKey, + setSelfHighlightCallback, + setSeparatorProps, + setSelfUpdatePropsCallback, + ]); + + const separators = { + highlight: () => { + setLeadingSeparatorHighlighted(true); + setSeparatorHighlighted(true); + if (prevCellKey != null) { + updateHighlightFor(prevCellKey, true); + } + }, + unhighlight: () => { + setLeadingSeparatorHighlighted(false); + setSeparatorHighlighted(false); + if (prevCellKey != null) { + updateHighlightFor(prevCellKey, false); + } + }, + updateProps: ( + select: 'leading' | 'trailing', + newProps: $Shape, + ) => { + if (select === 'leading') { + if (LeadingSeparatorComponent != null) { + setLeadingSeparatorProps({...leadingSeparatorProps, ...newProps}); + } else if (prevCellKey != null) { + // update the previous item's separator + updatePropsFor(prevCellKey, {...leadingSeparatorProps, ...newProps}); + } + } else if (select === 'trailing' && SeparatorComponent != null) { + setSeparatorProps({...separatorProps, ...newProps}); + } + }, + }; + const element = props.renderItem({ + item, + index, + section, + separators, + }); + const leadingSeparator = LeadingSeparatorComponent != null && ( + + ); + const separator = SeparatorComponent != null && ( + + ); + return leadingSeparator || separator ? ( + + {inverted === false ? leadingSeparator : separator} + {element} + {inverted === false ? separator : leadingSeparator} + + ) : ( + element + ); +} + +/* $FlowFixMe[class-object-subtyping] added when improving typing for this + * parameters */ +// $FlowFixMe[method-unbinding] +module.exports = (VirtualizedSectionList: React.AbstractComponent< + React.ElementConfig, + $ReadOnly<{ + getListRef: () => ?React.ElementRef, + scrollToLocation: (params: ScrollToLocationParamsType) => void, + ... + }>, +>); diff --git a/Libraries/Lists/__tests__/CellRenderMask-test.js b/packages/virtualized-lists/Lists/__tests__/CellRenderMask-test.js similarity index 100% rename from Libraries/Lists/__tests__/CellRenderMask-test.js rename to packages/virtualized-lists/Lists/__tests__/CellRenderMask-test.js diff --git a/Libraries/Lists/__tests__/FillRateHelper-test.js b/packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js similarity index 100% rename from Libraries/Lists/__tests__/FillRateHelper-test.js rename to packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js diff --git a/Libraries/Lists/__tests__/ViewabilityHelper-test.js b/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js similarity index 100% rename from Libraries/Lists/__tests__/ViewabilityHelper-test.js rename to packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js diff --git a/Libraries/Lists/__tests__/VirtualizeUtils-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js similarity index 100% rename from Libraries/Lists/__tests__/VirtualizeUtils-test.js rename to packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js diff --git a/Libraries/Lists/__tests__/VirtualizedList-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js similarity index 100% rename from Libraries/Lists/__tests__/VirtualizedList-test.js rename to packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js diff --git a/Libraries/Lists/__tests__/VirtualizedSectionList-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js similarity index 100% rename from Libraries/Lists/__tests__/VirtualizedSectionList-test.js rename to packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js diff --git a/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap b/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap similarity index 100% rename from Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap rename to packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap diff --git a/Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap b/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap similarity index 100% rename from Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap rename to packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap diff --git a/Libraries/Utilities/__tests__/clamp-test.js b/packages/virtualized-lists/Utilities/__tests__/clamp-test.js similarity index 100% rename from Libraries/Utilities/__tests__/clamp-test.js rename to packages/virtualized-lists/Utilities/__tests__/clamp-test.js diff --git a/Libraries/Utilities/clamp.js b/packages/virtualized-lists/Utilities/clamp.js similarity index 100% rename from Libraries/Utilities/clamp.js rename to packages/virtualized-lists/Utilities/clamp.js diff --git a/packages/virtualized-lists/Utilities/infoLog.js b/packages/virtualized-lists/Utilities/infoLog.js new file mode 100644 index 00000000000000..6cb6df8d414971 --- /dev/null +++ b/packages/virtualized-lists/Utilities/infoLog.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +'use strict'; + +/** + * Intentional info-level logging for clear separation from ad-hoc console debug logging. + */ +function infoLog(...args: Array): void { + return console.log(...args); +} + +module.exports = infoLog; diff --git a/packages/virtualized-lists/index.d.ts b/packages/virtualized-lists/index.d.ts new file mode 100644 index 00000000000000..c66fc20521e439 --- /dev/null +++ b/packages/virtualized-lists/index.d.ts @@ -0,0 +1,10 @@ +/** + * 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 + */ + +export * from './Lists/VirtualizedList'; diff --git a/packages/virtualized-lists/index.js b/packages/virtualized-lists/index.js new file mode 100644 index 00000000000000..31d47809b369d6 --- /dev/null +++ b/packages/virtualized-lists/index.js @@ -0,0 +1,51 @@ +/** + * 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 + */ + +'use strict'; + +import {keyExtractor} from './Lists/VirtualizeUtils'; + +import typeof VirtualizedList from './Lists/VirtualizedList'; +import typeof VirtualizedSectionList from './Lists/VirtualizedSectionList'; +import {typeof VirtualizedListContextResetter} from './Lists/VirtualizedListContext'; +import typeof ViewabilityHelper from './Lists/ViewabilityHelper'; + +export type { + ViewToken, + ViewabilityConfigCallbackPair, +} from './Lists/ViewabilityHelper'; +export type { + RenderItemProps, + RenderItemType, + Separators, +} from './Lists/VirtualizedListProps'; +export type { + Props as VirtualizedSectionListProps, + ScrollToLocationParamsType, + SectionBase, +} from './Lists/VirtualizedSectionList'; + +module.exports = { + keyExtractor, + + get VirtualizedList(): VirtualizedList { + return require('./Lists/VirtualizedList'); + }, + get VirtualizedSectionList(): VirtualizedSectionList { + return require('./Lists/VirtualizedSectionList'); + }, + get VirtualizedListContextResetter(): VirtualizedListContextResetter { + const VirtualizedListContext = require('./Lists/VirtualizedListContext'); + return VirtualizedListContext.VirtualizedListContextResetter; + }, + get ViewabilityHelper(): ViewabilityHelper { + return require('./Lists/ViewabilityHelper'); + }, +}; diff --git a/packages/virtualized-lists/package.json b/packages/virtualized-lists/package.json new file mode 100644 index 00000000000000..64480808cb2664 --- /dev/null +++ b/packages/virtualized-lists/package.json @@ -0,0 +1,18 @@ +{ + "name": "@react-native/virtualized-lists", + "version": "0.72.0", + "description": "Virtualized lists for React Native.", + "repository": { + "type": "git", + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/virtualized-lists" + }, + "license": "MIT", + "devDependencies": { + "react-test-renderer": "18.2.0" + }, + "peerDependencies": { + "react-native": "*", + "react-test-renderer": "18.2.0" + } +} diff --git a/types/index.d.ts b/types/index.d.ts index 97b34d5d2321db..349efc65a9fc8c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -115,7 +115,7 @@ export * from '../Libraries/LayoutAnimation/LayoutAnimation'; export * from '../Libraries/Linking/Linking'; export * from '../Libraries/Lists/FlatList'; export * from '../Libraries/Lists/SectionList'; -export * from '../Libraries/Lists/VirtualizedList'; +export * from '@react-native/virtualized-lists'; export * from '../Libraries/LogBox/LogBox'; export * from '../Libraries/Modal/Modal'; export * as Systrace from '../Libraries/Performance/Systrace'; From 1479b2ac26fded3840c596f53e6eb86a4b0c2c71 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Mon, 6 Feb 2023 13:39:13 -0800 Subject: [PATCH 46/65] refactor(arvr/xplat): update imports to resolve packaged virtualized list from react-native (#36035) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36035 Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D42805202 fbshipit-source-id: 1f1d6e36ec6e19a1b46ce340be095bb60b8048f4 --- BUCK | 2 ++ Libraries/Lists/FillRateHelper.js | 19 +++++++++++++++++++ Libraries/Lists/ViewabilityHelper.js | 1 + Libraries/Lists/VirtualizeUtils.js | 18 ++++++++++++++++++ Libraries/Lists/VirtualizedListContext.js | 18 ++++++++++++++++++ packages/virtualized-lists/index.js | 6 ++++++ packages/virtualized-lists/package.json | 3 +++ 7 files changed, 67 insertions(+) create mode 100644 Libraries/Lists/FillRateHelper.js create mode 100644 Libraries/Lists/VirtualizeUtils.js create mode 100644 Libraries/Lists/VirtualizedListContext.js diff --git a/BUCK b/BUCK index c33ea5e8e6264f..37f5bb531bae4f 100644 --- a/BUCK +++ b/BUCK @@ -724,6 +724,8 @@ rn_library( "Libraries/**/*.js", "Libraries/NewAppScreen/**/*.png", "Libraries/LogBox/**/*.png", + "packages/virtualized-lists/**/*.js", + "packages/virtualized-lists/**/*.json", ], exclude = [ "**/__*__/**", diff --git a/Libraries/Lists/FillRateHelper.js b/Libraries/Lists/FillRateHelper.js new file mode 100644 index 00000000000000..141fe98eb067bb --- /dev/null +++ b/Libraries/Lists/FillRateHelper.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import {typeof FillRateHelper as FillRateHelperType} from '@react-native/virtualized-lists'; + +const FillRateHelper: FillRateHelperType = + require('@react-native/virtualized-lists').FillRateHelper; + +export type {FillRateInfo} from '@react-native/virtualized-lists'; +module.exports = FillRateHelper; diff --git a/Libraries/Lists/ViewabilityHelper.js b/Libraries/Lists/ViewabilityHelper.js index 9a0a5aa694fb96..c7dedfdf496b93 100644 --- a/Libraries/Lists/ViewabilityHelper.js +++ b/Libraries/Lists/ViewabilityHelper.js @@ -12,6 +12,7 @@ export type { ViewToken, + ViewabilityConfig, ViewabilityConfigCallbackPair, } from '@react-native/virtualized-lists'; diff --git a/Libraries/Lists/VirtualizeUtils.js b/Libraries/Lists/VirtualizeUtils.js new file mode 100644 index 00000000000000..535d25b3abc538 --- /dev/null +++ b/Libraries/Lists/VirtualizeUtils.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import {typeof keyExtractor as KeyExtractorType} from '@react-native/virtualized-lists'; + +const keyExtractor: KeyExtractorType = + require('@react-native/virtualized-lists').keyExtractor; + +module.exports = {keyExtractor}; diff --git a/Libraries/Lists/VirtualizedListContext.js b/Libraries/Lists/VirtualizedListContext.js new file mode 100644 index 00000000000000..5686ccf372286c --- /dev/null +++ b/Libraries/Lists/VirtualizedListContext.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterType} from '@react-native/virtualized-lists'; + +const VirtualizedListContextResetter: VirtualizedListContextResetterType = + require('@react-native/virtualized-lists').VirtualizedListContextResetter; + +module.exports = {VirtualizedListContextResetter}; diff --git a/packages/virtualized-lists/index.js b/packages/virtualized-lists/index.js index 31d47809b369d6..8516f7ccf73289 100644 --- a/packages/virtualized-lists/index.js +++ b/packages/virtualized-lists/index.js @@ -16,9 +16,11 @@ import typeof VirtualizedList from './Lists/VirtualizedList'; import typeof VirtualizedSectionList from './Lists/VirtualizedSectionList'; import {typeof VirtualizedListContextResetter} from './Lists/VirtualizedListContext'; import typeof ViewabilityHelper from './Lists/ViewabilityHelper'; +import typeof FillRateHelper from './Lists/FillRateHelper'; export type { ViewToken, + ViewabilityConfig, ViewabilityConfigCallbackPair, } from './Lists/ViewabilityHelper'; export type { @@ -31,6 +33,7 @@ export type { ScrollToLocationParamsType, SectionBase, } from './Lists/VirtualizedSectionList'; +export type {FillRateInfo} from './Lists/FillRateHelper'; module.exports = { keyExtractor, @@ -48,4 +51,7 @@ module.exports = { get ViewabilityHelper(): ViewabilityHelper { return require('./Lists/ViewabilityHelper'); }, + get FillRateHelper(): FillRateHelper { + return require('./Lists/FillRateHelper'); + }, }; diff --git a/packages/virtualized-lists/package.json b/packages/virtualized-lists/package.json index 64480808cb2664..8b19cd2c199f7a 100644 --- a/packages/virtualized-lists/package.json +++ b/packages/virtualized-lists/package.json @@ -8,6 +8,9 @@ "directory": "packages/virtualized-lists" }, "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, "devDependencies": { "react-test-renderer": "18.2.0" }, From 5b8faae2aea620c5714477ab6144d50649e81476 Mon Sep 17 00:00:00 2001 From: hoxy Date: Mon, 6 Feb 2023 15:56:14 -0800 Subject: [PATCH 47/65] fix: re-export SectionBase & ScrollToLocationParamsType from virtualized-lists package Summary: Changelog: [Internal] - Fixes `flow-stable` build-break overriding_review_checks_triggers_an_audit_and_retroactive_review Differential Revision: https://internalfb.com/D43063551 fbshipit-source-id: b7a92669fa41e8fc69370a2d9e809ce2559dd600 --- Libraries/Lists/VirtualizedSectionList.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index 90c187bc659bbe..242dfe34c6b231 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -15,4 +15,8 @@ import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@reac const VirtualizedSectionList: VirtualizedSectionListType = require('@react-native/virtualized-lists').VirtualizedSectionList; +export type { + SectionBase, + ScrollToLocationParamsType, +} from '@react-native/virtualized-lists'; module.exports = VirtualizedSectionList; From d9f2491a713d872f2f3c8447dbf789fb17b94524 Mon Sep 17 00:00:00 2001 From: Xin Chen Date: Mon, 6 Feb 2023 19:12:46 -0800 Subject: [PATCH 48/65] Fix edge case when layout animation caused delete and create mutations in the same batch Summary: This is a two step (2/2) fix to a race that could caused a DELETE...CREATE mutations being sent over to the fabric mounting layer. Such combination was assumed not possible from the differ (https://fburl.com/code/kg8z9t4w), yet it happened at least in the existence of layout animation and when certain commits happen while animation is active. This diff fixes all potential races in the Fabric mounting layer directly. It captures all the `DELETE...CREATE` combinations and stop those from passing down to the native platforms. This should fix all such races should them not captured by the fix in the layout animation. To help understand other races better, I also logged here to indicate such race so that future crashes will have more context. Changelog: [General][Fixed] - Fix edge case when layout animation caused delete and create mutations in the same batch Reviewed By: javache Differential Revision: D41900201 fbshipit-source-id: 280502ca32ce87a9e483cd859b11bcd3e5c4a435 --- .../react/config/ReactFeatureFlags.java | 6 ++++ .../react/fabric/FabricMountingManager.cpp | 32 +++++++++++++++++-- .../jni/react/fabric/FabricMountingManager.h | 1 + 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 843bc6acc2dd07..44dbc60da5b8c6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -115,4 +115,10 @@ public class ReactFeatureFlags { * state in Fabric SurfaceMountingManager. */ public static boolean reduceDeleteCreateMutationLayoutAnimation = true; + + /** + * Allow fix to drop delete...create mutations which could cause missing view state in Fabric + * SurfaceMountingManager. + */ + public static boolean reduceDeleteCreateMutation = false; } diff --git a/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index 707ea46e50debb..b1097967515bfd 100644 --- a/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -40,7 +40,9 @@ FabricMountingManager::FabricMountingManager( std::shared_ptr &config, global_ref &javaUIManager) : javaUIManager_(javaUIManager), - useOverflowInset_(getFeatureFlagValue("useOverflowInset")) { + useOverflowInset_(getFeatureFlagValue("useOverflowInset")), + reduceDeleteCreateMutation_( + getFeatureFlagValue("reduceDeleteCreateMutation")) { CoreFeatures::enableMapBuffer = getFeatureFlagValue("useMapBufferProps"); } @@ -314,9 +316,33 @@ void FabricMountingManager::executeMount( bool isVirtual = mutation.mutatedViewIsVirtual(); switch (mutationType) { case ShadowViewMutation::Create: { - bool allocationCheck = + bool shouldCreateView = !allocatedViewTags.contains(newChildShadowView.tag); - bool shouldCreateView = allocationCheck; + if (reduceDeleteCreateMutation_) { + // Detect DELETE...CREATE situation on the same node and do NOT push + // back to the mount items. This is an edge case that may happen + // when for example animation runs while commit happened, and we + // want to filter them out here to capture all possible sources of + // such mutations. The re-ordering logic here assumes no + // DELETE...CREATE in the mutations, as we will re-order mutations + // and batch all DELETE instructions in the end. + auto it = std::remove_if( + cppDeleteMountItems.begin(), + cppDeleteMountItems.end(), + [&](auto &deletedMountItem) -> bool { + return deletedMountItem.oldChildShadowView.tag == + newChildShadowView.tag; + }); + bool hasDeletedViewsWithSameTag = it != cppDeleteMountItems.end(); + cppDeleteMountItems.erase(it, cppDeleteMountItems.end()); + + if (hasDeletedViewsWithSameTag) { + shouldCreateView = false; + LOG(ERROR) + << "XIN: Detect DELETE...CREATE on the same tag from mutations in the same batch. The DELETE and CREATE mutations are removed before sending to the native platforms"; + } + } + if (shouldCreateView) { cppCommonMountItems.push_back( CppMountItem::CreateMountItem(newChildShadowView)); diff --git a/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h b/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h index 57e9eff06a6244..37440bf50198b0 100644 --- a/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h +++ b/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h @@ -70,6 +70,7 @@ class FabricMountingManager final { std::recursive_mutex allocatedViewsMutex_; bool const useOverflowInset_{false}; + bool const reduceDeleteCreateMutation_{false}; jni::local_ref getProps( ShadowView const &oldShadowView, From ebaa00e327404ce11f0d35bbcbab13e65dfb4db7 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 6 Feb 2023 20:00:19 -0800 Subject: [PATCH 49/65] Reconnect VirtualizedList Source History 1/2 (Revert D41745930) Summary: This change reverts D41745930 (https://github.com/facebook/react-native/commit/2e3dbe9c2fbff52448e2d5a7c1e4c96b1016cf25) as part of a stack to splice back source history which was lost (Git registered the file moves as additions). It is expected this diff will individually fail. The entire stack should be applied at once. Changelog: [Internal] Reviewed By: hoxyq Differential Revision: D43068113 fbshipit-source-id: c8398629fe5dcc1ca4bf02f550adc00c78a8487a --- Libraries/Inspector/NetworkOverlay.js | 2 +- .../Interaction/Batchinator.js | 2 +- .../Interaction/__tests__/Batchinator-test.js | 4 + .../Lists/CellRenderMask.js | 0 .../Lists/ChildListCollection.js | 0 Libraries/Lists/FillRateHelper.js | 242 +- Libraries/Lists/FlatList.d.ts | 2 +- Libraries/Lists/FlatList.js | 11 +- Libraries/Lists/FlatList.js.flow | 9 +- Libraries/Lists/SectionList.d.ts | 2 +- Libraries/Lists/SectionList.js | 6 +- Libraries/Lists/SectionListModern.js | 6 +- .../Lists/StateSafePureComponent.js | 0 Libraries/Lists/ViewabilityHelper.js | 352 ++- Libraries/Lists/VirtualizeUtils.js | 248 ++- .../Lists/VirtualizedList.d.ts | 12 +- Libraries/Lists/VirtualizedList.js | 1949 +++++++++++++++- .../Lists/VirtualizedListCellRenderer.js | 10 +- Libraries/Lists/VirtualizedListContext.js | 110 +- .../Lists/VirtualizedListProps.js | 4 +- Libraries/Lists/VirtualizedSectionList.js | 613 +++++- .../Lists/__tests__/CellRenderMask-test.js | 0 .../Lists/__tests__/FillRateHelper-test.js | 0 .../Lists/__tests__/ViewabilityHelper-test.js | 0 .../Lists/__tests__/VirtualizeUtils-test.js | 0 .../Lists/__tests__/VirtualizedList-test.js | 0 .../__tests__/VirtualizedSectionList-test.js | 0 .../VirtualizedList-test.js.snap | 0 .../VirtualizedSectionList-test.js.snap | 0 Libraries/Modal/Modal.js | 2 +- Libraries/Utilities/ReactNativeTestTools.js | 2 +- .../Utilities/__tests__/clamp-test.js | 0 .../Utilities/clamp.js | 0 index.js | 2 +- package.json | 1 - .../js/examples/FlatList/FlatList-basic.js | 2 +- .../js/examples/FlatList/FlatList-nested.js | 4 +- .../FlatList-onViewableItemsChanged.js | 2 +- .../virtualized-lists/Lists/FillRateHelper.js | 253 --- .../Lists/ViewabilityHelper.js | 360 --- .../Lists/VirtualizeUtils.js | 258 --- .../Lists/VirtualizedList.js | 1955 ----------------- .../Lists/VirtualizedListContext.js | 116 - .../Lists/VirtualizedSectionList.js | 617 ------ .../virtualized-lists/Utilities/infoLog.js | 20 - packages/virtualized-lists/index.d.ts | 10 - packages/virtualized-lists/index.js | 57 - packages/virtualized-lists/package.json | 21 - types/index.d.ts | 2 +- 49 files changed, 3513 insertions(+), 3755 deletions(-) rename {packages/virtualized-lists => Libraries}/Interaction/Batchinator.js (97%) rename {packages/virtualized-lists => Libraries}/Interaction/__tests__/Batchinator-test.js (95%) rename {packages/virtualized-lists => Libraries}/Lists/CellRenderMask.js (100%) rename {packages/virtualized-lists => Libraries}/Lists/ChildListCollection.js (100%) rename {packages/virtualized-lists => Libraries}/Lists/StateSafePureComponent.js (100%) rename {packages/virtualized-lists => Libraries}/Lists/VirtualizedList.d.ts (97%) rename {packages/virtualized-lists => Libraries}/Lists/VirtualizedListCellRenderer.js (96%) rename {packages/virtualized-lists => Libraries}/Lists/VirtualizedListProps.js (98%) rename {packages/virtualized-lists => Libraries}/Lists/__tests__/CellRenderMask-test.js (100%) rename {packages/virtualized-lists => Libraries}/Lists/__tests__/FillRateHelper-test.js (100%) rename {packages/virtualized-lists => Libraries}/Lists/__tests__/ViewabilityHelper-test.js (100%) rename {packages/virtualized-lists => Libraries}/Lists/__tests__/VirtualizeUtils-test.js (100%) rename {packages/virtualized-lists => Libraries}/Lists/__tests__/VirtualizedList-test.js (100%) rename {packages/virtualized-lists => Libraries}/Lists/__tests__/VirtualizedSectionList-test.js (100%) rename {packages/virtualized-lists => Libraries}/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap (100%) rename {packages/virtualized-lists => Libraries}/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap (100%) rename {packages/virtualized-lists => Libraries}/Utilities/__tests__/clamp-test.js (100%) rename {packages/virtualized-lists => Libraries}/Utilities/clamp.js (100%) delete mode 100644 packages/virtualized-lists/Lists/FillRateHelper.js delete mode 100644 packages/virtualized-lists/Lists/ViewabilityHelper.js delete mode 100644 packages/virtualized-lists/Lists/VirtualizeUtils.js delete mode 100644 packages/virtualized-lists/Lists/VirtualizedList.js delete mode 100644 packages/virtualized-lists/Lists/VirtualizedListContext.js delete mode 100644 packages/virtualized-lists/Lists/VirtualizedSectionList.js delete mode 100644 packages/virtualized-lists/Utilities/infoLog.js delete mode 100644 packages/virtualized-lists/index.d.ts delete mode 100644 packages/virtualized-lists/index.js delete mode 100644 packages/virtualized-lists/package.json diff --git a/Libraries/Inspector/NetworkOverlay.js b/Libraries/Inspector/NetworkOverlay.js index c6d1ec33907e36..169318cea27f28 100644 --- a/Libraries/Inspector/NetworkOverlay.js +++ b/Libraries/Inspector/NetworkOverlay.js @@ -10,7 +10,7 @@ 'use strict'; -import type {RenderItemProps} from '@react-native/virtualized-lists'; +import type {RenderItemProps} from '../Lists/VirtualizedList'; const ScrollView = require('../Components/ScrollView/ScrollView'); const TouchableHighlight = require('../Components/Touchable/TouchableHighlight'); diff --git a/packages/virtualized-lists/Interaction/Batchinator.js b/Libraries/Interaction/Batchinator.js similarity index 97% rename from packages/virtualized-lists/Interaction/Batchinator.js rename to Libraries/Interaction/Batchinator.js index 4fbc1931ca5708..2ca2d7986d1a78 100644 --- a/packages/virtualized-lists/Interaction/Batchinator.js +++ b/Libraries/Interaction/Batchinator.js @@ -10,7 +10,7 @@ 'use strict'; -const {InteractionManager} = require('react-native'); +const InteractionManager = require('./InteractionManager'); /** * A simple class for batching up invocations of a low-pri callback. A timeout is set to run the diff --git a/packages/virtualized-lists/Interaction/__tests__/Batchinator-test.js b/Libraries/Interaction/__tests__/Batchinator-test.js similarity index 95% rename from packages/virtualized-lists/Interaction/__tests__/Batchinator-test.js rename to Libraries/Interaction/__tests__/Batchinator-test.js index b680e98c507d00..e8261b3515e23f 100644 --- a/packages/virtualized-lists/Interaction/__tests__/Batchinator-test.js +++ b/Libraries/Interaction/__tests__/Batchinator-test.js @@ -10,6 +10,10 @@ 'use strict'; +jest + .mock('../../vendor/core/ErrorUtils') + .mock('../../BatchedBridge/BatchedBridge'); + function expectToBeCalledOnce(fn) { expect(fn.mock.calls.length).toBe(1); } diff --git a/packages/virtualized-lists/Lists/CellRenderMask.js b/Libraries/Lists/CellRenderMask.js similarity index 100% rename from packages/virtualized-lists/Lists/CellRenderMask.js rename to Libraries/Lists/CellRenderMask.js diff --git a/packages/virtualized-lists/Lists/ChildListCollection.js b/Libraries/Lists/ChildListCollection.js similarity index 100% rename from packages/virtualized-lists/Lists/ChildListCollection.js rename to Libraries/Lists/ChildListCollection.js diff --git a/Libraries/Lists/FillRateHelper.js b/Libraries/Lists/FillRateHelper.js index 141fe98eb067bb..87482e73f6be3e 100644 --- a/Libraries/Lists/FillRateHelper.js +++ b/Libraries/Lists/FillRateHelper.js @@ -10,10 +10,244 @@ 'use strict'; -import {typeof FillRateHelper as FillRateHelperType} from '@react-native/virtualized-lists'; +import type {FrameMetricProps} from './VirtualizedListProps'; -const FillRateHelper: FillRateHelperType = - require('@react-native/virtualized-lists').FillRateHelper; +export type FillRateInfo = Info; + +class Info { + any_blank_count: number = 0; + any_blank_ms: number = 0; + any_blank_speed_sum: number = 0; + mostly_blank_count: number = 0; + mostly_blank_ms: number = 0; + pixels_blank: number = 0; + pixels_sampled: number = 0; + pixels_scrolled: number = 0; + total_time_spent: number = 0; + sample_count: number = 0; +} + +type FrameMetrics = { + inLayout?: boolean, + length: number, + offset: number, + ... +}; + +const DEBUG = false; + +let _listeners: Array<(Info) => void> = []; +let _minSampleCount = 10; +let _sampleRate = DEBUG ? 1 : null; + +/** + * A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded. + * By default the sampling rate is set to zero and this will do nothing. If you want to collect + * samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`. + * + * Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with + * `SceneTracker.getActiveScene` to determine the context of the events. + */ +class FillRateHelper { + _anyBlankStartTime: ?number = null; + _enabled = false; + _getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics; + _info: Info = new Info(); + _mostlyBlankStartTime: ?number = null; + _samplesStartTime: ?number = null; + + static addListener(callback: FillRateInfo => void): { + remove: () => void, + ... + } { + if (_sampleRate === null) { + console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.'); + } + _listeners.push(callback); + return { + remove: () => { + _listeners = _listeners.filter(listener => callback !== listener); + }, + }; + } + + static setSampleRate(sampleRate: number) { + _sampleRate = sampleRate; + } + + static setMinSampleCount(minSampleCount: number) { + _minSampleCount = minSampleCount; + } + + constructor( + getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics, + ) { + this._getFrameMetrics = getFrameMetrics; + this._enabled = (_sampleRate || 0) > Math.random(); + this._resetData(); + } + + activate() { + if (this._enabled && this._samplesStartTime == null) { + DEBUG && console.debug('FillRateHelper: activate'); + this._samplesStartTime = global.performance.now(); + } + } + + deactivateAndFlush() { + if (!this._enabled) { + return; + } + const start = this._samplesStartTime; // const for flow + if (start == null) { + DEBUG && + console.debug('FillRateHelper: bail on deactivate with no start time'); + return; + } + if (this._info.sample_count < _minSampleCount) { + // Don't bother with under-sampled events. + this._resetData(); + return; + } + const total_time_spent = global.performance.now() - start; + const info: any = { + ...this._info, + total_time_spent, + }; + if (DEBUG) { + const derived = { + avg_blankness: this._info.pixels_blank / this._info.pixels_sampled, + avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000), + avg_speed_when_any_blank: + this._info.any_blank_speed_sum / this._info.any_blank_count, + any_blank_per_min: + this._info.any_blank_count / (total_time_spent / 1000 / 60), + any_blank_time_frac: this._info.any_blank_ms / total_time_spent, + mostly_blank_per_min: + this._info.mostly_blank_count / (total_time_spent / 1000 / 60), + mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent, + }; + for (const key in derived) { + // $FlowFixMe[prop-missing] + derived[key] = Math.round(1000 * derived[key]) / 1000; + } + console.debug('FillRateHelper deactivateAndFlush: ', {derived, info}); + } + _listeners.forEach(listener => listener(info)); + this._resetData(); + } + + computeBlankness( + props: { + ...FrameMetricProps, + initialNumToRender?: ?number, + ... + }, + cellsAroundViewport: { + first: number, + last: number, + ... + }, + scrollMetrics: { + dOffset: number, + offset: number, + velocity: number, + visibleLength: number, + ... + }, + ): number { + if ( + !this._enabled || + props.getItemCount(props.data) === 0 || + cellsAroundViewport.last < cellsAroundViewport.first || + this._samplesStartTime == null + ) { + return 0; + } + const {dOffset, offset, velocity, visibleLength} = scrollMetrics; + + // Denominator metrics that we track for all events - most of the time there is no blankness and + // we want to capture that. + this._info.sample_count++; + this._info.pixels_sampled += Math.round(visibleLength); + this._info.pixels_scrolled += Math.round(Math.abs(dOffset)); + const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec + + // Whether blank now or not, record the elapsed time blank if we were blank last time. + const now = global.performance.now(); + if (this._anyBlankStartTime != null) { + this._info.any_blank_ms += now - this._anyBlankStartTime; + } + this._anyBlankStartTime = null; + if (this._mostlyBlankStartTime != null) { + this._info.mostly_blank_ms += now - this._mostlyBlankStartTime; + } + this._mostlyBlankStartTime = null; + + let blankTop = 0; + let first = cellsAroundViewport.first; + let firstFrame = this._getFrameMetrics(first, props); + while ( + first <= cellsAroundViewport.last && + (!firstFrame || !firstFrame.inLayout) + ) { + firstFrame = this._getFrameMetrics(first, props); + first++; + } + // Only count blankTop if we aren't rendering the first item, otherwise we will count the header + // as blank. + if (firstFrame && first > 0) { + blankTop = Math.min( + visibleLength, + Math.max(0, firstFrame.offset - offset), + ); + } + let blankBottom = 0; + let last = cellsAroundViewport.last; + let lastFrame = this._getFrameMetrics(last, props); + while ( + last >= cellsAroundViewport.first && + (!lastFrame || !lastFrame.inLayout) + ) { + lastFrame = this._getFrameMetrics(last, props); + last--; + } + // Only count blankBottom if we aren't rendering the last item, otherwise we will count the + // footer as blank. + if (lastFrame && last < props.getItemCount(props.data) - 1) { + const bottomEdge = lastFrame.offset + lastFrame.length; + blankBottom = Math.min( + visibleLength, + Math.max(0, offset + visibleLength - bottomEdge), + ); + } + const pixels_blank = Math.round(blankTop + blankBottom); + const blankness = pixels_blank / visibleLength; + if (blankness > 0) { + this._anyBlankStartTime = now; + this._info.any_blank_speed_sum += scrollSpeed; + this._info.any_blank_count++; + this._info.pixels_blank += pixels_blank; + if (blankness > 0.5) { + this._mostlyBlankStartTime = now; + this._info.mostly_blank_count++; + } + } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) { + this.deactivateAndFlush(); + } + return blankness; + } + + enabled(): boolean { + return this._enabled; + } + + _resetData() { + this._anyBlankStartTime = null; + this._info = new Info(); + this._mostlyBlankStartTime = null; + this._samplesStartTime = null; + } +} -export type {FillRateInfo} from '@react-native/virtualized-lists'; module.exports = FillRateHelper; diff --git a/Libraries/Lists/FlatList.d.ts b/Libraries/Lists/FlatList.d.ts index 6ac7f57fdcd843..344d5671359c45 100644 --- a/Libraries/Lists/FlatList.d.ts +++ b/Libraries/Lists/FlatList.d.ts @@ -12,7 +12,7 @@ import type { ListRenderItem, ViewToken, VirtualizedListProps, -} from '@react-native/virtualized-lists'; +} from './VirtualizedList'; import type {ScrollViewComponent} from '../Components/ScrollView/ScrollView'; import {StyleProp} from '../StyleSheet/StyleSheet'; import {ViewStyle} from '../StyleSheet/StyleSheetTypes'; diff --git a/Libraries/Lists/FlatList.js b/Libraries/Lists/FlatList.js index 40372bc70dd311..56748eaf75240d 100644 --- a/Libraries/Lists/FlatList.js +++ b/Libraries/Lists/FlatList.js @@ -11,17 +11,14 @@ import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; import type { - RenderItemProps, - RenderItemType, ViewabilityConfigCallbackPair, ViewToken, -} from '@react-native/virtualized-lists'; +} from './ViewabilityHelper'; +import type {RenderItemProps, RenderItemType} from './VirtualizedList'; import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import { - VirtualizedList, - keyExtractor as defaultKeyExtractor, -} from '@react-native/virtualized-lists'; +import VirtualizedList from './VirtualizedList'; +import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; import memoizeOne from 'memoize-one'; const View = require('../Components/View/View'); diff --git a/Libraries/Lists/FlatList.js.flow b/Libraries/Lists/FlatList.js.flow index 304a1006182186..10a7c9073257bd 100644 --- a/Libraries/Lists/FlatList.js.flow +++ b/Libraries/Lists/FlatList.js.flow @@ -14,13 +14,8 @@ const View = require('../Components/View/View'); import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; -import type { - RenderItemType, - RenderItemProps, - ViewToken, - ViewabilityConfigCallbackPair, -} from '@react-native/virtualized-lists'; -import {typeof VirtualizedList} from '@react-native/virtualized-lists'; +import type {RenderItemType} from './VirtualizedList'; +import typeof VirtualizedList from './VirtualizedList'; type RequiredProps = {| /** diff --git a/Libraries/Lists/SectionList.d.ts b/Libraries/Lists/SectionList.d.ts index 7ff5bbb0a3cbe1..ae1b10df46a10d 100644 --- a/Libraries/Lists/SectionList.d.ts +++ b/Libraries/Lists/SectionList.d.ts @@ -11,7 +11,7 @@ import type * as React from 'react'; import type { ListRenderItemInfo, VirtualizedListWithoutRenderItemProps, -} from '@react-native/virtualized-lists'; +} from './VirtualizedList'; import type { ScrollView, ScrollViewProps, diff --git a/Libraries/Lists/SectionList.js b/Libraries/Lists/SectionList.js index 0f199487b92a23..d452ee2b7f6419 100644 --- a/Libraries/Lists/SectionList.js +++ b/Libraries/Lists/SectionList.js @@ -12,13 +12,13 @@ import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type { + Props as VirtualizedSectionListProps, ScrollToLocationParamsType, SectionBase as _SectionBase, - VirtualizedSectionListProps, -} from '@react-native/virtualized-lists'; +} from './VirtualizedSectionList'; import Platform from '../Utilities/Platform'; -import {VirtualizedSectionList} from '@react-native/virtualized-lists'; +import VirtualizedSectionList from './VirtualizedSectionList'; import * as React from 'react'; type Item = any; diff --git a/Libraries/Lists/SectionListModern.js b/Libraries/Lists/SectionListModern.js index d9676f106f8cfc..c7856e13d9a666 100644 --- a/Libraries/Lists/SectionListModern.js +++ b/Libraries/Lists/SectionListModern.js @@ -12,14 +12,14 @@ import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type { + Props as VirtualizedSectionListProps, ScrollToLocationParamsType, SectionBase as _SectionBase, - VirtualizedSectionListProps, -} from '@react-native/virtualized-lists'; +} from './VirtualizedSectionList'; import type {AbstractComponent, Element, ElementRef} from 'react'; import Platform from '../Utilities/Platform'; -import {VirtualizedSectionList} from '@react-native/virtualized-lists'; +import VirtualizedSectionList from './VirtualizedSectionList'; import React, {forwardRef, useImperativeHandle, useRef} from 'react'; type Item = any; diff --git a/packages/virtualized-lists/Lists/StateSafePureComponent.js b/Libraries/Lists/StateSafePureComponent.js similarity index 100% rename from packages/virtualized-lists/Lists/StateSafePureComponent.js rename to Libraries/Lists/StateSafePureComponent.js diff --git a/Libraries/Lists/ViewabilityHelper.js b/Libraries/Lists/ViewabilityHelper.js index c7dedfdf496b93..33a9811825affd 100644 --- a/Libraries/Lists/ViewabilityHelper.js +++ b/Libraries/Lists/ViewabilityHelper.js @@ -10,15 +10,351 @@ 'use strict'; -export type { - ViewToken, - ViewabilityConfig, - ViewabilityConfigCallbackPair, -} from '@react-native/virtualized-lists'; +import type {FrameMetricProps} from './VirtualizedListProps'; -import {typeof ViewabilityHelper as ViewabilityHelperType} from '@react-native/virtualized-lists'; +const invariant = require('invariant'); -const ViewabilityHelper: ViewabilityHelperType = - require('@react-native/virtualized-lists').ViewabilityHelper; +export type ViewToken = { + item: any, + key: string, + index: ?number, + isViewable: boolean, + section?: any, + ... +}; + +export type ViewabilityConfigCallbackPair = { + viewabilityConfig: ViewabilityConfig, + onViewableItemsChanged: (info: { + viewableItems: Array, + changed: Array, + ... + }) => void, + ... +}; + +export type ViewabilityConfig = {| + /** + * Minimum amount of time (in milliseconds) that an item must be physically viewable before the + * viewability callback will be fired. A high number means that scrolling through content without + * stopping will not mark the content as viewable. + */ + minimumViewTime?: number, + + /** + * Percent of viewport that must be covered for a partially occluded item to count as + * "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means + * that a single pixel in the viewport makes the item viewable, and a value of 100 means that + * an item must be either entirely visible or cover the entire viewport to count as viewable. + */ + viewAreaCoveragePercentThreshold?: number, + + /** + * Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible, + * rather than the fraction of the viewable area it covers. + */ + itemVisiblePercentThreshold?: number, + + /** + * Nothing is considered viewable until the user scrolls or `recordInteraction` is called after + * render. + */ + waitForInteraction?: boolean, +|}; + +/** + * A Utility class for calculating viewable items based on current metrics like scroll position and + * layout. + * + * An item is said to be in a "viewable" state when any of the following + * is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction` + * is true): + * + * - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item + * visible in the view area >= `itemVisiblePercentThreshold`. + * - Entirely visible on screen + */ +class ViewabilityHelper { + _config: ViewabilityConfig; + _hasInteracted: boolean = false; + _timers: Set = new Set(); + _viewableIndices: Array = []; + _viewableItems: Map = new Map(); + + constructor( + config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0}, + ) { + this._config = config; + } + + /** + * Cleanup, e.g. on unmount. Clears any pending timers. + */ + dispose() { + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To see + * the error delete this comment and run Flow. */ + this._timers.forEach(clearTimeout); + } + + /** + * Determines which items are viewable based on the current metrics and config. + */ + computeViewableItems( + props: FrameMetricProps, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: ( + index: number, + props: FrameMetricProps, + ) => ?{ + length: number, + offset: number, + ... + }, + // Optional optimization to reduce the scan size + renderRange?: { + first: number, + last: number, + ... + }, + ): Array { + const itemCount = props.getItemCount(props.data); + const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} = + this._config; + const viewAreaMode = viewAreaCoveragePercentThreshold != null; + const viewablePercentThreshold = viewAreaMode + ? viewAreaCoveragePercentThreshold + : itemVisiblePercentThreshold; + invariant( + viewablePercentThreshold != null && + (itemVisiblePercentThreshold != null) !== + (viewAreaCoveragePercentThreshold != null), + 'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold', + ); + const viewableIndices = []; + if (itemCount === 0) { + return viewableIndices; + } + let firstVisible = -1; + const {first, last} = renderRange || {first: 0, last: itemCount - 1}; + if (last >= itemCount) { + console.warn( + 'Invalid render range computing viewability ' + + JSON.stringify({renderRange, itemCount}), + ); + return []; + } + for (let idx = first; idx <= last; idx++) { + const metrics = getFrameMetrics(idx, props); + if (!metrics) { + continue; + } + const top = metrics.offset - scrollOffset; + const bottom = top + metrics.length; + if (top < viewportHeight && bottom > 0) { + firstVisible = idx; + if ( + _isViewable( + viewAreaMode, + viewablePercentThreshold, + top, + bottom, + viewportHeight, + metrics.length, + ) + ) { + viewableIndices.push(idx); + } + } else if (firstVisible >= 0) { + break; + } + } + return viewableIndices; + } + + /** + * Figures out which items are viewable and how that has changed from before and calls + * `onViewableItemsChanged` as appropriate. + */ + onUpdate( + props: FrameMetricProps, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: ( + index: number, + props: FrameMetricProps, + ) => ?{ + length: number, + offset: number, + ... + }, + createViewToken: ( + index: number, + isViewable: boolean, + props: FrameMetricProps, + ) => ViewToken, + onViewableItemsChanged: ({ + viewableItems: Array, + changed: Array, + ... + }) => void, + // Optional optimization to reduce the scan size + renderRange?: { + first: number, + last: number, + ... + }, + ): void { + const itemCount = props.getItemCount(props.data); + if ( + (this._config.waitForInteraction && !this._hasInteracted) || + itemCount === 0 || + !getFrameMetrics(0, props) + ) { + return; + } + let viewableIndices: Array = []; + if (itemCount) { + viewableIndices = this.computeViewableItems( + props, + scrollOffset, + viewportHeight, + getFrameMetrics, + renderRange, + ); + } + if ( + this._viewableIndices.length === viewableIndices.length && + this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) + ) { + // We might get a lot of scroll events where visibility doesn't change and we don't want to do + // extra work in those cases. + return; + } + this._viewableIndices = viewableIndices; + if (this._config.minimumViewTime) { + const handle: TimeoutID = setTimeout(() => { + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To + * see the error delete this comment and run Flow. */ + this._timers.delete(handle); + this._onUpdateSync( + props, + viewableIndices, + onViewableItemsChanged, + createViewToken, + ); + }, this._config.minimumViewTime); + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To see + * the error delete this comment and run Flow. */ + this._timers.add(handle); + } else { + this._onUpdateSync( + props, + viewableIndices, + onViewableItemsChanged, + createViewToken, + ); + } + } + + /** + * clean-up cached _viewableIndices to evaluate changed items on next update + */ + resetViewableIndices() { + this._viewableIndices = []; + } + + /** + * Records that an interaction has happened even if there has been no scroll. + */ + recordInteraction() { + this._hasInteracted = true; + } + + _onUpdateSync( + props: FrameMetricProps, + viewableIndicesToCheck: Array, + onViewableItemsChanged: ({ + changed: Array, + viewableItems: Array, + ... + }) => void, + createViewToken: ( + index: number, + isViewable: boolean, + props: FrameMetricProps, + ) => ViewToken, + ) { + // Filter out indices that have gone out of view since this call was scheduled. + viewableIndicesToCheck = viewableIndicesToCheck.filter(ii => + this._viewableIndices.includes(ii), + ); + const prevItems = this._viewableItems; + const nextItems = new Map( + viewableIndicesToCheck.map(ii => { + const viewable = createViewToken(ii, true, props); + return [viewable.key, viewable]; + }), + ); + + const changed = []; + for (const [key, viewable] of nextItems) { + if (!prevItems.has(key)) { + changed.push(viewable); + } + } + for (const [key, viewable] of prevItems) { + if (!nextItems.has(key)) { + changed.push({...viewable, isViewable: false}); + } + } + if (changed.length > 0) { + this._viewableItems = nextItems; + onViewableItemsChanged({ + viewableItems: Array.from(nextItems.values()), + changed, + viewabilityConfig: this._config, + }); + } + } +} + +function _isViewable( + viewAreaMode: boolean, + viewablePercentThreshold: number, + top: number, + bottom: number, + viewportHeight: number, + itemLength: number, +): boolean { + if (_isEntirelyVisible(top, bottom, viewportHeight)) { + return true; + } else { + const pixels = _getPixelsVisible(top, bottom, viewportHeight); + const percent = + 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength); + return percent >= viewablePercentThreshold; + } +} + +function _getPixelsVisible( + top: number, + bottom: number, + viewportHeight: number, +): number { + const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); + return Math.max(0, visibleHeight); +} + +function _isEntirelyVisible( + top: number, + bottom: number, + viewportHeight: number, +): boolean { + return top >= 0 && bottom <= viewportHeight && bottom > top; +} module.exports = ViewabilityHelper; diff --git a/Libraries/Lists/VirtualizeUtils.js b/Libraries/Lists/VirtualizeUtils.js index 535d25b3abc538..3a70d9f683091e 100644 --- a/Libraries/Lists/VirtualizeUtils.js +++ b/Libraries/Lists/VirtualizeUtils.js @@ -10,9 +10,249 @@ 'use strict'; -import {typeof keyExtractor as KeyExtractorType} from '@react-native/virtualized-lists'; +import type {FrameMetricProps} from './VirtualizedListProps'; -const keyExtractor: KeyExtractorType = - require('@react-native/virtualized-lists').keyExtractor; +/** + * Used to find the indices of the frames that overlap the given offsets. Useful for finding the + * items that bound different windows of content, such as the visible area or the buffered overscan + * area. + */ +export function elementsThatOverlapOffsets( + offsets: Array, + props: FrameMetricProps, + getFrameMetrics: ( + index: number, + props: FrameMetricProps, + ) => { + length: number, + offset: number, + ... + }, + zoomScale: number = 1, +): Array { + const itemCount = props.getItemCount(props.data); + const result = []; + for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) { + const currentOffset = offsets[offsetIndex]; + let left = 0; + let right = itemCount - 1; + + while (left <= right) { + // eslint-disable-next-line no-bitwise + const mid = left + ((right - left) >>> 1); + const frame = getFrameMetrics(mid, props); + const scaledOffsetStart = frame.offset * zoomScale; + const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale; + + // We want the first frame that contains the offset, with inclusive bounds. Thus, for the + // first frame the scaledOffsetStart is inclusive, while for other frames it is exclusive. + if ( + (mid === 0 && currentOffset < scaledOffsetStart) || + (mid !== 0 && currentOffset <= scaledOffsetStart) + ) { + right = mid - 1; + } else if (currentOffset > scaledOffsetEnd) { + left = mid + 1; + } else { + result[offsetIndex] = mid; + break; + } + } + } + + return result; +} + +/** + * Computes the number of elements in the `next` range that are new compared to the `prev` range. + * Handy for calculating how many new items will be rendered when the render window changes so we + * can restrict the number of new items render at once so that content can appear on the screen + * faster. + */ +export function newRangeCount( + prev: { + first: number, + last: number, + ... + }, + next: { + first: number, + last: number, + ... + }, +): number { + return ( + next.last - + next.first + + 1 - + Math.max( + 0, + 1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first), + ) + ); +} + +/** + * Custom logic for determining which items should be rendered given the current frame and scroll + * metrics, as well as the previous render state. The algorithm may evolve over time, but generally + * prioritizes the visible area first, then expands that with overscan regions ahead and behind, + * biased in the direction of scroll. + */ +export function computeWindowedRenderLimits( + props: FrameMetricProps, + maxToRenderPerBatch: number, + windowSize: number, + prev: { + first: number, + last: number, + }, + getFrameMetricsApprox: ( + index: number, + props: FrameMetricProps, + ) => { + length: number, + offset: number, + ... + }, + scrollMetrics: { + dt: number, + offset: number, + velocity: number, + visibleLength: number, + zoomScale: number, + ... + }, +): { + first: number, + last: number, +} { + const itemCount = props.getItemCount(props.data); + if (itemCount === 0) { + return {first: 0, last: -1}; + } + const {offset, velocity, visibleLength, zoomScale = 1} = scrollMetrics; + + // Start with visible area, then compute maximum overscan region by expanding from there, biased + // in the direction of scroll. Total overscan area is capped, which should cap memory consumption + // too. + const visibleBegin = Math.max(0, offset); + const visibleEnd = visibleBegin + visibleLength; + const overscanLength = (windowSize - 1) * visibleLength; + + // Considering velocity seems to introduce more churn than it's worth. + const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5)); + + const fillPreference = + velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none'; + + const overscanBegin = Math.max( + 0, + visibleBegin - (1 - leadFactor) * overscanLength, + ); + const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength); + + const lastItemOffset = + getFrameMetricsApprox(itemCount - 1, props).offset * zoomScale; + if (lastItemOffset < overscanBegin) { + // Entire list is before our overscan window + return { + first: Math.max(0, itemCount - 1 - maxToRenderPerBatch), + last: itemCount - 1, + }; + } + + // Find the indices that correspond to the items at the render boundaries we're targeting. + let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets( + [overscanBegin, visibleBegin, visibleEnd, overscanEnd], + props, + getFrameMetricsApprox, + zoomScale, + ); + overscanFirst = overscanFirst == null ? 0 : overscanFirst; + first = first == null ? Math.max(0, overscanFirst) : first; + overscanLast = overscanLast == null ? itemCount - 1 : overscanLast; + last = + last == null + ? Math.min(overscanLast, first + maxToRenderPerBatch - 1) + : last; + const visible = {first, last}; + + // We want to limit the number of new cells we're rendering per batch so that we can fill the + // content on the screen quickly. If we rendered the entire overscan window at once, the user + // could be staring at white space for a long time waiting for a bunch of offscreen content to + // render. + let newCellCount = newRangeCount(prev, visible); + + while (true) { + if (first <= overscanFirst && last >= overscanLast) { + // If we fill the entire overscan range, we're done. + break; + } + const maxNewCells = newCellCount >= maxToRenderPerBatch; + const firstWillAddMore = first <= prev.first || first > prev.last; + const firstShouldIncrement = + first > overscanFirst && (!maxNewCells || !firstWillAddMore); + const lastWillAddMore = last >= prev.last || last < prev.first; + const lastShouldIncrement = + last < overscanLast && (!maxNewCells || !lastWillAddMore); + if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) { + // We only want to stop if we've hit maxNewCells AND we cannot increment first or last + // without rendering new items. This let's us preserve as many already rendered items as + // possible, reducing render churn and keeping the rendered overscan range as large as + // possible. + break; + } + if ( + firstShouldIncrement && + !(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore) + ) { + if (firstWillAddMore) { + newCellCount++; + } + first--; + } + if ( + lastShouldIncrement && + !(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore) + ) { + if (lastWillAddMore) { + newCellCount++; + } + last++; + } + } + if ( + !( + last >= first && + first >= 0 && + last < itemCount && + first >= overscanFirst && + last <= overscanLast && + first <= visible.first && + last >= visible.last + ) + ) { + throw new Error( + 'Bad window calculation ' + + JSON.stringify({ + first, + last, + itemCount, + overscanFirst, + overscanLast, + visible, + }), + ); + } + return {first, last}; +} -module.exports = {keyExtractor}; +export function keyExtractor(item: any, index: number): string { + if (typeof item === 'object' && item?.key != null) { + return item.key; + } + if (typeof item === 'object' && item?.id != null) { + return item.id; + } + return String(index); +} diff --git a/packages/virtualized-lists/Lists/VirtualizedList.d.ts b/Libraries/Lists/VirtualizedList.d.ts similarity index 97% rename from packages/virtualized-lists/Lists/VirtualizedList.d.ts rename to Libraries/Lists/VirtualizedList.d.ts index a2702d9101e107..d874dab4d10271 100644 --- a/packages/virtualized-lists/Lists/VirtualizedList.d.ts +++ b/Libraries/Lists/VirtualizedList.d.ts @@ -8,15 +8,15 @@ */ import type * as React from 'react'; +import type {LayoutChangeEvent} from '../../types'; +import {StyleProp} from '../StyleSheet/StyleSheet'; +import {ViewStyle} from '../StyleSheet/StyleSheetTypes'; import type { - StyleProp, - ViewStyle, - ScrollViewProps, - LayoutChangeEvent, - View, ScrollResponderMixin, ScrollView, -} from 'react-native'; + ScrollViewProps, +} from '../Components/ScrollView/ScrollView'; +import type {View} from '../Components/View/View'; export interface ViewToken { item: any; diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index 2488b1e5e37f57..efce2bed49bfcd 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -8,16 +8,1945 @@ * @format */ -'use strict'; - -import {typeof VirtualizedList as VirtualizedListType} from '@react-native/virtualized-lists'; - -const VirtualizedList: VirtualizedListType = - require('@react-native/virtualized-lists').VirtualizedList; - -export type { +import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; +import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; +import type {LayoutEvent, ScrollEvent} from '../Types/CoreEventTypes'; +import type {ViewToken} from './ViewabilityHelper'; +import type { + FrameMetricProps, + Item, + Props, RenderItemProps, RenderItemType, Separators, -} from '@react-native/virtualized-lists'; -module.exports = VirtualizedList; +} from './VirtualizedListProps'; + +import RefreshControl from '../Components/RefreshControl/RefreshControl'; +import ScrollView from '../Components/ScrollView/ScrollView'; +import View from '../Components/View/View'; +import Batchinator from '../Interaction/Batchinator'; +import {findNodeHandle} from '../ReactNative/RendererProxy'; +import flattenStyle from '../StyleSheet/flattenStyle'; +import StyleSheet from '../StyleSheet/StyleSheet'; +import clamp from '../Utilities/clamp'; +import infoLog from '../Utilities/infoLog'; +import {CellRenderMask} from './CellRenderMask'; +import ChildListCollection from './ChildListCollection'; +import FillRateHelper from './FillRateHelper'; +import StateSafePureComponent from './StateSafePureComponent'; +import ViewabilityHelper from './ViewabilityHelper'; +import CellRenderer from './VirtualizedListCellRenderer'; +import { + VirtualizedListCellContextProvider, + VirtualizedListContext, + VirtualizedListContextProvider, +} from './VirtualizedListContext.js'; +import { + computeWindowedRenderLimits, + keyExtractor as defaultKeyExtractor, +} from './VirtualizeUtils'; +import invariant from 'invariant'; +import * as React from 'react'; + +export type {RenderItemProps, RenderItemType, Separators}; + +const ON_EDGE_REACHED_EPSILON = 0.001; + +let _usedIndexForKey = false; +let _keylessItemComponentName: string = ''; + +type ViewabilityHelperCallbackTuple = { + viewabilityHelper: ViewabilityHelper, + onViewableItemsChanged: (info: { + viewableItems: Array, + changed: Array, + ... + }) => void, + ... +}; + +type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, +}; + +/** + * Default Props Helper Functions + * Use the following helper functions for default values + */ + +// horizontalOrDefault(this.props.horizontal) +function horizontalOrDefault(horizontal: ?boolean) { + return horizontal ?? false; +} + +// initialNumToRenderOrDefault(this.props.initialNumToRenderOrDefault) +function initialNumToRenderOrDefault(initialNumToRender: ?number) { + return initialNumToRender ?? 10; +} + +// maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch) +function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) { + return maxToRenderPerBatch ?? 10; +} + +// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold) +function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) { + return onStartReachedThreshold ?? 2; +} + +// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold) +function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) { + return onEndReachedThreshold ?? 2; +} + +// getScrollingThreshold(visibleLength, onEndReachedThreshold) +function getScrollingThreshold(threshold: number, visibleLength: number) { + return (threshold * visibleLength) / 2; +} + +// scrollEventThrottleOrDefault(this.props.scrollEventThrottle) +function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) { + return scrollEventThrottle ?? 50; +} + +// windowSizeOrDefault(this.props.windowSize) +function windowSizeOrDefault(windowSize: ?number) { + return windowSize ?? 21; +} + +function findLastWhere( + arr: $ReadOnlyArray, + predicate: (element: T) => boolean, +): T | null { + for (let i = arr.length - 1; i >= 0; i--) { + if (predicate(arr[i])) { + return arr[i]; + } + } + + return null; +} + +/** + * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) + * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better + * documented. In general, this should only really be used if you need more flexibility than + * `FlatList` provides, e.g. for use with immutable data instead of plain arrays. + * + * Virtualization massively improves memory consumption and performance of large lists by + * maintaining a finite render window of active items and replacing all items outside of the render + * window with appropriately sized blank space. The window adapts to scrolling behavior, and items + * are rendered incrementally with low-pri (after any running interactions) if they are far from the + * visible area, or with hi-pri otherwise to minimize the potential of seeing blank space. + * + * Some caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * - As an effort to remove defaultProps, use helper functions when referencing certain props + * + */ +export default class VirtualizedList extends StateSafePureComponent< + Props, + State, +> { + static contextType: typeof VirtualizedListContext = VirtualizedListContext; + + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { + const animated = params ? params.animated : true; + const veryLast = this.props.getItemCount(this.props.data) - 1; + if (veryLast < 0) { + return; + } + const frame = this.__getFrameMetricsApprox(veryLast, this.props); + const offset = Math.max( + 0, + frame.offset + + frame.length + + this._footerLength - + this._scrollMetrics.visibleLength, + ); + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + + this._scrollRef.scrollTo( + horizontalOrDefault(this.props.horizontal) + ? {x: offset, animated} + : {y: offset, animated}, + ); + } + + // scrollToIndex may be janky without getItemLayout prop + scrollToIndex(params: { + animated?: ?boolean, + index: number, + viewOffset?: number, + viewPosition?: number, + ... + }): $FlowFixMe { + const { + data, + horizontal, + getItemCount, + getItemLayout, + onScrollToIndexFailed, + } = this.props; + const {animated, index, viewOffset, viewPosition} = params; + invariant( + index >= 0, + `scrollToIndex out of range: requested index ${index} but minimum is 0`, + ); + invariant( + getItemCount(data) >= 1, + `scrollToIndex out of range: item length ${getItemCount( + data, + )} but minimum is 1`, + ); + invariant( + index < getItemCount(data), + `scrollToIndex out of range: requested index ${index} is out of 0 to ${ + getItemCount(data) - 1 + }`, + ); + if (!getItemLayout && index > this._highestMeasuredFrameIndex) { + invariant( + !!onScrollToIndexFailed, + 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' + + 'otherwise there is no way to know the location of offscreen indices or handle failures.', + ); + onScrollToIndexFailed({ + averageItemLength: this._averageCellLength, + highestMeasuredFrameIndex: this._highestMeasuredFrameIndex, + index, + }); + return; + } + const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props); + const offset = + Math.max( + 0, + this._getOffsetApprox(index, this.props) - + (viewPosition || 0) * + (this._scrollMetrics.visibleLength - frame.length), + ) - (viewOffset || 0); + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + + this._scrollRef.scrollTo( + horizontal ? {x: offset, animated} : {y: offset, animated}, + ); + } + + // scrollToItem may be janky without getItemLayout prop. Required linear scan through items - + // use scrollToIndex instead if possible. + scrollToItem(params: { + animated?: ?boolean, + item: Item, + viewOffset?: number, + viewPosition?: number, + ... + }) { + const {item} = params; + const {data, getItem, getItemCount} = this.props; + const itemCount = getItemCount(data); + for (let index = 0; index < itemCount; index++) { + if (getItem(data, index) === item) { + this.scrollToIndex({...params, index}); + break; + } + } + } + + /** + * Scroll to a specific content pixel offset in the list. + * + * Param `offset` expects the offset to scroll to. + * In case of `horizontal` is true, the offset is the x-value, + * in any other case the offset is the y-value. + * + * Param `animated` (`true` by default) defines whether the list + * should do an animation while scrolling. + */ + scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { + const {animated, offset} = params; + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + + this._scrollRef.scrollTo( + horizontalOrDefault(this.props.horizontal) + ? {x: offset, animated} + : {y: offset, animated}, + ); + } + + recordInteraction() { + this._nestedChildLists.forEach(childList => { + childList.recordInteraction(); + }); + this._viewabilityTuples.forEach(t => { + t.viewabilityHelper.recordInteraction(); + }); + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + } + + flashScrollIndicators() { + if (this._scrollRef == null) { + return; + } + + this._scrollRef.flashScrollIndicators(); + } + + /** + * Provides a handle to the underlying scroll responder. + * Note that `this._scrollRef` might not be a `ScrollView`, so we + * need to check that it responds to `getScrollResponder` before calling it. + */ + getScrollResponder(): ?ScrollResponderType { + if (this._scrollRef && this._scrollRef.getScrollResponder) { + return this._scrollRef.getScrollResponder(); + } + } + + getScrollableNode(): ?number { + if (this._scrollRef && this._scrollRef.getScrollableNode) { + return this._scrollRef.getScrollableNode(); + } else { + return findNodeHandle(this._scrollRef); + } + } + + getScrollRef(): + | ?React.ElementRef + | ?React.ElementRef { + if (this._scrollRef && this._scrollRef.getScrollRef) { + return this._scrollRef.getScrollRef(); + } else { + return this._scrollRef; + } + } + + setNativeProps(props: Object) { + if (this._scrollRef) { + this._scrollRef.setNativeProps(props); + } + } + + _getCellKey(): string { + return this.context?.cellKey || 'rootList'; + } + + // $FlowFixMe[missing-local-annot] + _getScrollMetrics = () => { + return this._scrollMetrics; + }; + + hasMore(): boolean { + return this._hasMore; + } + + // $FlowFixMe[missing-local-annot] + _getOutermostParentListRef = () => { + if (this._isNestedWithSameOrientation()) { + return this.context.getOutermostParentListRef(); + } else { + return this; + } + }; + + _registerAsNestedChild = (childList: { + cellKey: string, + ref: React.ElementRef, + }): void => { + this._nestedChildLists.add(childList.ref, childList.cellKey); + if (this._hasInteracted) { + childList.ref.recordInteraction(); + } + }; + + _unregisterAsNestedChild = (childList: { + ref: React.ElementRef, + }): void => { + this._nestedChildLists.remove(childList.ref); + }; + + state: State; + + constructor(props: Props) { + super(props); + invariant( + // $FlowFixMe[prop-missing] + !props.onScroll || !props.onScroll.__isNative, + 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + + 'to support native onScroll events with useNativeDriver', + ); + invariant( + windowSizeOrDefault(props.windowSize) > 0, + 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.', + ); + + invariant( + props.getItemCount, + 'VirtualizedList: The "getItemCount" prop must be provided', + ); + + this._fillRateHelper = new FillRateHelper(this._getFrameMetrics); + this._updateCellsToRenderBatcher = new Batchinator( + this._updateCellsToRender, + this.props.updateCellsBatchingPeriod ?? 50, + ); + + if (this.props.viewabilityConfigCallbackPairs) { + this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map( + pair => ({ + viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig), + onViewableItemsChanged: pair.onViewableItemsChanged, + }), + ); + } else { + const {onViewableItemsChanged, viewabilityConfig} = this.props; + if (onViewableItemsChanged) { + this._viewabilityTuples.push({ + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged, + }); + } + } + + invariant( + !this.context, + 'Unexpectedly saw VirtualizedListContext available in ctor', + ); + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); + + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), + }; + } + + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, + additionalRegions?: ?$ReadOnlyArray<{first: number, last: number}>, + ): CellRenderMask { + const itemCount = props.getItemCount(props.data); + + invariant( + cellsAroundViewport.first >= 0 && + cellsAroundViewport.last >= cellsAroundViewport.first - 1 && + cellsAroundViewport.last < itemCount, + `Invalid cells around viewport "[${cellsAroundViewport.first}, ${cellsAroundViewport.last}]" was passed to VirtualizedList._createRenderMask`, + ); + + const renderMask = new CellRenderMask(itemCount); + + if (itemCount > 0) { + const allRegions = [cellsAroundViewport, ...(additionalRegions ?? [])]; + for (const region of allRegions) { + renderMask.addCells(region); + } + + // The initially rendered cells are retained as part of the + // "scroll-to-top" optimization + if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) { + const initialRegion = VirtualizedList._initialRenderRegion(props); + renderMask.addCells(initialRegion); + } + + // The layout coordinates of sticker headers may be off-screen while the + // actual header is on-screen. Keep the most recent before the viewport + // rendered, even if its layout coordinates are not in viewport. + const stickyIndicesSet = new Set(props.stickyHeaderIndices); + VirtualizedList._ensureClosestStickyHeader( + props, + stickyIndicesSet, + renderMask, + cellsAroundViewport.first, + ); + } + + return renderMask; + } + + static _initialRenderRegion(props: Props): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); + const scrollIndex = Math.floor(Math.max(0, props.initialScrollIndex ?? 0)); + + return { + first: scrollIndex, + last: + Math.min( + itemCount, + scrollIndex + initialNumToRenderOrDefault(props.initialNumToRender), + ) - 1, + }; + } + + static _ensureClosestStickyHeader( + props: Props, + stickyIndicesSet: Set, + renderMask: CellRenderMask, + cellIdx: number, + ) { + const stickyOffset = props.ListHeaderComponent ? 1 : 0; + + for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) { + if (stickyIndicesSet.has(itemIdx + stickyOffset)) { + renderMask.addCells({first: itemIdx, last: itemIdx}); + break; + } + } + } + + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( + props.onEndReachedThreshold, + ); + this._updateViewableItems(props, cellsAroundViewport); + + const {contentLength, offset, visibleLength} = this._scrollMetrics; + const distanceFromEnd = contentLength - visibleLength - offset; + + // Wait until the scroll view metrics have been set up. And until then, + // we will trust the initialNumToRender suggestion + if (visibleLength <= 0 || contentLength <= 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; + } + + let newCellsAroundViewport: {first: number, last: number}; + if (props.disableVirtualization) { + const renderAhead = + distanceFromEnd < onEndReachedThreshold * visibleLength + ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) + : 0; + + newCellsAroundViewport = { + first: 0, + last: Math.min( + cellsAroundViewport.last + renderAhead, + getItemCount(data) - 1, + ), + }; + } else { + // If we have a non-zero initialScrollIndex and run this before we've scrolled, + // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. + // So let's wait until we've scrolled the view to the right place. And until then, + // we will trust the initialScrollIndex suggestion. + + // Thus, we want to recalculate the windowed render limits if any of the following hold: + // - initialScrollIndex is undefined or is 0 + // - initialScrollIndex > 0 AND scrolling is complete + // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case + // where the list is shorter than the visible area) + if ( + props.initialScrollIndex && + !this._scrollMetrics.offset && + Math.abs(distanceFromEnd) >= Number.EPSILON + ) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; + } + + newCellsAroundViewport = computeWindowedRenderLimits( + props, + maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), + windowSizeOrDefault(props.windowSize), + cellsAroundViewport, + this.__getFrameMetricsApprox, + this._scrollMetrics, + ); + invariant( + newCellsAroundViewport.last < getItemCount(data), + 'computeWindowedRenderLimits() should return range in-bounds', + ); + } + + if (this._nestedChildLists.size() > 0) { + // If some cell in the new state has a child list in it, we should only render + // up through that item, so that we give that list a chance to render. + // Otherwise there's churn from multiple child lists mounting and un-mounting + // their items. + + // Will this prevent rendering if the nested list doesn't realize the end? + const childIdx = this._findFirstChildWithMore( + newCellsAroundViewport.first, + newCellsAroundViewport.last, + ); + + newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last; + } + + return newCellsAroundViewport; + } + + _findFirstChildWithMore(first: number, last: number): number | null { + for (let ii = first; ii <= last; ii++) { + const cellKeyForIndex = this._indicesToKeys.get(ii); + if ( + cellKeyForIndex != null && + this._nestedChildLists.anyInCell(cellKeyForIndex, childList => + childList.hasMore(), + ) + ) { + return ii; + } + } + + return null; + } + + componentDidMount() { + if (this._isNestedWithSameOrientation()) { + this.context.registerAsNestedChild({ + ref: this, + cellKey: this.context.cellKey, + }); + } + } + + componentWillUnmount() { + if (this._isNestedWithSameOrientation()) { + this.context.unregisterAsNestedChild({ref: this}); + } + this._updateCellsToRenderBatcher.dispose({abort: true}); + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.dispose(); + }); + this._fillRateHelper.deactivateAndFlush(); + } + + static getDerivedStateFromProps(newProps: Props, prevState: State): State { + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + const itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } + + const constrainedCells = VirtualizedList._constrainToItemCount( + prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), + }; + } + + _pushCells( + cells: Array, + stickyHeaderIndices: Array, + stickyIndicesFromProps: Set, + first: number, + last: number, + inversionStyle: ViewStyleProp, + ) { + const { + CellRendererComponent, + ItemSeparatorComponent, + ListHeaderComponent, + ListItemComponent, + data, + debug, + getItem, + getItemCount, + getItemLayout, + horizontal, + renderItem, + } = this.props; + const stickyOffset = ListHeaderComponent ? 1 : 0; + const end = getItemCount(data) - 1; + let prevCellKey; + last = Math.min(end, last); + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); + const key = this._keyExtractor(item, ii, this.props); + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + stickyHeaderIndices.push(cells.length); + } + cells.push( + this._onCellFocusCapture(key)} + onUnmount={this._onCellUnmount} + ref={ref => { + this._cellRefs[key] = ref; + }} + renderItem={renderItem} + />, + ); + prevCellKey = key; + } + } + + static _constrainToItemCount( + cells: {first: number, last: number}, + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); + const last = Math.min(itemCount - 1, cells.last); + + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); + + return { + first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), + last, + }; + } + + _onUpdateSeparators = (keys: Array, newProps: Object) => { + keys.forEach(key => { + const ref = key != null && this._cellRefs[key]; + ref && ref.updateSeparatorProps(newProps); + }); + }; + + _isNestedWithSameOrientation(): boolean { + const nestedContext = this.context; + return !!( + nestedContext && + !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal) + ); + } + + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + + _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, + // $FlowFixMe[missing-local-annot] + ) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } + + const key = defaultKeyExtractor(item, index); + if (key === String(index)) { + _usedIndexForKey = true; + if (item.type && item.type.displayName) { + _keylessItemComponentName = item.type.displayName; + } + } + return key; + } + + render(): React.Node { + if (__DEV__) { + // $FlowFixMe[underconstrained-implicit-instantiation] + const flatStyles = flattenStyle(this.props.contentContainerStyle); + if (flatStyles != null && flatStyles.flexWrap === 'wrap') { + console.warn( + '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + + 'Consider using `numColumns` with `FlatList` instead.', + ); + } + } + const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = + this.props; + const {data, horizontal} = this.props; + const inversionStyle = this.props.inverted + ? horizontalOrDefault(this.props.horizontal) + ? styles.horizontallyInverted + : styles.verticallyInverted + : null; + const cells: Array = []; + const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); + const stickyHeaderIndices = []; + + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { + stickyHeaderIndices.push(0); + } + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + ); + cells.push( + + + { + // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors + element + } + + , + ); + } + + // 2a. Add a cell for ListEmptyComponent if applicable + const itemCount = this.props.getItemCount(data); + if (itemCount === 0 && ListEmptyComponent) { + const element: React.Element = ((React.isValidElement( + ListEmptyComponent, + ) ? ( + ListEmptyComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + )): any); + cells.push( + + {React.cloneElement(element, { + onLayout: (event: LayoutEvent) => { + this._onLayoutEmpty(event); + if (element.props.onLayout) { + element.props.onLayout(event); + } + }, + style: StyleSheet.compose(inversionStyle, element.props.style), + })} + , + ); + } + + // 2b. Add cells and spacers for each item + if (itemCount > 0) { + _usedIndexForKey = false; + _keylessItemComponentName = ''; + const spacerKey = this._getSpacerKey(!horizontal); + + const renderRegions = this.state.renderMask.enumerateRegions(); + const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); + + for (const section of renderRegions) { + if (section.isSpacer) { + // Legacy behavior is to avoid spacers when virtualization is + // disabled (including head spacers on initial render). + if (this.props.disableVirtualization) { + continue; + } + + // Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to + // prevent the user for hyperscrolling into un-measured area because otherwise content will + // likely jump around as it renders in above the viewport. + const isLastSpacer = section === lastSpacer; + const constrainToMeasured = isLastSpacer && !this.props.getItemLayout; + const last = constrainToMeasured + ? clamp( + section.first - 1, + section.last, + this._highestMeasuredFrameIndex, + ) + : section.last; + + const firstMetrics = this.__getFrameMetricsApprox( + section.first, + this.props, + ); + const lastMetrics = this.__getFrameMetricsApprox(last, this.props); + const spacerSize = + lastMetrics.offset + lastMetrics.length - firstMetrics.offset; + cells.push( + , + ); + } else { + this._pushCells( + cells, + stickyHeaderIndices, + stickyIndicesFromProps, + section.first, + section.last, + inversionStyle, + ); + } + } + + if (!this._hasWarned.keys && _usedIndexForKey) { + console.warn( + 'VirtualizedList: missing keys for items, make sure to specify a key or id property on each ' + + 'item or provide a custom keyExtractor.', + _keylessItemComponentName, + ); + this._hasWarned.keys = true; + } + } + + // 3. Add cell for ListFooterComponent + if (ListFooterComponent) { + const element = React.isValidElement(ListFooterComponent) ? ( + ListFooterComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + ); + cells.push( + + + { + // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors + element + } + + , + ); + } + + // 4. Render the ScrollView + const scrollProps = { + ...this.props, + onContentSizeChange: this._onContentSizeChange, + onLayout: this._onLayout, + onScroll: this._onScroll, + onScrollBeginDrag: this._onScrollBeginDrag, + onScrollEndDrag: this._onScrollEndDrag, + onMomentumScrollBegin: this._onMomentumScrollBegin, + onMomentumScrollEnd: this._onMomentumScrollEnd, + scrollEventThrottle: scrollEventThrottleOrDefault( + this.props.scrollEventThrottle, + ), // TODO: Android support + invertStickyHeaders: + this.props.invertStickyHeaders !== undefined + ? this.props.invertStickyHeaders + : this.props.inverted, + stickyHeaderIndices, + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, + }; + + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + + const innerRet = ( + + {React.cloneElement( + ( + this.props.renderScrollComponent || + this._defaultRenderScrollComponent + )(scrollProps), + { + ref: this._captureScrollRef, + }, + cells, + )} + + ); + let ret: React.Node = innerRet; + if (__DEV__) { + ret = ( + + {scrollContext => { + if ( + scrollContext != null && + !scrollContext.horizontal === + !horizontalOrDefault(this.props.horizontal) && + !this._hasWarned.nesting && + this.context == null && + this.props.scrollEnabled !== false + ) { + // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170 + console.error( + 'VirtualizedLists should never be nested inside plain ScrollViews with the same ' + + 'orientation because it can break windowing and other functionality - use another ' + + 'VirtualizedList-backed container instead.', + ); + this._hasWarned.nesting = true; + } + return innerRet; + }} + + ); + } + if (this.props.debug) { + return ( + + {ret} + {this._renderDebugOverlay()} + + ); + } else { + return ret; + } + } + + componentDidUpdate(prevProps: Props) { + const {data, extraData} = this.props; + if (data !== prevProps.data || extraData !== prevProps.extraData) { + // clear the viewableIndices cache to also trigger + // the onViewableItemsChanged callback with the new data + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.resetViewableIndices(); + }); + } + // The `this._hiPriInProgress` is guaranteeing a hiPri cell update will only happen + // once per fiber update. The `_scheduleCellsToRenderUpdate` will set it to true + // if a hiPri update needs to perform. If `componentDidUpdate` is triggered with + // `this._hiPriInProgress=true`, means it's triggered by the hiPri update. The + // `_scheduleCellsToRenderUpdate` will check this condition and not perform + // another hiPri update. + const hiPriInProgress = this._hiPriInProgress; + this._scheduleCellsToRenderUpdate(); + // Make sure setting `this._hiPriInProgress` back to false after `componentDidUpdate` + // is triggered with `this._hiPriInProgress = true` + if (hiPriInProgress) { + this._hiPriInProgress = false; + } + } + + _averageCellLength = 0; + _cellRefs: {[string]: null | CellRenderer} = {}; + _fillRateHelper: FillRateHelper; + _frames: { + [string]: { + inLayout?: boolean, + index: number, + length: number, + offset: number, + }, + } = {}; + _footerLength = 0; + // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex + _hasTriggeredInitialScrollToIndex = false; + _hasInteracted = false; + _hasMore = false; + _hasWarned: {[string]: boolean} = {}; + _headerLength = 0; + _hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update + _highestMeasuredFrameIndex = 0; + _indicesToKeys: Map = new Map(); + _lastFocusedCellKey: ?string = null; + _nestedChildLists: ChildListCollection = + new ChildListCollection(); + _offsetFromParentVirtualizedList: number = 0; + _prevParentOffset: number = 0; + // $FlowFixMe[missing-local-annot] + _scrollMetrics = { + contentLength: 0, + dOffset: 0, + dt: 10, + offset: 0, + timestamp: 0, + velocity: 0, + visibleLength: 0, + zoomScale: 1, + }; + _scrollRef: ?React.ElementRef = null; + _sentStartForContentLength = 0; + _sentEndForContentLength = 0; + _totalCellLength = 0; + _totalCellsMeasured = 0; + _updateCellsToRenderBatcher: Batchinator; + _viewabilityTuples: Array = []; + + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + _captureScrollRef = ref => { + this._scrollRef = ref; + }; + + _computeBlankness() { + this._fillRateHelper.computeBlankness( + this.props, + this.state.cellsAroundViewport, + this._scrollMetrics, + ); + } + + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; + } else if (onRefresh) { + invariant( + typeof props.refreshing === 'boolean', + '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + + JSON.stringify(props.refreshing ?? 'undefined') + + '`', + ); + return ( + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] + + ) : ( + props.refreshControl + ) + } + /> + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] + return ; + } + }; + + _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => { + const layout = e.nativeEvent.layout; + const next = { + offset: this._selectOffset(layout), + length: this._selectLength(layout), + index, + inLayout: true, + }; + const curr = this._frames[cellKey]; + if ( + !curr || + next.offset !== curr.offset || + next.length !== curr.length || + index !== curr.index + ) { + this._totalCellLength += next.length - (curr ? curr.length : 0); + this._totalCellsMeasured += curr ? 0 : 1; + this._averageCellLength = + this._totalCellLength / this._totalCellsMeasured; + this._frames[cellKey] = next; + this._highestMeasuredFrameIndex = Math.max( + this._highestMeasuredFrameIndex, + index, + ); + this._scheduleCellsToRenderUpdate(); + } else { + this._frames[cellKey].inLayout = true; + } + + this._triggerRemeasureForChildListsInCell(cellKey); + + this._computeBlankness(); + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + }; + + _onCellFocusCapture(cellKey: string) { + this._lastFocusedCellKey = cellKey; + const renderMask = VirtualizedList._createRenderMask( + this.props, + this.state.cellsAroundViewport, + this._getNonViewportRenderRegions(this.props), + ); + + this.setState(state => { + if (!renderMask.equals(state.renderMask)) { + return {renderMask}; + } + return null; + }); + } + + _onCellUnmount = (cellKey: string) => { + const curr = this._frames[cellKey]; + if (curr) { + this._frames[cellKey] = {...curr, inLayout: false}; + } + }; + + _triggerRemeasureForChildListsInCell(cellKey: string): void { + this._nestedChildLists.forEachInCell(cellKey, childList => { + childList.measureLayoutRelativeToContainingList(); + }); + } + + measureLayoutRelativeToContainingList(): void { + // TODO (T35574538): findNodeHandle sometimes crashes with "Unable to find + // node on an unmounted component" during scrolling + try { + if (!this._scrollRef) { + return; + } + // We are assuming that getOutermostParentListRef().getScrollRef() + // is a non-null reference to a ScrollView + this._scrollRef.measureLayout( + this.context.getOutermostParentListRef().getScrollRef(), + (x, y, width, height) => { + this._offsetFromParentVirtualizedList = this._selectOffset({x, y}); + this._scrollMetrics.contentLength = this._selectLength({ + width, + height, + }); + const scrollMetrics = this._convertParentScrollMetrics( + this.context.getScrollMetrics(), + ); + + const metricsChanged = + this._scrollMetrics.visibleLength !== scrollMetrics.visibleLength || + this._scrollMetrics.offset !== scrollMetrics.offset; + + if (metricsChanged) { + this._scrollMetrics.visibleLength = scrollMetrics.visibleLength; + this._scrollMetrics.offset = scrollMetrics.offset; + + // If metrics of the scrollView changed, then we triggered remeasure for child list + // to ensure VirtualizedList has the right information. + this._nestedChildLists.forEach(childList => { + childList.measureLayoutRelativeToContainingList(); + }); + } + }, + error => { + console.warn( + "VirtualizedList: Encountered an error while measuring a list's" + + ' offset from its containing VirtualizedList.', + ); + }, + ); + } catch (error) { + console.warn( + 'measureLayoutRelativeToContainingList threw an error', + error.stack, + ); + } + } + + _onLayout = (e: LayoutEvent) => { + if (this._isNestedWithSameOrientation()) { + // Need to adjust our scroll metrics to be relative to our containing + // VirtualizedList before we can make claims about list item viewability + this.measureLayoutRelativeToContainingList(); + } else { + this._scrollMetrics.visibleLength = this._selectLength( + e.nativeEvent.layout, + ); + } + this.props.onLayout && this.props.onLayout(e); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEdgeReached(); + }; + + _onLayoutEmpty = (e: LayoutEvent) => { + this.props.onLayout && this.props.onLayout(e); + }; + + _getFooterCellKey(): string { + return this._getCellKey() + '-footer'; + } + + _onLayoutFooter = (e: LayoutEvent) => { + this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()); + this._footerLength = this._selectLength(e.nativeEvent.layout); + }; + + _onLayoutHeader = (e: LayoutEvent) => { + this._headerLength = this._selectLength(e.nativeEvent.layout); + }; + + // $FlowFixMe[missing-local-annot] + _renderDebugOverlay() { + const normalize = + this._scrollMetrics.visibleLength / + (this._scrollMetrics.contentLength || 1); + const framesInLayout = []; + const itemCount = this.props.getItemCount(this.props.data); + for (let ii = 0; ii < itemCount; ii++) { + const frame = this.__getFrameMetricsApprox(ii, this.props); + /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { + framesInLayout.push(frame); + } + } + const windowTop = this.__getFrameMetricsApprox( + this.state.cellsAroundViewport.first, + this.props, + ).offset; + const frameLast = this.__getFrameMetricsApprox( + this.state.cellsAroundViewport.last, + this.props, + ); + const windowLen = frameLast.offset + frameLast.length - windowTop; + const visTop = this._scrollMetrics.offset; + const visLen = this._scrollMetrics.visibleLength; + + return ( + + {framesInLayout.map((f, ii) => ( + + ))} + + + + ); + } + + _selectLength( + metrics: $ReadOnly<{ + height: number, + width: number, + ... + }>, + ): number { + return !horizontalOrDefault(this.props.horizontal) + ? metrics.height + : metrics.width; + } + + _selectOffset( + metrics: $ReadOnly<{ + x: number, + y: number, + ... + }>, + ): number { + return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x; + } + + _maybeCallOnEdgeReached() { + const { + data, + getItemCount, + onStartReached, + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, + initialScrollIndex, + } = this.props; + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; + + // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0 + // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus + // be at the edge of the list with a distance approximating 0 but not quite there. + if (distanceFromStart < ON_EDGE_REACHED_EPSILON) { + distanceFromStart = 0; + } + if (distanceFromEnd < ON_EDGE_REACHED_EPSILON) { + distanceFromEnd = 0; + } + + // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px + // when oERT is not present (different from 2 viewports used elsewhere) + const DEFAULT_THRESHOLD_PX = 2; + + const startThreshold = + onStartReachedThreshold != null + ? onStartReachedThreshold * visibleLength + : DEFAULT_THRESHOLD_PX; + const endThreshold = + onEndReachedThreshold != null + ? onEndReachedThreshold * visibleLength + : DEFAULT_THRESHOLD_PX; + const isWithinStartThreshold = distanceFromStart <= startThreshold; + const isWithinEndThreshold = distanceFromEnd <= endThreshold; + + // First check if the user just scrolled within the end threshold + // and call onEndReached only once for a given content length, + // and only if onStartReached is not being executed + if ( + onEndReached && + this.state.cellsAroundViewport.last === getItemCount(data) - 1 && + isWithinEndThreshold && + this._scrollMetrics.contentLength !== this._sentEndForContentLength + ) { + this._sentEndForContentLength = this._scrollMetrics.contentLength; + onEndReached({distanceFromEnd}); + } + + // Next check if the user just scrolled within the start threshold + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if ( + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { + // On initial mount when using initialScrollIndex the offset will be 0 initially + // and will trigger an unexpected onStartReached. To avoid this we can use + // timestamp to differentiate between the initial scroll metrics and when we actually + // received the first scroll event. + if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { + this._sentStartForContentLength = this._scrollMetrics.contentLength; + onStartReached({distanceFromStart}); + } + } + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + else { + this._sentStartForContentLength = isWithinStartThreshold + ? this._sentStartForContentLength + : 0; + this._sentEndForContentLength = isWithinEndThreshold + ? this._sentEndForContentLength + : 0; + } + } + + _onContentSizeChange = (width: number, height: number) => { + if ( + width > 0 && + height > 0 && + this.props.initialScrollIndex != null && + this.props.initialScrollIndex > 0 && + !this._hasTriggeredInitialScrollToIndex + ) { + if (this.props.contentOffset == null) { + this.scrollToIndex({ + animated: false, + index: this.props.initialScrollIndex, + }); + } + this._hasTriggeredInitialScrollToIndex = true; + } + if (this.props.onContentSizeChange) { + this.props.onContentSizeChange(width, height); + } + this._scrollMetrics.contentLength = this._selectLength({height, width}); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEdgeReached(); + }; + + /* Translates metrics from a scroll event in a parent VirtualizedList into + * coordinates relative to the child list. + */ + _convertParentScrollMetrics = (metrics: { + visibleLength: number, + offset: number, + ... + }): $FlowFixMe => { + // Offset of the top of the nested list relative to the top of its parent's viewport + const offset = metrics.offset - this._offsetFromParentVirtualizedList; + // Child's visible length is the same as its parent's + const visibleLength = metrics.visibleLength; + const dOffset = offset - this._scrollMetrics.offset; + const contentLength = this._scrollMetrics.contentLength; + + return { + visibleLength, + contentLength, + offset, + dOffset, + }; + }; + + _onScroll = (e: Object) => { + this._nestedChildLists.forEach(childList => { + childList._onScroll(e); + }); + if (this.props.onScroll) { + this.props.onScroll(e); + } + const timestamp = e.timeStamp; + let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement); + let contentLength = this._selectLength(e.nativeEvent.contentSize); + let offset = this._selectOffset(e.nativeEvent.contentOffset); + let dOffset = offset - this._scrollMetrics.offset; + + if (this._isNestedWithSameOrientation()) { + if (this._scrollMetrics.contentLength === 0) { + // Ignore scroll events until onLayout has been called and we + // know our offset from our offset from our parent + return; + } + ({visibleLength, contentLength, offset, dOffset} = + this._convertParentScrollMetrics({ + visibleLength, + offset, + })); + } + + const dt = this._scrollMetrics.timestamp + ? Math.max(1, timestamp - this._scrollMetrics.timestamp) + : 1; + const velocity = dOffset / dt; + + if ( + dt > 500 && + this._scrollMetrics.dt > 500 && + contentLength > 5 * visibleLength && + !this._hasWarned.perf + ) { + infoLog( + 'VirtualizedList: You have a large list that is slow to update - make sure your ' + + 'renderItem function renders components that follow React performance best practices ' + + 'like PureComponent, shouldComponentUpdate, etc.', + {dt, prevDt: this._scrollMetrics.dt, contentLength}, + ); + this._hasWarned.perf = true; + } + + // For invalid negative values (w/ RTL), set this to 1. + const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale; + this._scrollMetrics = { + contentLength, + dt, + dOffset, + offset, + timestamp, + velocity, + visibleLength, + zoomScale, + }; + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; + } + this._maybeCallOnEdgeReached(); + if (velocity !== 0) { + this._fillRateHelper.activate(); + } + this._computeBlankness(); + this._scheduleCellsToRenderUpdate(); + }; + + _scheduleCellsToRenderUpdate() { + const {first, last} = this.state.cellsAroundViewport; + const {offset, visibleLength, velocity} = this._scrollMetrics; + const itemCount = this.props.getItemCount(this.props.data); + let hiPri = false; + const onStartReachedThreshold = onStartReachedThresholdOrDefault( + this.props.onStartReachedThreshold, + ); + const onEndReachedThreshold = onEndReachedThresholdOrDefault( + this.props.onEndReachedThreshold, + ); + // Mark as high priority if we're close to the start of the first item + // But only if there are items before the first rendered item + if (first > 0) { + const distTop = + offset - this.__getFrameMetricsApprox(first, this.props).offset; + hiPri = + distTop < 0 || + (velocity < -2 && + distTop < + getScrollingThreshold(onStartReachedThreshold, visibleLength)); + } + // Mark as high priority if we're close to the end of the last item + // But only if there are items after the last rendered item + if (!hiPri && last >= 0 && last < itemCount - 1) { + const distBottom = + this.__getFrameMetricsApprox(last, this.props).offset - + (offset + visibleLength); + hiPri = + distBottom < 0 || + (velocity > 2 && + distBottom < + getScrollingThreshold(onEndReachedThreshold, visibleLength)); + } + // Only trigger high-priority updates if we've actually rendered cells, + // and with that size estimate, accurately compute how many cells we should render. + // Otherwise, it would just render as many cells as it can (of zero dimension), + // each time through attempting to render more (limited by maxToRenderPerBatch), + // starving the renderer from actually laying out the objects and computing _averageCellLength. + // If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate + // We shouldn't do another hipri cellToRenderUpdate + if ( + hiPri && + (this._averageCellLength || this.props.getItemLayout) && + !this._hiPriInProgress + ) { + this._hiPriInProgress = true; + // Don't worry about interactions when scrolling quickly; focus on filling content as fast + // as possible. + this._updateCellsToRenderBatcher.dispose({abort: true}); + this._updateCellsToRender(); + return; + } else { + this._updateCellsToRenderBatcher.schedule(); + } + } + + _onScrollBeginDrag = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onScrollBeginDrag(e); + }); + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.recordInteraction(); + }); + this._hasInteracted = true; + this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); + }; + + _onScrollEndDrag = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onScrollEndDrag(e); + }); + const {velocity} = e.nativeEvent; + if (velocity) { + this._scrollMetrics.velocity = this._selectOffset(velocity); + } + this._computeBlankness(); + this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); + }; + + _onMomentumScrollBegin = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onMomentumScrollBegin(e); + }); + this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); + }; + + _onMomentumScrollEnd = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onMomentumScrollEnd(e); + }); + this._scrollMetrics.velocity = 0; + this._computeBlankness(); + this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); + }; + + _updateCellsToRender = () => { + this.setState((state, props) => { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, + ); + const renderMask = VirtualizedList._createRenderMask( + props, + cellsAroundViewport, + this._getNonViewportRenderRegions(props), + ); + + if ( + cellsAroundViewport.first === state.cellsAroundViewport.first && + cellsAroundViewport.last === state.cellsAroundViewport.last && + renderMask.equals(state.renderMask) + ) { + return null; + } + + return {cellsAroundViewport, renderMask}; + }); + }; + + _createViewToken = ( + index: number, + isViewable: boolean, + props: FrameMetricProps, + // $FlowFixMe[missing-local-annot] + ) => { + const {data, getItem} = props; + const item = getItem(data, index); + return { + index, + item, + key: this._keyExtractor(item, index, props), + isViewable, + }; + }; + + /** + * Gets an approximate offset to an item at a given index. Supports + * fractional indices. + */ + _getOffsetApprox = (index: number, props: FrameMetricProps): number => { + if (Number.isInteger(index)) { + return this.__getFrameMetricsApprox(index, props).offset; + } else { + const frameMetrics = this.__getFrameMetricsApprox( + Math.floor(index), + props, + ); + const remainder = index - Math.floor(index); + return frameMetrics.offset + remainder * frameMetrics.length; + } + }; + + __getFrameMetricsApprox: ( + index: number, + props: FrameMetricProps, + ) => { + length: number, + offset: number, + ... + } = (index, props) => { + const frame = this._getFrameMetrics(index, props); + if (frame && frame.index === index) { + // check for invalid frames due to row re-ordering + return frame; + } else { + const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); + invariant( + !getItemLayout, + 'Should not have to estimate frames when a measurement metrics function is provided', + ); + return { + length: this._averageCellLength, + offset: this._averageCellLength * index, + }; + } + }; + + _getFrameMetrics = ( + index: number, + props: FrameMetricProps, + ): ?{ + length: number, + offset: number, + index: number, + inLayout?: boolean, + ... + } => { + const {data, getItem, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); + const item = getItem(data, index); + const frame = this._frames[this._keyExtractor(item, index, props)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.63 was deployed. To see the error + * delete this comment and run Flow. */ + return getItemLayout(data, index); + } + } + return frame; + }; + + _getNonViewportRenderRegions = ( + props: FrameMetricProps, + ): $ReadOnlyArray<{ + first: number, + last: number, + }> => { + // Keep a viewport's worth of content around the last focused cell to allow + // random navigation around it without any blanking. E.g. tabbing from one + // focused item out of viewport to another. + if ( + !(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey]) + ) { + return []; + } + + const lastFocusedCellRenderer = this._cellRefs[this._lastFocusedCellKey]; + const focusedCellIndex = lastFocusedCellRenderer.props.index; + const itemCount = props.getItemCount(props.data); + + // The cell may have been unmounted and have a stale index + if ( + focusedCellIndex >= itemCount || + this._indicesToKeys.get(focusedCellIndex) !== this._lastFocusedCellKey + ) { + return []; + } + + let first = focusedCellIndex; + let heightOfCellsBeforeFocused = 0; + for ( + let i = first - 1; + i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength; + i-- + ) { + first--; + heightOfCellsBeforeFocused += this.__getFrameMetricsApprox( + i, + props, + ).length; + } + + let last = focusedCellIndex; + let heightOfCellsAfterFocused = 0; + for ( + let i = last + 1; + i < itemCount && + heightOfCellsAfterFocused < this._scrollMetrics.visibleLength; + i++ + ) { + last++; + heightOfCellsAfterFocused += this.__getFrameMetricsApprox( + i, + props, + ).length; + } + + return [{first, last}]; + }; + + _updateViewableItems( + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, + this._scrollMetrics.offset, + this._scrollMetrics.visibleLength, + this._getFrameMetrics, + this._createViewToken, + tuple.onViewableItemsChanged, + cellsAroundViewport, + ); + }); + } +} + +const styles = StyleSheet.create({ + verticallyInverted: { + transform: [{scaleY: -1}], + }, + horizontallyInverted: { + transform: [{scaleX: -1}], + }, + debug: { + flex: 1, + }, + debugOverlayBase: { + position: 'absolute', + top: 0, + right: 0, + }, + debugOverlay: { + bottom: 0, + width: 20, + borderColor: 'blue', + borderWidth: 1, + }, + debugOverlayFrame: { + left: 0, + backgroundColor: 'orange', + }, + debugOverlayFrameLast: { + left: 0, + borderColor: 'green', + borderWidth: 2, + }, + debugOverlayFrameVis: { + left: 0, + borderColor: 'red', + borderWidth: 2, + }, +}); diff --git a/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js b/Libraries/Lists/VirtualizedListCellRenderer.js similarity index 96% rename from packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js rename to Libraries/Lists/VirtualizedListCellRenderer.js index 0064f788b8a9c4..b16900d63b742b 100644 --- a/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js +++ b/Libraries/Lists/VirtualizedListCellRenderer.js @@ -8,15 +8,13 @@ * @format */ -import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; -import type { - FocusEvent, - LayoutEvent, -} from 'react-native/Libraries/Types/CoreEventTypes'; +import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; +import type {FocusEvent, LayoutEvent} from '../Types/CoreEventTypes'; import type FillRateHelper from './FillRateHelper'; import type {RenderItemType} from './VirtualizedListProps'; -import {View, StyleSheet} from 'react-native'; +import View from '../Components/View/View'; +import StyleSheet from '../StyleSheet/StyleSheet'; import {VirtualizedListCellContextProvider} from './VirtualizedListContext.js'; import invariant from 'invariant'; import * as React from 'react'; diff --git a/Libraries/Lists/VirtualizedListContext.js b/Libraries/Lists/VirtualizedListContext.js index 5686ccf372286c..bca5724498a356 100644 --- a/Libraries/Lists/VirtualizedListContext.js +++ b/Libraries/Lists/VirtualizedListContext.js @@ -4,15 +4,113 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow + * @flow strict-local * @format */ -'use strict'; +import typeof VirtualizedList from './VirtualizedList'; -import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterType} from '@react-native/virtualized-lists'; +import * as React from 'react'; +import {useContext, useMemo} from 'react'; -const VirtualizedListContextResetter: VirtualizedListContextResetterType = - require('@react-native/virtualized-lists').VirtualizedListContextResetter; +type Context = $ReadOnly<{ + cellKey: ?string, + getScrollMetrics: () => { + contentLength: number, + dOffset: number, + dt: number, + offset: number, + timestamp: number, + velocity: number, + visibleLength: number, + zoomScale: number, + }, + horizontal: ?boolean, + getOutermostParentListRef: () => React.ElementRef, + registerAsNestedChild: ({ + cellKey: string, + ref: React.ElementRef, + }) => void, + unregisterAsNestedChild: ({ + ref: React.ElementRef, + }) => void, +}>; -module.exports = {VirtualizedListContextResetter}; +export const VirtualizedListContext: React.Context = + React.createContext(null); +if (__DEV__) { + VirtualizedListContext.displayName = 'VirtualizedListContext'; +} + +/** + * Resets the context. Intended for use by portal-like components (e.g. Modal). + */ +export function VirtualizedListContextResetter({ + children, +}: { + children: React.Node, +}): React.Node { + return ( + + {children} + + ); +} + +/** + * Sets the context with memoization. Intended to be used by `VirtualizedList`. + */ +export function VirtualizedListContextProvider({ + children, + value, +}: { + children: React.Node, + value: Context, +}): React.Node { + // Avoid setting a newly created context object if the values are identical. + const context = useMemo( + () => ({ + cellKey: null, + getScrollMetrics: value.getScrollMetrics, + horizontal: value.horizontal, + getOutermostParentListRef: value.getOutermostParentListRef, + registerAsNestedChild: value.registerAsNestedChild, + unregisterAsNestedChild: value.unregisterAsNestedChild, + }), + [ + value.getScrollMetrics, + value.horizontal, + value.getOutermostParentListRef, + value.registerAsNestedChild, + value.unregisterAsNestedChild, + ], + ); + return ( + + {children} + + ); +} + +/** + * Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell. + */ +export function VirtualizedListCellContextProvider({ + cellKey, + children, +}: { + cellKey: string, + children: React.Node, +}): React.Node { + // Avoid setting a newly created context object if the values are identical. + const currContext = useContext(VirtualizedListContext); + const context = useMemo( + () => (currContext == null ? null : {...currContext, cellKey}), + [currContext, cellKey], + ); + return ( + + {children} + + ); +} diff --git a/packages/virtualized-lists/Lists/VirtualizedListProps.js b/Libraries/Lists/VirtualizedListProps.js similarity index 98% rename from packages/virtualized-lists/Lists/VirtualizedListProps.js rename to Libraries/Lists/VirtualizedListProps.js index bfc59673270a57..f4d497b1d467a8 100644 --- a/packages/virtualized-lists/Lists/VirtualizedListProps.js +++ b/Libraries/Lists/VirtualizedListProps.js @@ -8,8 +8,8 @@ * @format */ -import {typeof ScrollView} from 'react-native'; -import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import typeof ScrollView from '../Components/ScrollView/ScrollView'; +import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; import type { ViewabilityConfig, ViewabilityConfigCallbackPair, diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index 242dfe34c6b231..e4bf2fe58a0c44 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -8,15 +8,610 @@ * @format */ -'use strict'; +import type {ViewToken} from './ViewabilityHelper'; -import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@react-native/virtualized-lists'; +import View from '../Components/View/View'; +import VirtualizedList from './VirtualizedList'; +import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; +import invariant from 'invariant'; +import * as React from 'react'; -const VirtualizedSectionList: VirtualizedSectionListType = - require('@react-native/virtualized-lists').VirtualizedSectionList; +type Item = any; -export type { - SectionBase, - ScrollToLocationParamsType, -} from '@react-native/virtualized-lists'; -module.exports = VirtualizedSectionList; +export type SectionBase = { + /** + * The data for rendering items in this section. + */ + data: $ReadOnlyArray, + /** + * Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections, + * the array index will be used by default. + */ + key?: string, + // Optional props will override list-wide props just for this section. + renderItem?: ?(info: { + item: SectionItemT, + index: number, + section: SectionBase, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... + }, + ... + }) => null | React.Element, + ItemSeparatorComponent?: ?React.ComponentType, + keyExtractor?: (item: SectionItemT, index?: ?number) => string, + ... +}; + +type RequiredProps> = {| + sections: $ReadOnlyArray, +|}; + +type OptionalProps> = {| + /** + * Default renderer for every item in every section. + */ + renderItem?: (info: { + item: Item, + index: number, + section: SectionT, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... + }, + ... + }) => null | React.Element, + /** + * Rendered at the top of each section. These stick to the top of the `ScrollView` by default on + * iOS. See `stickySectionHeadersEnabled`. + */ + renderSectionHeader?: ?(info: { + section: SectionT, + ... + }) => null | React.Element, + /** + * Rendered at the bottom of each section. + */ + renderSectionFooter?: ?(info: { + section: SectionT, + ... + }) => null | React.Element, + /** + * Rendered at the top and bottom of each section (note this is different from + * `ItemSeparatorComponent` which is only rendered between items). These are intended to separate + * sections from the headers above and below and typically have the same highlight response as + * `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`, + * and any custom props from `separators.updateProps`. + */ + SectionSeparatorComponent?: ?React.ComponentType, + /** + * Makes section headers stick to the top of the screen until the next one pushes it off. Only + * enabled by default on iOS because that is the platform standard there. + */ + stickySectionHeadersEnabled?: boolean, + onEndReached?: ?({distanceFromEnd: number, ...}) => void, +|}; + +type VirtualizedListProps = React.ElementConfig; + +export type Props = {| + ...RequiredProps, + ...OptionalProps, + ...$Diff< + VirtualizedListProps, + { + renderItem: $PropertyType, + data: $PropertyType, + ... + }, + >, +|}; +export type ScrollToLocationParamsType = {| + animated?: ?boolean, + itemIndex: number, + sectionIndex: number, + viewOffset?: number, + viewPosition?: number, +|}; + +type State = {childProps: VirtualizedListProps, ...}; + +/** + * Right now this just flattens everything into one list and uses VirtualizedList under the + * hood. The only operation that might not scale well is concatting the data arrays of all the + * sections when new props are received, which should be plenty fast for up to ~10,000 items. + */ +class VirtualizedSectionList< + SectionT: SectionBase, +> extends React.PureComponent, State> { + scrollToLocation(params: ScrollToLocationParamsType) { + let index = params.itemIndex; + for (let i = 0; i < params.sectionIndex; i++) { + index += this.props.getItemCount(this.props.sections[i].data) + 2; + } + let viewOffset = params.viewOffset || 0; + if (this._listRef == null) { + return; + } + if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) { + const frame = this._listRef.__getFrameMetricsApprox( + index - params.itemIndex, + this._listRef.props, + ); + viewOffset += frame.length; + } + const toIndexParams = { + ...params, + viewOffset, + index, + }; + // $FlowFixMe[incompatible-use] + this._listRef.scrollToIndex(toIndexParams); + } + + getListRef(): ?React.ElementRef { + return this._listRef; + } + + render(): React.Node { + const { + ItemSeparatorComponent, // don't pass through, rendered with renderItem + SectionSeparatorComponent, + renderItem: _renderItem, + renderSectionFooter, + renderSectionHeader, + sections: _sections, + stickySectionHeadersEnabled, + ...passThroughProps + } = this.props; + + const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0; + + const stickyHeaderIndices = this.props.stickySectionHeadersEnabled + ? ([]: Array) + : undefined; + + let itemCount = 0; + for (const section of this.props.sections) { + // Track the section header indices + if (stickyHeaderIndices != null) { + stickyHeaderIndices.push(itemCount + listHeaderOffset); + } + + // Add two for the section header and footer. + itemCount += 2; + itemCount += this.props.getItemCount(section.data); + } + const renderItem = this._renderItem(itemCount); + + return ( + + this._getItem(this.props, sections, index) + } + getItemCount={() => itemCount} + onViewableItemsChanged={ + this.props.onViewableItemsChanged + ? this._onViewableItemsChanged + : undefined + } + ref={this._captureRef} + /> + ); + } + + _getItem( + props: Props, + sections: ?$ReadOnlyArray, + index: number, + ): ?Item { + if (!sections) { + return null; + } + let itemIdx = index - 1; + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + const sectionData = section.data; + const itemCount = props.getItemCount(sectionData); + if (itemIdx === -1 || itemIdx === itemCount) { + // We intend for there to be overflow by one on both ends of the list. + // This will be for headers and footers. When returning a header or footer + // item the section itself is the item. + return section; + } else if (itemIdx < itemCount) { + // If we are in the bounds of the list's data then return the item. + return props.getItem(sectionData, itemIdx); + } else { + itemIdx -= itemCount + 2; // Add two for the header and footer + } + } + return null; + } + + // $FlowFixMe[missing-local-annot] + _keyExtractor = (item: Item, index: number) => { + const info = this._subExtractor(index); + return (info && info.key) || String(index); + }; + + _subExtractor(index: number): ?{ + section: SectionT, + // Key of the section or combined key for section + item + key: string, + // Relative index within the section + index: ?number, + // True if this is the section header + header?: ?boolean, + leadingItem?: ?Item, + leadingSection?: ?SectionT, + trailingItem?: ?Item, + trailingSection?: ?SectionT, + ... + } { + let itemIndex = index; + const {getItem, getItemCount, keyExtractor, sections} = this.props; + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + const sectionData = section.data; + const key = section.key || String(i); + itemIndex -= 1; // The section adds an item for the header + if (itemIndex >= getItemCount(sectionData) + 1) { + itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer. + } else if (itemIndex === -1) { + return { + section, + key: key + ':header', + index: null, + header: true, + trailingSection: sections[i + 1], + }; + } else if (itemIndex === getItemCount(sectionData)) { + return { + section, + key: key + ':footer', + index: null, + header: false, + trailingSection: sections[i + 1], + }; + } else { + const extractor = + section.keyExtractor || keyExtractor || defaultKeyExtractor; + return { + section, + key: + key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex), + index: itemIndex, + leadingItem: getItem(sectionData, itemIndex - 1), + leadingSection: sections[i - 1], + trailingItem: getItem(sectionData, itemIndex + 1), + trailingSection: sections[i + 1], + }; + } + } + } + + _convertViewable = (viewable: ViewToken): ?ViewToken => { + invariant(viewable.index != null, 'Received a broken ViewToken'); + const info = this._subExtractor(viewable.index); + if (!info) { + return null; + } + const keyExtractorWithNullableIndex = info.section.keyExtractor; + const keyExtractorWithNonNullableIndex = + this.props.keyExtractor || defaultKeyExtractor; + const key = + keyExtractorWithNullableIndex != null + ? keyExtractorWithNullableIndex(viewable.item, info.index) + : keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0); + + return { + ...viewable, + index: info.index, + key, + section: info.section, + }; + }; + + _onViewableItemsChanged = ({ + viewableItems, + changed, + }: { + viewableItems: Array, + changed: Array, + ... + }) => { + const onViewableItemsChanged = this.props.onViewableItemsChanged; + if (onViewableItemsChanged != null) { + onViewableItemsChanged({ + viewableItems: viewableItems + .map(this._convertViewable, this) + .filter(Boolean), + changed: changed.map(this._convertViewable, this).filter(Boolean), + }); + } + }; + + _renderItem = + (listItemCount: number): $FlowFixMe => + // eslint-disable-next-line react/no-unstable-nested-components + ({item, index}: {item: Item, index: number, ...}) => { + const info = this._subExtractor(index); + if (!info) { + return null; + } + const infoIndex = info.index; + if (infoIndex == null) { + const {section} = info; + if (info.header === true) { + const {renderSectionHeader} = this.props; + return renderSectionHeader ? renderSectionHeader({section}) : null; + } else { + const {renderSectionFooter} = this.props; + return renderSectionFooter ? renderSectionFooter({section}) : null; + } + } else { + const renderItem = info.section.renderItem || this.props.renderItem; + const SeparatorComponent = this._getSeparatorComponent( + index, + info, + listItemCount, + ); + invariant(renderItem, 'no renderItem!'); + return ( + + ); + } + }; + + _updatePropsFor = (cellKey: string, value: any) => { + const updateProps = this._updatePropsMap[cellKey]; + if (updateProps != null) { + updateProps(value); + } + }; + + _updateHighlightFor = (cellKey: string, value: boolean) => { + const updateHighlight = this._updateHighlightMap[cellKey]; + if (updateHighlight != null) { + updateHighlight(value); + } + }; + + _setUpdateHighlightFor = ( + cellKey: string, + updateHighlightFn: ?(boolean) => void, + ) => { + if (updateHighlightFn != null) { + this._updateHighlightMap[cellKey] = updateHighlightFn; + } else { + // $FlowFixMe[prop-missing] + delete this._updateHighlightFor[cellKey]; + } + }; + + _setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => { + if (updatePropsFn != null) { + this._updatePropsMap[cellKey] = updatePropsFn; + } else { + delete this._updatePropsMap[cellKey]; + } + }; + + _getSeparatorComponent( + index: number, + info?: ?Object, + listItemCount: number, + ): ?React.ComponentType { + info = info || this._subExtractor(index); + if (!info) { + return null; + } + const ItemSeparatorComponent = + info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; + const {SectionSeparatorComponent} = this.props; + const isLastItemInList = index === listItemCount - 1; + const isLastItemInSection = + info.index === this.props.getItemCount(info.section.data) - 1; + if (SectionSeparatorComponent && isLastItemInSection) { + return SectionSeparatorComponent; + } + if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { + return ItemSeparatorComponent; + } + return null; + } + + _updateHighlightMap: {[string]: (boolean) => void} = {}; + _updatePropsMap: {[string]: void | (boolean => void)} = {}; + _listRef: ?React.ElementRef; + _captureRef = (ref: null | React$ElementRef>) => { + this._listRef = ref; + }; +} + +type ItemWithSeparatorCommonProps = $ReadOnly<{| + leadingItem: ?Item, + leadingSection: ?Object, + section: Object, + trailingItem: ?Item, + trailingSection: ?Object, +|}>; + +type ItemWithSeparatorProps = $ReadOnly<{| + ...ItemWithSeparatorCommonProps, + LeadingSeparatorComponent: ?React.ComponentType, + SeparatorComponent: ?React.ComponentType, + cellKey: string, + index: number, + item: Item, + setSelfHighlightCallback: ( + cellKey: string, + updateFn: ?(boolean) => void, + ) => void, + setSelfUpdatePropsCallback: ( + cellKey: string, + updateFn: ?(boolean) => void, + ) => void, + prevCellKey?: ?string, + updateHighlightFor: (prevCellKey: string, value: boolean) => void, + updatePropsFor: (prevCellKey: string, value: Object) => void, + renderItem: Function, + inverted: boolean, +|}>; + +function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node { + const { + LeadingSeparatorComponent, + // this is the trailing separator and is associated with this item + SeparatorComponent, + cellKey, + prevCellKey, + setSelfHighlightCallback, + updateHighlightFor, + setSelfUpdatePropsCallback, + updatePropsFor, + item, + index, + section, + inverted, + } = props; + + const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] = + React.useState(false); + + const [separatorHighlighted, setSeparatorHighlighted] = React.useState(false); + + const [leadingSeparatorProps, setLeadingSeparatorProps] = React.useState({ + leadingItem: props.leadingItem, + leadingSection: props.leadingSection, + section: props.section, + trailingItem: props.item, + trailingSection: props.trailingSection, + }); + const [separatorProps, setSeparatorProps] = React.useState({ + leadingItem: props.item, + leadingSection: props.leadingSection, + section: props.section, + trailingItem: props.trailingItem, + trailingSection: props.trailingSection, + }); + + React.useEffect(() => { + setSelfHighlightCallback(cellKey, setSeparatorHighlighted); + // $FlowFixMe[incompatible-call] + setSelfUpdatePropsCallback(cellKey, setSeparatorProps); + + return () => { + setSelfUpdatePropsCallback(cellKey, null); + setSelfHighlightCallback(cellKey, null); + }; + }, [ + cellKey, + setSelfHighlightCallback, + setSeparatorProps, + setSelfUpdatePropsCallback, + ]); + + const separators = { + highlight: () => { + setLeadingSeparatorHighlighted(true); + setSeparatorHighlighted(true); + if (prevCellKey != null) { + updateHighlightFor(prevCellKey, true); + } + }, + unhighlight: () => { + setLeadingSeparatorHighlighted(false); + setSeparatorHighlighted(false); + if (prevCellKey != null) { + updateHighlightFor(prevCellKey, false); + } + }, + updateProps: ( + select: 'leading' | 'trailing', + newProps: $Shape, + ) => { + if (select === 'leading') { + if (LeadingSeparatorComponent != null) { + setLeadingSeparatorProps({...leadingSeparatorProps, ...newProps}); + } else if (prevCellKey != null) { + // update the previous item's separator + updatePropsFor(prevCellKey, {...leadingSeparatorProps, ...newProps}); + } + } else if (select === 'trailing' && SeparatorComponent != null) { + setSeparatorProps({...separatorProps, ...newProps}); + } + }, + }; + const element = props.renderItem({ + item, + index, + section, + separators, + }); + const leadingSeparator = LeadingSeparatorComponent != null && ( + + ); + const separator = SeparatorComponent != null && ( + + ); + return leadingSeparator || separator ? ( + + {inverted === false ? leadingSeparator : separator} + {element} + {inverted === false ? separator : leadingSeparator} + + ) : ( + element + ); +} + +/* $FlowFixMe[class-object-subtyping] added when improving typing for this + * parameters */ +// $FlowFixMe[method-unbinding] +module.exports = (VirtualizedSectionList: React.AbstractComponent< + React.ElementConfig, + $ReadOnly<{ + getListRef: () => ?React.ElementRef, + scrollToLocation: (params: ScrollToLocationParamsType) => void, + ... + }>, +>); diff --git a/packages/virtualized-lists/Lists/__tests__/CellRenderMask-test.js b/Libraries/Lists/__tests__/CellRenderMask-test.js similarity index 100% rename from packages/virtualized-lists/Lists/__tests__/CellRenderMask-test.js rename to Libraries/Lists/__tests__/CellRenderMask-test.js diff --git a/packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js b/Libraries/Lists/__tests__/FillRateHelper-test.js similarity index 100% rename from packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js rename to Libraries/Lists/__tests__/FillRateHelper-test.js diff --git a/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js b/Libraries/Lists/__tests__/ViewabilityHelper-test.js similarity index 100% rename from packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js rename to Libraries/Lists/__tests__/ViewabilityHelper-test.js diff --git a/packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js b/Libraries/Lists/__tests__/VirtualizeUtils-test.js similarity index 100% rename from packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js rename to Libraries/Lists/__tests__/VirtualizeUtils-test.js diff --git a/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js b/Libraries/Lists/__tests__/VirtualizedList-test.js similarity index 100% rename from packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js rename to Libraries/Lists/__tests__/VirtualizedList-test.js diff --git a/packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js b/Libraries/Lists/__tests__/VirtualizedSectionList-test.js similarity index 100% rename from packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js rename to Libraries/Lists/__tests__/VirtualizedSectionList-test.js diff --git a/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap similarity index 100% rename from packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap rename to Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap diff --git a/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap similarity index 100% rename from packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap rename to Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index 9750d2e5be31d3..46e9e714d29262 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -13,11 +13,11 @@ import type {RootTag} from '../ReactNative/RootTag'; import type {DirectEventHandler} from '../Types/CodegenTypes'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; +import {VirtualizedListContextResetter} from '../Lists/VirtualizedListContext.js'; import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import ModalInjection from './ModalInjection'; import NativeModalManager from './NativeModalManager'; import RCTModalHostView from './RCTModalHostViewNativeComponent'; -import {VirtualizedListContextResetter} from '@react-native/virtualized-lists'; const ScrollView = require('../Components/ScrollView/ScrollView'); const View = require('../Components/View/View'); diff --git a/Libraries/Utilities/ReactNativeTestTools.js b/Libraries/Utilities/ReactNativeTestTools.js index 8671575054137c..5dc2221528dcee 100644 --- a/Libraries/Utilities/ReactNativeTestTools.js +++ b/Libraries/Utilities/ReactNativeTestTools.js @@ -15,8 +15,8 @@ import type {ReactTestRenderer as ReactTestRendererType} from 'react-test-render const Switch = require('../Components/Switch/Switch').default; const TextInput = require('../Components/TextInput/TextInput'); const View = require('../Components/View/View'); +const VirtualizedList = require('../Lists/VirtualizedList').default; const Text = require('../Text/Text'); -const {VirtualizedList} = require('@react-native/virtualized-lists'); const React = require('react'); const ShallowRenderer = require('react-shallow-renderer'); const ReactTestRenderer = require('react-test-renderer'); diff --git a/packages/virtualized-lists/Utilities/__tests__/clamp-test.js b/Libraries/Utilities/__tests__/clamp-test.js similarity index 100% rename from packages/virtualized-lists/Utilities/__tests__/clamp-test.js rename to Libraries/Utilities/__tests__/clamp-test.js diff --git a/packages/virtualized-lists/Utilities/clamp.js b/Libraries/Utilities/clamp.js similarity index 100% rename from packages/virtualized-lists/Utilities/clamp.js rename to Libraries/Utilities/clamp.js diff --git a/index.js b/index.js index 7149c6463b52fa..0b6e97c0522640 100644 --- a/index.js +++ b/index.js @@ -191,7 +191,7 @@ module.exports = { return require('./Libraries/Components/View/View'); }, get VirtualizedList(): VirtualizedList { - return require('./Libraries/Lists/VirtualizedList'); + return require('./Libraries/Lists/VirtualizedList').default; }, get VirtualizedSectionList(): VirtualizedSectionList { return require('./Libraries/Lists/VirtualizedSectionList'); diff --git a/package.json b/package.json index 225346a4a1d73d..109d1dd68e389f 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,6 @@ "@react-native/gradle-plugin": "^0.72.2", "@react-native/js-polyfills": "^0.72.0", "@react-native/normalize-colors": "^0.72.0", - "@react-native/virtualized-lists": "0.72.0", "abort-controller": "^3.0.0", "anser": "^1.4.9", "base64-js": "^1.1.2", diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js index 97f991265de3ba..25c511e570a3eb 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js @@ -12,7 +12,7 @@ import type {AnimatedComponentType} from 'react-native/Libraries/Animated/createAnimatedComponent'; import typeof FlatListType from 'react-native/Libraries/Lists/FlatList'; -import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList'; +import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedListProps'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import * as React from 'react'; diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-nested.js b/packages/rn-tester/js/examples/FlatList/FlatList-nested.js index cd1f96ded9d3af..bb53258da9c82c 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-nested.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-nested.js @@ -9,8 +9,8 @@ */ 'use strict'; -import type {ViewToken} from 'react-native/Libraries/Lists/ViewabilityHelper'; -import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList'; +import type {ViewToken} from '../../../../../Libraries/Lists/ViewabilityHelper'; +import type {RenderItemProps} from '../../../../../Libraries/Lists/VirtualizedListProps'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import RNTesterPage from '../../components/RNTesterPage'; diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js b/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js index f65111e16596ba..76e29fac105fe1 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js @@ -10,7 +10,7 @@ 'use strict'; -import type {ViewToken} from 'react-native/Libraries/Lists/ViewabilityHelper'; +import type {ViewToken} from '../../../../../Libraries/Lists/ViewabilityHelper'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import BaseFlatListExample from './BaseFlatListExample'; diff --git a/packages/virtualized-lists/Lists/FillRateHelper.js b/packages/virtualized-lists/Lists/FillRateHelper.js deleted file mode 100644 index 87482e73f6be3e..00000000000000 --- a/packages/virtualized-lists/Lists/FillRateHelper.js +++ /dev/null @@ -1,253 +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 - * @format - */ - -'use strict'; - -import type {FrameMetricProps} from './VirtualizedListProps'; - -export type FillRateInfo = Info; - -class Info { - any_blank_count: number = 0; - any_blank_ms: number = 0; - any_blank_speed_sum: number = 0; - mostly_blank_count: number = 0; - mostly_blank_ms: number = 0; - pixels_blank: number = 0; - pixels_sampled: number = 0; - pixels_scrolled: number = 0; - total_time_spent: number = 0; - sample_count: number = 0; -} - -type FrameMetrics = { - inLayout?: boolean, - length: number, - offset: number, - ... -}; - -const DEBUG = false; - -let _listeners: Array<(Info) => void> = []; -let _minSampleCount = 10; -let _sampleRate = DEBUG ? 1 : null; - -/** - * A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded. - * By default the sampling rate is set to zero and this will do nothing. If you want to collect - * samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`. - * - * Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with - * `SceneTracker.getActiveScene` to determine the context of the events. - */ -class FillRateHelper { - _anyBlankStartTime: ?number = null; - _enabled = false; - _getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics; - _info: Info = new Info(); - _mostlyBlankStartTime: ?number = null; - _samplesStartTime: ?number = null; - - static addListener(callback: FillRateInfo => void): { - remove: () => void, - ... - } { - if (_sampleRate === null) { - console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.'); - } - _listeners.push(callback); - return { - remove: () => { - _listeners = _listeners.filter(listener => callback !== listener); - }, - }; - } - - static setSampleRate(sampleRate: number) { - _sampleRate = sampleRate; - } - - static setMinSampleCount(minSampleCount: number) { - _minSampleCount = minSampleCount; - } - - constructor( - getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics, - ) { - this._getFrameMetrics = getFrameMetrics; - this._enabled = (_sampleRate || 0) > Math.random(); - this._resetData(); - } - - activate() { - if (this._enabled && this._samplesStartTime == null) { - DEBUG && console.debug('FillRateHelper: activate'); - this._samplesStartTime = global.performance.now(); - } - } - - deactivateAndFlush() { - if (!this._enabled) { - return; - } - const start = this._samplesStartTime; // const for flow - if (start == null) { - DEBUG && - console.debug('FillRateHelper: bail on deactivate with no start time'); - return; - } - if (this._info.sample_count < _minSampleCount) { - // Don't bother with under-sampled events. - this._resetData(); - return; - } - const total_time_spent = global.performance.now() - start; - const info: any = { - ...this._info, - total_time_spent, - }; - if (DEBUG) { - const derived = { - avg_blankness: this._info.pixels_blank / this._info.pixels_sampled, - avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000), - avg_speed_when_any_blank: - this._info.any_blank_speed_sum / this._info.any_blank_count, - any_blank_per_min: - this._info.any_blank_count / (total_time_spent / 1000 / 60), - any_blank_time_frac: this._info.any_blank_ms / total_time_spent, - mostly_blank_per_min: - this._info.mostly_blank_count / (total_time_spent / 1000 / 60), - mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent, - }; - for (const key in derived) { - // $FlowFixMe[prop-missing] - derived[key] = Math.round(1000 * derived[key]) / 1000; - } - console.debug('FillRateHelper deactivateAndFlush: ', {derived, info}); - } - _listeners.forEach(listener => listener(info)); - this._resetData(); - } - - computeBlankness( - props: { - ...FrameMetricProps, - initialNumToRender?: ?number, - ... - }, - cellsAroundViewport: { - first: number, - last: number, - ... - }, - scrollMetrics: { - dOffset: number, - offset: number, - velocity: number, - visibleLength: number, - ... - }, - ): number { - if ( - !this._enabled || - props.getItemCount(props.data) === 0 || - cellsAroundViewport.last < cellsAroundViewport.first || - this._samplesStartTime == null - ) { - return 0; - } - const {dOffset, offset, velocity, visibleLength} = scrollMetrics; - - // Denominator metrics that we track for all events - most of the time there is no blankness and - // we want to capture that. - this._info.sample_count++; - this._info.pixels_sampled += Math.round(visibleLength); - this._info.pixels_scrolled += Math.round(Math.abs(dOffset)); - const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec - - // Whether blank now or not, record the elapsed time blank if we were blank last time. - const now = global.performance.now(); - if (this._anyBlankStartTime != null) { - this._info.any_blank_ms += now - this._anyBlankStartTime; - } - this._anyBlankStartTime = null; - if (this._mostlyBlankStartTime != null) { - this._info.mostly_blank_ms += now - this._mostlyBlankStartTime; - } - this._mostlyBlankStartTime = null; - - let blankTop = 0; - let first = cellsAroundViewport.first; - let firstFrame = this._getFrameMetrics(first, props); - while ( - first <= cellsAroundViewport.last && - (!firstFrame || !firstFrame.inLayout) - ) { - firstFrame = this._getFrameMetrics(first, props); - first++; - } - // Only count blankTop if we aren't rendering the first item, otherwise we will count the header - // as blank. - if (firstFrame && first > 0) { - blankTop = Math.min( - visibleLength, - Math.max(0, firstFrame.offset - offset), - ); - } - let blankBottom = 0; - let last = cellsAroundViewport.last; - let lastFrame = this._getFrameMetrics(last, props); - while ( - last >= cellsAroundViewport.first && - (!lastFrame || !lastFrame.inLayout) - ) { - lastFrame = this._getFrameMetrics(last, props); - last--; - } - // Only count blankBottom if we aren't rendering the last item, otherwise we will count the - // footer as blank. - if (lastFrame && last < props.getItemCount(props.data) - 1) { - const bottomEdge = lastFrame.offset + lastFrame.length; - blankBottom = Math.min( - visibleLength, - Math.max(0, offset + visibleLength - bottomEdge), - ); - } - const pixels_blank = Math.round(blankTop + blankBottom); - const blankness = pixels_blank / visibleLength; - if (blankness > 0) { - this._anyBlankStartTime = now; - this._info.any_blank_speed_sum += scrollSpeed; - this._info.any_blank_count++; - this._info.pixels_blank += pixels_blank; - if (blankness > 0.5) { - this._mostlyBlankStartTime = now; - this._info.mostly_blank_count++; - } - } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) { - this.deactivateAndFlush(); - } - return blankness; - } - - enabled(): boolean { - return this._enabled; - } - - _resetData() { - this._anyBlankStartTime = null; - this._info = new Info(); - this._mostlyBlankStartTime = null; - this._samplesStartTime = null; - } -} - -module.exports = FillRateHelper; diff --git a/packages/virtualized-lists/Lists/ViewabilityHelper.js b/packages/virtualized-lists/Lists/ViewabilityHelper.js deleted file mode 100644 index 33a9811825affd..00000000000000 --- a/packages/virtualized-lists/Lists/ViewabilityHelper.js +++ /dev/null @@ -1,360 +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 - * @format - */ - -'use strict'; - -import type {FrameMetricProps} from './VirtualizedListProps'; - -const invariant = require('invariant'); - -export type ViewToken = { - item: any, - key: string, - index: ?number, - isViewable: boolean, - section?: any, - ... -}; - -export type ViewabilityConfigCallbackPair = { - viewabilityConfig: ViewabilityConfig, - onViewableItemsChanged: (info: { - viewableItems: Array, - changed: Array, - ... - }) => void, - ... -}; - -export type ViewabilityConfig = {| - /** - * Minimum amount of time (in milliseconds) that an item must be physically viewable before the - * viewability callback will be fired. A high number means that scrolling through content without - * stopping will not mark the content as viewable. - */ - minimumViewTime?: number, - - /** - * Percent of viewport that must be covered for a partially occluded item to count as - * "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means - * that a single pixel in the viewport makes the item viewable, and a value of 100 means that - * an item must be either entirely visible or cover the entire viewport to count as viewable. - */ - viewAreaCoveragePercentThreshold?: number, - - /** - * Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible, - * rather than the fraction of the viewable area it covers. - */ - itemVisiblePercentThreshold?: number, - - /** - * Nothing is considered viewable until the user scrolls or `recordInteraction` is called after - * render. - */ - waitForInteraction?: boolean, -|}; - -/** - * A Utility class for calculating viewable items based on current metrics like scroll position and - * layout. - * - * An item is said to be in a "viewable" state when any of the following - * is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction` - * is true): - * - * - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item - * visible in the view area >= `itemVisiblePercentThreshold`. - * - Entirely visible on screen - */ -class ViewabilityHelper { - _config: ViewabilityConfig; - _hasInteracted: boolean = false; - _timers: Set = new Set(); - _viewableIndices: Array = []; - _viewableItems: Map = new Map(); - - constructor( - config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0}, - ) { - this._config = config; - } - - /** - * Cleanup, e.g. on unmount. Clears any pending timers. - */ - dispose() { - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To see - * the error delete this comment and run Flow. */ - this._timers.forEach(clearTimeout); - } - - /** - * Determines which items are viewable based on the current metrics and config. - */ - computeViewableItems( - props: FrameMetricProps, - scrollOffset: number, - viewportHeight: number, - getFrameMetrics: ( - index: number, - props: FrameMetricProps, - ) => ?{ - length: number, - offset: number, - ... - }, - // Optional optimization to reduce the scan size - renderRange?: { - first: number, - last: number, - ... - }, - ): Array { - const itemCount = props.getItemCount(props.data); - const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} = - this._config; - const viewAreaMode = viewAreaCoveragePercentThreshold != null; - const viewablePercentThreshold = viewAreaMode - ? viewAreaCoveragePercentThreshold - : itemVisiblePercentThreshold; - invariant( - viewablePercentThreshold != null && - (itemVisiblePercentThreshold != null) !== - (viewAreaCoveragePercentThreshold != null), - 'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold', - ); - const viewableIndices = []; - if (itemCount === 0) { - return viewableIndices; - } - let firstVisible = -1; - const {first, last} = renderRange || {first: 0, last: itemCount - 1}; - if (last >= itemCount) { - console.warn( - 'Invalid render range computing viewability ' + - JSON.stringify({renderRange, itemCount}), - ); - return []; - } - for (let idx = first; idx <= last; idx++) { - const metrics = getFrameMetrics(idx, props); - if (!metrics) { - continue; - } - const top = metrics.offset - scrollOffset; - const bottom = top + metrics.length; - if (top < viewportHeight && bottom > 0) { - firstVisible = idx; - if ( - _isViewable( - viewAreaMode, - viewablePercentThreshold, - top, - bottom, - viewportHeight, - metrics.length, - ) - ) { - viewableIndices.push(idx); - } - } else if (firstVisible >= 0) { - break; - } - } - return viewableIndices; - } - - /** - * Figures out which items are viewable and how that has changed from before and calls - * `onViewableItemsChanged` as appropriate. - */ - onUpdate( - props: FrameMetricProps, - scrollOffset: number, - viewportHeight: number, - getFrameMetrics: ( - index: number, - props: FrameMetricProps, - ) => ?{ - length: number, - offset: number, - ... - }, - createViewToken: ( - index: number, - isViewable: boolean, - props: FrameMetricProps, - ) => ViewToken, - onViewableItemsChanged: ({ - viewableItems: Array, - changed: Array, - ... - }) => void, - // Optional optimization to reduce the scan size - renderRange?: { - first: number, - last: number, - ... - }, - ): void { - const itemCount = props.getItemCount(props.data); - if ( - (this._config.waitForInteraction && !this._hasInteracted) || - itemCount === 0 || - !getFrameMetrics(0, props) - ) { - return; - } - let viewableIndices: Array = []; - if (itemCount) { - viewableIndices = this.computeViewableItems( - props, - scrollOffset, - viewportHeight, - getFrameMetrics, - renderRange, - ); - } - if ( - this._viewableIndices.length === viewableIndices.length && - this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) - ) { - // We might get a lot of scroll events where visibility doesn't change and we don't want to do - // extra work in those cases. - return; - } - this._viewableIndices = viewableIndices; - if (this._config.minimumViewTime) { - const handle: TimeoutID = setTimeout(() => { - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To - * see the error delete this comment and run Flow. */ - this._timers.delete(handle); - this._onUpdateSync( - props, - viewableIndices, - onViewableItemsChanged, - createViewToken, - ); - }, this._config.minimumViewTime); - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To see - * the error delete this comment and run Flow. */ - this._timers.add(handle); - } else { - this._onUpdateSync( - props, - viewableIndices, - onViewableItemsChanged, - createViewToken, - ); - } - } - - /** - * clean-up cached _viewableIndices to evaluate changed items on next update - */ - resetViewableIndices() { - this._viewableIndices = []; - } - - /** - * Records that an interaction has happened even if there has been no scroll. - */ - recordInteraction() { - this._hasInteracted = true; - } - - _onUpdateSync( - props: FrameMetricProps, - viewableIndicesToCheck: Array, - onViewableItemsChanged: ({ - changed: Array, - viewableItems: Array, - ... - }) => void, - createViewToken: ( - index: number, - isViewable: boolean, - props: FrameMetricProps, - ) => ViewToken, - ) { - // Filter out indices that have gone out of view since this call was scheduled. - viewableIndicesToCheck = viewableIndicesToCheck.filter(ii => - this._viewableIndices.includes(ii), - ); - const prevItems = this._viewableItems; - const nextItems = new Map( - viewableIndicesToCheck.map(ii => { - const viewable = createViewToken(ii, true, props); - return [viewable.key, viewable]; - }), - ); - - const changed = []; - for (const [key, viewable] of nextItems) { - if (!prevItems.has(key)) { - changed.push(viewable); - } - } - for (const [key, viewable] of prevItems) { - if (!nextItems.has(key)) { - changed.push({...viewable, isViewable: false}); - } - } - if (changed.length > 0) { - this._viewableItems = nextItems; - onViewableItemsChanged({ - viewableItems: Array.from(nextItems.values()), - changed, - viewabilityConfig: this._config, - }); - } - } -} - -function _isViewable( - viewAreaMode: boolean, - viewablePercentThreshold: number, - top: number, - bottom: number, - viewportHeight: number, - itemLength: number, -): boolean { - if (_isEntirelyVisible(top, bottom, viewportHeight)) { - return true; - } else { - const pixels = _getPixelsVisible(top, bottom, viewportHeight); - const percent = - 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength); - return percent >= viewablePercentThreshold; - } -} - -function _getPixelsVisible( - top: number, - bottom: number, - viewportHeight: number, -): number { - const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); - return Math.max(0, visibleHeight); -} - -function _isEntirelyVisible( - top: number, - bottom: number, - viewportHeight: number, -): boolean { - return top >= 0 && bottom <= viewportHeight && bottom > top; -} - -module.exports = ViewabilityHelper; diff --git a/packages/virtualized-lists/Lists/VirtualizeUtils.js b/packages/virtualized-lists/Lists/VirtualizeUtils.js deleted file mode 100644 index 3a70d9f683091e..00000000000000 --- a/packages/virtualized-lists/Lists/VirtualizeUtils.js +++ /dev/null @@ -1,258 +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 - * @format - */ - -'use strict'; - -import type {FrameMetricProps} from './VirtualizedListProps'; - -/** - * Used to find the indices of the frames that overlap the given offsets. Useful for finding the - * items that bound different windows of content, such as the visible area or the buffered overscan - * area. - */ -export function elementsThatOverlapOffsets( - offsets: Array, - props: FrameMetricProps, - getFrameMetrics: ( - index: number, - props: FrameMetricProps, - ) => { - length: number, - offset: number, - ... - }, - zoomScale: number = 1, -): Array { - const itemCount = props.getItemCount(props.data); - const result = []; - for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) { - const currentOffset = offsets[offsetIndex]; - let left = 0; - let right = itemCount - 1; - - while (left <= right) { - // eslint-disable-next-line no-bitwise - const mid = left + ((right - left) >>> 1); - const frame = getFrameMetrics(mid, props); - const scaledOffsetStart = frame.offset * zoomScale; - const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale; - - // We want the first frame that contains the offset, with inclusive bounds. Thus, for the - // first frame the scaledOffsetStart is inclusive, while for other frames it is exclusive. - if ( - (mid === 0 && currentOffset < scaledOffsetStart) || - (mid !== 0 && currentOffset <= scaledOffsetStart) - ) { - right = mid - 1; - } else if (currentOffset > scaledOffsetEnd) { - left = mid + 1; - } else { - result[offsetIndex] = mid; - break; - } - } - } - - return result; -} - -/** - * Computes the number of elements in the `next` range that are new compared to the `prev` range. - * Handy for calculating how many new items will be rendered when the render window changes so we - * can restrict the number of new items render at once so that content can appear on the screen - * faster. - */ -export function newRangeCount( - prev: { - first: number, - last: number, - ... - }, - next: { - first: number, - last: number, - ... - }, -): number { - return ( - next.last - - next.first + - 1 - - Math.max( - 0, - 1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first), - ) - ); -} - -/** - * Custom logic for determining which items should be rendered given the current frame and scroll - * metrics, as well as the previous render state. The algorithm may evolve over time, but generally - * prioritizes the visible area first, then expands that with overscan regions ahead and behind, - * biased in the direction of scroll. - */ -export function computeWindowedRenderLimits( - props: FrameMetricProps, - maxToRenderPerBatch: number, - windowSize: number, - prev: { - first: number, - last: number, - }, - getFrameMetricsApprox: ( - index: number, - props: FrameMetricProps, - ) => { - length: number, - offset: number, - ... - }, - scrollMetrics: { - dt: number, - offset: number, - velocity: number, - visibleLength: number, - zoomScale: number, - ... - }, -): { - first: number, - last: number, -} { - const itemCount = props.getItemCount(props.data); - if (itemCount === 0) { - return {first: 0, last: -1}; - } - const {offset, velocity, visibleLength, zoomScale = 1} = scrollMetrics; - - // Start with visible area, then compute maximum overscan region by expanding from there, biased - // in the direction of scroll. Total overscan area is capped, which should cap memory consumption - // too. - const visibleBegin = Math.max(0, offset); - const visibleEnd = visibleBegin + visibleLength; - const overscanLength = (windowSize - 1) * visibleLength; - - // Considering velocity seems to introduce more churn than it's worth. - const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5)); - - const fillPreference = - velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none'; - - const overscanBegin = Math.max( - 0, - visibleBegin - (1 - leadFactor) * overscanLength, - ); - const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength); - - const lastItemOffset = - getFrameMetricsApprox(itemCount - 1, props).offset * zoomScale; - if (lastItemOffset < overscanBegin) { - // Entire list is before our overscan window - return { - first: Math.max(0, itemCount - 1 - maxToRenderPerBatch), - last: itemCount - 1, - }; - } - - // Find the indices that correspond to the items at the render boundaries we're targeting. - let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets( - [overscanBegin, visibleBegin, visibleEnd, overscanEnd], - props, - getFrameMetricsApprox, - zoomScale, - ); - overscanFirst = overscanFirst == null ? 0 : overscanFirst; - first = first == null ? Math.max(0, overscanFirst) : first; - overscanLast = overscanLast == null ? itemCount - 1 : overscanLast; - last = - last == null - ? Math.min(overscanLast, first + maxToRenderPerBatch - 1) - : last; - const visible = {first, last}; - - // We want to limit the number of new cells we're rendering per batch so that we can fill the - // content on the screen quickly. If we rendered the entire overscan window at once, the user - // could be staring at white space for a long time waiting for a bunch of offscreen content to - // render. - let newCellCount = newRangeCount(prev, visible); - - while (true) { - if (first <= overscanFirst && last >= overscanLast) { - // If we fill the entire overscan range, we're done. - break; - } - const maxNewCells = newCellCount >= maxToRenderPerBatch; - const firstWillAddMore = first <= prev.first || first > prev.last; - const firstShouldIncrement = - first > overscanFirst && (!maxNewCells || !firstWillAddMore); - const lastWillAddMore = last >= prev.last || last < prev.first; - const lastShouldIncrement = - last < overscanLast && (!maxNewCells || !lastWillAddMore); - if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) { - // We only want to stop if we've hit maxNewCells AND we cannot increment first or last - // without rendering new items. This let's us preserve as many already rendered items as - // possible, reducing render churn and keeping the rendered overscan range as large as - // possible. - break; - } - if ( - firstShouldIncrement && - !(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore) - ) { - if (firstWillAddMore) { - newCellCount++; - } - first--; - } - if ( - lastShouldIncrement && - !(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore) - ) { - if (lastWillAddMore) { - newCellCount++; - } - last++; - } - } - if ( - !( - last >= first && - first >= 0 && - last < itemCount && - first >= overscanFirst && - last <= overscanLast && - first <= visible.first && - last >= visible.last - ) - ) { - throw new Error( - 'Bad window calculation ' + - JSON.stringify({ - first, - last, - itemCount, - overscanFirst, - overscanLast, - visible, - }), - ); - } - return {first, last}; -} - -export function keyExtractor(item: any, index: number): string { - if (typeof item === 'object' && item?.key != null) { - return item.key; - } - if (typeof item === 'object' && item?.id != null) { - return item.id; - } - return String(index); -} diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js deleted file mode 100644 index b93878c5658295..00000000000000 --- a/packages/virtualized-lists/Lists/VirtualizedList.js +++ /dev/null @@ -1,1955 +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 - * @format - */ - -import type {ScrollResponderType} from 'react-native/Libraries/Components/ScrollView/ScrollView'; -import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; -import type { - LayoutEvent, - ScrollEvent, -} from 'react-native/Libraries/Types/CoreEventTypes'; -import type {ViewToken} from './ViewabilityHelper'; -import type { - FrameMetricProps, - Item, - Props, - RenderItemProps, - RenderItemType, - Separators, -} from './VirtualizedListProps'; - -import { - RefreshControl, - ScrollView, - View, - StyleSheet, - findNodeHandle, -} from 'react-native'; -import Batchinator from '../Interaction/Batchinator'; -import clamp from '../Utilities/clamp'; -import infoLog from '../Utilities/infoLog'; -import {CellRenderMask} from './CellRenderMask'; -import ChildListCollection from './ChildListCollection'; -import FillRateHelper from './FillRateHelper'; -import StateSafePureComponent from './StateSafePureComponent'; -import ViewabilityHelper from './ViewabilityHelper'; -import CellRenderer from './VirtualizedListCellRenderer'; -import { - VirtualizedListCellContextProvider, - VirtualizedListContext, - VirtualizedListContextProvider, -} from './VirtualizedListContext.js'; -import { - computeWindowedRenderLimits, - keyExtractor as defaultKeyExtractor, -} from './VirtualizeUtils'; -import invariant from 'invariant'; -import * as React from 'react'; - -export type {RenderItemProps, RenderItemType, Separators}; - -const ON_EDGE_REACHED_EPSILON = 0.001; - -let _usedIndexForKey = false; -let _keylessItemComponentName: string = ''; - -type ViewabilityHelperCallbackTuple = { - viewabilityHelper: ViewabilityHelper, - onViewableItemsChanged: (info: { - viewableItems: Array, - changed: Array, - ... - }) => void, - ... -}; - -type State = { - renderMask: CellRenderMask, - cellsAroundViewport: {first: number, last: number}, -}; - -/** - * Default Props Helper Functions - * Use the following helper functions for default values - */ - -// horizontalOrDefault(this.props.horizontal) -function horizontalOrDefault(horizontal: ?boolean) { - return horizontal ?? false; -} - -// initialNumToRenderOrDefault(this.props.initialNumToRenderOrDefault) -function initialNumToRenderOrDefault(initialNumToRender: ?number) { - return initialNumToRender ?? 10; -} - -// maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch) -function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) { - return maxToRenderPerBatch ?? 10; -} - -// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold) -function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) { - return onStartReachedThreshold ?? 2; -} - -// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold) -function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) { - return onEndReachedThreshold ?? 2; -} - -// getScrollingThreshold(visibleLength, onEndReachedThreshold) -function getScrollingThreshold(threshold: number, visibleLength: number) { - return (threshold * visibleLength) / 2; -} - -// scrollEventThrottleOrDefault(this.props.scrollEventThrottle) -function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) { - return scrollEventThrottle ?? 50; -} - -// windowSizeOrDefault(this.props.windowSize) -function windowSizeOrDefault(windowSize: ?number) { - return windowSize ?? 21; -} - -function findLastWhere( - arr: $ReadOnlyArray, - predicate: (element: T) => boolean, -): T | null { - for (let i = arr.length - 1; i >= 0; i--) { - if (predicate(arr[i])) { - return arr[i]; - } - } - - return null; -} - -/** - * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) - * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better - * documented. In general, this should only really be used if you need more flexibility than - * `FlatList` provides, e.g. for use with immutable data instead of plain arrays. - * - * Virtualization massively improves memory consumption and performance of large lists by - * maintaining a finite render window of active items and replacing all items outside of the render - * window with appropriately sized blank space. The window adapts to scrolling behavior, and items - * are rendered incrementally with low-pri (after any running interactions) if they are far from the - * visible area, or with hi-pri otherwise to minimize the potential of seeing blank space. - * - * Some caveats: - * - * - Internal state is not preserved when content scrolls out of the render window. Make sure all - * your data is captured in the item data or external stores like Flux, Redux, or Relay. - * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- - * equal. Make sure that everything your `renderItem` function depends on is passed as a prop - * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on - * changes. This includes the `data` prop and parent component state. - * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously - * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see - * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, - * and we are working on improving it behind the scenes. - * - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key. - * Alternatively, you can provide a custom `keyExtractor` prop. - * - As an effort to remove defaultProps, use helper functions when referencing certain props - * - */ -class VirtualizedList extends StateSafePureComponent { - static contextType: typeof VirtualizedListContext = VirtualizedListContext; - - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params?: ?{animated?: ?boolean, ...}) { - const animated = params ? params.animated : true; - const veryLast = this.props.getItemCount(this.props.data) - 1; - if (veryLast < 0) { - return; - } - const frame = this.__getFrameMetricsApprox(veryLast, this.props); - const offset = Math.max( - 0, - frame.offset + - frame.length + - this._footerLength - - this._scrollMetrics.visibleLength, - ); - - if (this._scrollRef == null) { - return; - } - - if (this._scrollRef.scrollTo == null) { - console.warn( - 'No scrollTo method provided. This may be because you have two nested ' + - 'VirtualizedLists with the same orientation, or because you are ' + - 'using a custom component that does not implement scrollTo.', - ); - return; - } - - this._scrollRef.scrollTo( - horizontalOrDefault(this.props.horizontal) - ? {x: offset, animated} - : {y: offset, animated}, - ); - } - - // scrollToIndex may be janky without getItemLayout prop - scrollToIndex(params: { - animated?: ?boolean, - index: number, - viewOffset?: number, - viewPosition?: number, - ... - }): $FlowFixMe { - const { - data, - horizontal, - getItemCount, - getItemLayout, - onScrollToIndexFailed, - } = this.props; - const {animated, index, viewOffset, viewPosition} = params; - invariant( - index >= 0, - `scrollToIndex out of range: requested index ${index} but minimum is 0`, - ); - invariant( - getItemCount(data) >= 1, - `scrollToIndex out of range: item length ${getItemCount( - data, - )} but minimum is 1`, - ); - invariant( - index < getItemCount(data), - `scrollToIndex out of range: requested index ${index} is out of 0 to ${ - getItemCount(data) - 1 - }`, - ); - if (!getItemLayout && index > this._highestMeasuredFrameIndex) { - invariant( - !!onScrollToIndexFailed, - 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' + - 'otherwise there is no way to know the location of offscreen indices or handle failures.', - ); - onScrollToIndexFailed({ - averageItemLength: this._averageCellLength, - highestMeasuredFrameIndex: this._highestMeasuredFrameIndex, - index, - }); - return; - } - const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props); - const offset = - Math.max( - 0, - this._getOffsetApprox(index, this.props) - - (viewPosition || 0) * - (this._scrollMetrics.visibleLength - frame.length), - ) - (viewOffset || 0); - - if (this._scrollRef == null) { - return; - } - - if (this._scrollRef.scrollTo == null) { - console.warn( - 'No scrollTo method provided. This may be because you have two nested ' + - 'VirtualizedLists with the same orientation, or because you are ' + - 'using a custom component that does not implement scrollTo.', - ); - return; - } - - this._scrollRef.scrollTo( - horizontal ? {x: offset, animated} : {y: offset, animated}, - ); - } - - // scrollToItem may be janky without getItemLayout prop. Required linear scan through items - - // use scrollToIndex instead if possible. - scrollToItem(params: { - animated?: ?boolean, - item: Item, - viewOffset?: number, - viewPosition?: number, - ... - }) { - const {item} = params; - const {data, getItem, getItemCount} = this.props; - const itemCount = getItemCount(data); - for (let index = 0; index < itemCount; index++) { - if (getItem(data, index) === item) { - this.scrollToIndex({...params, index}); - break; - } - } - } - - /** - * Scroll to a specific content pixel offset in the list. - * - * Param `offset` expects the offset to scroll to. - * In case of `horizontal` is true, the offset is the x-value, - * in any other case the offset is the y-value. - * - * Param `animated` (`true` by default) defines whether the list - * should do an animation while scrolling. - */ - scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { - const {animated, offset} = params; - - if (this._scrollRef == null) { - return; - } - - if (this._scrollRef.scrollTo == null) { - console.warn( - 'No scrollTo method provided. This may be because you have two nested ' + - 'VirtualizedLists with the same orientation, or because you are ' + - 'using a custom component that does not implement scrollTo.', - ); - return; - } - - this._scrollRef.scrollTo( - horizontalOrDefault(this.props.horizontal) - ? {x: offset, animated} - : {y: offset, animated}, - ); - } - - recordInteraction() { - this._nestedChildLists.forEach(childList => { - childList.recordInteraction(); - }); - this._viewabilityTuples.forEach(t => { - t.viewabilityHelper.recordInteraction(); - }); - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - } - - flashScrollIndicators() { - if (this._scrollRef == null) { - return; - } - - this._scrollRef.flashScrollIndicators(); - } - - /** - * Provides a handle to the underlying scroll responder. - * Note that `this._scrollRef` might not be a `ScrollView`, so we - * need to check that it responds to `getScrollResponder` before calling it. - */ - getScrollResponder(): ?ScrollResponderType { - if (this._scrollRef && this._scrollRef.getScrollResponder) { - return this._scrollRef.getScrollResponder(); - } - } - - getScrollableNode(): ?number { - if (this._scrollRef && this._scrollRef.getScrollableNode) { - return this._scrollRef.getScrollableNode(); - } else { - return findNodeHandle(this._scrollRef); - } - } - - getScrollRef(): - | ?React.ElementRef - | ?React.ElementRef { - if (this._scrollRef && this._scrollRef.getScrollRef) { - return this._scrollRef.getScrollRef(); - } else { - return this._scrollRef; - } - } - - setNativeProps(props: Object) { - if (this._scrollRef) { - this._scrollRef.setNativeProps(props); - } - } - - _getCellKey(): string { - return this.context?.cellKey || 'rootList'; - } - - // $FlowFixMe[missing-local-annot] - _getScrollMetrics = () => { - return this._scrollMetrics; - }; - - hasMore(): boolean { - return this._hasMore; - } - - // $FlowFixMe[missing-local-annot] - _getOutermostParentListRef = () => { - if (this._isNestedWithSameOrientation()) { - return this.context.getOutermostParentListRef(); - } else { - return this; - } - }; - - _registerAsNestedChild = (childList: { - cellKey: string, - ref: React.ElementRef, - }): void => { - this._nestedChildLists.add(childList.ref, childList.cellKey); - if (this._hasInteracted) { - childList.ref.recordInteraction(); - } - }; - - _unregisterAsNestedChild = (childList: { - ref: React.ElementRef, - }): void => { - this._nestedChildLists.remove(childList.ref); - }; - - state: State; - - constructor(props: Props) { - super(props); - invariant( - // $FlowFixMe[prop-missing] - !props.onScroll || !props.onScroll.__isNative, - 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + - 'to support native onScroll events with useNativeDriver', - ); - invariant( - windowSizeOrDefault(props.windowSize) > 0, - 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.', - ); - - invariant( - props.getItemCount, - 'VirtualizedList: The "getItemCount" prop must be provided', - ); - - this._fillRateHelper = new FillRateHelper(this._getFrameMetrics); - this._updateCellsToRenderBatcher = new Batchinator( - this._updateCellsToRender, - this.props.updateCellsBatchingPeriod ?? 50, - ); - - if (this.props.viewabilityConfigCallbackPairs) { - this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map( - pair => ({ - viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig), - onViewableItemsChanged: pair.onViewableItemsChanged, - }), - ); - } else { - const {onViewableItemsChanged, viewabilityConfig} = this.props; - if (onViewableItemsChanged) { - this._viewabilityTuples.push({ - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged, - }); - } - } - - invariant( - !this.context, - 'Unexpectedly saw VirtualizedListContext available in ctor', - ); - - const initialRenderRegion = VirtualizedList._initialRenderRegion(props); - - this.state = { - cellsAroundViewport: initialRenderRegion, - renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), - }; - } - - static _createRenderMask( - props: Props, - cellsAroundViewport: {first: number, last: number}, - additionalRegions?: ?$ReadOnlyArray<{first: number, last: number}>, - ): CellRenderMask { - const itemCount = props.getItemCount(props.data); - - invariant( - cellsAroundViewport.first >= 0 && - cellsAroundViewport.last >= cellsAroundViewport.first - 1 && - cellsAroundViewport.last < itemCount, - `Invalid cells around viewport "[${cellsAroundViewport.first}, ${cellsAroundViewport.last}]" was passed to VirtualizedList._createRenderMask`, - ); - - const renderMask = new CellRenderMask(itemCount); - - if (itemCount > 0) { - const allRegions = [cellsAroundViewport, ...(additionalRegions ?? [])]; - for (const region of allRegions) { - renderMask.addCells(region); - } - - // The initially rendered cells are retained as part of the - // "scroll-to-top" optimization - if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) { - const initialRegion = VirtualizedList._initialRenderRegion(props); - renderMask.addCells(initialRegion); - } - - // The layout coordinates of sticker headers may be off-screen while the - // actual header is on-screen. Keep the most recent before the viewport - // rendered, even if its layout coordinates are not in viewport. - const stickyIndicesSet = new Set(props.stickyHeaderIndices); - VirtualizedList._ensureClosestStickyHeader( - props, - stickyIndicesSet, - renderMask, - cellsAroundViewport.first, - ); - } - - return renderMask; - } - - static _initialRenderRegion(props: Props): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); - const scrollIndex = Math.floor(Math.max(0, props.initialScrollIndex ?? 0)); - - return { - first: scrollIndex, - last: - Math.min( - itemCount, - scrollIndex + initialNumToRenderOrDefault(props.initialNumToRender), - ) - 1, - }; - } - - static _ensureClosestStickyHeader( - props: Props, - stickyIndicesSet: Set, - renderMask: CellRenderMask, - cellIdx: number, - ) { - const stickyOffset = props.ListHeaderComponent ? 1 : 0; - - for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) { - if (stickyIndicesSet.has(itemIdx + stickyOffset)) { - renderMask.addCells({first: itemIdx, last: itemIdx}); - break; - } - } - } - - _adjustCellsAroundViewport( - props: Props, - cellsAroundViewport: {first: number, last: number}, - ): {first: number, last: number} { - const {data, getItemCount} = props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( - props.onEndReachedThreshold, - ); - this._updateViewableItems(props, cellsAroundViewport); - - const {contentLength, offset, visibleLength} = this._scrollMetrics; - const distanceFromEnd = contentLength - visibleLength - offset; - - // Wait until the scroll view metrics have been set up. And until then, - // we will trust the initialNumToRender suggestion - if (visibleLength <= 0 || contentLength <= 0) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; - } - - let newCellsAroundViewport: {first: number, last: number}; - if (props.disableVirtualization) { - const renderAhead = - distanceFromEnd < onEndReachedThreshold * visibleLength - ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) - : 0; - - newCellsAroundViewport = { - first: 0, - last: Math.min( - cellsAroundViewport.last + renderAhead, - getItemCount(data) - 1, - ), - }; - } else { - // If we have a non-zero initialScrollIndex and run this before we've scrolled, - // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. - // So let's wait until we've scrolled the view to the right place. And until then, - // we will trust the initialScrollIndex suggestion. - - // Thus, we want to recalculate the windowed render limits if any of the following hold: - // - initialScrollIndex is undefined or is 0 - // - initialScrollIndex > 0 AND scrolling is complete - // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case - // where the list is shorter than the visible area) - if ( - props.initialScrollIndex && - !this._scrollMetrics.offset && - Math.abs(distanceFromEnd) >= Number.EPSILON - ) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; - } - - newCellsAroundViewport = computeWindowedRenderLimits( - props, - maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), - windowSizeOrDefault(props.windowSize), - cellsAroundViewport, - this.__getFrameMetricsApprox, - this._scrollMetrics, - ); - invariant( - newCellsAroundViewport.last < getItemCount(data), - 'computeWindowedRenderLimits() should return range in-bounds', - ); - } - - if (this._nestedChildLists.size() > 0) { - // If some cell in the new state has a child list in it, we should only render - // up through that item, so that we give that list a chance to render. - // Otherwise there's churn from multiple child lists mounting and un-mounting - // their items. - - // Will this prevent rendering if the nested list doesn't realize the end? - const childIdx = this._findFirstChildWithMore( - newCellsAroundViewport.first, - newCellsAroundViewport.last, - ); - - newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last; - } - - return newCellsAroundViewport; - } - - _findFirstChildWithMore(first: number, last: number): number | null { - for (let ii = first; ii <= last; ii++) { - const cellKeyForIndex = this._indicesToKeys.get(ii); - if ( - cellKeyForIndex != null && - this._nestedChildLists.anyInCell(cellKeyForIndex, childList => - childList.hasMore(), - ) - ) { - return ii; - } - } - - return null; - } - - componentDidMount() { - if (this._isNestedWithSameOrientation()) { - this.context.registerAsNestedChild({ - ref: this, - cellKey: this.context.cellKey, - }); - } - } - - componentWillUnmount() { - if (this._isNestedWithSameOrientation()) { - this.context.unregisterAsNestedChild({ref: this}); - } - this._updateCellsToRenderBatcher.dispose({abort: true}); - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.dispose(); - }); - this._fillRateHelper.deactivateAndFlush(); - } - - static getDerivedStateFromProps(newProps: Props, prevState: State): State { - // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make - // sure we're rendering a reasonable range here. - const itemCount = newProps.getItemCount(newProps.data); - if (itemCount === prevState.renderMask.numCells()) { - return prevState; - } - - const constrainedCells = VirtualizedList._constrainToItemCount( - prevState.cellsAroundViewport, - newProps, - ); - - return { - cellsAroundViewport: constrainedCells, - renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), - }; - } - - _pushCells( - cells: Array, - stickyHeaderIndices: Array, - stickyIndicesFromProps: Set, - first: number, - last: number, - inversionStyle: ViewStyleProp, - ) { - const { - CellRendererComponent, - ItemSeparatorComponent, - ListHeaderComponent, - ListItemComponent, - data, - debug, - getItem, - getItemCount, - getItemLayout, - horizontal, - renderItem, - } = this.props; - const stickyOffset = ListHeaderComponent ? 1 : 0; - const end = getItemCount(data) - 1; - let prevCellKey; - last = Math.min(end, last); - for (let ii = first; ii <= last; ii++) { - const item = getItem(data, ii); - const key = this._keyExtractor(item, ii, this.props); - this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { - stickyHeaderIndices.push(cells.length); - } - cells.push( - this._onCellFocusCapture(key)} - onUnmount={this._onCellUnmount} - ref={ref => { - this._cellRefs[key] = ref; - }} - renderItem={renderItem} - />, - ); - prevCellKey = key; - } - } - - static _constrainToItemCount( - cells: {first: number, last: number}, - props: Props, - ): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); - const last = Math.min(itemCount - 1, cells.last); - - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - props.maxToRenderPerBatch, - ); - - return { - first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), - last, - }; - } - - _onUpdateSeparators = (keys: Array, newProps: Object) => { - keys.forEach(key => { - const ref = key != null && this._cellRefs[key]; - ref && ref.updateSeparatorProps(newProps); - }); - }; - - _isNestedWithSameOrientation(): boolean { - const nestedContext = this.context; - return !!( - nestedContext && - !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal) - ); - } - - _getSpacerKey = (isVertical: boolean): string => - isVertical ? 'height' : 'width'; - - _keyExtractor( - item: Item, - index: number, - props: { - keyExtractor?: ?(item: Item, index: number) => string, - ... - }, - // $FlowFixMe[missing-local-annot] - ) { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } - - const key = defaultKeyExtractor(item, index); - if (key === String(index)) { - _usedIndexForKey = true; - if (item.type && item.type.displayName) { - _keylessItemComponentName = item.type.displayName; - } - } - return key; - } - - render(): React.Node { - if (__DEV__) { - // $FlowFixMe[underconstrained-implicit-instantiation] - const flatStyles = StyleSheet.flatten(this.props.contentContainerStyle); - if (flatStyles != null && flatStyles.flexWrap === 'wrap') { - console.warn( - '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + - 'Consider using `numColumns` with `FlatList` instead.', - ); - } - } - const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = - this.props; - const {data, horizontal} = this.props; - const inversionStyle = this.props.inverted - ? horizontalOrDefault(this.props.horizontal) - ? styles.horizontallyInverted - : styles.verticallyInverted - : null; - const cells: Array = []; - const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); - const stickyHeaderIndices = []; - - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { - stickyHeaderIndices.push(0); - } - const element = React.isValidElement(ListHeaderComponent) ? ( - ListHeaderComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - ); - cells.push( - - - { - // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors - element - } - - , - ); - } - - // 2a. Add a cell for ListEmptyComponent if applicable - const itemCount = this.props.getItemCount(data); - if (itemCount === 0 && ListEmptyComponent) { - const element: React.Element = ((React.isValidElement( - ListEmptyComponent, - ) ? ( - ListEmptyComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - )): any); - cells.push( - - {React.cloneElement(element, { - onLayout: (event: LayoutEvent) => { - this._onLayoutEmpty(event); - if (element.props.onLayout) { - element.props.onLayout(event); - } - }, - style: StyleSheet.compose(inversionStyle, element.props.style), - })} - , - ); - } - - // 2b. Add cells and spacers for each item - if (itemCount > 0) { - _usedIndexForKey = false; - _keylessItemComponentName = ''; - const spacerKey = this._getSpacerKey(!horizontal); - - const renderRegions = this.state.renderMask.enumerateRegions(); - const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); - - for (const section of renderRegions) { - if (section.isSpacer) { - // Legacy behavior is to avoid spacers when virtualization is - // disabled (including head spacers on initial render). - if (this.props.disableVirtualization) { - continue; - } - - // Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to - // prevent the user for hyperscrolling into un-measured area because otherwise content will - // likely jump around as it renders in above the viewport. - const isLastSpacer = section === lastSpacer; - const constrainToMeasured = isLastSpacer && !this.props.getItemLayout; - const last = constrainToMeasured - ? clamp( - section.first - 1, - section.last, - this._highestMeasuredFrameIndex, - ) - : section.last; - - const firstMetrics = this.__getFrameMetricsApprox( - section.first, - this.props, - ); - const lastMetrics = this.__getFrameMetricsApprox(last, this.props); - const spacerSize = - lastMetrics.offset + lastMetrics.length - firstMetrics.offset; - cells.push( - , - ); - } else { - this._pushCells( - cells, - stickyHeaderIndices, - stickyIndicesFromProps, - section.first, - section.last, - inversionStyle, - ); - } - } - - if (!this._hasWarned.keys && _usedIndexForKey) { - console.warn( - 'VirtualizedList: missing keys for items, make sure to specify a key or id property on each ' + - 'item or provide a custom keyExtractor.', - _keylessItemComponentName, - ); - this._hasWarned.keys = true; - } - } - - // 3. Add cell for ListFooterComponent - if (ListFooterComponent) { - const element = React.isValidElement(ListFooterComponent) ? ( - ListFooterComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - ); - cells.push( - - - { - // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors - element - } - - , - ); - } - - // 4. Render the ScrollView - const scrollProps = { - ...this.props, - onContentSizeChange: this._onContentSizeChange, - onLayout: this._onLayout, - onScroll: this._onScroll, - onScrollBeginDrag: this._onScrollBeginDrag, - onScrollEndDrag: this._onScrollEndDrag, - onMomentumScrollBegin: this._onMomentumScrollBegin, - onMomentumScrollEnd: this._onMomentumScrollEnd, - scrollEventThrottle: scrollEventThrottleOrDefault( - this.props.scrollEventThrottle, - ), // TODO: Android support - invertStickyHeaders: - this.props.invertStickyHeaders !== undefined - ? this.props.invertStickyHeaders - : this.props.inverted, - stickyHeaderIndices, - style: inversionStyle - ? [inversionStyle, this.props.style] - : this.props.style, - }; - - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; - - const innerRet = ( - - {React.cloneElement( - ( - this.props.renderScrollComponent || - this._defaultRenderScrollComponent - )(scrollProps), - { - ref: this._captureScrollRef, - }, - cells, - )} - - ); - let ret: React.Node = innerRet; - if (__DEV__) { - ret = ( - - {scrollContext => { - if ( - scrollContext != null && - !scrollContext.horizontal === - !horizontalOrDefault(this.props.horizontal) && - !this._hasWarned.nesting && - this.context == null && - this.props.scrollEnabled !== false - ) { - // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170 - console.error( - 'VirtualizedLists should never be nested inside plain ScrollViews with the same ' + - 'orientation because it can break windowing and other functionality - use another ' + - 'VirtualizedList-backed container instead.', - ); - this._hasWarned.nesting = true; - } - return innerRet; - }} - - ); - } - if (this.props.debug) { - return ( - - {ret} - {this._renderDebugOverlay()} - - ); - } else { - return ret; - } - } - - componentDidUpdate(prevProps: Props) { - const {data, extraData} = this.props; - if (data !== prevProps.data || extraData !== prevProps.extraData) { - // clear the viewableIndices cache to also trigger - // the onViewableItemsChanged callback with the new data - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.resetViewableIndices(); - }); - } - // The `this._hiPriInProgress` is guaranteeing a hiPri cell update will only happen - // once per fiber update. The `_scheduleCellsToRenderUpdate` will set it to true - // if a hiPri update needs to perform. If `componentDidUpdate` is triggered with - // `this._hiPriInProgress=true`, means it's triggered by the hiPri update. The - // `_scheduleCellsToRenderUpdate` will check this condition and not perform - // another hiPri update. - const hiPriInProgress = this._hiPriInProgress; - this._scheduleCellsToRenderUpdate(); - // Make sure setting `this._hiPriInProgress` back to false after `componentDidUpdate` - // is triggered with `this._hiPriInProgress = true` - if (hiPriInProgress) { - this._hiPriInProgress = false; - } - } - - _averageCellLength = 0; - _cellRefs: {[string]: null | CellRenderer} = {}; - _fillRateHelper: FillRateHelper; - _frames: { - [string]: { - inLayout?: boolean, - index: number, - length: number, - offset: number, - }, - } = {}; - _footerLength = 0; - // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex - _hasTriggeredInitialScrollToIndex = false; - _hasInteracted = false; - _hasMore = false; - _hasWarned: {[string]: boolean} = {}; - _headerLength = 0; - _hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update - _highestMeasuredFrameIndex = 0; - _indicesToKeys: Map = new Map(); - _lastFocusedCellKey: ?string = null; - _nestedChildLists: ChildListCollection = - new ChildListCollection(); - _offsetFromParentVirtualizedList: number = 0; - _prevParentOffset: number = 0; - // $FlowFixMe[missing-local-annot] - _scrollMetrics = { - contentLength: 0, - dOffset: 0, - dt: 10, - offset: 0, - timestamp: 0, - velocity: 0, - visibleLength: 0, - zoomScale: 1, - }; - _scrollRef: ?React.ElementRef = null; - _sentStartForContentLength = 0; - _sentEndForContentLength = 0; - _totalCellLength = 0; - _totalCellsMeasured = 0; - _updateCellsToRenderBatcher: Batchinator; - _viewabilityTuples: Array = []; - - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ - _captureScrollRef = ref => { - this._scrollRef = ref; - }; - - _computeBlankness() { - this._fillRateHelper.computeBlankness( - this.props, - this.state.cellsAroundViewport, - this._scrollMetrics, - ); - } - - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; - } else if (onRefresh) { - invariant( - typeof props.refreshing === 'boolean', - '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + - JSON.stringify(props.refreshing ?? 'undefined') + - '`', - ); - return ( - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] - - ) : ( - props.refreshControl - ) - } - /> - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] - return ; - } - }; - - _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => { - const layout = e.nativeEvent.layout; - const next = { - offset: this._selectOffset(layout), - length: this._selectLength(layout), - index, - inLayout: true, - }; - const curr = this._frames[cellKey]; - if ( - !curr || - next.offset !== curr.offset || - next.length !== curr.length || - index !== curr.index - ) { - this._totalCellLength += next.length - (curr ? curr.length : 0); - this._totalCellsMeasured += curr ? 0 : 1; - this._averageCellLength = - this._totalCellLength / this._totalCellsMeasured; - this._frames[cellKey] = next; - this._highestMeasuredFrameIndex = Math.max( - this._highestMeasuredFrameIndex, - index, - ); - this._scheduleCellsToRenderUpdate(); - } else { - this._frames[cellKey].inLayout = true; - } - - this._triggerRemeasureForChildListsInCell(cellKey); - - this._computeBlankness(); - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - }; - - _onCellFocusCapture(cellKey: string) { - this._lastFocusedCellKey = cellKey; - const renderMask = VirtualizedList._createRenderMask( - this.props, - this.state.cellsAroundViewport, - this._getNonViewportRenderRegions(this.props), - ); - - this.setState(state => { - if (!renderMask.equals(state.renderMask)) { - return {renderMask}; - } - return null; - }); - } - - _onCellUnmount = (cellKey: string) => { - const curr = this._frames[cellKey]; - if (curr) { - this._frames[cellKey] = {...curr, inLayout: false}; - } - }; - - _triggerRemeasureForChildListsInCell(cellKey: string): void { - this._nestedChildLists.forEachInCell(cellKey, childList => { - childList.measureLayoutRelativeToContainingList(); - }); - } - - measureLayoutRelativeToContainingList(): void { - // TODO (T35574538): findNodeHandle sometimes crashes with "Unable to find - // node on an unmounted component" during scrolling - try { - if (!this._scrollRef) { - return; - } - // We are assuming that getOutermostParentListRef().getScrollRef() - // is a non-null reference to a ScrollView - this._scrollRef.measureLayout( - this.context.getOutermostParentListRef().getScrollRef(), - (x, y, width, height) => { - this._offsetFromParentVirtualizedList = this._selectOffset({x, y}); - this._scrollMetrics.contentLength = this._selectLength({ - width, - height, - }); - const scrollMetrics = this._convertParentScrollMetrics( - this.context.getScrollMetrics(), - ); - - const metricsChanged = - this._scrollMetrics.visibleLength !== scrollMetrics.visibleLength || - this._scrollMetrics.offset !== scrollMetrics.offset; - - if (metricsChanged) { - this._scrollMetrics.visibleLength = scrollMetrics.visibleLength; - this._scrollMetrics.offset = scrollMetrics.offset; - - // If metrics of the scrollView changed, then we triggered remeasure for child list - // to ensure VirtualizedList has the right information. - this._nestedChildLists.forEach(childList => { - childList.measureLayoutRelativeToContainingList(); - }); - } - }, - error => { - console.warn( - "VirtualizedList: Encountered an error while measuring a list's" + - ' offset from its containing VirtualizedList.', - ); - }, - ); - } catch (error) { - console.warn( - 'measureLayoutRelativeToContainingList threw an error', - error.stack, - ); - } - } - - _onLayout = (e: LayoutEvent) => { - if (this._isNestedWithSameOrientation()) { - // Need to adjust our scroll metrics to be relative to our containing - // VirtualizedList before we can make claims about list item viewability - this.measureLayoutRelativeToContainingList(); - } else { - this._scrollMetrics.visibleLength = this._selectLength( - e.nativeEvent.layout, - ); - } - this.props.onLayout && this.props.onLayout(e); - this._scheduleCellsToRenderUpdate(); - this._maybeCallOnEdgeReached(); - }; - - _onLayoutEmpty = (e: LayoutEvent) => { - this.props.onLayout && this.props.onLayout(e); - }; - - _getFooterCellKey(): string { - return this._getCellKey() + '-footer'; - } - - _onLayoutFooter = (e: LayoutEvent) => { - this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()); - this._footerLength = this._selectLength(e.nativeEvent.layout); - }; - - _onLayoutHeader = (e: LayoutEvent) => { - this._headerLength = this._selectLength(e.nativeEvent.layout); - }; - - // $FlowFixMe[missing-local-annot] - _renderDebugOverlay() { - const normalize = - this._scrollMetrics.visibleLength / - (this._scrollMetrics.contentLength || 1); - const framesInLayout = []; - const itemCount = this.props.getItemCount(this.props.data); - for (let ii = 0; ii < itemCount; ii++) { - const frame = this.__getFrameMetricsApprox(ii, this.props); - /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { - framesInLayout.push(frame); - } - } - const windowTop = this.__getFrameMetricsApprox( - this.state.cellsAroundViewport.first, - this.props, - ).offset; - const frameLast = this.__getFrameMetricsApprox( - this.state.cellsAroundViewport.last, - this.props, - ); - const windowLen = frameLast.offset + frameLast.length - windowTop; - const visTop = this._scrollMetrics.offset; - const visLen = this._scrollMetrics.visibleLength; - - return ( - - {framesInLayout.map((f, ii) => ( - - ))} - - - - ); - } - - _selectLength( - metrics: $ReadOnly<{ - height: number, - width: number, - ... - }>, - ): number { - return !horizontalOrDefault(this.props.horizontal) - ? metrics.height - : metrics.width; - } - - _selectOffset( - metrics: $ReadOnly<{ - x: number, - y: number, - ... - }>, - ): number { - return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x; - } - - _maybeCallOnEdgeReached() { - const { - data, - getItemCount, - onStartReached, - onStartReachedThreshold, - onEndReached, - onEndReachedThreshold, - initialScrollIndex, - } = this.props; - const {contentLength, visibleLength, offset} = this._scrollMetrics; - let distanceFromStart = offset; - let distanceFromEnd = contentLength - visibleLength - offset; - - // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0 - // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus - // be at the edge of the list with a distance approximating 0 but not quite there. - if (distanceFromStart < ON_EDGE_REACHED_EPSILON) { - distanceFromStart = 0; - } - if (distanceFromEnd < ON_EDGE_REACHED_EPSILON) { - distanceFromEnd = 0; - } - - // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px - // when oERT is not present (different from 2 viewports used elsewhere) - const DEFAULT_THRESHOLD_PX = 2; - - const startThreshold = - onStartReachedThreshold != null - ? onStartReachedThreshold * visibleLength - : DEFAULT_THRESHOLD_PX; - const endThreshold = - onEndReachedThreshold != null - ? onEndReachedThreshold * visibleLength - : DEFAULT_THRESHOLD_PX; - const isWithinStartThreshold = distanceFromStart <= startThreshold; - const isWithinEndThreshold = distanceFromEnd <= endThreshold; - - // First check if the user just scrolled within the end threshold - // and call onEndReached only once for a given content length, - // and only if onStartReached is not being executed - if ( - onEndReached && - this.state.cellsAroundViewport.last === getItemCount(data) - 1 && - isWithinEndThreshold && - this._scrollMetrics.contentLength !== this._sentEndForContentLength - ) { - this._sentEndForContentLength = this._scrollMetrics.contentLength; - onEndReached({distanceFromEnd}); - } - - // Next check if the user just scrolled within the start threshold - // and call onStartReached only once for a given content length, - // and only if onEndReached is not being executed - else if ( - onStartReached != null && - this.state.cellsAroundViewport.first === 0 && - isWithinStartThreshold && - this._scrollMetrics.contentLength !== this._sentStartForContentLength - ) { - // On initial mount when using initialScrollIndex the offset will be 0 initially - // and will trigger an unexpected onStartReached. To avoid this we can use - // timestamp to differentiate between the initial scroll metrics and when we actually - // received the first scroll event. - if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { - this._sentStartForContentLength = this._scrollMetrics.contentLength; - onStartReached({distanceFromStart}); - } - } - - // If the user scrolls away from the start or end and back again, - // cause onStartReached or onEndReached to be triggered again - else { - this._sentStartForContentLength = isWithinStartThreshold - ? this._sentStartForContentLength - : 0; - this._sentEndForContentLength = isWithinEndThreshold - ? this._sentEndForContentLength - : 0; - } - } - - _onContentSizeChange = (width: number, height: number) => { - if ( - width > 0 && - height > 0 && - this.props.initialScrollIndex != null && - this.props.initialScrollIndex > 0 && - !this._hasTriggeredInitialScrollToIndex - ) { - if (this.props.contentOffset == null) { - this.scrollToIndex({ - animated: false, - index: this.props.initialScrollIndex, - }); - } - this._hasTriggeredInitialScrollToIndex = true; - } - if (this.props.onContentSizeChange) { - this.props.onContentSizeChange(width, height); - } - this._scrollMetrics.contentLength = this._selectLength({height, width}); - this._scheduleCellsToRenderUpdate(); - this._maybeCallOnEdgeReached(); - }; - - /* Translates metrics from a scroll event in a parent VirtualizedList into - * coordinates relative to the child list. - */ - _convertParentScrollMetrics = (metrics: { - visibleLength: number, - offset: number, - ... - }): $FlowFixMe => { - // Offset of the top of the nested list relative to the top of its parent's viewport - const offset = metrics.offset - this._offsetFromParentVirtualizedList; - // Child's visible length is the same as its parent's - const visibleLength = metrics.visibleLength; - const dOffset = offset - this._scrollMetrics.offset; - const contentLength = this._scrollMetrics.contentLength; - - return { - visibleLength, - contentLength, - offset, - dOffset, - }; - }; - - _onScroll = (e: Object) => { - this._nestedChildLists.forEach(childList => { - childList._onScroll(e); - }); - if (this.props.onScroll) { - this.props.onScroll(e); - } - const timestamp = e.timeStamp; - let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement); - let contentLength = this._selectLength(e.nativeEvent.contentSize); - let offset = this._selectOffset(e.nativeEvent.contentOffset); - let dOffset = offset - this._scrollMetrics.offset; - - if (this._isNestedWithSameOrientation()) { - if (this._scrollMetrics.contentLength === 0) { - // Ignore scroll events until onLayout has been called and we - // know our offset from our offset from our parent - return; - } - ({visibleLength, contentLength, offset, dOffset} = - this._convertParentScrollMetrics({ - visibleLength, - offset, - })); - } - - const dt = this._scrollMetrics.timestamp - ? Math.max(1, timestamp - this._scrollMetrics.timestamp) - : 1; - const velocity = dOffset / dt; - - if ( - dt > 500 && - this._scrollMetrics.dt > 500 && - contentLength > 5 * visibleLength && - !this._hasWarned.perf - ) { - infoLog( - 'VirtualizedList: You have a large list that is slow to update - make sure your ' + - 'renderItem function renders components that follow React performance best practices ' + - 'like PureComponent, shouldComponentUpdate, etc.', - {dt, prevDt: this._scrollMetrics.dt, contentLength}, - ); - this._hasWarned.perf = true; - } - - // For invalid negative values (w/ RTL), set this to 1. - const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale; - this._scrollMetrics = { - contentLength, - dt, - dOffset, - offset, - timestamp, - velocity, - visibleLength, - zoomScale, - }; - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; - } - this._maybeCallOnEdgeReached(); - if (velocity !== 0) { - this._fillRateHelper.activate(); - } - this._computeBlankness(); - this._scheduleCellsToRenderUpdate(); - }; - - _scheduleCellsToRenderUpdate() { - const {first, last} = this.state.cellsAroundViewport; - const {offset, visibleLength, velocity} = this._scrollMetrics; - const itemCount = this.props.getItemCount(this.props.data); - let hiPri = false; - const onStartReachedThreshold = onStartReachedThresholdOrDefault( - this.props.onStartReachedThreshold, - ); - const onEndReachedThreshold = onEndReachedThresholdOrDefault( - this.props.onEndReachedThreshold, - ); - // Mark as high priority if we're close to the start of the first item - // But only if there are items before the first rendered item - if (first > 0) { - const distTop = - offset - this.__getFrameMetricsApprox(first, this.props).offset; - hiPri = - distTop < 0 || - (velocity < -2 && - distTop < - getScrollingThreshold(onStartReachedThreshold, visibleLength)); - } - // Mark as high priority if we're close to the end of the last item - // But only if there are items after the last rendered item - if (!hiPri && last >= 0 && last < itemCount - 1) { - const distBottom = - this.__getFrameMetricsApprox(last, this.props).offset - - (offset + visibleLength); - hiPri = - distBottom < 0 || - (velocity > 2 && - distBottom < - getScrollingThreshold(onEndReachedThreshold, visibleLength)); - } - // Only trigger high-priority updates if we've actually rendered cells, - // and with that size estimate, accurately compute how many cells we should render. - // Otherwise, it would just render as many cells as it can (of zero dimension), - // each time through attempting to render more (limited by maxToRenderPerBatch), - // starving the renderer from actually laying out the objects and computing _averageCellLength. - // If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate - // We shouldn't do another hipri cellToRenderUpdate - if ( - hiPri && - (this._averageCellLength || this.props.getItemLayout) && - !this._hiPriInProgress - ) { - this._hiPriInProgress = true; - // Don't worry about interactions when scrolling quickly; focus on filling content as fast - // as possible. - this._updateCellsToRenderBatcher.dispose({abort: true}); - this._updateCellsToRender(); - return; - } else { - this._updateCellsToRenderBatcher.schedule(); - } - } - - _onScrollBeginDrag = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onScrollBeginDrag(e); - }); - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.recordInteraction(); - }); - this._hasInteracted = true; - this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); - }; - - _onScrollEndDrag = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onScrollEndDrag(e); - }); - const {velocity} = e.nativeEvent; - if (velocity) { - this._scrollMetrics.velocity = this._selectOffset(velocity); - } - this._computeBlankness(); - this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); - }; - - _onMomentumScrollBegin = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onMomentumScrollBegin(e); - }); - this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); - }; - - _onMomentumScrollEnd = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onMomentumScrollEnd(e); - }); - this._scrollMetrics.velocity = 0; - this._computeBlankness(); - this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); - }; - - _updateCellsToRender = () => { - this.setState((state, props) => { - const cellsAroundViewport = this._adjustCellsAroundViewport( - props, - state.cellsAroundViewport, - ); - const renderMask = VirtualizedList._createRenderMask( - props, - cellsAroundViewport, - this._getNonViewportRenderRegions(props), - ); - - if ( - cellsAroundViewport.first === state.cellsAroundViewport.first && - cellsAroundViewport.last === state.cellsAroundViewport.last && - renderMask.equals(state.renderMask) - ) { - return null; - } - - return {cellsAroundViewport, renderMask}; - }); - }; - - _createViewToken = ( - index: number, - isViewable: boolean, - props: FrameMetricProps, - // $FlowFixMe[missing-local-annot] - ) => { - const {data, getItem} = props; - const item = getItem(data, index); - return { - index, - item, - key: this._keyExtractor(item, index, props), - isViewable, - }; - }; - - /** - * Gets an approximate offset to an item at a given index. Supports - * fractional indices. - */ - _getOffsetApprox = (index: number, props: FrameMetricProps): number => { - if (Number.isInteger(index)) { - return this.__getFrameMetricsApprox(index, props).offset; - } else { - const frameMetrics = this.__getFrameMetricsApprox( - Math.floor(index), - props, - ); - const remainder = index - Math.floor(index); - return frameMetrics.offset + remainder * frameMetrics.length; - } - }; - - __getFrameMetricsApprox: ( - index: number, - props: FrameMetricProps, - ) => { - length: number, - offset: number, - ... - } = (index, props) => { - const frame = this._getFrameMetrics(index, props); - if (frame && frame.index === index) { - // check for invalid frames due to row re-ordering - return frame; - } else { - const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); - invariant( - !getItemLayout, - 'Should not have to estimate frames when a measurement metrics function is provided', - ); - return { - length: this._averageCellLength, - offset: this._averageCellLength * index, - }; - } - }; - - _getFrameMetrics = ( - index: number, - props: FrameMetricProps, - ): ?{ - length: number, - offset: number, - index: number, - inLayout?: boolean, - ... - } => { - const {data, getItem, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); - const item = getItem(data, index); - const frame = this._frames[this._keyExtractor(item, index, props)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.63 was deployed. To see the error - * delete this comment and run Flow. */ - return getItemLayout(data, index); - } - } - return frame; - }; - - _getNonViewportRenderRegions = ( - props: FrameMetricProps, - ): $ReadOnlyArray<{ - first: number, - last: number, - }> => { - // Keep a viewport's worth of content around the last focused cell to allow - // random navigation around it without any blanking. E.g. tabbing from one - // focused item out of viewport to another. - if ( - !(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey]) - ) { - return []; - } - - const lastFocusedCellRenderer = this._cellRefs[this._lastFocusedCellKey]; - const focusedCellIndex = lastFocusedCellRenderer.props.index; - const itemCount = props.getItemCount(props.data); - - // The cell may have been unmounted and have a stale index - if ( - focusedCellIndex >= itemCount || - this._indicesToKeys.get(focusedCellIndex) !== this._lastFocusedCellKey - ) { - return []; - } - - let first = focusedCellIndex; - let heightOfCellsBeforeFocused = 0; - for ( - let i = first - 1; - i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength; - i-- - ) { - first--; - heightOfCellsBeforeFocused += this.__getFrameMetricsApprox( - i, - props, - ).length; - } - - let last = focusedCellIndex; - let heightOfCellsAfterFocused = 0; - for ( - let i = last + 1; - i < itemCount && - heightOfCellsAfterFocused < this._scrollMetrics.visibleLength; - i++ - ) { - last++; - heightOfCellsAfterFocused += this.__getFrameMetricsApprox( - i, - props, - ).length; - } - - return [{first, last}]; - }; - - _updateViewableItems( - props: FrameMetricProps, - cellsAroundViewport: {first: number, last: number}, - ) { - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate( - props, - this._scrollMetrics.offset, - this._scrollMetrics.visibleLength, - this._getFrameMetrics, - this._createViewToken, - tuple.onViewableItemsChanged, - cellsAroundViewport, - ); - }); - } -} - -const styles = StyleSheet.create({ - verticallyInverted: { - transform: [{scaleY: -1}], - }, - horizontallyInverted: { - transform: [{scaleX: -1}], - }, - debug: { - flex: 1, - }, - debugOverlayBase: { - position: 'absolute', - top: 0, - right: 0, - }, - debugOverlay: { - bottom: 0, - width: 20, - borderColor: 'blue', - borderWidth: 1, - }, - debugOverlayFrame: { - left: 0, - backgroundColor: 'orange', - }, - debugOverlayFrameLast: { - left: 0, - borderColor: 'green', - borderWidth: 2, - }, - debugOverlayFrameVis: { - left: 0, - borderColor: 'red', - borderWidth: 2, - }, -}); - -module.exports = VirtualizedList; diff --git a/packages/virtualized-lists/Lists/VirtualizedListContext.js b/packages/virtualized-lists/Lists/VirtualizedListContext.js deleted file mode 100644 index bca5724498a356..00000000000000 --- a/packages/virtualized-lists/Lists/VirtualizedListContext.js +++ /dev/null @@ -1,116 +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 - */ - -import typeof VirtualizedList from './VirtualizedList'; - -import * as React from 'react'; -import {useContext, useMemo} from 'react'; - -type Context = $ReadOnly<{ - cellKey: ?string, - getScrollMetrics: () => { - contentLength: number, - dOffset: number, - dt: number, - offset: number, - timestamp: number, - velocity: number, - visibleLength: number, - zoomScale: number, - }, - horizontal: ?boolean, - getOutermostParentListRef: () => React.ElementRef, - registerAsNestedChild: ({ - cellKey: string, - ref: React.ElementRef, - }) => void, - unregisterAsNestedChild: ({ - ref: React.ElementRef, - }) => void, -}>; - -export const VirtualizedListContext: React.Context = - React.createContext(null); -if (__DEV__) { - VirtualizedListContext.displayName = 'VirtualizedListContext'; -} - -/** - * Resets the context. Intended for use by portal-like components (e.g. Modal). - */ -export function VirtualizedListContextResetter({ - children, -}: { - children: React.Node, -}): React.Node { - return ( - - {children} - - ); -} - -/** - * Sets the context with memoization. Intended to be used by `VirtualizedList`. - */ -export function VirtualizedListContextProvider({ - children, - value, -}: { - children: React.Node, - value: Context, -}): React.Node { - // Avoid setting a newly created context object if the values are identical. - const context = useMemo( - () => ({ - cellKey: null, - getScrollMetrics: value.getScrollMetrics, - horizontal: value.horizontal, - getOutermostParentListRef: value.getOutermostParentListRef, - registerAsNestedChild: value.registerAsNestedChild, - unregisterAsNestedChild: value.unregisterAsNestedChild, - }), - [ - value.getScrollMetrics, - value.horizontal, - value.getOutermostParentListRef, - value.registerAsNestedChild, - value.unregisterAsNestedChild, - ], - ); - return ( - - {children} - - ); -} - -/** - * Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell. - */ -export function VirtualizedListCellContextProvider({ - cellKey, - children, -}: { - cellKey: string, - children: React.Node, -}): React.Node { - // Avoid setting a newly created context object if the values are identical. - const currContext = useContext(VirtualizedListContext); - const context = useMemo( - () => (currContext == null ? null : {...currContext, cellKey}), - [currContext, cellKey], - ); - return ( - - {children} - - ); -} diff --git a/packages/virtualized-lists/Lists/VirtualizedSectionList.js b/packages/virtualized-lists/Lists/VirtualizedSectionList.js deleted file mode 100644 index 61519bca4dc866..00000000000000 --- a/packages/virtualized-lists/Lists/VirtualizedSectionList.js +++ /dev/null @@ -1,617 +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 - * @format - */ - -import type {ViewToken} from './ViewabilityHelper'; - -import {View} from 'react-native'; -import VirtualizedList from './VirtualizedList'; -import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; -import invariant from 'invariant'; -import * as React from 'react'; - -type Item = any; - -export type SectionBase = { - /** - * The data for rendering items in this section. - */ - data: $ReadOnlyArray, - /** - * Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections, - * the array index will be used by default. - */ - key?: string, - // Optional props will override list-wide props just for this section. - renderItem?: ?(info: { - item: SectionItemT, - index: number, - section: SectionBase, - separators: { - highlight: () => void, - unhighlight: () => void, - updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, - ... - }, - ... - }) => null | React.Element, - ItemSeparatorComponent?: ?React.ComponentType, - keyExtractor?: (item: SectionItemT, index?: ?number) => string, - ... -}; - -type RequiredProps> = {| - sections: $ReadOnlyArray, -|}; - -type OptionalProps> = {| - /** - * Default renderer for every item in every section. - */ - renderItem?: (info: { - item: Item, - index: number, - section: SectionT, - separators: { - highlight: () => void, - unhighlight: () => void, - updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, - ... - }, - ... - }) => null | React.Element, - /** - * Rendered at the top of each section. These stick to the top of the `ScrollView` by default on - * iOS. See `stickySectionHeadersEnabled`. - */ - renderSectionHeader?: ?(info: { - section: SectionT, - ... - }) => null | React.Element, - /** - * Rendered at the bottom of each section. - */ - renderSectionFooter?: ?(info: { - section: SectionT, - ... - }) => null | React.Element, - /** - * Rendered at the top and bottom of each section (note this is different from - * `ItemSeparatorComponent` which is only rendered between items). These are intended to separate - * sections from the headers above and below and typically have the same highlight response as - * `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`, - * and any custom props from `separators.updateProps`. - */ - SectionSeparatorComponent?: ?React.ComponentType, - /** - * Makes section headers stick to the top of the screen until the next one pushes it off. Only - * enabled by default on iOS because that is the platform standard there. - */ - stickySectionHeadersEnabled?: boolean, - onEndReached?: ?({distanceFromEnd: number, ...}) => void, -|}; - -type VirtualizedListProps = React.ElementConfig; - -export type Props = {| - ...RequiredProps, - ...OptionalProps, - ...$Diff< - VirtualizedListProps, - { - renderItem: $PropertyType, - data: $PropertyType, - ... - }, - >, -|}; -export type ScrollToLocationParamsType = {| - animated?: ?boolean, - itemIndex: number, - sectionIndex: number, - viewOffset?: number, - viewPosition?: number, -|}; - -type State = {childProps: VirtualizedListProps, ...}; - -/** - * Right now this just flattens everything into one list and uses VirtualizedList under the - * hood. The only operation that might not scale well is concatting the data arrays of all the - * sections when new props are received, which should be plenty fast for up to ~10,000 items. - */ -class VirtualizedSectionList< - SectionT: SectionBase, -> extends React.PureComponent, State> { - scrollToLocation(params: ScrollToLocationParamsType) { - let index = params.itemIndex; - for (let i = 0; i < params.sectionIndex; i++) { - index += this.props.getItemCount(this.props.sections[i].data) + 2; - } - let viewOffset = params.viewOffset || 0; - if (this._listRef == null) { - return; - } - if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) { - const frame = this._listRef.__getFrameMetricsApprox( - index - params.itemIndex, - this._listRef.props, - ); - viewOffset += frame.length; - } - const toIndexParams = { - ...params, - viewOffset, - index, - }; - // $FlowFixMe[incompatible-use] - this._listRef.scrollToIndex(toIndexParams); - } - - getListRef(): ?React.ElementRef { - return this._listRef; - } - - render(): React.Node { - const { - ItemSeparatorComponent, // don't pass through, rendered with renderItem - SectionSeparatorComponent, - renderItem: _renderItem, - renderSectionFooter, - renderSectionHeader, - sections: _sections, - stickySectionHeadersEnabled, - ...passThroughProps - } = this.props; - - const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0; - - const stickyHeaderIndices = this.props.stickySectionHeadersEnabled - ? ([]: Array) - : undefined; - - let itemCount = 0; - for (const section of this.props.sections) { - // Track the section header indices - if (stickyHeaderIndices != null) { - stickyHeaderIndices.push(itemCount + listHeaderOffset); - } - - // Add two for the section header and footer. - itemCount += 2; - itemCount += this.props.getItemCount(section.data); - } - const renderItem = this._renderItem(itemCount); - - return ( - - this._getItem(this.props, sections, index) - } - getItemCount={() => itemCount} - onViewableItemsChanged={ - this.props.onViewableItemsChanged - ? this._onViewableItemsChanged - : undefined - } - ref={this._captureRef} - /> - ); - } - - _getItem( - props: Props, - sections: ?$ReadOnlyArray, - index: number, - ): ?Item { - if (!sections) { - return null; - } - let itemIdx = index - 1; - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - const sectionData = section.data; - const itemCount = props.getItemCount(sectionData); - if (itemIdx === -1 || itemIdx === itemCount) { - // We intend for there to be overflow by one on both ends of the list. - // This will be for headers and footers. When returning a header or footer - // item the section itself is the item. - return section; - } else if (itemIdx < itemCount) { - // If we are in the bounds of the list's data then return the item. - return props.getItem(sectionData, itemIdx); - } else { - itemIdx -= itemCount + 2; // Add two for the header and footer - } - } - return null; - } - - // $FlowFixMe[missing-local-annot] - _keyExtractor = (item: Item, index: number) => { - const info = this._subExtractor(index); - return (info && info.key) || String(index); - }; - - _subExtractor(index: number): ?{ - section: SectionT, - // Key of the section or combined key for section + item - key: string, - // Relative index within the section - index: ?number, - // True if this is the section header - header?: ?boolean, - leadingItem?: ?Item, - leadingSection?: ?SectionT, - trailingItem?: ?Item, - trailingSection?: ?SectionT, - ... - } { - let itemIndex = index; - const {getItem, getItemCount, keyExtractor, sections} = this.props; - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - const sectionData = section.data; - const key = section.key || String(i); - itemIndex -= 1; // The section adds an item for the header - if (itemIndex >= getItemCount(sectionData) + 1) { - itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer. - } else if (itemIndex === -1) { - return { - section, - key: key + ':header', - index: null, - header: true, - trailingSection: sections[i + 1], - }; - } else if (itemIndex === getItemCount(sectionData)) { - return { - section, - key: key + ':footer', - index: null, - header: false, - trailingSection: sections[i + 1], - }; - } else { - const extractor = - section.keyExtractor || keyExtractor || defaultKeyExtractor; - return { - section, - key: - key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex), - index: itemIndex, - leadingItem: getItem(sectionData, itemIndex - 1), - leadingSection: sections[i - 1], - trailingItem: getItem(sectionData, itemIndex + 1), - trailingSection: sections[i + 1], - }; - } - } - } - - _convertViewable = (viewable: ViewToken): ?ViewToken => { - invariant(viewable.index != null, 'Received a broken ViewToken'); - const info = this._subExtractor(viewable.index); - if (!info) { - return null; - } - const keyExtractorWithNullableIndex = info.section.keyExtractor; - const keyExtractorWithNonNullableIndex = - this.props.keyExtractor || defaultKeyExtractor; - const key = - keyExtractorWithNullableIndex != null - ? keyExtractorWithNullableIndex(viewable.item, info.index) - : keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0); - - return { - ...viewable, - index: info.index, - key, - section: info.section, - }; - }; - - _onViewableItemsChanged = ({ - viewableItems, - changed, - }: { - viewableItems: Array, - changed: Array, - ... - }) => { - const onViewableItemsChanged = this.props.onViewableItemsChanged; - if (onViewableItemsChanged != null) { - onViewableItemsChanged({ - viewableItems: viewableItems - .map(this._convertViewable, this) - .filter(Boolean), - changed: changed.map(this._convertViewable, this).filter(Boolean), - }); - } - }; - - _renderItem = - (listItemCount: number): $FlowFixMe => - // eslint-disable-next-line react/no-unstable-nested-components - ({item, index}: {item: Item, index: number, ...}) => { - const info = this._subExtractor(index); - if (!info) { - return null; - } - const infoIndex = info.index; - if (infoIndex == null) { - const {section} = info; - if (info.header === true) { - const {renderSectionHeader} = this.props; - return renderSectionHeader ? renderSectionHeader({section}) : null; - } else { - const {renderSectionFooter} = this.props; - return renderSectionFooter ? renderSectionFooter({section}) : null; - } - } else { - const renderItem = info.section.renderItem || this.props.renderItem; - const SeparatorComponent = this._getSeparatorComponent( - index, - info, - listItemCount, - ); - invariant(renderItem, 'no renderItem!'); - return ( - - ); - } - }; - - _updatePropsFor = (cellKey: string, value: any) => { - const updateProps = this._updatePropsMap[cellKey]; - if (updateProps != null) { - updateProps(value); - } - }; - - _updateHighlightFor = (cellKey: string, value: boolean) => { - const updateHighlight = this._updateHighlightMap[cellKey]; - if (updateHighlight != null) { - updateHighlight(value); - } - }; - - _setUpdateHighlightFor = ( - cellKey: string, - updateHighlightFn: ?(boolean) => void, - ) => { - if (updateHighlightFn != null) { - this._updateHighlightMap[cellKey] = updateHighlightFn; - } else { - // $FlowFixMe[prop-missing] - delete this._updateHighlightFor[cellKey]; - } - }; - - _setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => { - if (updatePropsFn != null) { - this._updatePropsMap[cellKey] = updatePropsFn; - } else { - delete this._updatePropsMap[cellKey]; - } - }; - - _getSeparatorComponent( - index: number, - info?: ?Object, - listItemCount: number, - ): ?React.ComponentType { - info = info || this._subExtractor(index); - if (!info) { - return null; - } - const ItemSeparatorComponent = - info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; - const {SectionSeparatorComponent} = this.props; - const isLastItemInList = index === listItemCount - 1; - const isLastItemInSection = - info.index === this.props.getItemCount(info.section.data) - 1; - if (SectionSeparatorComponent && isLastItemInSection) { - return SectionSeparatorComponent; - } - if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { - return ItemSeparatorComponent; - } - return null; - } - - _updateHighlightMap: {[string]: (boolean) => void} = {}; - _updatePropsMap: {[string]: void | (boolean => void)} = {}; - _listRef: ?React.ElementRef; - _captureRef = (ref: null | React$ElementRef>) => { - this._listRef = ref; - }; -} - -type ItemWithSeparatorCommonProps = $ReadOnly<{| - leadingItem: ?Item, - leadingSection: ?Object, - section: Object, - trailingItem: ?Item, - trailingSection: ?Object, -|}>; - -type ItemWithSeparatorProps = $ReadOnly<{| - ...ItemWithSeparatorCommonProps, - LeadingSeparatorComponent: ?React.ComponentType, - SeparatorComponent: ?React.ComponentType, - cellKey: string, - index: number, - item: Item, - setSelfHighlightCallback: ( - cellKey: string, - updateFn: ?(boolean) => void, - ) => void, - setSelfUpdatePropsCallback: ( - cellKey: string, - updateFn: ?(boolean) => void, - ) => void, - prevCellKey?: ?string, - updateHighlightFor: (prevCellKey: string, value: boolean) => void, - updatePropsFor: (prevCellKey: string, value: Object) => void, - renderItem: Function, - inverted: boolean, -|}>; - -function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node { - const { - LeadingSeparatorComponent, - // this is the trailing separator and is associated with this item - SeparatorComponent, - cellKey, - prevCellKey, - setSelfHighlightCallback, - updateHighlightFor, - setSelfUpdatePropsCallback, - updatePropsFor, - item, - index, - section, - inverted, - } = props; - - const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] = - React.useState(false); - - const [separatorHighlighted, setSeparatorHighlighted] = React.useState(false); - - const [leadingSeparatorProps, setLeadingSeparatorProps] = React.useState({ - leadingItem: props.leadingItem, - leadingSection: props.leadingSection, - section: props.section, - trailingItem: props.item, - trailingSection: props.trailingSection, - }); - const [separatorProps, setSeparatorProps] = React.useState({ - leadingItem: props.item, - leadingSection: props.leadingSection, - section: props.section, - trailingItem: props.trailingItem, - trailingSection: props.trailingSection, - }); - - React.useEffect(() => { - setSelfHighlightCallback(cellKey, setSeparatorHighlighted); - // $FlowFixMe[incompatible-call] - setSelfUpdatePropsCallback(cellKey, setSeparatorProps); - - return () => { - setSelfUpdatePropsCallback(cellKey, null); - setSelfHighlightCallback(cellKey, null); - }; - }, [ - cellKey, - setSelfHighlightCallback, - setSeparatorProps, - setSelfUpdatePropsCallback, - ]); - - const separators = { - highlight: () => { - setLeadingSeparatorHighlighted(true); - setSeparatorHighlighted(true); - if (prevCellKey != null) { - updateHighlightFor(prevCellKey, true); - } - }, - unhighlight: () => { - setLeadingSeparatorHighlighted(false); - setSeparatorHighlighted(false); - if (prevCellKey != null) { - updateHighlightFor(prevCellKey, false); - } - }, - updateProps: ( - select: 'leading' | 'trailing', - newProps: $Shape, - ) => { - if (select === 'leading') { - if (LeadingSeparatorComponent != null) { - setLeadingSeparatorProps({...leadingSeparatorProps, ...newProps}); - } else if (prevCellKey != null) { - // update the previous item's separator - updatePropsFor(prevCellKey, {...leadingSeparatorProps, ...newProps}); - } - } else if (select === 'trailing' && SeparatorComponent != null) { - setSeparatorProps({...separatorProps, ...newProps}); - } - }, - }; - const element = props.renderItem({ - item, - index, - section, - separators, - }); - const leadingSeparator = LeadingSeparatorComponent != null && ( - - ); - const separator = SeparatorComponent != null && ( - - ); - return leadingSeparator || separator ? ( - - {inverted === false ? leadingSeparator : separator} - {element} - {inverted === false ? separator : leadingSeparator} - - ) : ( - element - ); -} - -/* $FlowFixMe[class-object-subtyping] added when improving typing for this - * parameters */ -// $FlowFixMe[method-unbinding] -module.exports = (VirtualizedSectionList: React.AbstractComponent< - React.ElementConfig, - $ReadOnly<{ - getListRef: () => ?React.ElementRef, - scrollToLocation: (params: ScrollToLocationParamsType) => void, - ... - }>, ->); diff --git a/packages/virtualized-lists/Utilities/infoLog.js b/packages/virtualized-lists/Utilities/infoLog.js deleted file mode 100644 index 6cb6df8d414971..00000000000000 --- a/packages/virtualized-lists/Utilities/infoLog.js +++ /dev/null @@ -1,20 +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'; - -/** - * Intentional info-level logging for clear separation from ad-hoc console debug logging. - */ -function infoLog(...args: Array): void { - return console.log(...args); -} - -module.exports = infoLog; diff --git a/packages/virtualized-lists/index.d.ts b/packages/virtualized-lists/index.d.ts deleted file mode 100644 index c66fc20521e439..00000000000000 --- a/packages/virtualized-lists/index.d.ts +++ /dev/null @@ -1,10 +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 - */ - -export * from './Lists/VirtualizedList'; diff --git a/packages/virtualized-lists/index.js b/packages/virtualized-lists/index.js deleted file mode 100644 index 8516f7ccf73289..00000000000000 --- a/packages/virtualized-lists/index.js +++ /dev/null @@ -1,57 +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 - */ - -'use strict'; - -import {keyExtractor} from './Lists/VirtualizeUtils'; - -import typeof VirtualizedList from './Lists/VirtualizedList'; -import typeof VirtualizedSectionList from './Lists/VirtualizedSectionList'; -import {typeof VirtualizedListContextResetter} from './Lists/VirtualizedListContext'; -import typeof ViewabilityHelper from './Lists/ViewabilityHelper'; -import typeof FillRateHelper from './Lists/FillRateHelper'; - -export type { - ViewToken, - ViewabilityConfig, - ViewabilityConfigCallbackPair, -} from './Lists/ViewabilityHelper'; -export type { - RenderItemProps, - RenderItemType, - Separators, -} from './Lists/VirtualizedListProps'; -export type { - Props as VirtualizedSectionListProps, - ScrollToLocationParamsType, - SectionBase, -} from './Lists/VirtualizedSectionList'; -export type {FillRateInfo} from './Lists/FillRateHelper'; - -module.exports = { - keyExtractor, - - get VirtualizedList(): VirtualizedList { - return require('./Lists/VirtualizedList'); - }, - get VirtualizedSectionList(): VirtualizedSectionList { - return require('./Lists/VirtualizedSectionList'); - }, - get VirtualizedListContextResetter(): VirtualizedListContextResetter { - const VirtualizedListContext = require('./Lists/VirtualizedListContext'); - return VirtualizedListContext.VirtualizedListContextResetter; - }, - get ViewabilityHelper(): ViewabilityHelper { - return require('./Lists/ViewabilityHelper'); - }, - get FillRateHelper(): FillRateHelper { - return require('./Lists/FillRateHelper'); - }, -}; diff --git a/packages/virtualized-lists/package.json b/packages/virtualized-lists/package.json deleted file mode 100644 index 8b19cd2c199f7a..00000000000000 --- a/packages/virtualized-lists/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@react-native/virtualized-lists", - "version": "0.72.0", - "description": "Virtualized lists for React Native.", - "repository": { - "type": "git", - "url": "git@github.com:facebook/react-native.git", - "directory": "packages/virtualized-lists" - }, - "license": "MIT", - "dependencies": { - "invariant": "^2.2.4" - }, - "devDependencies": { - "react-test-renderer": "18.2.0" - }, - "peerDependencies": { - "react-native": "*", - "react-test-renderer": "18.2.0" - } -} diff --git a/types/index.d.ts b/types/index.d.ts index 349efc65a9fc8c..97b34d5d2321db 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -115,7 +115,7 @@ export * from '../Libraries/LayoutAnimation/LayoutAnimation'; export * from '../Libraries/Linking/Linking'; export * from '../Libraries/Lists/FlatList'; export * from '../Libraries/Lists/SectionList'; -export * from '@react-native/virtualized-lists'; +export * from '../Libraries/Lists/VirtualizedList'; export * from '../Libraries/LogBox/LogBox'; export * from '../Libraries/Modal/Modal'; export * as Systrace from '../Libraries/Performance/Systrace'; From 0daf83ac51ee3d3138f69b388a3ae3dc1ee4a3a6 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 6 Feb 2023 20:00:19 -0800 Subject: [PATCH 50/65] Reconnect VirtualizedList Source History 2/2 (Apply D41745930 + history, D42805202, D43063551) Summary: This change re-applies D41745930 (https://github.com/facebook/react-native/commit/2e3dbe9c2fbff52448e2d5a7c1e4c96b1016cf25) (and D42805202 (https://github.com/facebook/react-native/commit/1479b2ac26fded3840c596f53e6eb86a4b0c2c71) which was also partially reverted), re-registers additions as moves, then applies D43063551 which has been added to the changes since migration. Changelog: [Internal] Reviewed By: hoxyq Differential Revision: D43068114 fbshipit-source-id: 72997700bf9962d82a988599481e255b69e68a9b --- Libraries/Inspector/NetworkOverlay.js | 2 +- Libraries/Lists/FillRateHelper.js | 242 +- Libraries/Lists/FlatList.d.ts | 2 +- Libraries/Lists/FlatList.js | 11 +- Libraries/Lists/FlatList.js.flow | 9 +- Libraries/Lists/SectionList.d.ts | 2 +- Libraries/Lists/SectionList.js | 6 +- Libraries/Lists/SectionListModern.js | 6 +- Libraries/Lists/ViewabilityHelper.js | 352 +-- Libraries/Lists/VirtualizeUtils.js | 248 +-- Libraries/Lists/VirtualizedList.js | 1949 +--------------- Libraries/Lists/VirtualizedListContext.js | 110 +- Libraries/Lists/VirtualizedSectionList.js | 613 +----- Libraries/Modal/Modal.js | 2 +- Libraries/Utilities/ReactNativeTestTools.js | 2 +- index.js | 2 +- package.json | 1 + .../js/examples/FlatList/FlatList-basic.js | 2 +- .../js/examples/FlatList/FlatList-nested.js | 4 +- .../FlatList-onViewableItemsChanged.js | 2 +- .../Interaction/Batchinator.js | 2 +- .../Interaction/__tests__/Batchinator-test.js | 4 - .../Lists/CellRenderMask.js | 0 .../Lists/ChildListCollection.js | 0 .../virtualized-lists/Lists/FillRateHelper.js | 253 +++ .../Lists/StateSafePureComponent.js | 0 .../Lists/ViewabilityHelper.js | 360 +++ .../Lists/VirtualizeUtils.js | 258 +++ .../Lists/VirtualizedList.d.ts | 12 +- .../Lists/VirtualizedList.js | 1955 +++++++++++++++++ .../Lists/VirtualizedListCellRenderer.js | 10 +- .../Lists/VirtualizedListContext.js | 116 + .../Lists/VirtualizedListProps.js | 4 +- .../Lists/VirtualizedSectionList.js | 617 ++++++ .../Lists/__tests__/CellRenderMask-test.js | 0 .../Lists/__tests__/FillRateHelper-test.js | 0 .../Lists/__tests__/ViewabilityHelper-test.js | 0 .../Lists/__tests__/VirtualizeUtils-test.js | 0 .../Lists/__tests__/VirtualizedList-test.js | 0 .../__tests__/VirtualizedSectionList-test.js | 0 .../VirtualizedList-test.js.snap | 0 .../VirtualizedSectionList-test.js.snap | 0 .../Utilities/__tests__/clamp-test.js | 0 .../virtualized-lists}/Utilities/clamp.js | 0 .../virtualized-lists/Utilities/infoLog.js | 20 + packages/virtualized-lists/index.d.ts | 10 + packages/virtualized-lists/index.js | 57 + packages/virtualized-lists/package.json | 21 + types/index.d.ts | 2 +- 49 files changed, 3755 insertions(+), 3513 deletions(-) rename {Libraries => packages/virtualized-lists}/Interaction/Batchinator.js (97%) rename {Libraries => packages/virtualized-lists}/Interaction/__tests__/Batchinator-test.js (95%) rename {Libraries => packages/virtualized-lists}/Lists/CellRenderMask.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/ChildListCollection.js (100%) create mode 100644 packages/virtualized-lists/Lists/FillRateHelper.js rename {Libraries => packages/virtualized-lists}/Lists/StateSafePureComponent.js (100%) create mode 100644 packages/virtualized-lists/Lists/ViewabilityHelper.js create mode 100644 packages/virtualized-lists/Lists/VirtualizeUtils.js rename {Libraries => packages/virtualized-lists}/Lists/VirtualizedList.d.ts (97%) create mode 100644 packages/virtualized-lists/Lists/VirtualizedList.js rename {Libraries => packages/virtualized-lists}/Lists/VirtualizedListCellRenderer.js (96%) create mode 100644 packages/virtualized-lists/Lists/VirtualizedListContext.js rename {Libraries => packages/virtualized-lists}/Lists/VirtualizedListProps.js (98%) create mode 100644 packages/virtualized-lists/Lists/VirtualizedSectionList.js rename {Libraries => packages/virtualized-lists}/Lists/__tests__/CellRenderMask-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/FillRateHelper-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/ViewabilityHelper-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/VirtualizeUtils-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/VirtualizedList-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/VirtualizedSectionList-test.js (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap (100%) rename {Libraries => packages/virtualized-lists}/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap (100%) rename {Libraries => packages/virtualized-lists}/Utilities/__tests__/clamp-test.js (100%) rename {Libraries => packages/virtualized-lists}/Utilities/clamp.js (100%) create mode 100644 packages/virtualized-lists/Utilities/infoLog.js create mode 100644 packages/virtualized-lists/index.d.ts create mode 100644 packages/virtualized-lists/index.js create mode 100644 packages/virtualized-lists/package.json diff --git a/Libraries/Inspector/NetworkOverlay.js b/Libraries/Inspector/NetworkOverlay.js index 169318cea27f28..c6d1ec33907e36 100644 --- a/Libraries/Inspector/NetworkOverlay.js +++ b/Libraries/Inspector/NetworkOverlay.js @@ -10,7 +10,7 @@ 'use strict'; -import type {RenderItemProps} from '../Lists/VirtualizedList'; +import type {RenderItemProps} from '@react-native/virtualized-lists'; const ScrollView = require('../Components/ScrollView/ScrollView'); const TouchableHighlight = require('../Components/Touchable/TouchableHighlight'); diff --git a/Libraries/Lists/FillRateHelper.js b/Libraries/Lists/FillRateHelper.js index 87482e73f6be3e..141fe98eb067bb 100644 --- a/Libraries/Lists/FillRateHelper.js +++ b/Libraries/Lists/FillRateHelper.js @@ -10,244 +10,10 @@ 'use strict'; -import type {FrameMetricProps} from './VirtualizedListProps'; +import {typeof FillRateHelper as FillRateHelperType} from '@react-native/virtualized-lists'; -export type FillRateInfo = Info; - -class Info { - any_blank_count: number = 0; - any_blank_ms: number = 0; - any_blank_speed_sum: number = 0; - mostly_blank_count: number = 0; - mostly_blank_ms: number = 0; - pixels_blank: number = 0; - pixels_sampled: number = 0; - pixels_scrolled: number = 0; - total_time_spent: number = 0; - sample_count: number = 0; -} - -type FrameMetrics = { - inLayout?: boolean, - length: number, - offset: number, - ... -}; - -const DEBUG = false; - -let _listeners: Array<(Info) => void> = []; -let _minSampleCount = 10; -let _sampleRate = DEBUG ? 1 : null; - -/** - * A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded. - * By default the sampling rate is set to zero and this will do nothing. If you want to collect - * samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`. - * - * Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with - * `SceneTracker.getActiveScene` to determine the context of the events. - */ -class FillRateHelper { - _anyBlankStartTime: ?number = null; - _enabled = false; - _getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics; - _info: Info = new Info(); - _mostlyBlankStartTime: ?number = null; - _samplesStartTime: ?number = null; - - static addListener(callback: FillRateInfo => void): { - remove: () => void, - ... - } { - if (_sampleRate === null) { - console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.'); - } - _listeners.push(callback); - return { - remove: () => { - _listeners = _listeners.filter(listener => callback !== listener); - }, - }; - } - - static setSampleRate(sampleRate: number) { - _sampleRate = sampleRate; - } - - static setMinSampleCount(minSampleCount: number) { - _minSampleCount = minSampleCount; - } - - constructor( - getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics, - ) { - this._getFrameMetrics = getFrameMetrics; - this._enabled = (_sampleRate || 0) > Math.random(); - this._resetData(); - } - - activate() { - if (this._enabled && this._samplesStartTime == null) { - DEBUG && console.debug('FillRateHelper: activate'); - this._samplesStartTime = global.performance.now(); - } - } - - deactivateAndFlush() { - if (!this._enabled) { - return; - } - const start = this._samplesStartTime; // const for flow - if (start == null) { - DEBUG && - console.debug('FillRateHelper: bail on deactivate with no start time'); - return; - } - if (this._info.sample_count < _minSampleCount) { - // Don't bother with under-sampled events. - this._resetData(); - return; - } - const total_time_spent = global.performance.now() - start; - const info: any = { - ...this._info, - total_time_spent, - }; - if (DEBUG) { - const derived = { - avg_blankness: this._info.pixels_blank / this._info.pixels_sampled, - avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000), - avg_speed_when_any_blank: - this._info.any_blank_speed_sum / this._info.any_blank_count, - any_blank_per_min: - this._info.any_blank_count / (total_time_spent / 1000 / 60), - any_blank_time_frac: this._info.any_blank_ms / total_time_spent, - mostly_blank_per_min: - this._info.mostly_blank_count / (total_time_spent / 1000 / 60), - mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent, - }; - for (const key in derived) { - // $FlowFixMe[prop-missing] - derived[key] = Math.round(1000 * derived[key]) / 1000; - } - console.debug('FillRateHelper deactivateAndFlush: ', {derived, info}); - } - _listeners.forEach(listener => listener(info)); - this._resetData(); - } - - computeBlankness( - props: { - ...FrameMetricProps, - initialNumToRender?: ?number, - ... - }, - cellsAroundViewport: { - first: number, - last: number, - ... - }, - scrollMetrics: { - dOffset: number, - offset: number, - velocity: number, - visibleLength: number, - ... - }, - ): number { - if ( - !this._enabled || - props.getItemCount(props.data) === 0 || - cellsAroundViewport.last < cellsAroundViewport.first || - this._samplesStartTime == null - ) { - return 0; - } - const {dOffset, offset, velocity, visibleLength} = scrollMetrics; - - // Denominator metrics that we track for all events - most of the time there is no blankness and - // we want to capture that. - this._info.sample_count++; - this._info.pixels_sampled += Math.round(visibleLength); - this._info.pixels_scrolled += Math.round(Math.abs(dOffset)); - const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec - - // Whether blank now or not, record the elapsed time blank if we were blank last time. - const now = global.performance.now(); - if (this._anyBlankStartTime != null) { - this._info.any_blank_ms += now - this._anyBlankStartTime; - } - this._anyBlankStartTime = null; - if (this._mostlyBlankStartTime != null) { - this._info.mostly_blank_ms += now - this._mostlyBlankStartTime; - } - this._mostlyBlankStartTime = null; - - let blankTop = 0; - let first = cellsAroundViewport.first; - let firstFrame = this._getFrameMetrics(first, props); - while ( - first <= cellsAroundViewport.last && - (!firstFrame || !firstFrame.inLayout) - ) { - firstFrame = this._getFrameMetrics(first, props); - first++; - } - // Only count blankTop if we aren't rendering the first item, otherwise we will count the header - // as blank. - if (firstFrame && first > 0) { - blankTop = Math.min( - visibleLength, - Math.max(0, firstFrame.offset - offset), - ); - } - let blankBottom = 0; - let last = cellsAroundViewport.last; - let lastFrame = this._getFrameMetrics(last, props); - while ( - last >= cellsAroundViewport.first && - (!lastFrame || !lastFrame.inLayout) - ) { - lastFrame = this._getFrameMetrics(last, props); - last--; - } - // Only count blankBottom if we aren't rendering the last item, otherwise we will count the - // footer as blank. - if (lastFrame && last < props.getItemCount(props.data) - 1) { - const bottomEdge = lastFrame.offset + lastFrame.length; - blankBottom = Math.min( - visibleLength, - Math.max(0, offset + visibleLength - bottomEdge), - ); - } - const pixels_blank = Math.round(blankTop + blankBottom); - const blankness = pixels_blank / visibleLength; - if (blankness > 0) { - this._anyBlankStartTime = now; - this._info.any_blank_speed_sum += scrollSpeed; - this._info.any_blank_count++; - this._info.pixels_blank += pixels_blank; - if (blankness > 0.5) { - this._mostlyBlankStartTime = now; - this._info.mostly_blank_count++; - } - } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) { - this.deactivateAndFlush(); - } - return blankness; - } - - enabled(): boolean { - return this._enabled; - } - - _resetData() { - this._anyBlankStartTime = null; - this._info = new Info(); - this._mostlyBlankStartTime = null; - this._samplesStartTime = null; - } -} +const FillRateHelper: FillRateHelperType = + require('@react-native/virtualized-lists').FillRateHelper; +export type {FillRateInfo} from '@react-native/virtualized-lists'; module.exports = FillRateHelper; diff --git a/Libraries/Lists/FlatList.d.ts b/Libraries/Lists/FlatList.d.ts index 344d5671359c45..6ac7f57fdcd843 100644 --- a/Libraries/Lists/FlatList.d.ts +++ b/Libraries/Lists/FlatList.d.ts @@ -12,7 +12,7 @@ import type { ListRenderItem, ViewToken, VirtualizedListProps, -} from './VirtualizedList'; +} from '@react-native/virtualized-lists'; import type {ScrollViewComponent} from '../Components/ScrollView/ScrollView'; import {StyleProp} from '../StyleSheet/StyleSheet'; import {ViewStyle} from '../StyleSheet/StyleSheetTypes'; diff --git a/Libraries/Lists/FlatList.js b/Libraries/Lists/FlatList.js index 56748eaf75240d..40372bc70dd311 100644 --- a/Libraries/Lists/FlatList.js +++ b/Libraries/Lists/FlatList.js @@ -11,14 +11,17 @@ import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; import type { + RenderItemProps, + RenderItemType, ViewabilityConfigCallbackPair, ViewToken, -} from './ViewabilityHelper'; -import type {RenderItemProps, RenderItemType} from './VirtualizedList'; +} from '@react-native/virtualized-lists'; import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import VirtualizedList from './VirtualizedList'; -import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; +import { + VirtualizedList, + keyExtractor as defaultKeyExtractor, +} from '@react-native/virtualized-lists'; import memoizeOne from 'memoize-one'; const View = require('../Components/View/View'); diff --git a/Libraries/Lists/FlatList.js.flow b/Libraries/Lists/FlatList.js.flow index 10a7c9073257bd..304a1006182186 100644 --- a/Libraries/Lists/FlatList.js.flow +++ b/Libraries/Lists/FlatList.js.flow @@ -14,8 +14,13 @@ const View = require('../Components/View/View'); import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; -import type {RenderItemType} from './VirtualizedList'; -import typeof VirtualizedList from './VirtualizedList'; +import type { + RenderItemType, + RenderItemProps, + ViewToken, + ViewabilityConfigCallbackPair, +} from '@react-native/virtualized-lists'; +import {typeof VirtualizedList} from '@react-native/virtualized-lists'; type RequiredProps = {| /** diff --git a/Libraries/Lists/SectionList.d.ts b/Libraries/Lists/SectionList.d.ts index ae1b10df46a10d..7ff5bbb0a3cbe1 100644 --- a/Libraries/Lists/SectionList.d.ts +++ b/Libraries/Lists/SectionList.d.ts @@ -11,7 +11,7 @@ import type * as React from 'react'; import type { ListRenderItemInfo, VirtualizedListWithoutRenderItemProps, -} from './VirtualizedList'; +} from '@react-native/virtualized-lists'; import type { ScrollView, ScrollViewProps, diff --git a/Libraries/Lists/SectionList.js b/Libraries/Lists/SectionList.js index d452ee2b7f6419..0f199487b92a23 100644 --- a/Libraries/Lists/SectionList.js +++ b/Libraries/Lists/SectionList.js @@ -12,13 +12,13 @@ import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type { - Props as VirtualizedSectionListProps, ScrollToLocationParamsType, SectionBase as _SectionBase, -} from './VirtualizedSectionList'; + VirtualizedSectionListProps, +} from '@react-native/virtualized-lists'; import Platform from '../Utilities/Platform'; -import VirtualizedSectionList from './VirtualizedSectionList'; +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; import * as React from 'react'; type Item = any; diff --git a/Libraries/Lists/SectionListModern.js b/Libraries/Lists/SectionListModern.js index c7856e13d9a666..d9676f106f8cfc 100644 --- a/Libraries/Lists/SectionListModern.js +++ b/Libraries/Lists/SectionListModern.js @@ -12,14 +12,14 @@ import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; import type { - Props as VirtualizedSectionListProps, ScrollToLocationParamsType, SectionBase as _SectionBase, -} from './VirtualizedSectionList'; + VirtualizedSectionListProps, +} from '@react-native/virtualized-lists'; import type {AbstractComponent, Element, ElementRef} from 'react'; import Platform from '../Utilities/Platform'; -import VirtualizedSectionList from './VirtualizedSectionList'; +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; import React, {forwardRef, useImperativeHandle, useRef} from 'react'; type Item = any; diff --git a/Libraries/Lists/ViewabilityHelper.js b/Libraries/Lists/ViewabilityHelper.js index 33a9811825affd..c7dedfdf496b93 100644 --- a/Libraries/Lists/ViewabilityHelper.js +++ b/Libraries/Lists/ViewabilityHelper.js @@ -10,351 +10,15 @@ 'use strict'; -import type {FrameMetricProps} from './VirtualizedListProps'; +export type { + ViewToken, + ViewabilityConfig, + ViewabilityConfigCallbackPair, +} from '@react-native/virtualized-lists'; -const invariant = require('invariant'); +import {typeof ViewabilityHelper as ViewabilityHelperType} from '@react-native/virtualized-lists'; -export type ViewToken = { - item: any, - key: string, - index: ?number, - isViewable: boolean, - section?: any, - ... -}; - -export type ViewabilityConfigCallbackPair = { - viewabilityConfig: ViewabilityConfig, - onViewableItemsChanged: (info: { - viewableItems: Array, - changed: Array, - ... - }) => void, - ... -}; - -export type ViewabilityConfig = {| - /** - * Minimum amount of time (in milliseconds) that an item must be physically viewable before the - * viewability callback will be fired. A high number means that scrolling through content without - * stopping will not mark the content as viewable. - */ - minimumViewTime?: number, - - /** - * Percent of viewport that must be covered for a partially occluded item to count as - * "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means - * that a single pixel in the viewport makes the item viewable, and a value of 100 means that - * an item must be either entirely visible or cover the entire viewport to count as viewable. - */ - viewAreaCoveragePercentThreshold?: number, - - /** - * Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible, - * rather than the fraction of the viewable area it covers. - */ - itemVisiblePercentThreshold?: number, - - /** - * Nothing is considered viewable until the user scrolls or `recordInteraction` is called after - * render. - */ - waitForInteraction?: boolean, -|}; - -/** - * A Utility class for calculating viewable items based on current metrics like scroll position and - * layout. - * - * An item is said to be in a "viewable" state when any of the following - * is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction` - * is true): - * - * - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item - * visible in the view area >= `itemVisiblePercentThreshold`. - * - Entirely visible on screen - */ -class ViewabilityHelper { - _config: ViewabilityConfig; - _hasInteracted: boolean = false; - _timers: Set = new Set(); - _viewableIndices: Array = []; - _viewableItems: Map = new Map(); - - constructor( - config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0}, - ) { - this._config = config; - } - - /** - * Cleanup, e.g. on unmount. Clears any pending timers. - */ - dispose() { - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To see - * the error delete this comment and run Flow. */ - this._timers.forEach(clearTimeout); - } - - /** - * Determines which items are viewable based on the current metrics and config. - */ - computeViewableItems( - props: FrameMetricProps, - scrollOffset: number, - viewportHeight: number, - getFrameMetrics: ( - index: number, - props: FrameMetricProps, - ) => ?{ - length: number, - offset: number, - ... - }, - // Optional optimization to reduce the scan size - renderRange?: { - first: number, - last: number, - ... - }, - ): Array { - const itemCount = props.getItemCount(props.data); - const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} = - this._config; - const viewAreaMode = viewAreaCoveragePercentThreshold != null; - const viewablePercentThreshold = viewAreaMode - ? viewAreaCoveragePercentThreshold - : itemVisiblePercentThreshold; - invariant( - viewablePercentThreshold != null && - (itemVisiblePercentThreshold != null) !== - (viewAreaCoveragePercentThreshold != null), - 'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold', - ); - const viewableIndices = []; - if (itemCount === 0) { - return viewableIndices; - } - let firstVisible = -1; - const {first, last} = renderRange || {first: 0, last: itemCount - 1}; - if (last >= itemCount) { - console.warn( - 'Invalid render range computing viewability ' + - JSON.stringify({renderRange, itemCount}), - ); - return []; - } - for (let idx = first; idx <= last; idx++) { - const metrics = getFrameMetrics(idx, props); - if (!metrics) { - continue; - } - const top = metrics.offset - scrollOffset; - const bottom = top + metrics.length; - if (top < viewportHeight && bottom > 0) { - firstVisible = idx; - if ( - _isViewable( - viewAreaMode, - viewablePercentThreshold, - top, - bottom, - viewportHeight, - metrics.length, - ) - ) { - viewableIndices.push(idx); - } - } else if (firstVisible >= 0) { - break; - } - } - return viewableIndices; - } - - /** - * Figures out which items are viewable and how that has changed from before and calls - * `onViewableItemsChanged` as appropriate. - */ - onUpdate( - props: FrameMetricProps, - scrollOffset: number, - viewportHeight: number, - getFrameMetrics: ( - index: number, - props: FrameMetricProps, - ) => ?{ - length: number, - offset: number, - ... - }, - createViewToken: ( - index: number, - isViewable: boolean, - props: FrameMetricProps, - ) => ViewToken, - onViewableItemsChanged: ({ - viewableItems: Array, - changed: Array, - ... - }) => void, - // Optional optimization to reduce the scan size - renderRange?: { - first: number, - last: number, - ... - }, - ): void { - const itemCount = props.getItemCount(props.data); - if ( - (this._config.waitForInteraction && !this._hasInteracted) || - itemCount === 0 || - !getFrameMetrics(0, props) - ) { - return; - } - let viewableIndices: Array = []; - if (itemCount) { - viewableIndices = this.computeViewableItems( - props, - scrollOffset, - viewportHeight, - getFrameMetrics, - renderRange, - ); - } - if ( - this._viewableIndices.length === viewableIndices.length && - this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) - ) { - // We might get a lot of scroll events where visibility doesn't change and we don't want to do - // extra work in those cases. - return; - } - this._viewableIndices = viewableIndices; - if (this._config.minimumViewTime) { - const handle: TimeoutID = setTimeout(() => { - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To - * see the error delete this comment and run Flow. */ - this._timers.delete(handle); - this._onUpdateSync( - props, - viewableIndices, - onViewableItemsChanged, - createViewToken, - ); - }, this._config.minimumViewTime); - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To see - * the error delete this comment and run Flow. */ - this._timers.add(handle); - } else { - this._onUpdateSync( - props, - viewableIndices, - onViewableItemsChanged, - createViewToken, - ); - } - } - - /** - * clean-up cached _viewableIndices to evaluate changed items on next update - */ - resetViewableIndices() { - this._viewableIndices = []; - } - - /** - * Records that an interaction has happened even if there has been no scroll. - */ - recordInteraction() { - this._hasInteracted = true; - } - - _onUpdateSync( - props: FrameMetricProps, - viewableIndicesToCheck: Array, - onViewableItemsChanged: ({ - changed: Array, - viewableItems: Array, - ... - }) => void, - createViewToken: ( - index: number, - isViewable: boolean, - props: FrameMetricProps, - ) => ViewToken, - ) { - // Filter out indices that have gone out of view since this call was scheduled. - viewableIndicesToCheck = viewableIndicesToCheck.filter(ii => - this._viewableIndices.includes(ii), - ); - const prevItems = this._viewableItems; - const nextItems = new Map( - viewableIndicesToCheck.map(ii => { - const viewable = createViewToken(ii, true, props); - return [viewable.key, viewable]; - }), - ); - - const changed = []; - for (const [key, viewable] of nextItems) { - if (!prevItems.has(key)) { - changed.push(viewable); - } - } - for (const [key, viewable] of prevItems) { - if (!nextItems.has(key)) { - changed.push({...viewable, isViewable: false}); - } - } - if (changed.length > 0) { - this._viewableItems = nextItems; - onViewableItemsChanged({ - viewableItems: Array.from(nextItems.values()), - changed, - viewabilityConfig: this._config, - }); - } - } -} - -function _isViewable( - viewAreaMode: boolean, - viewablePercentThreshold: number, - top: number, - bottom: number, - viewportHeight: number, - itemLength: number, -): boolean { - if (_isEntirelyVisible(top, bottom, viewportHeight)) { - return true; - } else { - const pixels = _getPixelsVisible(top, bottom, viewportHeight); - const percent = - 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength); - return percent >= viewablePercentThreshold; - } -} - -function _getPixelsVisible( - top: number, - bottom: number, - viewportHeight: number, -): number { - const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); - return Math.max(0, visibleHeight); -} - -function _isEntirelyVisible( - top: number, - bottom: number, - viewportHeight: number, -): boolean { - return top >= 0 && bottom <= viewportHeight && bottom > top; -} +const ViewabilityHelper: ViewabilityHelperType = + require('@react-native/virtualized-lists').ViewabilityHelper; module.exports = ViewabilityHelper; diff --git a/Libraries/Lists/VirtualizeUtils.js b/Libraries/Lists/VirtualizeUtils.js index 3a70d9f683091e..535d25b3abc538 100644 --- a/Libraries/Lists/VirtualizeUtils.js +++ b/Libraries/Lists/VirtualizeUtils.js @@ -10,249 +10,9 @@ 'use strict'; -import type {FrameMetricProps} from './VirtualizedListProps'; +import {typeof keyExtractor as KeyExtractorType} from '@react-native/virtualized-lists'; -/** - * Used to find the indices of the frames that overlap the given offsets. Useful for finding the - * items that bound different windows of content, such as the visible area or the buffered overscan - * area. - */ -export function elementsThatOverlapOffsets( - offsets: Array, - props: FrameMetricProps, - getFrameMetrics: ( - index: number, - props: FrameMetricProps, - ) => { - length: number, - offset: number, - ... - }, - zoomScale: number = 1, -): Array { - const itemCount = props.getItemCount(props.data); - const result = []; - for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) { - const currentOffset = offsets[offsetIndex]; - let left = 0; - let right = itemCount - 1; - - while (left <= right) { - // eslint-disable-next-line no-bitwise - const mid = left + ((right - left) >>> 1); - const frame = getFrameMetrics(mid, props); - const scaledOffsetStart = frame.offset * zoomScale; - const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale; - - // We want the first frame that contains the offset, with inclusive bounds. Thus, for the - // first frame the scaledOffsetStart is inclusive, while for other frames it is exclusive. - if ( - (mid === 0 && currentOffset < scaledOffsetStart) || - (mid !== 0 && currentOffset <= scaledOffsetStart) - ) { - right = mid - 1; - } else if (currentOffset > scaledOffsetEnd) { - left = mid + 1; - } else { - result[offsetIndex] = mid; - break; - } - } - } - - return result; -} - -/** - * Computes the number of elements in the `next` range that are new compared to the `prev` range. - * Handy for calculating how many new items will be rendered when the render window changes so we - * can restrict the number of new items render at once so that content can appear on the screen - * faster. - */ -export function newRangeCount( - prev: { - first: number, - last: number, - ... - }, - next: { - first: number, - last: number, - ... - }, -): number { - return ( - next.last - - next.first + - 1 - - Math.max( - 0, - 1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first), - ) - ); -} - -/** - * Custom logic for determining which items should be rendered given the current frame and scroll - * metrics, as well as the previous render state. The algorithm may evolve over time, but generally - * prioritizes the visible area first, then expands that with overscan regions ahead and behind, - * biased in the direction of scroll. - */ -export function computeWindowedRenderLimits( - props: FrameMetricProps, - maxToRenderPerBatch: number, - windowSize: number, - prev: { - first: number, - last: number, - }, - getFrameMetricsApprox: ( - index: number, - props: FrameMetricProps, - ) => { - length: number, - offset: number, - ... - }, - scrollMetrics: { - dt: number, - offset: number, - velocity: number, - visibleLength: number, - zoomScale: number, - ... - }, -): { - first: number, - last: number, -} { - const itemCount = props.getItemCount(props.data); - if (itemCount === 0) { - return {first: 0, last: -1}; - } - const {offset, velocity, visibleLength, zoomScale = 1} = scrollMetrics; - - // Start with visible area, then compute maximum overscan region by expanding from there, biased - // in the direction of scroll. Total overscan area is capped, which should cap memory consumption - // too. - const visibleBegin = Math.max(0, offset); - const visibleEnd = visibleBegin + visibleLength; - const overscanLength = (windowSize - 1) * visibleLength; - - // Considering velocity seems to introduce more churn than it's worth. - const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5)); - - const fillPreference = - velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none'; - - const overscanBegin = Math.max( - 0, - visibleBegin - (1 - leadFactor) * overscanLength, - ); - const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength); - - const lastItemOffset = - getFrameMetricsApprox(itemCount - 1, props).offset * zoomScale; - if (lastItemOffset < overscanBegin) { - // Entire list is before our overscan window - return { - first: Math.max(0, itemCount - 1 - maxToRenderPerBatch), - last: itemCount - 1, - }; - } - - // Find the indices that correspond to the items at the render boundaries we're targeting. - let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets( - [overscanBegin, visibleBegin, visibleEnd, overscanEnd], - props, - getFrameMetricsApprox, - zoomScale, - ); - overscanFirst = overscanFirst == null ? 0 : overscanFirst; - first = first == null ? Math.max(0, overscanFirst) : first; - overscanLast = overscanLast == null ? itemCount - 1 : overscanLast; - last = - last == null - ? Math.min(overscanLast, first + maxToRenderPerBatch - 1) - : last; - const visible = {first, last}; - - // We want to limit the number of new cells we're rendering per batch so that we can fill the - // content on the screen quickly. If we rendered the entire overscan window at once, the user - // could be staring at white space for a long time waiting for a bunch of offscreen content to - // render. - let newCellCount = newRangeCount(prev, visible); - - while (true) { - if (first <= overscanFirst && last >= overscanLast) { - // If we fill the entire overscan range, we're done. - break; - } - const maxNewCells = newCellCount >= maxToRenderPerBatch; - const firstWillAddMore = first <= prev.first || first > prev.last; - const firstShouldIncrement = - first > overscanFirst && (!maxNewCells || !firstWillAddMore); - const lastWillAddMore = last >= prev.last || last < prev.first; - const lastShouldIncrement = - last < overscanLast && (!maxNewCells || !lastWillAddMore); - if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) { - // We only want to stop if we've hit maxNewCells AND we cannot increment first or last - // without rendering new items. This let's us preserve as many already rendered items as - // possible, reducing render churn and keeping the rendered overscan range as large as - // possible. - break; - } - if ( - firstShouldIncrement && - !(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore) - ) { - if (firstWillAddMore) { - newCellCount++; - } - first--; - } - if ( - lastShouldIncrement && - !(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore) - ) { - if (lastWillAddMore) { - newCellCount++; - } - last++; - } - } - if ( - !( - last >= first && - first >= 0 && - last < itemCount && - first >= overscanFirst && - last <= overscanLast && - first <= visible.first && - last >= visible.last - ) - ) { - throw new Error( - 'Bad window calculation ' + - JSON.stringify({ - first, - last, - itemCount, - overscanFirst, - overscanLast, - visible, - }), - ); - } - return {first, last}; -} +const keyExtractor: KeyExtractorType = + require('@react-native/virtualized-lists').keyExtractor; -export function keyExtractor(item: any, index: number): string { - if (typeof item === 'object' && item?.key != null) { - return item.key; - } - if (typeof item === 'object' && item?.id != null) { - return item.id; - } - return String(index); -} +module.exports = {keyExtractor}; diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index efce2bed49bfcd..2488b1e5e37f57 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -8,1945 +8,16 @@ * @format */ -import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; -import type {LayoutEvent, ScrollEvent} from '../Types/CoreEventTypes'; -import type {ViewToken} from './ViewabilityHelper'; -import type { - FrameMetricProps, - Item, - Props, - RenderItemProps, - RenderItemType, - Separators, -} from './VirtualizedListProps'; - -import RefreshControl from '../Components/RefreshControl/RefreshControl'; -import ScrollView from '../Components/ScrollView/ScrollView'; -import View from '../Components/View/View'; -import Batchinator from '../Interaction/Batchinator'; -import {findNodeHandle} from '../ReactNative/RendererProxy'; -import flattenStyle from '../StyleSheet/flattenStyle'; -import StyleSheet from '../StyleSheet/StyleSheet'; -import clamp from '../Utilities/clamp'; -import infoLog from '../Utilities/infoLog'; -import {CellRenderMask} from './CellRenderMask'; -import ChildListCollection from './ChildListCollection'; -import FillRateHelper from './FillRateHelper'; -import StateSafePureComponent from './StateSafePureComponent'; -import ViewabilityHelper from './ViewabilityHelper'; -import CellRenderer from './VirtualizedListCellRenderer'; -import { - VirtualizedListCellContextProvider, - VirtualizedListContext, - VirtualizedListContextProvider, -} from './VirtualizedListContext.js'; -import { - computeWindowedRenderLimits, - keyExtractor as defaultKeyExtractor, -} from './VirtualizeUtils'; -import invariant from 'invariant'; -import * as React from 'react'; - -export type {RenderItemProps, RenderItemType, Separators}; - -const ON_EDGE_REACHED_EPSILON = 0.001; - -let _usedIndexForKey = false; -let _keylessItemComponentName: string = ''; - -type ViewabilityHelperCallbackTuple = { - viewabilityHelper: ViewabilityHelper, - onViewableItemsChanged: (info: { - viewableItems: Array, - changed: Array, - ... - }) => void, - ... -}; - -type State = { - renderMask: CellRenderMask, - cellsAroundViewport: {first: number, last: number}, -}; - -/** - * Default Props Helper Functions - * Use the following helper functions for default values - */ - -// horizontalOrDefault(this.props.horizontal) -function horizontalOrDefault(horizontal: ?boolean) { - return horizontal ?? false; -} - -// initialNumToRenderOrDefault(this.props.initialNumToRenderOrDefault) -function initialNumToRenderOrDefault(initialNumToRender: ?number) { - return initialNumToRender ?? 10; -} - -// maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch) -function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) { - return maxToRenderPerBatch ?? 10; -} - -// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold) -function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) { - return onStartReachedThreshold ?? 2; -} - -// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold) -function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) { - return onEndReachedThreshold ?? 2; -} - -// getScrollingThreshold(visibleLength, onEndReachedThreshold) -function getScrollingThreshold(threshold: number, visibleLength: number) { - return (threshold * visibleLength) / 2; -} - -// scrollEventThrottleOrDefault(this.props.scrollEventThrottle) -function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) { - return scrollEventThrottle ?? 50; -} - -// windowSizeOrDefault(this.props.windowSize) -function windowSizeOrDefault(windowSize: ?number) { - return windowSize ?? 21; -} - -function findLastWhere( - arr: $ReadOnlyArray, - predicate: (element: T) => boolean, -): T | null { - for (let i = arr.length - 1; i >= 0; i--) { - if (predicate(arr[i])) { - return arr[i]; - } - } - - return null; -} - -/** - * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) - * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better - * documented. In general, this should only really be used if you need more flexibility than - * `FlatList` provides, e.g. for use with immutable data instead of plain arrays. - * - * Virtualization massively improves memory consumption and performance of large lists by - * maintaining a finite render window of active items and replacing all items outside of the render - * window with appropriately sized blank space. The window adapts to scrolling behavior, and items - * are rendered incrementally with low-pri (after any running interactions) if they are far from the - * visible area, or with hi-pri otherwise to minimize the potential of seeing blank space. - * - * Some caveats: - * - * - Internal state is not preserved when content scrolls out of the render window. Make sure all - * your data is captured in the item data or external stores like Flux, Redux, or Relay. - * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- - * equal. Make sure that everything your `renderItem` function depends on is passed as a prop - * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on - * changes. This includes the `data` prop and parent component state. - * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously - * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see - * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, - * and we are working on improving it behind the scenes. - * - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key. - * Alternatively, you can provide a custom `keyExtractor` prop. - * - As an effort to remove defaultProps, use helper functions when referencing certain props - * - */ -export default class VirtualizedList extends StateSafePureComponent< - Props, - State, -> { - static contextType: typeof VirtualizedListContext = VirtualizedListContext; - - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params?: ?{animated?: ?boolean, ...}) { - const animated = params ? params.animated : true; - const veryLast = this.props.getItemCount(this.props.data) - 1; - if (veryLast < 0) { - return; - } - const frame = this.__getFrameMetricsApprox(veryLast, this.props); - const offset = Math.max( - 0, - frame.offset + - frame.length + - this._footerLength - - this._scrollMetrics.visibleLength, - ); - - if (this._scrollRef == null) { - return; - } - - if (this._scrollRef.scrollTo == null) { - console.warn( - 'No scrollTo method provided. This may be because you have two nested ' + - 'VirtualizedLists with the same orientation, or because you are ' + - 'using a custom component that does not implement scrollTo.', - ); - return; - } - - this._scrollRef.scrollTo( - horizontalOrDefault(this.props.horizontal) - ? {x: offset, animated} - : {y: offset, animated}, - ); - } - - // scrollToIndex may be janky without getItemLayout prop - scrollToIndex(params: { - animated?: ?boolean, - index: number, - viewOffset?: number, - viewPosition?: number, - ... - }): $FlowFixMe { - const { - data, - horizontal, - getItemCount, - getItemLayout, - onScrollToIndexFailed, - } = this.props; - const {animated, index, viewOffset, viewPosition} = params; - invariant( - index >= 0, - `scrollToIndex out of range: requested index ${index} but minimum is 0`, - ); - invariant( - getItemCount(data) >= 1, - `scrollToIndex out of range: item length ${getItemCount( - data, - )} but minimum is 1`, - ); - invariant( - index < getItemCount(data), - `scrollToIndex out of range: requested index ${index} is out of 0 to ${ - getItemCount(data) - 1 - }`, - ); - if (!getItemLayout && index > this._highestMeasuredFrameIndex) { - invariant( - !!onScrollToIndexFailed, - 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' + - 'otherwise there is no way to know the location of offscreen indices or handle failures.', - ); - onScrollToIndexFailed({ - averageItemLength: this._averageCellLength, - highestMeasuredFrameIndex: this._highestMeasuredFrameIndex, - index, - }); - return; - } - const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props); - const offset = - Math.max( - 0, - this._getOffsetApprox(index, this.props) - - (viewPosition || 0) * - (this._scrollMetrics.visibleLength - frame.length), - ) - (viewOffset || 0); - - if (this._scrollRef == null) { - return; - } - - if (this._scrollRef.scrollTo == null) { - console.warn( - 'No scrollTo method provided. This may be because you have two nested ' + - 'VirtualizedLists with the same orientation, or because you are ' + - 'using a custom component that does not implement scrollTo.', - ); - return; - } - - this._scrollRef.scrollTo( - horizontal ? {x: offset, animated} : {y: offset, animated}, - ); - } - - // scrollToItem may be janky without getItemLayout prop. Required linear scan through items - - // use scrollToIndex instead if possible. - scrollToItem(params: { - animated?: ?boolean, - item: Item, - viewOffset?: number, - viewPosition?: number, - ... - }) { - const {item} = params; - const {data, getItem, getItemCount} = this.props; - const itemCount = getItemCount(data); - for (let index = 0; index < itemCount; index++) { - if (getItem(data, index) === item) { - this.scrollToIndex({...params, index}); - break; - } - } - } - - /** - * Scroll to a specific content pixel offset in the list. - * - * Param `offset` expects the offset to scroll to. - * In case of `horizontal` is true, the offset is the x-value, - * in any other case the offset is the y-value. - * - * Param `animated` (`true` by default) defines whether the list - * should do an animation while scrolling. - */ - scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { - const {animated, offset} = params; - - if (this._scrollRef == null) { - return; - } - - if (this._scrollRef.scrollTo == null) { - console.warn( - 'No scrollTo method provided. This may be because you have two nested ' + - 'VirtualizedLists with the same orientation, or because you are ' + - 'using a custom component that does not implement scrollTo.', - ); - return; - } - - this._scrollRef.scrollTo( - horizontalOrDefault(this.props.horizontal) - ? {x: offset, animated} - : {y: offset, animated}, - ); - } - - recordInteraction() { - this._nestedChildLists.forEach(childList => { - childList.recordInteraction(); - }); - this._viewabilityTuples.forEach(t => { - t.viewabilityHelper.recordInteraction(); - }); - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - } - - flashScrollIndicators() { - if (this._scrollRef == null) { - return; - } - - this._scrollRef.flashScrollIndicators(); - } - - /** - * Provides a handle to the underlying scroll responder. - * Note that `this._scrollRef` might not be a `ScrollView`, so we - * need to check that it responds to `getScrollResponder` before calling it. - */ - getScrollResponder(): ?ScrollResponderType { - if (this._scrollRef && this._scrollRef.getScrollResponder) { - return this._scrollRef.getScrollResponder(); - } - } - - getScrollableNode(): ?number { - if (this._scrollRef && this._scrollRef.getScrollableNode) { - return this._scrollRef.getScrollableNode(); - } else { - return findNodeHandle(this._scrollRef); - } - } - - getScrollRef(): - | ?React.ElementRef - | ?React.ElementRef { - if (this._scrollRef && this._scrollRef.getScrollRef) { - return this._scrollRef.getScrollRef(); - } else { - return this._scrollRef; - } - } - - setNativeProps(props: Object) { - if (this._scrollRef) { - this._scrollRef.setNativeProps(props); - } - } - - _getCellKey(): string { - return this.context?.cellKey || 'rootList'; - } - - // $FlowFixMe[missing-local-annot] - _getScrollMetrics = () => { - return this._scrollMetrics; - }; - - hasMore(): boolean { - return this._hasMore; - } - - // $FlowFixMe[missing-local-annot] - _getOutermostParentListRef = () => { - if (this._isNestedWithSameOrientation()) { - return this.context.getOutermostParentListRef(); - } else { - return this; - } - }; - - _registerAsNestedChild = (childList: { - cellKey: string, - ref: React.ElementRef, - }): void => { - this._nestedChildLists.add(childList.ref, childList.cellKey); - if (this._hasInteracted) { - childList.ref.recordInteraction(); - } - }; - - _unregisterAsNestedChild = (childList: { - ref: React.ElementRef, - }): void => { - this._nestedChildLists.remove(childList.ref); - }; - - state: State; - - constructor(props: Props) { - super(props); - invariant( - // $FlowFixMe[prop-missing] - !props.onScroll || !props.onScroll.__isNative, - 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + - 'to support native onScroll events with useNativeDriver', - ); - invariant( - windowSizeOrDefault(props.windowSize) > 0, - 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.', - ); - - invariant( - props.getItemCount, - 'VirtualizedList: The "getItemCount" prop must be provided', - ); - - this._fillRateHelper = new FillRateHelper(this._getFrameMetrics); - this._updateCellsToRenderBatcher = new Batchinator( - this._updateCellsToRender, - this.props.updateCellsBatchingPeriod ?? 50, - ); - - if (this.props.viewabilityConfigCallbackPairs) { - this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map( - pair => ({ - viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig), - onViewableItemsChanged: pair.onViewableItemsChanged, - }), - ); - } else { - const {onViewableItemsChanged, viewabilityConfig} = this.props; - if (onViewableItemsChanged) { - this._viewabilityTuples.push({ - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged, - }); - } - } - - invariant( - !this.context, - 'Unexpectedly saw VirtualizedListContext available in ctor', - ); - - const initialRenderRegion = VirtualizedList._initialRenderRegion(props); - - this.state = { - cellsAroundViewport: initialRenderRegion, - renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), - }; - } - - static _createRenderMask( - props: Props, - cellsAroundViewport: {first: number, last: number}, - additionalRegions?: ?$ReadOnlyArray<{first: number, last: number}>, - ): CellRenderMask { - const itemCount = props.getItemCount(props.data); - - invariant( - cellsAroundViewport.first >= 0 && - cellsAroundViewport.last >= cellsAroundViewport.first - 1 && - cellsAroundViewport.last < itemCount, - `Invalid cells around viewport "[${cellsAroundViewport.first}, ${cellsAroundViewport.last}]" was passed to VirtualizedList._createRenderMask`, - ); - - const renderMask = new CellRenderMask(itemCount); - - if (itemCount > 0) { - const allRegions = [cellsAroundViewport, ...(additionalRegions ?? [])]; - for (const region of allRegions) { - renderMask.addCells(region); - } - - // The initially rendered cells are retained as part of the - // "scroll-to-top" optimization - if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) { - const initialRegion = VirtualizedList._initialRenderRegion(props); - renderMask.addCells(initialRegion); - } - - // The layout coordinates of sticker headers may be off-screen while the - // actual header is on-screen. Keep the most recent before the viewport - // rendered, even if its layout coordinates are not in viewport. - const stickyIndicesSet = new Set(props.stickyHeaderIndices); - VirtualizedList._ensureClosestStickyHeader( - props, - stickyIndicesSet, - renderMask, - cellsAroundViewport.first, - ); - } - - return renderMask; - } - - static _initialRenderRegion(props: Props): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); - const scrollIndex = Math.floor(Math.max(0, props.initialScrollIndex ?? 0)); - - return { - first: scrollIndex, - last: - Math.min( - itemCount, - scrollIndex + initialNumToRenderOrDefault(props.initialNumToRender), - ) - 1, - }; - } - - static _ensureClosestStickyHeader( - props: Props, - stickyIndicesSet: Set, - renderMask: CellRenderMask, - cellIdx: number, - ) { - const stickyOffset = props.ListHeaderComponent ? 1 : 0; - - for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) { - if (stickyIndicesSet.has(itemIdx + stickyOffset)) { - renderMask.addCells({first: itemIdx, last: itemIdx}); - break; - } - } - } - - _adjustCellsAroundViewport( - props: Props, - cellsAroundViewport: {first: number, last: number}, - ): {first: number, last: number} { - const {data, getItemCount} = props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( - props.onEndReachedThreshold, - ); - this._updateViewableItems(props, cellsAroundViewport); - - const {contentLength, offset, visibleLength} = this._scrollMetrics; - const distanceFromEnd = contentLength - visibleLength - offset; - - // Wait until the scroll view metrics have been set up. And until then, - // we will trust the initialNumToRender suggestion - if (visibleLength <= 0 || contentLength <= 0) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; - } - - let newCellsAroundViewport: {first: number, last: number}; - if (props.disableVirtualization) { - const renderAhead = - distanceFromEnd < onEndReachedThreshold * visibleLength - ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) - : 0; - - newCellsAroundViewport = { - first: 0, - last: Math.min( - cellsAroundViewport.last + renderAhead, - getItemCount(data) - 1, - ), - }; - } else { - // If we have a non-zero initialScrollIndex and run this before we've scrolled, - // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. - // So let's wait until we've scrolled the view to the right place. And until then, - // we will trust the initialScrollIndex suggestion. - - // Thus, we want to recalculate the windowed render limits if any of the following hold: - // - initialScrollIndex is undefined or is 0 - // - initialScrollIndex > 0 AND scrolling is complete - // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case - // where the list is shorter than the visible area) - if ( - props.initialScrollIndex && - !this._scrollMetrics.offset && - Math.abs(distanceFromEnd) >= Number.EPSILON - ) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; - } - - newCellsAroundViewport = computeWindowedRenderLimits( - props, - maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), - windowSizeOrDefault(props.windowSize), - cellsAroundViewport, - this.__getFrameMetricsApprox, - this._scrollMetrics, - ); - invariant( - newCellsAroundViewport.last < getItemCount(data), - 'computeWindowedRenderLimits() should return range in-bounds', - ); - } - - if (this._nestedChildLists.size() > 0) { - // If some cell in the new state has a child list in it, we should only render - // up through that item, so that we give that list a chance to render. - // Otherwise there's churn from multiple child lists mounting and un-mounting - // their items. - - // Will this prevent rendering if the nested list doesn't realize the end? - const childIdx = this._findFirstChildWithMore( - newCellsAroundViewport.first, - newCellsAroundViewport.last, - ); - - newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last; - } - - return newCellsAroundViewport; - } - - _findFirstChildWithMore(first: number, last: number): number | null { - for (let ii = first; ii <= last; ii++) { - const cellKeyForIndex = this._indicesToKeys.get(ii); - if ( - cellKeyForIndex != null && - this._nestedChildLists.anyInCell(cellKeyForIndex, childList => - childList.hasMore(), - ) - ) { - return ii; - } - } - - return null; - } +'use strict'; - componentDidMount() { - if (this._isNestedWithSameOrientation()) { - this.context.registerAsNestedChild({ - ref: this, - cellKey: this.context.cellKey, - }); - } - } +import {typeof VirtualizedList as VirtualizedListType} from '@react-native/virtualized-lists'; - componentWillUnmount() { - if (this._isNestedWithSameOrientation()) { - this.context.unregisterAsNestedChild({ref: this}); - } - this._updateCellsToRenderBatcher.dispose({abort: true}); - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.dispose(); - }); - this._fillRateHelper.deactivateAndFlush(); - } +const VirtualizedList: VirtualizedListType = + require('@react-native/virtualized-lists').VirtualizedList; - static getDerivedStateFromProps(newProps: Props, prevState: State): State { - // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make - // sure we're rendering a reasonable range here. - const itemCount = newProps.getItemCount(newProps.data); - if (itemCount === prevState.renderMask.numCells()) { - return prevState; - } - - const constrainedCells = VirtualizedList._constrainToItemCount( - prevState.cellsAroundViewport, - newProps, - ); - - return { - cellsAroundViewport: constrainedCells, - renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), - }; - } - - _pushCells( - cells: Array, - stickyHeaderIndices: Array, - stickyIndicesFromProps: Set, - first: number, - last: number, - inversionStyle: ViewStyleProp, - ) { - const { - CellRendererComponent, - ItemSeparatorComponent, - ListHeaderComponent, - ListItemComponent, - data, - debug, - getItem, - getItemCount, - getItemLayout, - horizontal, - renderItem, - } = this.props; - const stickyOffset = ListHeaderComponent ? 1 : 0; - const end = getItemCount(data) - 1; - let prevCellKey; - last = Math.min(end, last); - for (let ii = first; ii <= last; ii++) { - const item = getItem(data, ii); - const key = this._keyExtractor(item, ii, this.props); - this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { - stickyHeaderIndices.push(cells.length); - } - cells.push( - this._onCellFocusCapture(key)} - onUnmount={this._onCellUnmount} - ref={ref => { - this._cellRefs[key] = ref; - }} - renderItem={renderItem} - />, - ); - prevCellKey = key; - } - } - - static _constrainToItemCount( - cells: {first: number, last: number}, - props: Props, - ): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); - const last = Math.min(itemCount - 1, cells.last); - - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - props.maxToRenderPerBatch, - ); - - return { - first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), - last, - }; - } - - _onUpdateSeparators = (keys: Array, newProps: Object) => { - keys.forEach(key => { - const ref = key != null && this._cellRefs[key]; - ref && ref.updateSeparatorProps(newProps); - }); - }; - - _isNestedWithSameOrientation(): boolean { - const nestedContext = this.context; - return !!( - nestedContext && - !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal) - ); - } - - _getSpacerKey = (isVertical: boolean): string => - isVertical ? 'height' : 'width'; - - _keyExtractor( - item: Item, - index: number, - props: { - keyExtractor?: ?(item: Item, index: number) => string, - ... - }, - // $FlowFixMe[missing-local-annot] - ) { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } - - const key = defaultKeyExtractor(item, index); - if (key === String(index)) { - _usedIndexForKey = true; - if (item.type && item.type.displayName) { - _keylessItemComponentName = item.type.displayName; - } - } - return key; - } - - render(): React.Node { - if (__DEV__) { - // $FlowFixMe[underconstrained-implicit-instantiation] - const flatStyles = flattenStyle(this.props.contentContainerStyle); - if (flatStyles != null && flatStyles.flexWrap === 'wrap') { - console.warn( - '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + - 'Consider using `numColumns` with `FlatList` instead.', - ); - } - } - const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = - this.props; - const {data, horizontal} = this.props; - const inversionStyle = this.props.inverted - ? horizontalOrDefault(this.props.horizontal) - ? styles.horizontallyInverted - : styles.verticallyInverted - : null; - const cells: Array = []; - const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); - const stickyHeaderIndices = []; - - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { - stickyHeaderIndices.push(0); - } - const element = React.isValidElement(ListHeaderComponent) ? ( - ListHeaderComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - ); - cells.push( - - - { - // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors - element - } - - , - ); - } - - // 2a. Add a cell for ListEmptyComponent if applicable - const itemCount = this.props.getItemCount(data); - if (itemCount === 0 && ListEmptyComponent) { - const element: React.Element = ((React.isValidElement( - ListEmptyComponent, - ) ? ( - ListEmptyComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - )): any); - cells.push( - - {React.cloneElement(element, { - onLayout: (event: LayoutEvent) => { - this._onLayoutEmpty(event); - if (element.props.onLayout) { - element.props.onLayout(event); - } - }, - style: StyleSheet.compose(inversionStyle, element.props.style), - })} - , - ); - } - - // 2b. Add cells and spacers for each item - if (itemCount > 0) { - _usedIndexForKey = false; - _keylessItemComponentName = ''; - const spacerKey = this._getSpacerKey(!horizontal); - - const renderRegions = this.state.renderMask.enumerateRegions(); - const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); - - for (const section of renderRegions) { - if (section.isSpacer) { - // Legacy behavior is to avoid spacers when virtualization is - // disabled (including head spacers on initial render). - if (this.props.disableVirtualization) { - continue; - } - - // Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to - // prevent the user for hyperscrolling into un-measured area because otherwise content will - // likely jump around as it renders in above the viewport. - const isLastSpacer = section === lastSpacer; - const constrainToMeasured = isLastSpacer && !this.props.getItemLayout; - const last = constrainToMeasured - ? clamp( - section.first - 1, - section.last, - this._highestMeasuredFrameIndex, - ) - : section.last; - - const firstMetrics = this.__getFrameMetricsApprox( - section.first, - this.props, - ); - const lastMetrics = this.__getFrameMetricsApprox(last, this.props); - const spacerSize = - lastMetrics.offset + lastMetrics.length - firstMetrics.offset; - cells.push( - , - ); - } else { - this._pushCells( - cells, - stickyHeaderIndices, - stickyIndicesFromProps, - section.first, - section.last, - inversionStyle, - ); - } - } - - if (!this._hasWarned.keys && _usedIndexForKey) { - console.warn( - 'VirtualizedList: missing keys for items, make sure to specify a key or id property on each ' + - 'item or provide a custom keyExtractor.', - _keylessItemComponentName, - ); - this._hasWarned.keys = true; - } - } - - // 3. Add cell for ListFooterComponent - if (ListFooterComponent) { - const element = React.isValidElement(ListFooterComponent) ? ( - ListFooterComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - ); - cells.push( - - - { - // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors - element - } - - , - ); - } - - // 4. Render the ScrollView - const scrollProps = { - ...this.props, - onContentSizeChange: this._onContentSizeChange, - onLayout: this._onLayout, - onScroll: this._onScroll, - onScrollBeginDrag: this._onScrollBeginDrag, - onScrollEndDrag: this._onScrollEndDrag, - onMomentumScrollBegin: this._onMomentumScrollBegin, - onMomentumScrollEnd: this._onMomentumScrollEnd, - scrollEventThrottle: scrollEventThrottleOrDefault( - this.props.scrollEventThrottle, - ), // TODO: Android support - invertStickyHeaders: - this.props.invertStickyHeaders !== undefined - ? this.props.invertStickyHeaders - : this.props.inverted, - stickyHeaderIndices, - style: inversionStyle - ? [inversionStyle, this.props.style] - : this.props.style, - }; - - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; - - const innerRet = ( - - {React.cloneElement( - ( - this.props.renderScrollComponent || - this._defaultRenderScrollComponent - )(scrollProps), - { - ref: this._captureScrollRef, - }, - cells, - )} - - ); - let ret: React.Node = innerRet; - if (__DEV__) { - ret = ( - - {scrollContext => { - if ( - scrollContext != null && - !scrollContext.horizontal === - !horizontalOrDefault(this.props.horizontal) && - !this._hasWarned.nesting && - this.context == null && - this.props.scrollEnabled !== false - ) { - // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170 - console.error( - 'VirtualizedLists should never be nested inside plain ScrollViews with the same ' + - 'orientation because it can break windowing and other functionality - use another ' + - 'VirtualizedList-backed container instead.', - ); - this._hasWarned.nesting = true; - } - return innerRet; - }} - - ); - } - if (this.props.debug) { - return ( - - {ret} - {this._renderDebugOverlay()} - - ); - } else { - return ret; - } - } - - componentDidUpdate(prevProps: Props) { - const {data, extraData} = this.props; - if (data !== prevProps.data || extraData !== prevProps.extraData) { - // clear the viewableIndices cache to also trigger - // the onViewableItemsChanged callback with the new data - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.resetViewableIndices(); - }); - } - // The `this._hiPriInProgress` is guaranteeing a hiPri cell update will only happen - // once per fiber update. The `_scheduleCellsToRenderUpdate` will set it to true - // if a hiPri update needs to perform. If `componentDidUpdate` is triggered with - // `this._hiPriInProgress=true`, means it's triggered by the hiPri update. The - // `_scheduleCellsToRenderUpdate` will check this condition and not perform - // another hiPri update. - const hiPriInProgress = this._hiPriInProgress; - this._scheduleCellsToRenderUpdate(); - // Make sure setting `this._hiPriInProgress` back to false after `componentDidUpdate` - // is triggered with `this._hiPriInProgress = true` - if (hiPriInProgress) { - this._hiPriInProgress = false; - } - } - - _averageCellLength = 0; - _cellRefs: {[string]: null | CellRenderer} = {}; - _fillRateHelper: FillRateHelper; - _frames: { - [string]: { - inLayout?: boolean, - index: number, - length: number, - offset: number, - }, - } = {}; - _footerLength = 0; - // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex - _hasTriggeredInitialScrollToIndex = false; - _hasInteracted = false; - _hasMore = false; - _hasWarned: {[string]: boolean} = {}; - _headerLength = 0; - _hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update - _highestMeasuredFrameIndex = 0; - _indicesToKeys: Map = new Map(); - _lastFocusedCellKey: ?string = null; - _nestedChildLists: ChildListCollection = - new ChildListCollection(); - _offsetFromParentVirtualizedList: number = 0; - _prevParentOffset: number = 0; - // $FlowFixMe[missing-local-annot] - _scrollMetrics = { - contentLength: 0, - dOffset: 0, - dt: 10, - offset: 0, - timestamp: 0, - velocity: 0, - visibleLength: 0, - zoomScale: 1, - }; - _scrollRef: ?React.ElementRef = null; - _sentStartForContentLength = 0; - _sentEndForContentLength = 0; - _totalCellLength = 0; - _totalCellsMeasured = 0; - _updateCellsToRenderBatcher: Batchinator; - _viewabilityTuples: Array = []; - - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ - _captureScrollRef = ref => { - this._scrollRef = ref; - }; - - _computeBlankness() { - this._fillRateHelper.computeBlankness( - this.props, - this.state.cellsAroundViewport, - this._scrollMetrics, - ); - } - - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; - } else if (onRefresh) { - invariant( - typeof props.refreshing === 'boolean', - '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + - JSON.stringify(props.refreshing ?? 'undefined') + - '`', - ); - return ( - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] - - ) : ( - props.refreshControl - ) - } - /> - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] - return ; - } - }; - - _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => { - const layout = e.nativeEvent.layout; - const next = { - offset: this._selectOffset(layout), - length: this._selectLength(layout), - index, - inLayout: true, - }; - const curr = this._frames[cellKey]; - if ( - !curr || - next.offset !== curr.offset || - next.length !== curr.length || - index !== curr.index - ) { - this._totalCellLength += next.length - (curr ? curr.length : 0); - this._totalCellsMeasured += curr ? 0 : 1; - this._averageCellLength = - this._totalCellLength / this._totalCellsMeasured; - this._frames[cellKey] = next; - this._highestMeasuredFrameIndex = Math.max( - this._highestMeasuredFrameIndex, - index, - ); - this._scheduleCellsToRenderUpdate(); - } else { - this._frames[cellKey].inLayout = true; - } - - this._triggerRemeasureForChildListsInCell(cellKey); - - this._computeBlankness(); - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - }; - - _onCellFocusCapture(cellKey: string) { - this._lastFocusedCellKey = cellKey; - const renderMask = VirtualizedList._createRenderMask( - this.props, - this.state.cellsAroundViewport, - this._getNonViewportRenderRegions(this.props), - ); - - this.setState(state => { - if (!renderMask.equals(state.renderMask)) { - return {renderMask}; - } - return null; - }); - } - - _onCellUnmount = (cellKey: string) => { - const curr = this._frames[cellKey]; - if (curr) { - this._frames[cellKey] = {...curr, inLayout: false}; - } - }; - - _triggerRemeasureForChildListsInCell(cellKey: string): void { - this._nestedChildLists.forEachInCell(cellKey, childList => { - childList.measureLayoutRelativeToContainingList(); - }); - } - - measureLayoutRelativeToContainingList(): void { - // TODO (T35574538): findNodeHandle sometimes crashes with "Unable to find - // node on an unmounted component" during scrolling - try { - if (!this._scrollRef) { - return; - } - // We are assuming that getOutermostParentListRef().getScrollRef() - // is a non-null reference to a ScrollView - this._scrollRef.measureLayout( - this.context.getOutermostParentListRef().getScrollRef(), - (x, y, width, height) => { - this._offsetFromParentVirtualizedList = this._selectOffset({x, y}); - this._scrollMetrics.contentLength = this._selectLength({ - width, - height, - }); - const scrollMetrics = this._convertParentScrollMetrics( - this.context.getScrollMetrics(), - ); - - const metricsChanged = - this._scrollMetrics.visibleLength !== scrollMetrics.visibleLength || - this._scrollMetrics.offset !== scrollMetrics.offset; - - if (metricsChanged) { - this._scrollMetrics.visibleLength = scrollMetrics.visibleLength; - this._scrollMetrics.offset = scrollMetrics.offset; - - // If metrics of the scrollView changed, then we triggered remeasure for child list - // to ensure VirtualizedList has the right information. - this._nestedChildLists.forEach(childList => { - childList.measureLayoutRelativeToContainingList(); - }); - } - }, - error => { - console.warn( - "VirtualizedList: Encountered an error while measuring a list's" + - ' offset from its containing VirtualizedList.', - ); - }, - ); - } catch (error) { - console.warn( - 'measureLayoutRelativeToContainingList threw an error', - error.stack, - ); - } - } - - _onLayout = (e: LayoutEvent) => { - if (this._isNestedWithSameOrientation()) { - // Need to adjust our scroll metrics to be relative to our containing - // VirtualizedList before we can make claims about list item viewability - this.measureLayoutRelativeToContainingList(); - } else { - this._scrollMetrics.visibleLength = this._selectLength( - e.nativeEvent.layout, - ); - } - this.props.onLayout && this.props.onLayout(e); - this._scheduleCellsToRenderUpdate(); - this._maybeCallOnEdgeReached(); - }; - - _onLayoutEmpty = (e: LayoutEvent) => { - this.props.onLayout && this.props.onLayout(e); - }; - - _getFooterCellKey(): string { - return this._getCellKey() + '-footer'; - } - - _onLayoutFooter = (e: LayoutEvent) => { - this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()); - this._footerLength = this._selectLength(e.nativeEvent.layout); - }; - - _onLayoutHeader = (e: LayoutEvent) => { - this._headerLength = this._selectLength(e.nativeEvent.layout); - }; - - // $FlowFixMe[missing-local-annot] - _renderDebugOverlay() { - const normalize = - this._scrollMetrics.visibleLength / - (this._scrollMetrics.contentLength || 1); - const framesInLayout = []; - const itemCount = this.props.getItemCount(this.props.data); - for (let ii = 0; ii < itemCount; ii++) { - const frame = this.__getFrameMetricsApprox(ii, this.props); - /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { - framesInLayout.push(frame); - } - } - const windowTop = this.__getFrameMetricsApprox( - this.state.cellsAroundViewport.first, - this.props, - ).offset; - const frameLast = this.__getFrameMetricsApprox( - this.state.cellsAroundViewport.last, - this.props, - ); - const windowLen = frameLast.offset + frameLast.length - windowTop; - const visTop = this._scrollMetrics.offset; - const visLen = this._scrollMetrics.visibleLength; - - return ( - - {framesInLayout.map((f, ii) => ( - - ))} - - - - ); - } - - _selectLength( - metrics: $ReadOnly<{ - height: number, - width: number, - ... - }>, - ): number { - return !horizontalOrDefault(this.props.horizontal) - ? metrics.height - : metrics.width; - } - - _selectOffset( - metrics: $ReadOnly<{ - x: number, - y: number, - ... - }>, - ): number { - return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x; - } - - _maybeCallOnEdgeReached() { - const { - data, - getItemCount, - onStartReached, - onStartReachedThreshold, - onEndReached, - onEndReachedThreshold, - initialScrollIndex, - } = this.props; - const {contentLength, visibleLength, offset} = this._scrollMetrics; - let distanceFromStart = offset; - let distanceFromEnd = contentLength - visibleLength - offset; - - // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0 - // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus - // be at the edge of the list with a distance approximating 0 but not quite there. - if (distanceFromStart < ON_EDGE_REACHED_EPSILON) { - distanceFromStart = 0; - } - if (distanceFromEnd < ON_EDGE_REACHED_EPSILON) { - distanceFromEnd = 0; - } - - // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px - // when oERT is not present (different from 2 viewports used elsewhere) - const DEFAULT_THRESHOLD_PX = 2; - - const startThreshold = - onStartReachedThreshold != null - ? onStartReachedThreshold * visibleLength - : DEFAULT_THRESHOLD_PX; - const endThreshold = - onEndReachedThreshold != null - ? onEndReachedThreshold * visibleLength - : DEFAULT_THRESHOLD_PX; - const isWithinStartThreshold = distanceFromStart <= startThreshold; - const isWithinEndThreshold = distanceFromEnd <= endThreshold; - - // First check if the user just scrolled within the end threshold - // and call onEndReached only once for a given content length, - // and only if onStartReached is not being executed - if ( - onEndReached && - this.state.cellsAroundViewport.last === getItemCount(data) - 1 && - isWithinEndThreshold && - this._scrollMetrics.contentLength !== this._sentEndForContentLength - ) { - this._sentEndForContentLength = this._scrollMetrics.contentLength; - onEndReached({distanceFromEnd}); - } - - // Next check if the user just scrolled within the start threshold - // and call onStartReached only once for a given content length, - // and only if onEndReached is not being executed - else if ( - onStartReached != null && - this.state.cellsAroundViewport.first === 0 && - isWithinStartThreshold && - this._scrollMetrics.contentLength !== this._sentStartForContentLength - ) { - // On initial mount when using initialScrollIndex the offset will be 0 initially - // and will trigger an unexpected onStartReached. To avoid this we can use - // timestamp to differentiate between the initial scroll metrics and when we actually - // received the first scroll event. - if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { - this._sentStartForContentLength = this._scrollMetrics.contentLength; - onStartReached({distanceFromStart}); - } - } - - // If the user scrolls away from the start or end and back again, - // cause onStartReached or onEndReached to be triggered again - else { - this._sentStartForContentLength = isWithinStartThreshold - ? this._sentStartForContentLength - : 0; - this._sentEndForContentLength = isWithinEndThreshold - ? this._sentEndForContentLength - : 0; - } - } - - _onContentSizeChange = (width: number, height: number) => { - if ( - width > 0 && - height > 0 && - this.props.initialScrollIndex != null && - this.props.initialScrollIndex > 0 && - !this._hasTriggeredInitialScrollToIndex - ) { - if (this.props.contentOffset == null) { - this.scrollToIndex({ - animated: false, - index: this.props.initialScrollIndex, - }); - } - this._hasTriggeredInitialScrollToIndex = true; - } - if (this.props.onContentSizeChange) { - this.props.onContentSizeChange(width, height); - } - this._scrollMetrics.contentLength = this._selectLength({height, width}); - this._scheduleCellsToRenderUpdate(); - this._maybeCallOnEdgeReached(); - }; - - /* Translates metrics from a scroll event in a parent VirtualizedList into - * coordinates relative to the child list. - */ - _convertParentScrollMetrics = (metrics: { - visibleLength: number, - offset: number, - ... - }): $FlowFixMe => { - // Offset of the top of the nested list relative to the top of its parent's viewport - const offset = metrics.offset - this._offsetFromParentVirtualizedList; - // Child's visible length is the same as its parent's - const visibleLength = metrics.visibleLength; - const dOffset = offset - this._scrollMetrics.offset; - const contentLength = this._scrollMetrics.contentLength; - - return { - visibleLength, - contentLength, - offset, - dOffset, - }; - }; - - _onScroll = (e: Object) => { - this._nestedChildLists.forEach(childList => { - childList._onScroll(e); - }); - if (this.props.onScroll) { - this.props.onScroll(e); - } - const timestamp = e.timeStamp; - let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement); - let contentLength = this._selectLength(e.nativeEvent.contentSize); - let offset = this._selectOffset(e.nativeEvent.contentOffset); - let dOffset = offset - this._scrollMetrics.offset; - - if (this._isNestedWithSameOrientation()) { - if (this._scrollMetrics.contentLength === 0) { - // Ignore scroll events until onLayout has been called and we - // know our offset from our offset from our parent - return; - } - ({visibleLength, contentLength, offset, dOffset} = - this._convertParentScrollMetrics({ - visibleLength, - offset, - })); - } - - const dt = this._scrollMetrics.timestamp - ? Math.max(1, timestamp - this._scrollMetrics.timestamp) - : 1; - const velocity = dOffset / dt; - - if ( - dt > 500 && - this._scrollMetrics.dt > 500 && - contentLength > 5 * visibleLength && - !this._hasWarned.perf - ) { - infoLog( - 'VirtualizedList: You have a large list that is slow to update - make sure your ' + - 'renderItem function renders components that follow React performance best practices ' + - 'like PureComponent, shouldComponentUpdate, etc.', - {dt, prevDt: this._scrollMetrics.dt, contentLength}, - ); - this._hasWarned.perf = true; - } - - // For invalid negative values (w/ RTL), set this to 1. - const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale; - this._scrollMetrics = { - contentLength, - dt, - dOffset, - offset, - timestamp, - velocity, - visibleLength, - zoomScale, - }; - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; - } - this._maybeCallOnEdgeReached(); - if (velocity !== 0) { - this._fillRateHelper.activate(); - } - this._computeBlankness(); - this._scheduleCellsToRenderUpdate(); - }; - - _scheduleCellsToRenderUpdate() { - const {first, last} = this.state.cellsAroundViewport; - const {offset, visibleLength, velocity} = this._scrollMetrics; - const itemCount = this.props.getItemCount(this.props.data); - let hiPri = false; - const onStartReachedThreshold = onStartReachedThresholdOrDefault( - this.props.onStartReachedThreshold, - ); - const onEndReachedThreshold = onEndReachedThresholdOrDefault( - this.props.onEndReachedThreshold, - ); - // Mark as high priority if we're close to the start of the first item - // But only if there are items before the first rendered item - if (first > 0) { - const distTop = - offset - this.__getFrameMetricsApprox(first, this.props).offset; - hiPri = - distTop < 0 || - (velocity < -2 && - distTop < - getScrollingThreshold(onStartReachedThreshold, visibleLength)); - } - // Mark as high priority if we're close to the end of the last item - // But only if there are items after the last rendered item - if (!hiPri && last >= 0 && last < itemCount - 1) { - const distBottom = - this.__getFrameMetricsApprox(last, this.props).offset - - (offset + visibleLength); - hiPri = - distBottom < 0 || - (velocity > 2 && - distBottom < - getScrollingThreshold(onEndReachedThreshold, visibleLength)); - } - // Only trigger high-priority updates if we've actually rendered cells, - // and with that size estimate, accurately compute how many cells we should render. - // Otherwise, it would just render as many cells as it can (of zero dimension), - // each time through attempting to render more (limited by maxToRenderPerBatch), - // starving the renderer from actually laying out the objects and computing _averageCellLength. - // If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate - // We shouldn't do another hipri cellToRenderUpdate - if ( - hiPri && - (this._averageCellLength || this.props.getItemLayout) && - !this._hiPriInProgress - ) { - this._hiPriInProgress = true; - // Don't worry about interactions when scrolling quickly; focus on filling content as fast - // as possible. - this._updateCellsToRenderBatcher.dispose({abort: true}); - this._updateCellsToRender(); - return; - } else { - this._updateCellsToRenderBatcher.schedule(); - } - } - - _onScrollBeginDrag = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onScrollBeginDrag(e); - }); - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.recordInteraction(); - }); - this._hasInteracted = true; - this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); - }; - - _onScrollEndDrag = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onScrollEndDrag(e); - }); - const {velocity} = e.nativeEvent; - if (velocity) { - this._scrollMetrics.velocity = this._selectOffset(velocity); - } - this._computeBlankness(); - this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); - }; - - _onMomentumScrollBegin = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onMomentumScrollBegin(e); - }); - this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); - }; - - _onMomentumScrollEnd = (e: ScrollEvent): void => { - this._nestedChildLists.forEach(childList => { - childList._onMomentumScrollEnd(e); - }); - this._scrollMetrics.velocity = 0; - this._computeBlankness(); - this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); - }; - - _updateCellsToRender = () => { - this.setState((state, props) => { - const cellsAroundViewport = this._adjustCellsAroundViewport( - props, - state.cellsAroundViewport, - ); - const renderMask = VirtualizedList._createRenderMask( - props, - cellsAroundViewport, - this._getNonViewportRenderRegions(props), - ); - - if ( - cellsAroundViewport.first === state.cellsAroundViewport.first && - cellsAroundViewport.last === state.cellsAroundViewport.last && - renderMask.equals(state.renderMask) - ) { - return null; - } - - return {cellsAroundViewport, renderMask}; - }); - }; - - _createViewToken = ( - index: number, - isViewable: boolean, - props: FrameMetricProps, - // $FlowFixMe[missing-local-annot] - ) => { - const {data, getItem} = props; - const item = getItem(data, index); - return { - index, - item, - key: this._keyExtractor(item, index, props), - isViewable, - }; - }; - - /** - * Gets an approximate offset to an item at a given index. Supports - * fractional indices. - */ - _getOffsetApprox = (index: number, props: FrameMetricProps): number => { - if (Number.isInteger(index)) { - return this.__getFrameMetricsApprox(index, props).offset; - } else { - const frameMetrics = this.__getFrameMetricsApprox( - Math.floor(index), - props, - ); - const remainder = index - Math.floor(index); - return frameMetrics.offset + remainder * frameMetrics.length; - } - }; - - __getFrameMetricsApprox: ( - index: number, - props: FrameMetricProps, - ) => { - length: number, - offset: number, - ... - } = (index, props) => { - const frame = this._getFrameMetrics(index, props); - if (frame && frame.index === index) { - // check for invalid frames due to row re-ordering - return frame; - } else { - const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); - invariant( - !getItemLayout, - 'Should not have to estimate frames when a measurement metrics function is provided', - ); - return { - length: this._averageCellLength, - offset: this._averageCellLength * index, - }; - } - }; - - _getFrameMetrics = ( - index: number, - props: FrameMetricProps, - ): ?{ - length: number, - offset: number, - index: number, - inLayout?: boolean, - ... - } => { - const {data, getItem, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); - const item = getItem(data, index); - const frame = this._frames[this._keyExtractor(item, index, props)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.63 was deployed. To see the error - * delete this comment and run Flow. */ - return getItemLayout(data, index); - } - } - return frame; - }; - - _getNonViewportRenderRegions = ( - props: FrameMetricProps, - ): $ReadOnlyArray<{ - first: number, - last: number, - }> => { - // Keep a viewport's worth of content around the last focused cell to allow - // random navigation around it without any blanking. E.g. tabbing from one - // focused item out of viewport to another. - if ( - !(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey]) - ) { - return []; - } - - const lastFocusedCellRenderer = this._cellRefs[this._lastFocusedCellKey]; - const focusedCellIndex = lastFocusedCellRenderer.props.index; - const itemCount = props.getItemCount(props.data); - - // The cell may have been unmounted and have a stale index - if ( - focusedCellIndex >= itemCount || - this._indicesToKeys.get(focusedCellIndex) !== this._lastFocusedCellKey - ) { - return []; - } - - let first = focusedCellIndex; - let heightOfCellsBeforeFocused = 0; - for ( - let i = first - 1; - i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength; - i-- - ) { - first--; - heightOfCellsBeforeFocused += this.__getFrameMetricsApprox( - i, - props, - ).length; - } - - let last = focusedCellIndex; - let heightOfCellsAfterFocused = 0; - for ( - let i = last + 1; - i < itemCount && - heightOfCellsAfterFocused < this._scrollMetrics.visibleLength; - i++ - ) { - last++; - heightOfCellsAfterFocused += this.__getFrameMetricsApprox( - i, - props, - ).length; - } - - return [{first, last}]; - }; - - _updateViewableItems( - props: FrameMetricProps, - cellsAroundViewport: {first: number, last: number}, - ) { - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate( - props, - this._scrollMetrics.offset, - this._scrollMetrics.visibleLength, - this._getFrameMetrics, - this._createViewToken, - tuple.onViewableItemsChanged, - cellsAroundViewport, - ); - }); - } -} - -const styles = StyleSheet.create({ - verticallyInverted: { - transform: [{scaleY: -1}], - }, - horizontallyInverted: { - transform: [{scaleX: -1}], - }, - debug: { - flex: 1, - }, - debugOverlayBase: { - position: 'absolute', - top: 0, - right: 0, - }, - debugOverlay: { - bottom: 0, - width: 20, - borderColor: 'blue', - borderWidth: 1, - }, - debugOverlayFrame: { - left: 0, - backgroundColor: 'orange', - }, - debugOverlayFrameLast: { - left: 0, - borderColor: 'green', - borderWidth: 2, - }, - debugOverlayFrameVis: { - left: 0, - borderColor: 'red', - borderWidth: 2, - }, -}); +export type { + RenderItemProps, + RenderItemType, + Separators, +} from '@react-native/virtualized-lists'; +module.exports = VirtualizedList; diff --git a/Libraries/Lists/VirtualizedListContext.js b/Libraries/Lists/VirtualizedListContext.js index bca5724498a356..5686ccf372286c 100644 --- a/Libraries/Lists/VirtualizedListContext.js +++ b/Libraries/Lists/VirtualizedListContext.js @@ -4,113 +4,15 @@ * 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 + * @flow * @format */ -import typeof VirtualizedList from './VirtualizedList'; +'use strict'; -import * as React from 'react'; -import {useContext, useMemo} from 'react'; +import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterType} from '@react-native/virtualized-lists'; -type Context = $ReadOnly<{ - cellKey: ?string, - getScrollMetrics: () => { - contentLength: number, - dOffset: number, - dt: number, - offset: number, - timestamp: number, - velocity: number, - visibleLength: number, - zoomScale: number, - }, - horizontal: ?boolean, - getOutermostParentListRef: () => React.ElementRef, - registerAsNestedChild: ({ - cellKey: string, - ref: React.ElementRef, - }) => void, - unregisterAsNestedChild: ({ - ref: React.ElementRef, - }) => void, -}>; +const VirtualizedListContextResetter: VirtualizedListContextResetterType = + require('@react-native/virtualized-lists').VirtualizedListContextResetter; -export const VirtualizedListContext: React.Context = - React.createContext(null); -if (__DEV__) { - VirtualizedListContext.displayName = 'VirtualizedListContext'; -} - -/** - * Resets the context. Intended for use by portal-like components (e.g. Modal). - */ -export function VirtualizedListContextResetter({ - children, -}: { - children: React.Node, -}): React.Node { - return ( - - {children} - - ); -} - -/** - * Sets the context with memoization. Intended to be used by `VirtualizedList`. - */ -export function VirtualizedListContextProvider({ - children, - value, -}: { - children: React.Node, - value: Context, -}): React.Node { - // Avoid setting a newly created context object if the values are identical. - const context = useMemo( - () => ({ - cellKey: null, - getScrollMetrics: value.getScrollMetrics, - horizontal: value.horizontal, - getOutermostParentListRef: value.getOutermostParentListRef, - registerAsNestedChild: value.registerAsNestedChild, - unregisterAsNestedChild: value.unregisterAsNestedChild, - }), - [ - value.getScrollMetrics, - value.horizontal, - value.getOutermostParentListRef, - value.registerAsNestedChild, - value.unregisterAsNestedChild, - ], - ); - return ( - - {children} - - ); -} - -/** - * Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell. - */ -export function VirtualizedListCellContextProvider({ - cellKey, - children, -}: { - cellKey: string, - children: React.Node, -}): React.Node { - // Avoid setting a newly created context object if the values are identical. - const currContext = useContext(VirtualizedListContext); - const context = useMemo( - () => (currContext == null ? null : {...currContext, cellKey}), - [currContext, cellKey], - ); - return ( - - {children} - - ); -} +module.exports = {VirtualizedListContextResetter}; diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index e4bf2fe58a0c44..242dfe34c6b231 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -8,610 +8,15 @@ * @format */ -import type {ViewToken} from './ViewabilityHelper'; +'use strict'; -import View from '../Components/View/View'; -import VirtualizedList from './VirtualizedList'; -import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; -import invariant from 'invariant'; -import * as React from 'react'; +import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@react-native/virtualized-lists'; -type Item = any; +const VirtualizedSectionList: VirtualizedSectionListType = + require('@react-native/virtualized-lists').VirtualizedSectionList; -export type SectionBase = { - /** - * The data for rendering items in this section. - */ - data: $ReadOnlyArray, - /** - * Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections, - * the array index will be used by default. - */ - key?: string, - // Optional props will override list-wide props just for this section. - renderItem?: ?(info: { - item: SectionItemT, - index: number, - section: SectionBase, - separators: { - highlight: () => void, - unhighlight: () => void, - updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, - ... - }, - ... - }) => null | React.Element, - ItemSeparatorComponent?: ?React.ComponentType, - keyExtractor?: (item: SectionItemT, index?: ?number) => string, - ... -}; - -type RequiredProps> = {| - sections: $ReadOnlyArray, -|}; - -type OptionalProps> = {| - /** - * Default renderer for every item in every section. - */ - renderItem?: (info: { - item: Item, - index: number, - section: SectionT, - separators: { - highlight: () => void, - unhighlight: () => void, - updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, - ... - }, - ... - }) => null | React.Element, - /** - * Rendered at the top of each section. These stick to the top of the `ScrollView` by default on - * iOS. See `stickySectionHeadersEnabled`. - */ - renderSectionHeader?: ?(info: { - section: SectionT, - ... - }) => null | React.Element, - /** - * Rendered at the bottom of each section. - */ - renderSectionFooter?: ?(info: { - section: SectionT, - ... - }) => null | React.Element, - /** - * Rendered at the top and bottom of each section (note this is different from - * `ItemSeparatorComponent` which is only rendered between items). These are intended to separate - * sections from the headers above and below and typically have the same highlight response as - * `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`, - * and any custom props from `separators.updateProps`. - */ - SectionSeparatorComponent?: ?React.ComponentType, - /** - * Makes section headers stick to the top of the screen until the next one pushes it off. Only - * enabled by default on iOS because that is the platform standard there. - */ - stickySectionHeadersEnabled?: boolean, - onEndReached?: ?({distanceFromEnd: number, ...}) => void, -|}; - -type VirtualizedListProps = React.ElementConfig; - -export type Props = {| - ...RequiredProps, - ...OptionalProps, - ...$Diff< - VirtualizedListProps, - { - renderItem: $PropertyType, - data: $PropertyType, - ... - }, - >, -|}; -export type ScrollToLocationParamsType = {| - animated?: ?boolean, - itemIndex: number, - sectionIndex: number, - viewOffset?: number, - viewPosition?: number, -|}; - -type State = {childProps: VirtualizedListProps, ...}; - -/** - * Right now this just flattens everything into one list and uses VirtualizedList under the - * hood. The only operation that might not scale well is concatting the data arrays of all the - * sections when new props are received, which should be plenty fast for up to ~10,000 items. - */ -class VirtualizedSectionList< - SectionT: SectionBase, -> extends React.PureComponent, State> { - scrollToLocation(params: ScrollToLocationParamsType) { - let index = params.itemIndex; - for (let i = 0; i < params.sectionIndex; i++) { - index += this.props.getItemCount(this.props.sections[i].data) + 2; - } - let viewOffset = params.viewOffset || 0; - if (this._listRef == null) { - return; - } - if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) { - const frame = this._listRef.__getFrameMetricsApprox( - index - params.itemIndex, - this._listRef.props, - ); - viewOffset += frame.length; - } - const toIndexParams = { - ...params, - viewOffset, - index, - }; - // $FlowFixMe[incompatible-use] - this._listRef.scrollToIndex(toIndexParams); - } - - getListRef(): ?React.ElementRef { - return this._listRef; - } - - render(): React.Node { - const { - ItemSeparatorComponent, // don't pass through, rendered with renderItem - SectionSeparatorComponent, - renderItem: _renderItem, - renderSectionFooter, - renderSectionHeader, - sections: _sections, - stickySectionHeadersEnabled, - ...passThroughProps - } = this.props; - - const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0; - - const stickyHeaderIndices = this.props.stickySectionHeadersEnabled - ? ([]: Array) - : undefined; - - let itemCount = 0; - for (const section of this.props.sections) { - // Track the section header indices - if (stickyHeaderIndices != null) { - stickyHeaderIndices.push(itemCount + listHeaderOffset); - } - - // Add two for the section header and footer. - itemCount += 2; - itemCount += this.props.getItemCount(section.data); - } - const renderItem = this._renderItem(itemCount); - - return ( - - this._getItem(this.props, sections, index) - } - getItemCount={() => itemCount} - onViewableItemsChanged={ - this.props.onViewableItemsChanged - ? this._onViewableItemsChanged - : undefined - } - ref={this._captureRef} - /> - ); - } - - _getItem( - props: Props, - sections: ?$ReadOnlyArray, - index: number, - ): ?Item { - if (!sections) { - return null; - } - let itemIdx = index - 1; - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - const sectionData = section.data; - const itemCount = props.getItemCount(sectionData); - if (itemIdx === -1 || itemIdx === itemCount) { - // We intend for there to be overflow by one on both ends of the list. - // This will be for headers and footers. When returning a header or footer - // item the section itself is the item. - return section; - } else if (itemIdx < itemCount) { - // If we are in the bounds of the list's data then return the item. - return props.getItem(sectionData, itemIdx); - } else { - itemIdx -= itemCount + 2; // Add two for the header and footer - } - } - return null; - } - - // $FlowFixMe[missing-local-annot] - _keyExtractor = (item: Item, index: number) => { - const info = this._subExtractor(index); - return (info && info.key) || String(index); - }; - - _subExtractor(index: number): ?{ - section: SectionT, - // Key of the section or combined key for section + item - key: string, - // Relative index within the section - index: ?number, - // True if this is the section header - header?: ?boolean, - leadingItem?: ?Item, - leadingSection?: ?SectionT, - trailingItem?: ?Item, - trailingSection?: ?SectionT, - ... - } { - let itemIndex = index; - const {getItem, getItemCount, keyExtractor, sections} = this.props; - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - const sectionData = section.data; - const key = section.key || String(i); - itemIndex -= 1; // The section adds an item for the header - if (itemIndex >= getItemCount(sectionData) + 1) { - itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer. - } else if (itemIndex === -1) { - return { - section, - key: key + ':header', - index: null, - header: true, - trailingSection: sections[i + 1], - }; - } else if (itemIndex === getItemCount(sectionData)) { - return { - section, - key: key + ':footer', - index: null, - header: false, - trailingSection: sections[i + 1], - }; - } else { - const extractor = - section.keyExtractor || keyExtractor || defaultKeyExtractor; - return { - section, - key: - key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex), - index: itemIndex, - leadingItem: getItem(sectionData, itemIndex - 1), - leadingSection: sections[i - 1], - trailingItem: getItem(sectionData, itemIndex + 1), - trailingSection: sections[i + 1], - }; - } - } - } - - _convertViewable = (viewable: ViewToken): ?ViewToken => { - invariant(viewable.index != null, 'Received a broken ViewToken'); - const info = this._subExtractor(viewable.index); - if (!info) { - return null; - } - const keyExtractorWithNullableIndex = info.section.keyExtractor; - const keyExtractorWithNonNullableIndex = - this.props.keyExtractor || defaultKeyExtractor; - const key = - keyExtractorWithNullableIndex != null - ? keyExtractorWithNullableIndex(viewable.item, info.index) - : keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0); - - return { - ...viewable, - index: info.index, - key, - section: info.section, - }; - }; - - _onViewableItemsChanged = ({ - viewableItems, - changed, - }: { - viewableItems: Array, - changed: Array, - ... - }) => { - const onViewableItemsChanged = this.props.onViewableItemsChanged; - if (onViewableItemsChanged != null) { - onViewableItemsChanged({ - viewableItems: viewableItems - .map(this._convertViewable, this) - .filter(Boolean), - changed: changed.map(this._convertViewable, this).filter(Boolean), - }); - } - }; - - _renderItem = - (listItemCount: number): $FlowFixMe => - // eslint-disable-next-line react/no-unstable-nested-components - ({item, index}: {item: Item, index: number, ...}) => { - const info = this._subExtractor(index); - if (!info) { - return null; - } - const infoIndex = info.index; - if (infoIndex == null) { - const {section} = info; - if (info.header === true) { - const {renderSectionHeader} = this.props; - return renderSectionHeader ? renderSectionHeader({section}) : null; - } else { - const {renderSectionFooter} = this.props; - return renderSectionFooter ? renderSectionFooter({section}) : null; - } - } else { - const renderItem = info.section.renderItem || this.props.renderItem; - const SeparatorComponent = this._getSeparatorComponent( - index, - info, - listItemCount, - ); - invariant(renderItem, 'no renderItem!'); - return ( - - ); - } - }; - - _updatePropsFor = (cellKey: string, value: any) => { - const updateProps = this._updatePropsMap[cellKey]; - if (updateProps != null) { - updateProps(value); - } - }; - - _updateHighlightFor = (cellKey: string, value: boolean) => { - const updateHighlight = this._updateHighlightMap[cellKey]; - if (updateHighlight != null) { - updateHighlight(value); - } - }; - - _setUpdateHighlightFor = ( - cellKey: string, - updateHighlightFn: ?(boolean) => void, - ) => { - if (updateHighlightFn != null) { - this._updateHighlightMap[cellKey] = updateHighlightFn; - } else { - // $FlowFixMe[prop-missing] - delete this._updateHighlightFor[cellKey]; - } - }; - - _setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => { - if (updatePropsFn != null) { - this._updatePropsMap[cellKey] = updatePropsFn; - } else { - delete this._updatePropsMap[cellKey]; - } - }; - - _getSeparatorComponent( - index: number, - info?: ?Object, - listItemCount: number, - ): ?React.ComponentType { - info = info || this._subExtractor(index); - if (!info) { - return null; - } - const ItemSeparatorComponent = - info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; - const {SectionSeparatorComponent} = this.props; - const isLastItemInList = index === listItemCount - 1; - const isLastItemInSection = - info.index === this.props.getItemCount(info.section.data) - 1; - if (SectionSeparatorComponent && isLastItemInSection) { - return SectionSeparatorComponent; - } - if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { - return ItemSeparatorComponent; - } - return null; - } - - _updateHighlightMap: {[string]: (boolean) => void} = {}; - _updatePropsMap: {[string]: void | (boolean => void)} = {}; - _listRef: ?React.ElementRef; - _captureRef = (ref: null | React$ElementRef>) => { - this._listRef = ref; - }; -} - -type ItemWithSeparatorCommonProps = $ReadOnly<{| - leadingItem: ?Item, - leadingSection: ?Object, - section: Object, - trailingItem: ?Item, - trailingSection: ?Object, -|}>; - -type ItemWithSeparatorProps = $ReadOnly<{| - ...ItemWithSeparatorCommonProps, - LeadingSeparatorComponent: ?React.ComponentType, - SeparatorComponent: ?React.ComponentType, - cellKey: string, - index: number, - item: Item, - setSelfHighlightCallback: ( - cellKey: string, - updateFn: ?(boolean) => void, - ) => void, - setSelfUpdatePropsCallback: ( - cellKey: string, - updateFn: ?(boolean) => void, - ) => void, - prevCellKey?: ?string, - updateHighlightFor: (prevCellKey: string, value: boolean) => void, - updatePropsFor: (prevCellKey: string, value: Object) => void, - renderItem: Function, - inverted: boolean, -|}>; - -function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node { - const { - LeadingSeparatorComponent, - // this is the trailing separator and is associated with this item - SeparatorComponent, - cellKey, - prevCellKey, - setSelfHighlightCallback, - updateHighlightFor, - setSelfUpdatePropsCallback, - updatePropsFor, - item, - index, - section, - inverted, - } = props; - - const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] = - React.useState(false); - - const [separatorHighlighted, setSeparatorHighlighted] = React.useState(false); - - const [leadingSeparatorProps, setLeadingSeparatorProps] = React.useState({ - leadingItem: props.leadingItem, - leadingSection: props.leadingSection, - section: props.section, - trailingItem: props.item, - trailingSection: props.trailingSection, - }); - const [separatorProps, setSeparatorProps] = React.useState({ - leadingItem: props.item, - leadingSection: props.leadingSection, - section: props.section, - trailingItem: props.trailingItem, - trailingSection: props.trailingSection, - }); - - React.useEffect(() => { - setSelfHighlightCallback(cellKey, setSeparatorHighlighted); - // $FlowFixMe[incompatible-call] - setSelfUpdatePropsCallback(cellKey, setSeparatorProps); - - return () => { - setSelfUpdatePropsCallback(cellKey, null); - setSelfHighlightCallback(cellKey, null); - }; - }, [ - cellKey, - setSelfHighlightCallback, - setSeparatorProps, - setSelfUpdatePropsCallback, - ]); - - const separators = { - highlight: () => { - setLeadingSeparatorHighlighted(true); - setSeparatorHighlighted(true); - if (prevCellKey != null) { - updateHighlightFor(prevCellKey, true); - } - }, - unhighlight: () => { - setLeadingSeparatorHighlighted(false); - setSeparatorHighlighted(false); - if (prevCellKey != null) { - updateHighlightFor(prevCellKey, false); - } - }, - updateProps: ( - select: 'leading' | 'trailing', - newProps: $Shape, - ) => { - if (select === 'leading') { - if (LeadingSeparatorComponent != null) { - setLeadingSeparatorProps({...leadingSeparatorProps, ...newProps}); - } else if (prevCellKey != null) { - // update the previous item's separator - updatePropsFor(prevCellKey, {...leadingSeparatorProps, ...newProps}); - } - } else if (select === 'trailing' && SeparatorComponent != null) { - setSeparatorProps({...separatorProps, ...newProps}); - } - }, - }; - const element = props.renderItem({ - item, - index, - section, - separators, - }); - const leadingSeparator = LeadingSeparatorComponent != null && ( - - ); - const separator = SeparatorComponent != null && ( - - ); - return leadingSeparator || separator ? ( - - {inverted === false ? leadingSeparator : separator} - {element} - {inverted === false ? separator : leadingSeparator} - - ) : ( - element - ); -} - -/* $FlowFixMe[class-object-subtyping] added when improving typing for this - * parameters */ -// $FlowFixMe[method-unbinding] -module.exports = (VirtualizedSectionList: React.AbstractComponent< - React.ElementConfig, - $ReadOnly<{ - getListRef: () => ?React.ElementRef, - scrollToLocation: (params: ScrollToLocationParamsType) => void, - ... - }>, ->); +export type { + SectionBase, + ScrollToLocationParamsType, +} from '@react-native/virtualized-lists'; +module.exports = VirtualizedSectionList; diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index 46e9e714d29262..9750d2e5be31d3 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -13,11 +13,11 @@ import type {RootTag} from '../ReactNative/RootTag'; import type {DirectEventHandler} from '../Types/CodegenTypes'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; -import {VirtualizedListContextResetter} from '../Lists/VirtualizedListContext.js'; import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import ModalInjection from './ModalInjection'; import NativeModalManager from './NativeModalManager'; import RCTModalHostView from './RCTModalHostViewNativeComponent'; +import {VirtualizedListContextResetter} from '@react-native/virtualized-lists'; const ScrollView = require('../Components/ScrollView/ScrollView'); const View = require('../Components/View/View'); diff --git a/Libraries/Utilities/ReactNativeTestTools.js b/Libraries/Utilities/ReactNativeTestTools.js index 5dc2221528dcee..8671575054137c 100644 --- a/Libraries/Utilities/ReactNativeTestTools.js +++ b/Libraries/Utilities/ReactNativeTestTools.js @@ -15,8 +15,8 @@ import type {ReactTestRenderer as ReactTestRendererType} from 'react-test-render const Switch = require('../Components/Switch/Switch').default; const TextInput = require('../Components/TextInput/TextInput'); const View = require('../Components/View/View'); -const VirtualizedList = require('../Lists/VirtualizedList').default; const Text = require('../Text/Text'); +const {VirtualizedList} = require('@react-native/virtualized-lists'); const React = require('react'); const ShallowRenderer = require('react-shallow-renderer'); const ReactTestRenderer = require('react-test-renderer'); diff --git a/index.js b/index.js index 0b6e97c0522640..7149c6463b52fa 100644 --- a/index.js +++ b/index.js @@ -191,7 +191,7 @@ module.exports = { return require('./Libraries/Components/View/View'); }, get VirtualizedList(): VirtualizedList { - return require('./Libraries/Lists/VirtualizedList').default; + return require('./Libraries/Lists/VirtualizedList'); }, get VirtualizedSectionList(): VirtualizedSectionList { return require('./Libraries/Lists/VirtualizedSectionList'); diff --git a/package.json b/package.json index 109d1dd68e389f..225346a4a1d73d 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "@react-native/gradle-plugin": "^0.72.2", "@react-native/js-polyfills": "^0.72.0", "@react-native/normalize-colors": "^0.72.0", + "@react-native/virtualized-lists": "0.72.0", "abort-controller": "^3.0.0", "anser": "^1.4.9", "base64-js": "^1.1.2", diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js index 25c511e570a3eb..97f991265de3ba 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-basic.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-basic.js @@ -12,7 +12,7 @@ import type {AnimatedComponentType} from 'react-native/Libraries/Animated/createAnimatedComponent'; import typeof FlatListType from 'react-native/Libraries/Lists/FlatList'; -import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedListProps'; +import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import * as React from 'react'; diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-nested.js b/packages/rn-tester/js/examples/FlatList/FlatList-nested.js index bb53258da9c82c..cd1f96ded9d3af 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-nested.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-nested.js @@ -9,8 +9,8 @@ */ 'use strict'; -import type {ViewToken} from '../../../../../Libraries/Lists/ViewabilityHelper'; -import type {RenderItemProps} from '../../../../../Libraries/Lists/VirtualizedListProps'; +import type {ViewToken} from 'react-native/Libraries/Lists/ViewabilityHelper'; +import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import RNTesterPage from '../../components/RNTesterPage'; diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js b/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js index 76e29fac105fe1..f65111e16596ba 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-onViewableItemsChanged.js @@ -10,7 +10,7 @@ 'use strict'; -import type {ViewToken} from '../../../../../Libraries/Lists/ViewabilityHelper'; +import type {ViewToken} from 'react-native/Libraries/Lists/ViewabilityHelper'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import BaseFlatListExample from './BaseFlatListExample'; diff --git a/Libraries/Interaction/Batchinator.js b/packages/virtualized-lists/Interaction/Batchinator.js similarity index 97% rename from Libraries/Interaction/Batchinator.js rename to packages/virtualized-lists/Interaction/Batchinator.js index 2ca2d7986d1a78..4fbc1931ca5708 100644 --- a/Libraries/Interaction/Batchinator.js +++ b/packages/virtualized-lists/Interaction/Batchinator.js @@ -10,7 +10,7 @@ 'use strict'; -const InteractionManager = require('./InteractionManager'); +const {InteractionManager} = require('react-native'); /** * A simple class for batching up invocations of a low-pri callback. A timeout is set to run the diff --git a/Libraries/Interaction/__tests__/Batchinator-test.js b/packages/virtualized-lists/Interaction/__tests__/Batchinator-test.js similarity index 95% rename from Libraries/Interaction/__tests__/Batchinator-test.js rename to packages/virtualized-lists/Interaction/__tests__/Batchinator-test.js index e8261b3515e23f..b680e98c507d00 100644 --- a/Libraries/Interaction/__tests__/Batchinator-test.js +++ b/packages/virtualized-lists/Interaction/__tests__/Batchinator-test.js @@ -10,10 +10,6 @@ 'use strict'; -jest - .mock('../../vendor/core/ErrorUtils') - .mock('../../BatchedBridge/BatchedBridge'); - function expectToBeCalledOnce(fn) { expect(fn.mock.calls.length).toBe(1); } diff --git a/Libraries/Lists/CellRenderMask.js b/packages/virtualized-lists/Lists/CellRenderMask.js similarity index 100% rename from Libraries/Lists/CellRenderMask.js rename to packages/virtualized-lists/Lists/CellRenderMask.js diff --git a/Libraries/Lists/ChildListCollection.js b/packages/virtualized-lists/Lists/ChildListCollection.js similarity index 100% rename from Libraries/Lists/ChildListCollection.js rename to packages/virtualized-lists/Lists/ChildListCollection.js diff --git a/packages/virtualized-lists/Lists/FillRateHelper.js b/packages/virtualized-lists/Lists/FillRateHelper.js new file mode 100644 index 00000000000000..87482e73f6be3e --- /dev/null +++ b/packages/virtualized-lists/Lists/FillRateHelper.js @@ -0,0 +1,253 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import type {FrameMetricProps} from './VirtualizedListProps'; + +export type FillRateInfo = Info; + +class Info { + any_blank_count: number = 0; + any_blank_ms: number = 0; + any_blank_speed_sum: number = 0; + mostly_blank_count: number = 0; + mostly_blank_ms: number = 0; + pixels_blank: number = 0; + pixels_sampled: number = 0; + pixels_scrolled: number = 0; + total_time_spent: number = 0; + sample_count: number = 0; +} + +type FrameMetrics = { + inLayout?: boolean, + length: number, + offset: number, + ... +}; + +const DEBUG = false; + +let _listeners: Array<(Info) => void> = []; +let _minSampleCount = 10; +let _sampleRate = DEBUG ? 1 : null; + +/** + * A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded. + * By default the sampling rate is set to zero and this will do nothing. If you want to collect + * samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`. + * + * Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with + * `SceneTracker.getActiveScene` to determine the context of the events. + */ +class FillRateHelper { + _anyBlankStartTime: ?number = null; + _enabled = false; + _getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics; + _info: Info = new Info(); + _mostlyBlankStartTime: ?number = null; + _samplesStartTime: ?number = null; + + static addListener(callback: FillRateInfo => void): { + remove: () => void, + ... + } { + if (_sampleRate === null) { + console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.'); + } + _listeners.push(callback); + return { + remove: () => { + _listeners = _listeners.filter(listener => callback !== listener); + }, + }; + } + + static setSampleRate(sampleRate: number) { + _sampleRate = sampleRate; + } + + static setMinSampleCount(minSampleCount: number) { + _minSampleCount = minSampleCount; + } + + constructor( + getFrameMetrics: (index: number, props: FrameMetricProps) => ?FrameMetrics, + ) { + this._getFrameMetrics = getFrameMetrics; + this._enabled = (_sampleRate || 0) > Math.random(); + this._resetData(); + } + + activate() { + if (this._enabled && this._samplesStartTime == null) { + DEBUG && console.debug('FillRateHelper: activate'); + this._samplesStartTime = global.performance.now(); + } + } + + deactivateAndFlush() { + if (!this._enabled) { + return; + } + const start = this._samplesStartTime; // const for flow + if (start == null) { + DEBUG && + console.debug('FillRateHelper: bail on deactivate with no start time'); + return; + } + if (this._info.sample_count < _minSampleCount) { + // Don't bother with under-sampled events. + this._resetData(); + return; + } + const total_time_spent = global.performance.now() - start; + const info: any = { + ...this._info, + total_time_spent, + }; + if (DEBUG) { + const derived = { + avg_blankness: this._info.pixels_blank / this._info.pixels_sampled, + avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000), + avg_speed_when_any_blank: + this._info.any_blank_speed_sum / this._info.any_blank_count, + any_blank_per_min: + this._info.any_blank_count / (total_time_spent / 1000 / 60), + any_blank_time_frac: this._info.any_blank_ms / total_time_spent, + mostly_blank_per_min: + this._info.mostly_blank_count / (total_time_spent / 1000 / 60), + mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent, + }; + for (const key in derived) { + // $FlowFixMe[prop-missing] + derived[key] = Math.round(1000 * derived[key]) / 1000; + } + console.debug('FillRateHelper deactivateAndFlush: ', {derived, info}); + } + _listeners.forEach(listener => listener(info)); + this._resetData(); + } + + computeBlankness( + props: { + ...FrameMetricProps, + initialNumToRender?: ?number, + ... + }, + cellsAroundViewport: { + first: number, + last: number, + ... + }, + scrollMetrics: { + dOffset: number, + offset: number, + velocity: number, + visibleLength: number, + ... + }, + ): number { + if ( + !this._enabled || + props.getItemCount(props.data) === 0 || + cellsAroundViewport.last < cellsAroundViewport.first || + this._samplesStartTime == null + ) { + return 0; + } + const {dOffset, offset, velocity, visibleLength} = scrollMetrics; + + // Denominator metrics that we track for all events - most of the time there is no blankness and + // we want to capture that. + this._info.sample_count++; + this._info.pixels_sampled += Math.round(visibleLength); + this._info.pixels_scrolled += Math.round(Math.abs(dOffset)); + const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec + + // Whether blank now or not, record the elapsed time blank if we were blank last time. + const now = global.performance.now(); + if (this._anyBlankStartTime != null) { + this._info.any_blank_ms += now - this._anyBlankStartTime; + } + this._anyBlankStartTime = null; + if (this._mostlyBlankStartTime != null) { + this._info.mostly_blank_ms += now - this._mostlyBlankStartTime; + } + this._mostlyBlankStartTime = null; + + let blankTop = 0; + let first = cellsAroundViewport.first; + let firstFrame = this._getFrameMetrics(first, props); + while ( + first <= cellsAroundViewport.last && + (!firstFrame || !firstFrame.inLayout) + ) { + firstFrame = this._getFrameMetrics(first, props); + first++; + } + // Only count blankTop if we aren't rendering the first item, otherwise we will count the header + // as blank. + if (firstFrame && first > 0) { + blankTop = Math.min( + visibleLength, + Math.max(0, firstFrame.offset - offset), + ); + } + let blankBottom = 0; + let last = cellsAroundViewport.last; + let lastFrame = this._getFrameMetrics(last, props); + while ( + last >= cellsAroundViewport.first && + (!lastFrame || !lastFrame.inLayout) + ) { + lastFrame = this._getFrameMetrics(last, props); + last--; + } + // Only count blankBottom if we aren't rendering the last item, otherwise we will count the + // footer as blank. + if (lastFrame && last < props.getItemCount(props.data) - 1) { + const bottomEdge = lastFrame.offset + lastFrame.length; + blankBottom = Math.min( + visibleLength, + Math.max(0, offset + visibleLength - bottomEdge), + ); + } + const pixels_blank = Math.round(blankTop + blankBottom); + const blankness = pixels_blank / visibleLength; + if (blankness > 0) { + this._anyBlankStartTime = now; + this._info.any_blank_speed_sum += scrollSpeed; + this._info.any_blank_count++; + this._info.pixels_blank += pixels_blank; + if (blankness > 0.5) { + this._mostlyBlankStartTime = now; + this._info.mostly_blank_count++; + } + } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) { + this.deactivateAndFlush(); + } + return blankness; + } + + enabled(): boolean { + return this._enabled; + } + + _resetData() { + this._anyBlankStartTime = null; + this._info = new Info(); + this._mostlyBlankStartTime = null; + this._samplesStartTime = null; + } +} + +module.exports = FillRateHelper; diff --git a/Libraries/Lists/StateSafePureComponent.js b/packages/virtualized-lists/Lists/StateSafePureComponent.js similarity index 100% rename from Libraries/Lists/StateSafePureComponent.js rename to packages/virtualized-lists/Lists/StateSafePureComponent.js diff --git a/packages/virtualized-lists/Lists/ViewabilityHelper.js b/packages/virtualized-lists/Lists/ViewabilityHelper.js new file mode 100644 index 00000000000000..33a9811825affd --- /dev/null +++ b/packages/virtualized-lists/Lists/ViewabilityHelper.js @@ -0,0 +1,360 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import type {FrameMetricProps} from './VirtualizedListProps'; + +const invariant = require('invariant'); + +export type ViewToken = { + item: any, + key: string, + index: ?number, + isViewable: boolean, + section?: any, + ... +}; + +export type ViewabilityConfigCallbackPair = { + viewabilityConfig: ViewabilityConfig, + onViewableItemsChanged: (info: { + viewableItems: Array, + changed: Array, + ... + }) => void, + ... +}; + +export type ViewabilityConfig = {| + /** + * Minimum amount of time (in milliseconds) that an item must be physically viewable before the + * viewability callback will be fired. A high number means that scrolling through content without + * stopping will not mark the content as viewable. + */ + minimumViewTime?: number, + + /** + * Percent of viewport that must be covered for a partially occluded item to count as + * "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means + * that a single pixel in the viewport makes the item viewable, and a value of 100 means that + * an item must be either entirely visible or cover the entire viewport to count as viewable. + */ + viewAreaCoveragePercentThreshold?: number, + + /** + * Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible, + * rather than the fraction of the viewable area it covers. + */ + itemVisiblePercentThreshold?: number, + + /** + * Nothing is considered viewable until the user scrolls or `recordInteraction` is called after + * render. + */ + waitForInteraction?: boolean, +|}; + +/** + * A Utility class for calculating viewable items based on current metrics like scroll position and + * layout. + * + * An item is said to be in a "viewable" state when any of the following + * is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction` + * is true): + * + * - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item + * visible in the view area >= `itemVisiblePercentThreshold`. + * - Entirely visible on screen + */ +class ViewabilityHelper { + _config: ViewabilityConfig; + _hasInteracted: boolean = false; + _timers: Set = new Set(); + _viewableIndices: Array = []; + _viewableItems: Map = new Map(); + + constructor( + config: ViewabilityConfig = {viewAreaCoveragePercentThreshold: 0}, + ) { + this._config = config; + } + + /** + * Cleanup, e.g. on unmount. Clears any pending timers. + */ + dispose() { + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To see + * the error delete this comment and run Flow. */ + this._timers.forEach(clearTimeout); + } + + /** + * Determines which items are viewable based on the current metrics and config. + */ + computeViewableItems( + props: FrameMetricProps, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: ( + index: number, + props: FrameMetricProps, + ) => ?{ + length: number, + offset: number, + ... + }, + // Optional optimization to reduce the scan size + renderRange?: { + first: number, + last: number, + ... + }, + ): Array { + const itemCount = props.getItemCount(props.data); + const {itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold} = + this._config; + const viewAreaMode = viewAreaCoveragePercentThreshold != null; + const viewablePercentThreshold = viewAreaMode + ? viewAreaCoveragePercentThreshold + : itemVisiblePercentThreshold; + invariant( + viewablePercentThreshold != null && + (itemVisiblePercentThreshold != null) !== + (viewAreaCoveragePercentThreshold != null), + 'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold', + ); + const viewableIndices = []; + if (itemCount === 0) { + return viewableIndices; + } + let firstVisible = -1; + const {first, last} = renderRange || {first: 0, last: itemCount - 1}; + if (last >= itemCount) { + console.warn( + 'Invalid render range computing viewability ' + + JSON.stringify({renderRange, itemCount}), + ); + return []; + } + for (let idx = first; idx <= last; idx++) { + const metrics = getFrameMetrics(idx, props); + if (!metrics) { + continue; + } + const top = metrics.offset - scrollOffset; + const bottom = top + metrics.length; + if (top < viewportHeight && bottom > 0) { + firstVisible = idx; + if ( + _isViewable( + viewAreaMode, + viewablePercentThreshold, + top, + bottom, + viewportHeight, + metrics.length, + ) + ) { + viewableIndices.push(idx); + } + } else if (firstVisible >= 0) { + break; + } + } + return viewableIndices; + } + + /** + * Figures out which items are viewable and how that has changed from before and calls + * `onViewableItemsChanged` as appropriate. + */ + onUpdate( + props: FrameMetricProps, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: ( + index: number, + props: FrameMetricProps, + ) => ?{ + length: number, + offset: number, + ... + }, + createViewToken: ( + index: number, + isViewable: boolean, + props: FrameMetricProps, + ) => ViewToken, + onViewableItemsChanged: ({ + viewableItems: Array, + changed: Array, + ... + }) => void, + // Optional optimization to reduce the scan size + renderRange?: { + first: number, + last: number, + ... + }, + ): void { + const itemCount = props.getItemCount(props.data); + if ( + (this._config.waitForInteraction && !this._hasInteracted) || + itemCount === 0 || + !getFrameMetrics(0, props) + ) { + return; + } + let viewableIndices: Array = []; + if (itemCount) { + viewableIndices = this.computeViewableItems( + props, + scrollOffset, + viewportHeight, + getFrameMetrics, + renderRange, + ); + } + if ( + this._viewableIndices.length === viewableIndices.length && + this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) + ) { + // We might get a lot of scroll events where visibility doesn't change and we don't want to do + // extra work in those cases. + return; + } + this._viewableIndices = viewableIndices; + if (this._config.minimumViewTime) { + const handle: TimeoutID = setTimeout(() => { + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To + * see the error delete this comment and run Flow. */ + this._timers.delete(handle); + this._onUpdateSync( + props, + viewableIndices, + onViewableItemsChanged, + createViewToken, + ); + }, this._config.minimumViewTime); + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To see + * the error delete this comment and run Flow. */ + this._timers.add(handle); + } else { + this._onUpdateSync( + props, + viewableIndices, + onViewableItemsChanged, + createViewToken, + ); + } + } + + /** + * clean-up cached _viewableIndices to evaluate changed items on next update + */ + resetViewableIndices() { + this._viewableIndices = []; + } + + /** + * Records that an interaction has happened even if there has been no scroll. + */ + recordInteraction() { + this._hasInteracted = true; + } + + _onUpdateSync( + props: FrameMetricProps, + viewableIndicesToCheck: Array, + onViewableItemsChanged: ({ + changed: Array, + viewableItems: Array, + ... + }) => void, + createViewToken: ( + index: number, + isViewable: boolean, + props: FrameMetricProps, + ) => ViewToken, + ) { + // Filter out indices that have gone out of view since this call was scheduled. + viewableIndicesToCheck = viewableIndicesToCheck.filter(ii => + this._viewableIndices.includes(ii), + ); + const prevItems = this._viewableItems; + const nextItems = new Map( + viewableIndicesToCheck.map(ii => { + const viewable = createViewToken(ii, true, props); + return [viewable.key, viewable]; + }), + ); + + const changed = []; + for (const [key, viewable] of nextItems) { + if (!prevItems.has(key)) { + changed.push(viewable); + } + } + for (const [key, viewable] of prevItems) { + if (!nextItems.has(key)) { + changed.push({...viewable, isViewable: false}); + } + } + if (changed.length > 0) { + this._viewableItems = nextItems; + onViewableItemsChanged({ + viewableItems: Array.from(nextItems.values()), + changed, + viewabilityConfig: this._config, + }); + } + } +} + +function _isViewable( + viewAreaMode: boolean, + viewablePercentThreshold: number, + top: number, + bottom: number, + viewportHeight: number, + itemLength: number, +): boolean { + if (_isEntirelyVisible(top, bottom, viewportHeight)) { + return true; + } else { + const pixels = _getPixelsVisible(top, bottom, viewportHeight); + const percent = + 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength); + return percent >= viewablePercentThreshold; + } +} + +function _getPixelsVisible( + top: number, + bottom: number, + viewportHeight: number, +): number { + const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); + return Math.max(0, visibleHeight); +} + +function _isEntirelyVisible( + top: number, + bottom: number, + viewportHeight: number, +): boolean { + return top >= 0 && bottom <= viewportHeight && bottom > top; +} + +module.exports = ViewabilityHelper; diff --git a/packages/virtualized-lists/Lists/VirtualizeUtils.js b/packages/virtualized-lists/Lists/VirtualizeUtils.js new file mode 100644 index 00000000000000..3a70d9f683091e --- /dev/null +++ b/packages/virtualized-lists/Lists/VirtualizeUtils.js @@ -0,0 +1,258 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import type {FrameMetricProps} from './VirtualizedListProps'; + +/** + * Used to find the indices of the frames that overlap the given offsets. Useful for finding the + * items that bound different windows of content, such as the visible area or the buffered overscan + * area. + */ +export function elementsThatOverlapOffsets( + offsets: Array, + props: FrameMetricProps, + getFrameMetrics: ( + index: number, + props: FrameMetricProps, + ) => { + length: number, + offset: number, + ... + }, + zoomScale: number = 1, +): Array { + const itemCount = props.getItemCount(props.data); + const result = []; + for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) { + const currentOffset = offsets[offsetIndex]; + let left = 0; + let right = itemCount - 1; + + while (left <= right) { + // eslint-disable-next-line no-bitwise + const mid = left + ((right - left) >>> 1); + const frame = getFrameMetrics(mid, props); + const scaledOffsetStart = frame.offset * zoomScale; + const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale; + + // We want the first frame that contains the offset, with inclusive bounds. Thus, for the + // first frame the scaledOffsetStart is inclusive, while for other frames it is exclusive. + if ( + (mid === 0 && currentOffset < scaledOffsetStart) || + (mid !== 0 && currentOffset <= scaledOffsetStart) + ) { + right = mid - 1; + } else if (currentOffset > scaledOffsetEnd) { + left = mid + 1; + } else { + result[offsetIndex] = mid; + break; + } + } + } + + return result; +} + +/** + * Computes the number of elements in the `next` range that are new compared to the `prev` range. + * Handy for calculating how many new items will be rendered when the render window changes so we + * can restrict the number of new items render at once so that content can appear on the screen + * faster. + */ +export function newRangeCount( + prev: { + first: number, + last: number, + ... + }, + next: { + first: number, + last: number, + ... + }, +): number { + return ( + next.last - + next.first + + 1 - + Math.max( + 0, + 1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first), + ) + ); +} + +/** + * Custom logic for determining which items should be rendered given the current frame and scroll + * metrics, as well as the previous render state. The algorithm may evolve over time, but generally + * prioritizes the visible area first, then expands that with overscan regions ahead and behind, + * biased in the direction of scroll. + */ +export function computeWindowedRenderLimits( + props: FrameMetricProps, + maxToRenderPerBatch: number, + windowSize: number, + prev: { + first: number, + last: number, + }, + getFrameMetricsApprox: ( + index: number, + props: FrameMetricProps, + ) => { + length: number, + offset: number, + ... + }, + scrollMetrics: { + dt: number, + offset: number, + velocity: number, + visibleLength: number, + zoomScale: number, + ... + }, +): { + first: number, + last: number, +} { + const itemCount = props.getItemCount(props.data); + if (itemCount === 0) { + return {first: 0, last: -1}; + } + const {offset, velocity, visibleLength, zoomScale = 1} = scrollMetrics; + + // Start with visible area, then compute maximum overscan region by expanding from there, biased + // in the direction of scroll. Total overscan area is capped, which should cap memory consumption + // too. + const visibleBegin = Math.max(0, offset); + const visibleEnd = visibleBegin + visibleLength; + const overscanLength = (windowSize - 1) * visibleLength; + + // Considering velocity seems to introduce more churn than it's worth. + const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5)); + + const fillPreference = + velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none'; + + const overscanBegin = Math.max( + 0, + visibleBegin - (1 - leadFactor) * overscanLength, + ); + const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength); + + const lastItemOffset = + getFrameMetricsApprox(itemCount - 1, props).offset * zoomScale; + if (lastItemOffset < overscanBegin) { + // Entire list is before our overscan window + return { + first: Math.max(0, itemCount - 1 - maxToRenderPerBatch), + last: itemCount - 1, + }; + } + + // Find the indices that correspond to the items at the render boundaries we're targeting. + let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets( + [overscanBegin, visibleBegin, visibleEnd, overscanEnd], + props, + getFrameMetricsApprox, + zoomScale, + ); + overscanFirst = overscanFirst == null ? 0 : overscanFirst; + first = first == null ? Math.max(0, overscanFirst) : first; + overscanLast = overscanLast == null ? itemCount - 1 : overscanLast; + last = + last == null + ? Math.min(overscanLast, first + maxToRenderPerBatch - 1) + : last; + const visible = {first, last}; + + // We want to limit the number of new cells we're rendering per batch so that we can fill the + // content on the screen quickly. If we rendered the entire overscan window at once, the user + // could be staring at white space for a long time waiting for a bunch of offscreen content to + // render. + let newCellCount = newRangeCount(prev, visible); + + while (true) { + if (first <= overscanFirst && last >= overscanLast) { + // If we fill the entire overscan range, we're done. + break; + } + const maxNewCells = newCellCount >= maxToRenderPerBatch; + const firstWillAddMore = first <= prev.first || first > prev.last; + const firstShouldIncrement = + first > overscanFirst && (!maxNewCells || !firstWillAddMore); + const lastWillAddMore = last >= prev.last || last < prev.first; + const lastShouldIncrement = + last < overscanLast && (!maxNewCells || !lastWillAddMore); + if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) { + // We only want to stop if we've hit maxNewCells AND we cannot increment first or last + // without rendering new items. This let's us preserve as many already rendered items as + // possible, reducing render churn and keeping the rendered overscan range as large as + // possible. + break; + } + if ( + firstShouldIncrement && + !(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore) + ) { + if (firstWillAddMore) { + newCellCount++; + } + first--; + } + if ( + lastShouldIncrement && + !(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore) + ) { + if (lastWillAddMore) { + newCellCount++; + } + last++; + } + } + if ( + !( + last >= first && + first >= 0 && + last < itemCount && + first >= overscanFirst && + last <= overscanLast && + first <= visible.first && + last >= visible.last + ) + ) { + throw new Error( + 'Bad window calculation ' + + JSON.stringify({ + first, + last, + itemCount, + overscanFirst, + overscanLast, + visible, + }), + ); + } + return {first, last}; +} + +export function keyExtractor(item: any, index: number): string { + if (typeof item === 'object' && item?.key != null) { + return item.key; + } + if (typeof item === 'object' && item?.id != null) { + return item.id; + } + return String(index); +} diff --git a/Libraries/Lists/VirtualizedList.d.ts b/packages/virtualized-lists/Lists/VirtualizedList.d.ts similarity index 97% rename from Libraries/Lists/VirtualizedList.d.ts rename to packages/virtualized-lists/Lists/VirtualizedList.d.ts index d874dab4d10271..a2702d9101e107 100644 --- a/Libraries/Lists/VirtualizedList.d.ts +++ b/packages/virtualized-lists/Lists/VirtualizedList.d.ts @@ -8,15 +8,15 @@ */ import type * as React from 'react'; -import type {LayoutChangeEvent} from '../../types'; -import {StyleProp} from '../StyleSheet/StyleSheet'; -import {ViewStyle} from '../StyleSheet/StyleSheetTypes'; import type { + StyleProp, + ViewStyle, + ScrollViewProps, + LayoutChangeEvent, + View, ScrollResponderMixin, ScrollView, - ScrollViewProps, -} from '../Components/ScrollView/ScrollView'; -import type {View} from '../Components/View/View'; +} from 'react-native'; export interface ViewToken { item: any; diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js new file mode 100644 index 00000000000000..b93878c5658295 --- /dev/null +++ b/packages/virtualized-lists/Lists/VirtualizedList.js @@ -0,0 +1,1955 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +import type {ScrollResponderType} from 'react-native/Libraries/Components/ScrollView/ScrollView'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import type { + LayoutEvent, + ScrollEvent, +} from 'react-native/Libraries/Types/CoreEventTypes'; +import type {ViewToken} from './ViewabilityHelper'; +import type { + FrameMetricProps, + Item, + Props, + RenderItemProps, + RenderItemType, + Separators, +} from './VirtualizedListProps'; + +import { + RefreshControl, + ScrollView, + View, + StyleSheet, + findNodeHandle, +} from 'react-native'; +import Batchinator from '../Interaction/Batchinator'; +import clamp from '../Utilities/clamp'; +import infoLog from '../Utilities/infoLog'; +import {CellRenderMask} from './CellRenderMask'; +import ChildListCollection from './ChildListCollection'; +import FillRateHelper from './FillRateHelper'; +import StateSafePureComponent from './StateSafePureComponent'; +import ViewabilityHelper from './ViewabilityHelper'; +import CellRenderer from './VirtualizedListCellRenderer'; +import { + VirtualizedListCellContextProvider, + VirtualizedListContext, + VirtualizedListContextProvider, +} from './VirtualizedListContext.js'; +import { + computeWindowedRenderLimits, + keyExtractor as defaultKeyExtractor, +} from './VirtualizeUtils'; +import invariant from 'invariant'; +import * as React from 'react'; + +export type {RenderItemProps, RenderItemType, Separators}; + +const ON_EDGE_REACHED_EPSILON = 0.001; + +let _usedIndexForKey = false; +let _keylessItemComponentName: string = ''; + +type ViewabilityHelperCallbackTuple = { + viewabilityHelper: ViewabilityHelper, + onViewableItemsChanged: (info: { + viewableItems: Array, + changed: Array, + ... + }) => void, + ... +}; + +type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, +}; + +/** + * Default Props Helper Functions + * Use the following helper functions for default values + */ + +// horizontalOrDefault(this.props.horizontal) +function horizontalOrDefault(horizontal: ?boolean) { + return horizontal ?? false; +} + +// initialNumToRenderOrDefault(this.props.initialNumToRenderOrDefault) +function initialNumToRenderOrDefault(initialNumToRender: ?number) { + return initialNumToRender ?? 10; +} + +// maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch) +function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) { + return maxToRenderPerBatch ?? 10; +} + +// onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold) +function onStartReachedThresholdOrDefault(onStartReachedThreshold: ?number) { + return onStartReachedThreshold ?? 2; +} + +// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold) +function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) { + return onEndReachedThreshold ?? 2; +} + +// getScrollingThreshold(visibleLength, onEndReachedThreshold) +function getScrollingThreshold(threshold: number, visibleLength: number) { + return (threshold * visibleLength) / 2; +} + +// scrollEventThrottleOrDefault(this.props.scrollEventThrottle) +function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) { + return scrollEventThrottle ?? 50; +} + +// windowSizeOrDefault(this.props.windowSize) +function windowSizeOrDefault(windowSize: ?number) { + return windowSize ?? 21; +} + +function findLastWhere( + arr: $ReadOnlyArray, + predicate: (element: T) => boolean, +): T | null { + for (let i = arr.length - 1; i >= 0; i--) { + if (predicate(arr[i])) { + return arr[i]; + } + } + + return null; +} + +/** + * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) + * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better + * documented. In general, this should only really be used if you need more flexibility than + * `FlatList` provides, e.g. for use with immutable data instead of plain arrays. + * + * Virtualization massively improves memory consumption and performance of large lists by + * maintaining a finite render window of active items and replacing all items outside of the render + * window with appropriately sized blank space. The window adapts to scrolling behavior, and items + * are rendered incrementally with low-pri (after any running interactions) if they are far from the + * visible area, or with hi-pri otherwise to minimize the potential of seeing blank space. + * + * Some caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * - As an effort to remove defaultProps, use helper functions when referencing certain props + * + */ +class VirtualizedList extends StateSafePureComponent { + static contextType: typeof VirtualizedListContext = VirtualizedListContext; + + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { + const animated = params ? params.animated : true; + const veryLast = this.props.getItemCount(this.props.data) - 1; + if (veryLast < 0) { + return; + } + const frame = this.__getFrameMetricsApprox(veryLast, this.props); + const offset = Math.max( + 0, + frame.offset + + frame.length + + this._footerLength - + this._scrollMetrics.visibleLength, + ); + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + + this._scrollRef.scrollTo( + horizontalOrDefault(this.props.horizontal) + ? {x: offset, animated} + : {y: offset, animated}, + ); + } + + // scrollToIndex may be janky without getItemLayout prop + scrollToIndex(params: { + animated?: ?boolean, + index: number, + viewOffset?: number, + viewPosition?: number, + ... + }): $FlowFixMe { + const { + data, + horizontal, + getItemCount, + getItemLayout, + onScrollToIndexFailed, + } = this.props; + const {animated, index, viewOffset, viewPosition} = params; + invariant( + index >= 0, + `scrollToIndex out of range: requested index ${index} but minimum is 0`, + ); + invariant( + getItemCount(data) >= 1, + `scrollToIndex out of range: item length ${getItemCount( + data, + )} but minimum is 1`, + ); + invariant( + index < getItemCount(data), + `scrollToIndex out of range: requested index ${index} is out of 0 to ${ + getItemCount(data) - 1 + }`, + ); + if (!getItemLayout && index > this._highestMeasuredFrameIndex) { + invariant( + !!onScrollToIndexFailed, + 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' + + 'otherwise there is no way to know the location of offscreen indices or handle failures.', + ); + onScrollToIndexFailed({ + averageItemLength: this._averageCellLength, + highestMeasuredFrameIndex: this._highestMeasuredFrameIndex, + index, + }); + return; + } + const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props); + const offset = + Math.max( + 0, + this._getOffsetApprox(index, this.props) - + (viewPosition || 0) * + (this._scrollMetrics.visibleLength - frame.length), + ) - (viewOffset || 0); + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + + this._scrollRef.scrollTo( + horizontal ? {x: offset, animated} : {y: offset, animated}, + ); + } + + // scrollToItem may be janky without getItemLayout prop. Required linear scan through items - + // use scrollToIndex instead if possible. + scrollToItem(params: { + animated?: ?boolean, + item: Item, + viewOffset?: number, + viewPosition?: number, + ... + }) { + const {item} = params; + const {data, getItem, getItemCount} = this.props; + const itemCount = getItemCount(data); + for (let index = 0; index < itemCount; index++) { + if (getItem(data, index) === item) { + this.scrollToIndex({...params, index}); + break; + } + } + } + + /** + * Scroll to a specific content pixel offset in the list. + * + * Param `offset` expects the offset to scroll to. + * In case of `horizontal` is true, the offset is the x-value, + * in any other case the offset is the y-value. + * + * Param `animated` (`true` by default) defines whether the list + * should do an animation while scrolling. + */ + scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { + const {animated, offset} = params; + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + + this._scrollRef.scrollTo( + horizontalOrDefault(this.props.horizontal) + ? {x: offset, animated} + : {y: offset, animated}, + ); + } + + recordInteraction() { + this._nestedChildLists.forEach(childList => { + childList.recordInteraction(); + }); + this._viewabilityTuples.forEach(t => { + t.viewabilityHelper.recordInteraction(); + }); + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + } + + flashScrollIndicators() { + if (this._scrollRef == null) { + return; + } + + this._scrollRef.flashScrollIndicators(); + } + + /** + * Provides a handle to the underlying scroll responder. + * Note that `this._scrollRef` might not be a `ScrollView`, so we + * need to check that it responds to `getScrollResponder` before calling it. + */ + getScrollResponder(): ?ScrollResponderType { + if (this._scrollRef && this._scrollRef.getScrollResponder) { + return this._scrollRef.getScrollResponder(); + } + } + + getScrollableNode(): ?number { + if (this._scrollRef && this._scrollRef.getScrollableNode) { + return this._scrollRef.getScrollableNode(); + } else { + return findNodeHandle(this._scrollRef); + } + } + + getScrollRef(): + | ?React.ElementRef + | ?React.ElementRef { + if (this._scrollRef && this._scrollRef.getScrollRef) { + return this._scrollRef.getScrollRef(); + } else { + return this._scrollRef; + } + } + + setNativeProps(props: Object) { + if (this._scrollRef) { + this._scrollRef.setNativeProps(props); + } + } + + _getCellKey(): string { + return this.context?.cellKey || 'rootList'; + } + + // $FlowFixMe[missing-local-annot] + _getScrollMetrics = () => { + return this._scrollMetrics; + }; + + hasMore(): boolean { + return this._hasMore; + } + + // $FlowFixMe[missing-local-annot] + _getOutermostParentListRef = () => { + if (this._isNestedWithSameOrientation()) { + return this.context.getOutermostParentListRef(); + } else { + return this; + } + }; + + _registerAsNestedChild = (childList: { + cellKey: string, + ref: React.ElementRef, + }): void => { + this._nestedChildLists.add(childList.ref, childList.cellKey); + if (this._hasInteracted) { + childList.ref.recordInteraction(); + } + }; + + _unregisterAsNestedChild = (childList: { + ref: React.ElementRef, + }): void => { + this._nestedChildLists.remove(childList.ref); + }; + + state: State; + + constructor(props: Props) { + super(props); + invariant( + // $FlowFixMe[prop-missing] + !props.onScroll || !props.onScroll.__isNative, + 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + + 'to support native onScroll events with useNativeDriver', + ); + invariant( + windowSizeOrDefault(props.windowSize) > 0, + 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.', + ); + + invariant( + props.getItemCount, + 'VirtualizedList: The "getItemCount" prop must be provided', + ); + + this._fillRateHelper = new FillRateHelper(this._getFrameMetrics); + this._updateCellsToRenderBatcher = new Batchinator( + this._updateCellsToRender, + this.props.updateCellsBatchingPeriod ?? 50, + ); + + if (this.props.viewabilityConfigCallbackPairs) { + this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map( + pair => ({ + viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig), + onViewableItemsChanged: pair.onViewableItemsChanged, + }), + ); + } else { + const {onViewableItemsChanged, viewabilityConfig} = this.props; + if (onViewableItemsChanged) { + this._viewabilityTuples.push({ + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged, + }); + } + } + + invariant( + !this.context, + 'Unexpectedly saw VirtualizedListContext available in ctor', + ); + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); + + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), + }; + } + + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, + additionalRegions?: ?$ReadOnlyArray<{first: number, last: number}>, + ): CellRenderMask { + const itemCount = props.getItemCount(props.data); + + invariant( + cellsAroundViewport.first >= 0 && + cellsAroundViewport.last >= cellsAroundViewport.first - 1 && + cellsAroundViewport.last < itemCount, + `Invalid cells around viewport "[${cellsAroundViewport.first}, ${cellsAroundViewport.last}]" was passed to VirtualizedList._createRenderMask`, + ); + + const renderMask = new CellRenderMask(itemCount); + + if (itemCount > 0) { + const allRegions = [cellsAroundViewport, ...(additionalRegions ?? [])]; + for (const region of allRegions) { + renderMask.addCells(region); + } + + // The initially rendered cells are retained as part of the + // "scroll-to-top" optimization + if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) { + const initialRegion = VirtualizedList._initialRenderRegion(props); + renderMask.addCells(initialRegion); + } + + // The layout coordinates of sticker headers may be off-screen while the + // actual header is on-screen. Keep the most recent before the viewport + // rendered, even if its layout coordinates are not in viewport. + const stickyIndicesSet = new Set(props.stickyHeaderIndices); + VirtualizedList._ensureClosestStickyHeader( + props, + stickyIndicesSet, + renderMask, + cellsAroundViewport.first, + ); + } + + return renderMask; + } + + static _initialRenderRegion(props: Props): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); + const scrollIndex = Math.floor(Math.max(0, props.initialScrollIndex ?? 0)); + + return { + first: scrollIndex, + last: + Math.min( + itemCount, + scrollIndex + initialNumToRenderOrDefault(props.initialNumToRender), + ) - 1, + }; + } + + static _ensureClosestStickyHeader( + props: Props, + stickyIndicesSet: Set, + renderMask: CellRenderMask, + cellIdx: number, + ) { + const stickyOffset = props.ListHeaderComponent ? 1 : 0; + + for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) { + if (stickyIndicesSet.has(itemIdx + stickyOffset)) { + renderMask.addCells({first: itemIdx, last: itemIdx}); + break; + } + } + } + + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( + props.onEndReachedThreshold, + ); + this._updateViewableItems(props, cellsAroundViewport); + + const {contentLength, offset, visibleLength} = this._scrollMetrics; + const distanceFromEnd = contentLength - visibleLength - offset; + + // Wait until the scroll view metrics have been set up. And until then, + // we will trust the initialNumToRender suggestion + if (visibleLength <= 0 || contentLength <= 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; + } + + let newCellsAroundViewport: {first: number, last: number}; + if (props.disableVirtualization) { + const renderAhead = + distanceFromEnd < onEndReachedThreshold * visibleLength + ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) + : 0; + + newCellsAroundViewport = { + first: 0, + last: Math.min( + cellsAroundViewport.last + renderAhead, + getItemCount(data) - 1, + ), + }; + } else { + // If we have a non-zero initialScrollIndex and run this before we've scrolled, + // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. + // So let's wait until we've scrolled the view to the right place. And until then, + // we will trust the initialScrollIndex suggestion. + + // Thus, we want to recalculate the windowed render limits if any of the following hold: + // - initialScrollIndex is undefined or is 0 + // - initialScrollIndex > 0 AND scrolling is complete + // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case + // where the list is shorter than the visible area) + if ( + props.initialScrollIndex && + !this._scrollMetrics.offset && + Math.abs(distanceFromEnd) >= Number.EPSILON + ) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; + } + + newCellsAroundViewport = computeWindowedRenderLimits( + props, + maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), + windowSizeOrDefault(props.windowSize), + cellsAroundViewport, + this.__getFrameMetricsApprox, + this._scrollMetrics, + ); + invariant( + newCellsAroundViewport.last < getItemCount(data), + 'computeWindowedRenderLimits() should return range in-bounds', + ); + } + + if (this._nestedChildLists.size() > 0) { + // If some cell in the new state has a child list in it, we should only render + // up through that item, so that we give that list a chance to render. + // Otherwise there's churn from multiple child lists mounting and un-mounting + // their items. + + // Will this prevent rendering if the nested list doesn't realize the end? + const childIdx = this._findFirstChildWithMore( + newCellsAroundViewport.first, + newCellsAroundViewport.last, + ); + + newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last; + } + + return newCellsAroundViewport; + } + + _findFirstChildWithMore(first: number, last: number): number | null { + for (let ii = first; ii <= last; ii++) { + const cellKeyForIndex = this._indicesToKeys.get(ii); + if ( + cellKeyForIndex != null && + this._nestedChildLists.anyInCell(cellKeyForIndex, childList => + childList.hasMore(), + ) + ) { + return ii; + } + } + + return null; + } + + componentDidMount() { + if (this._isNestedWithSameOrientation()) { + this.context.registerAsNestedChild({ + ref: this, + cellKey: this.context.cellKey, + }); + } + } + + componentWillUnmount() { + if (this._isNestedWithSameOrientation()) { + this.context.unregisterAsNestedChild({ref: this}); + } + this._updateCellsToRenderBatcher.dispose({abort: true}); + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.dispose(); + }); + this._fillRateHelper.deactivateAndFlush(); + } + + static getDerivedStateFromProps(newProps: Props, prevState: State): State { + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + const itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } + + const constrainedCells = VirtualizedList._constrainToItemCount( + prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), + }; + } + + _pushCells( + cells: Array, + stickyHeaderIndices: Array, + stickyIndicesFromProps: Set, + first: number, + last: number, + inversionStyle: ViewStyleProp, + ) { + const { + CellRendererComponent, + ItemSeparatorComponent, + ListHeaderComponent, + ListItemComponent, + data, + debug, + getItem, + getItemCount, + getItemLayout, + horizontal, + renderItem, + } = this.props; + const stickyOffset = ListHeaderComponent ? 1 : 0; + const end = getItemCount(data) - 1; + let prevCellKey; + last = Math.min(end, last); + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); + const key = this._keyExtractor(item, ii, this.props); + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + stickyHeaderIndices.push(cells.length); + } + cells.push( + this._onCellFocusCapture(key)} + onUnmount={this._onCellUnmount} + ref={ref => { + this._cellRefs[key] = ref; + }} + renderItem={renderItem} + />, + ); + prevCellKey = key; + } + } + + static _constrainToItemCount( + cells: {first: number, last: number}, + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); + const last = Math.min(itemCount - 1, cells.last); + + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); + + return { + first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), + last, + }; + } + + _onUpdateSeparators = (keys: Array, newProps: Object) => { + keys.forEach(key => { + const ref = key != null && this._cellRefs[key]; + ref && ref.updateSeparatorProps(newProps); + }); + }; + + _isNestedWithSameOrientation(): boolean { + const nestedContext = this.context; + return !!( + nestedContext && + !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal) + ); + } + + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + + _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, + // $FlowFixMe[missing-local-annot] + ) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } + + const key = defaultKeyExtractor(item, index); + if (key === String(index)) { + _usedIndexForKey = true; + if (item.type && item.type.displayName) { + _keylessItemComponentName = item.type.displayName; + } + } + return key; + } + + render(): React.Node { + if (__DEV__) { + // $FlowFixMe[underconstrained-implicit-instantiation] + const flatStyles = StyleSheet.flatten(this.props.contentContainerStyle); + if (flatStyles != null && flatStyles.flexWrap === 'wrap') { + console.warn( + '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + + 'Consider using `numColumns` with `FlatList` instead.', + ); + } + } + const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = + this.props; + const {data, horizontal} = this.props; + const inversionStyle = this.props.inverted + ? horizontalOrDefault(this.props.horizontal) + ? styles.horizontallyInverted + : styles.verticallyInverted + : null; + const cells: Array = []; + const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); + const stickyHeaderIndices = []; + + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { + stickyHeaderIndices.push(0); + } + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + ); + cells.push( + + + { + // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors + element + } + + , + ); + } + + // 2a. Add a cell for ListEmptyComponent if applicable + const itemCount = this.props.getItemCount(data); + if (itemCount === 0 && ListEmptyComponent) { + const element: React.Element = ((React.isValidElement( + ListEmptyComponent, + ) ? ( + ListEmptyComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + )): any); + cells.push( + + {React.cloneElement(element, { + onLayout: (event: LayoutEvent) => { + this._onLayoutEmpty(event); + if (element.props.onLayout) { + element.props.onLayout(event); + } + }, + style: StyleSheet.compose(inversionStyle, element.props.style), + })} + , + ); + } + + // 2b. Add cells and spacers for each item + if (itemCount > 0) { + _usedIndexForKey = false; + _keylessItemComponentName = ''; + const spacerKey = this._getSpacerKey(!horizontal); + + const renderRegions = this.state.renderMask.enumerateRegions(); + const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); + + for (const section of renderRegions) { + if (section.isSpacer) { + // Legacy behavior is to avoid spacers when virtualization is + // disabled (including head spacers on initial render). + if (this.props.disableVirtualization) { + continue; + } + + // Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to + // prevent the user for hyperscrolling into un-measured area because otherwise content will + // likely jump around as it renders in above the viewport. + const isLastSpacer = section === lastSpacer; + const constrainToMeasured = isLastSpacer && !this.props.getItemLayout; + const last = constrainToMeasured + ? clamp( + section.first - 1, + section.last, + this._highestMeasuredFrameIndex, + ) + : section.last; + + const firstMetrics = this.__getFrameMetricsApprox( + section.first, + this.props, + ); + const lastMetrics = this.__getFrameMetricsApprox(last, this.props); + const spacerSize = + lastMetrics.offset + lastMetrics.length - firstMetrics.offset; + cells.push( + , + ); + } else { + this._pushCells( + cells, + stickyHeaderIndices, + stickyIndicesFromProps, + section.first, + section.last, + inversionStyle, + ); + } + } + + if (!this._hasWarned.keys && _usedIndexForKey) { + console.warn( + 'VirtualizedList: missing keys for items, make sure to specify a key or id property on each ' + + 'item or provide a custom keyExtractor.', + _keylessItemComponentName, + ); + this._hasWarned.keys = true; + } + } + + // 3. Add cell for ListFooterComponent + if (ListFooterComponent) { + const element = React.isValidElement(ListFooterComponent) ? ( + ListFooterComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + ); + cells.push( + + + { + // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors + element + } + + , + ); + } + + // 4. Render the ScrollView + const scrollProps = { + ...this.props, + onContentSizeChange: this._onContentSizeChange, + onLayout: this._onLayout, + onScroll: this._onScroll, + onScrollBeginDrag: this._onScrollBeginDrag, + onScrollEndDrag: this._onScrollEndDrag, + onMomentumScrollBegin: this._onMomentumScrollBegin, + onMomentumScrollEnd: this._onMomentumScrollEnd, + scrollEventThrottle: scrollEventThrottleOrDefault( + this.props.scrollEventThrottle, + ), // TODO: Android support + invertStickyHeaders: + this.props.invertStickyHeaders !== undefined + ? this.props.invertStickyHeaders + : this.props.inverted, + stickyHeaderIndices, + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, + }; + + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + + const innerRet = ( + + {React.cloneElement( + ( + this.props.renderScrollComponent || + this._defaultRenderScrollComponent + )(scrollProps), + { + ref: this._captureScrollRef, + }, + cells, + )} + + ); + let ret: React.Node = innerRet; + if (__DEV__) { + ret = ( + + {scrollContext => { + if ( + scrollContext != null && + !scrollContext.horizontal === + !horizontalOrDefault(this.props.horizontal) && + !this._hasWarned.nesting && + this.context == null && + this.props.scrollEnabled !== false + ) { + // TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170 + console.error( + 'VirtualizedLists should never be nested inside plain ScrollViews with the same ' + + 'orientation because it can break windowing and other functionality - use another ' + + 'VirtualizedList-backed container instead.', + ); + this._hasWarned.nesting = true; + } + return innerRet; + }} + + ); + } + if (this.props.debug) { + return ( + + {ret} + {this._renderDebugOverlay()} + + ); + } else { + return ret; + } + } + + componentDidUpdate(prevProps: Props) { + const {data, extraData} = this.props; + if (data !== prevProps.data || extraData !== prevProps.extraData) { + // clear the viewableIndices cache to also trigger + // the onViewableItemsChanged callback with the new data + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.resetViewableIndices(); + }); + } + // The `this._hiPriInProgress` is guaranteeing a hiPri cell update will only happen + // once per fiber update. The `_scheduleCellsToRenderUpdate` will set it to true + // if a hiPri update needs to perform. If `componentDidUpdate` is triggered with + // `this._hiPriInProgress=true`, means it's triggered by the hiPri update. The + // `_scheduleCellsToRenderUpdate` will check this condition and not perform + // another hiPri update. + const hiPriInProgress = this._hiPriInProgress; + this._scheduleCellsToRenderUpdate(); + // Make sure setting `this._hiPriInProgress` back to false after `componentDidUpdate` + // is triggered with `this._hiPriInProgress = true` + if (hiPriInProgress) { + this._hiPriInProgress = false; + } + } + + _averageCellLength = 0; + _cellRefs: {[string]: null | CellRenderer} = {}; + _fillRateHelper: FillRateHelper; + _frames: { + [string]: { + inLayout?: boolean, + index: number, + length: number, + offset: number, + }, + } = {}; + _footerLength = 0; + // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex + _hasTriggeredInitialScrollToIndex = false; + _hasInteracted = false; + _hasMore = false; + _hasWarned: {[string]: boolean} = {}; + _headerLength = 0; + _hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update + _highestMeasuredFrameIndex = 0; + _indicesToKeys: Map = new Map(); + _lastFocusedCellKey: ?string = null; + _nestedChildLists: ChildListCollection = + new ChildListCollection(); + _offsetFromParentVirtualizedList: number = 0; + _prevParentOffset: number = 0; + // $FlowFixMe[missing-local-annot] + _scrollMetrics = { + contentLength: 0, + dOffset: 0, + dt: 10, + offset: 0, + timestamp: 0, + velocity: 0, + visibleLength: 0, + zoomScale: 1, + }; + _scrollRef: ?React.ElementRef = null; + _sentStartForContentLength = 0; + _sentEndForContentLength = 0; + _totalCellLength = 0; + _totalCellsMeasured = 0; + _updateCellsToRenderBatcher: Batchinator; + _viewabilityTuples: Array = []; + + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + _captureScrollRef = ref => { + this._scrollRef = ref; + }; + + _computeBlankness() { + this._fillRateHelper.computeBlankness( + this.props, + this.state.cellsAroundViewport, + this._scrollMetrics, + ); + } + + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; + } else if (onRefresh) { + invariant( + typeof props.refreshing === 'boolean', + '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + + JSON.stringify(props.refreshing ?? 'undefined') + + '`', + ); + return ( + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] + + ) : ( + props.refreshControl + ) + } + /> + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] + return ; + } + }; + + _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => { + const layout = e.nativeEvent.layout; + const next = { + offset: this._selectOffset(layout), + length: this._selectLength(layout), + index, + inLayout: true, + }; + const curr = this._frames[cellKey]; + if ( + !curr || + next.offset !== curr.offset || + next.length !== curr.length || + index !== curr.index + ) { + this._totalCellLength += next.length - (curr ? curr.length : 0); + this._totalCellsMeasured += curr ? 0 : 1; + this._averageCellLength = + this._totalCellLength / this._totalCellsMeasured; + this._frames[cellKey] = next; + this._highestMeasuredFrameIndex = Math.max( + this._highestMeasuredFrameIndex, + index, + ); + this._scheduleCellsToRenderUpdate(); + } else { + this._frames[cellKey].inLayout = true; + } + + this._triggerRemeasureForChildListsInCell(cellKey); + + this._computeBlankness(); + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + }; + + _onCellFocusCapture(cellKey: string) { + this._lastFocusedCellKey = cellKey; + const renderMask = VirtualizedList._createRenderMask( + this.props, + this.state.cellsAroundViewport, + this._getNonViewportRenderRegions(this.props), + ); + + this.setState(state => { + if (!renderMask.equals(state.renderMask)) { + return {renderMask}; + } + return null; + }); + } + + _onCellUnmount = (cellKey: string) => { + const curr = this._frames[cellKey]; + if (curr) { + this._frames[cellKey] = {...curr, inLayout: false}; + } + }; + + _triggerRemeasureForChildListsInCell(cellKey: string): void { + this._nestedChildLists.forEachInCell(cellKey, childList => { + childList.measureLayoutRelativeToContainingList(); + }); + } + + measureLayoutRelativeToContainingList(): void { + // TODO (T35574538): findNodeHandle sometimes crashes with "Unable to find + // node on an unmounted component" during scrolling + try { + if (!this._scrollRef) { + return; + } + // We are assuming that getOutermostParentListRef().getScrollRef() + // is a non-null reference to a ScrollView + this._scrollRef.measureLayout( + this.context.getOutermostParentListRef().getScrollRef(), + (x, y, width, height) => { + this._offsetFromParentVirtualizedList = this._selectOffset({x, y}); + this._scrollMetrics.contentLength = this._selectLength({ + width, + height, + }); + const scrollMetrics = this._convertParentScrollMetrics( + this.context.getScrollMetrics(), + ); + + const metricsChanged = + this._scrollMetrics.visibleLength !== scrollMetrics.visibleLength || + this._scrollMetrics.offset !== scrollMetrics.offset; + + if (metricsChanged) { + this._scrollMetrics.visibleLength = scrollMetrics.visibleLength; + this._scrollMetrics.offset = scrollMetrics.offset; + + // If metrics of the scrollView changed, then we triggered remeasure for child list + // to ensure VirtualizedList has the right information. + this._nestedChildLists.forEach(childList => { + childList.measureLayoutRelativeToContainingList(); + }); + } + }, + error => { + console.warn( + "VirtualizedList: Encountered an error while measuring a list's" + + ' offset from its containing VirtualizedList.', + ); + }, + ); + } catch (error) { + console.warn( + 'measureLayoutRelativeToContainingList threw an error', + error.stack, + ); + } + } + + _onLayout = (e: LayoutEvent) => { + if (this._isNestedWithSameOrientation()) { + // Need to adjust our scroll metrics to be relative to our containing + // VirtualizedList before we can make claims about list item viewability + this.measureLayoutRelativeToContainingList(); + } else { + this._scrollMetrics.visibleLength = this._selectLength( + e.nativeEvent.layout, + ); + } + this.props.onLayout && this.props.onLayout(e); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEdgeReached(); + }; + + _onLayoutEmpty = (e: LayoutEvent) => { + this.props.onLayout && this.props.onLayout(e); + }; + + _getFooterCellKey(): string { + return this._getCellKey() + '-footer'; + } + + _onLayoutFooter = (e: LayoutEvent) => { + this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()); + this._footerLength = this._selectLength(e.nativeEvent.layout); + }; + + _onLayoutHeader = (e: LayoutEvent) => { + this._headerLength = this._selectLength(e.nativeEvent.layout); + }; + + // $FlowFixMe[missing-local-annot] + _renderDebugOverlay() { + const normalize = + this._scrollMetrics.visibleLength / + (this._scrollMetrics.contentLength || 1); + const framesInLayout = []; + const itemCount = this.props.getItemCount(this.props.data); + for (let ii = 0; ii < itemCount; ii++) { + const frame = this.__getFrameMetricsApprox(ii, this.props); + /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { + framesInLayout.push(frame); + } + } + const windowTop = this.__getFrameMetricsApprox( + this.state.cellsAroundViewport.first, + this.props, + ).offset; + const frameLast = this.__getFrameMetricsApprox( + this.state.cellsAroundViewport.last, + this.props, + ); + const windowLen = frameLast.offset + frameLast.length - windowTop; + const visTop = this._scrollMetrics.offset; + const visLen = this._scrollMetrics.visibleLength; + + return ( + + {framesInLayout.map((f, ii) => ( + + ))} + + + + ); + } + + _selectLength( + metrics: $ReadOnly<{ + height: number, + width: number, + ... + }>, + ): number { + return !horizontalOrDefault(this.props.horizontal) + ? metrics.height + : metrics.width; + } + + _selectOffset( + metrics: $ReadOnly<{ + x: number, + y: number, + ... + }>, + ): number { + return !horizontalOrDefault(this.props.horizontal) ? metrics.y : metrics.x; + } + + _maybeCallOnEdgeReached() { + const { + data, + getItemCount, + onStartReached, + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, + initialScrollIndex, + } = this.props; + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; + + // Especially when oERT is zero it's necessary to 'floor' very small distance values to be 0 + // since debouncing causes us to not fire this event for every single "pixel" we scroll and can thus + // be at the edge of the list with a distance approximating 0 but not quite there. + if (distanceFromStart < ON_EDGE_REACHED_EPSILON) { + distanceFromStart = 0; + } + if (distanceFromEnd < ON_EDGE_REACHED_EPSILON) { + distanceFromEnd = 0; + } + + // TODO: T121172172 Look into why we're "defaulting" to a threshold of 2px + // when oERT is not present (different from 2 viewports used elsewhere) + const DEFAULT_THRESHOLD_PX = 2; + + const startThreshold = + onStartReachedThreshold != null + ? onStartReachedThreshold * visibleLength + : DEFAULT_THRESHOLD_PX; + const endThreshold = + onEndReachedThreshold != null + ? onEndReachedThreshold * visibleLength + : DEFAULT_THRESHOLD_PX; + const isWithinStartThreshold = distanceFromStart <= startThreshold; + const isWithinEndThreshold = distanceFromEnd <= endThreshold; + + // First check if the user just scrolled within the end threshold + // and call onEndReached only once for a given content length, + // and only if onStartReached is not being executed + if ( + onEndReached && + this.state.cellsAroundViewport.last === getItemCount(data) - 1 && + isWithinEndThreshold && + this._scrollMetrics.contentLength !== this._sentEndForContentLength + ) { + this._sentEndForContentLength = this._scrollMetrics.contentLength; + onEndReached({distanceFromEnd}); + } + + // Next check if the user just scrolled within the start threshold + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if ( + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { + // On initial mount when using initialScrollIndex the offset will be 0 initially + // and will trigger an unexpected onStartReached. To avoid this we can use + // timestamp to differentiate between the initial scroll metrics and when we actually + // received the first scroll event. + if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { + this._sentStartForContentLength = this._scrollMetrics.contentLength; + onStartReached({distanceFromStart}); + } + } + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + else { + this._sentStartForContentLength = isWithinStartThreshold + ? this._sentStartForContentLength + : 0; + this._sentEndForContentLength = isWithinEndThreshold + ? this._sentEndForContentLength + : 0; + } + } + + _onContentSizeChange = (width: number, height: number) => { + if ( + width > 0 && + height > 0 && + this.props.initialScrollIndex != null && + this.props.initialScrollIndex > 0 && + !this._hasTriggeredInitialScrollToIndex + ) { + if (this.props.contentOffset == null) { + this.scrollToIndex({ + animated: false, + index: this.props.initialScrollIndex, + }); + } + this._hasTriggeredInitialScrollToIndex = true; + } + if (this.props.onContentSizeChange) { + this.props.onContentSizeChange(width, height); + } + this._scrollMetrics.contentLength = this._selectLength({height, width}); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEdgeReached(); + }; + + /* Translates metrics from a scroll event in a parent VirtualizedList into + * coordinates relative to the child list. + */ + _convertParentScrollMetrics = (metrics: { + visibleLength: number, + offset: number, + ... + }): $FlowFixMe => { + // Offset of the top of the nested list relative to the top of its parent's viewport + const offset = metrics.offset - this._offsetFromParentVirtualizedList; + // Child's visible length is the same as its parent's + const visibleLength = metrics.visibleLength; + const dOffset = offset - this._scrollMetrics.offset; + const contentLength = this._scrollMetrics.contentLength; + + return { + visibleLength, + contentLength, + offset, + dOffset, + }; + }; + + _onScroll = (e: Object) => { + this._nestedChildLists.forEach(childList => { + childList._onScroll(e); + }); + if (this.props.onScroll) { + this.props.onScroll(e); + } + const timestamp = e.timeStamp; + let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement); + let contentLength = this._selectLength(e.nativeEvent.contentSize); + let offset = this._selectOffset(e.nativeEvent.contentOffset); + let dOffset = offset - this._scrollMetrics.offset; + + if (this._isNestedWithSameOrientation()) { + if (this._scrollMetrics.contentLength === 0) { + // Ignore scroll events until onLayout has been called and we + // know our offset from our offset from our parent + return; + } + ({visibleLength, contentLength, offset, dOffset} = + this._convertParentScrollMetrics({ + visibleLength, + offset, + })); + } + + const dt = this._scrollMetrics.timestamp + ? Math.max(1, timestamp - this._scrollMetrics.timestamp) + : 1; + const velocity = dOffset / dt; + + if ( + dt > 500 && + this._scrollMetrics.dt > 500 && + contentLength > 5 * visibleLength && + !this._hasWarned.perf + ) { + infoLog( + 'VirtualizedList: You have a large list that is slow to update - make sure your ' + + 'renderItem function renders components that follow React performance best practices ' + + 'like PureComponent, shouldComponentUpdate, etc.', + {dt, prevDt: this._scrollMetrics.dt, contentLength}, + ); + this._hasWarned.perf = true; + } + + // For invalid negative values (w/ RTL), set this to 1. + const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale; + this._scrollMetrics = { + contentLength, + dt, + dOffset, + offset, + timestamp, + velocity, + visibleLength, + zoomScale, + }; + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; + } + this._maybeCallOnEdgeReached(); + if (velocity !== 0) { + this._fillRateHelper.activate(); + } + this._computeBlankness(); + this._scheduleCellsToRenderUpdate(); + }; + + _scheduleCellsToRenderUpdate() { + const {first, last} = this.state.cellsAroundViewport; + const {offset, visibleLength, velocity} = this._scrollMetrics; + const itemCount = this.props.getItemCount(this.props.data); + let hiPri = false; + const onStartReachedThreshold = onStartReachedThresholdOrDefault( + this.props.onStartReachedThreshold, + ); + const onEndReachedThreshold = onEndReachedThresholdOrDefault( + this.props.onEndReachedThreshold, + ); + // Mark as high priority if we're close to the start of the first item + // But only if there are items before the first rendered item + if (first > 0) { + const distTop = + offset - this.__getFrameMetricsApprox(first, this.props).offset; + hiPri = + distTop < 0 || + (velocity < -2 && + distTop < + getScrollingThreshold(onStartReachedThreshold, visibleLength)); + } + // Mark as high priority if we're close to the end of the last item + // But only if there are items after the last rendered item + if (!hiPri && last >= 0 && last < itemCount - 1) { + const distBottom = + this.__getFrameMetricsApprox(last, this.props).offset - + (offset + visibleLength); + hiPri = + distBottom < 0 || + (velocity > 2 && + distBottom < + getScrollingThreshold(onEndReachedThreshold, visibleLength)); + } + // Only trigger high-priority updates if we've actually rendered cells, + // and with that size estimate, accurately compute how many cells we should render. + // Otherwise, it would just render as many cells as it can (of zero dimension), + // each time through attempting to render more (limited by maxToRenderPerBatch), + // starving the renderer from actually laying out the objects and computing _averageCellLength. + // If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate + // We shouldn't do another hipri cellToRenderUpdate + if ( + hiPri && + (this._averageCellLength || this.props.getItemLayout) && + !this._hiPriInProgress + ) { + this._hiPriInProgress = true; + // Don't worry about interactions when scrolling quickly; focus on filling content as fast + // as possible. + this._updateCellsToRenderBatcher.dispose({abort: true}); + this._updateCellsToRender(); + return; + } else { + this._updateCellsToRenderBatcher.schedule(); + } + } + + _onScrollBeginDrag = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onScrollBeginDrag(e); + }); + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.recordInteraction(); + }); + this._hasInteracted = true; + this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); + }; + + _onScrollEndDrag = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onScrollEndDrag(e); + }); + const {velocity} = e.nativeEvent; + if (velocity) { + this._scrollMetrics.velocity = this._selectOffset(velocity); + } + this._computeBlankness(); + this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); + }; + + _onMomentumScrollBegin = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onMomentumScrollBegin(e); + }); + this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); + }; + + _onMomentumScrollEnd = (e: ScrollEvent): void => { + this._nestedChildLists.forEach(childList => { + childList._onMomentumScrollEnd(e); + }); + this._scrollMetrics.velocity = 0; + this._computeBlankness(); + this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); + }; + + _updateCellsToRender = () => { + this.setState((state, props) => { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, + ); + const renderMask = VirtualizedList._createRenderMask( + props, + cellsAroundViewport, + this._getNonViewportRenderRegions(props), + ); + + if ( + cellsAroundViewport.first === state.cellsAroundViewport.first && + cellsAroundViewport.last === state.cellsAroundViewport.last && + renderMask.equals(state.renderMask) + ) { + return null; + } + + return {cellsAroundViewport, renderMask}; + }); + }; + + _createViewToken = ( + index: number, + isViewable: boolean, + props: FrameMetricProps, + // $FlowFixMe[missing-local-annot] + ) => { + const {data, getItem} = props; + const item = getItem(data, index); + return { + index, + item, + key: this._keyExtractor(item, index, props), + isViewable, + }; + }; + + /** + * Gets an approximate offset to an item at a given index. Supports + * fractional indices. + */ + _getOffsetApprox = (index: number, props: FrameMetricProps): number => { + if (Number.isInteger(index)) { + return this.__getFrameMetricsApprox(index, props).offset; + } else { + const frameMetrics = this.__getFrameMetricsApprox( + Math.floor(index), + props, + ); + const remainder = index - Math.floor(index); + return frameMetrics.offset + remainder * frameMetrics.length; + } + }; + + __getFrameMetricsApprox: ( + index: number, + props: FrameMetricProps, + ) => { + length: number, + offset: number, + ... + } = (index, props) => { + const frame = this._getFrameMetrics(index, props); + if (frame && frame.index === index) { + // check for invalid frames due to row re-ordering + return frame; + } else { + const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); + invariant( + !getItemLayout, + 'Should not have to estimate frames when a measurement metrics function is provided', + ); + return { + length: this._averageCellLength, + offset: this._averageCellLength * index, + }; + } + }; + + _getFrameMetrics = ( + index: number, + props: FrameMetricProps, + ): ?{ + length: number, + offset: number, + index: number, + inLayout?: boolean, + ... + } => { + const {data, getItem, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); + const item = getItem(data, index); + const frame = this._frames[this._keyExtractor(item, index, props)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.63 was deployed. To see the error + * delete this comment and run Flow. */ + return getItemLayout(data, index); + } + } + return frame; + }; + + _getNonViewportRenderRegions = ( + props: FrameMetricProps, + ): $ReadOnlyArray<{ + first: number, + last: number, + }> => { + // Keep a viewport's worth of content around the last focused cell to allow + // random navigation around it without any blanking. E.g. tabbing from one + // focused item out of viewport to another. + if ( + !(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey]) + ) { + return []; + } + + const lastFocusedCellRenderer = this._cellRefs[this._lastFocusedCellKey]; + const focusedCellIndex = lastFocusedCellRenderer.props.index; + const itemCount = props.getItemCount(props.data); + + // The cell may have been unmounted and have a stale index + if ( + focusedCellIndex >= itemCount || + this._indicesToKeys.get(focusedCellIndex) !== this._lastFocusedCellKey + ) { + return []; + } + + let first = focusedCellIndex; + let heightOfCellsBeforeFocused = 0; + for ( + let i = first - 1; + i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength; + i-- + ) { + first--; + heightOfCellsBeforeFocused += this.__getFrameMetricsApprox( + i, + props, + ).length; + } + + let last = focusedCellIndex; + let heightOfCellsAfterFocused = 0; + for ( + let i = last + 1; + i < itemCount && + heightOfCellsAfterFocused < this._scrollMetrics.visibleLength; + i++ + ) { + last++; + heightOfCellsAfterFocused += this.__getFrameMetricsApprox( + i, + props, + ).length; + } + + return [{first, last}]; + }; + + _updateViewableItems( + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, + this._scrollMetrics.offset, + this._scrollMetrics.visibleLength, + this._getFrameMetrics, + this._createViewToken, + tuple.onViewableItemsChanged, + cellsAroundViewport, + ); + }); + } +} + +const styles = StyleSheet.create({ + verticallyInverted: { + transform: [{scaleY: -1}], + }, + horizontallyInverted: { + transform: [{scaleX: -1}], + }, + debug: { + flex: 1, + }, + debugOverlayBase: { + position: 'absolute', + top: 0, + right: 0, + }, + debugOverlay: { + bottom: 0, + width: 20, + borderColor: 'blue', + borderWidth: 1, + }, + debugOverlayFrame: { + left: 0, + backgroundColor: 'orange', + }, + debugOverlayFrameLast: { + left: 0, + borderColor: 'green', + borderWidth: 2, + }, + debugOverlayFrameVis: { + left: 0, + borderColor: 'red', + borderWidth: 2, + }, +}); + +module.exports = VirtualizedList; diff --git a/Libraries/Lists/VirtualizedListCellRenderer.js b/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js similarity index 96% rename from Libraries/Lists/VirtualizedListCellRenderer.js rename to packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js index b16900d63b742b..0064f788b8a9c4 100644 --- a/Libraries/Lists/VirtualizedListCellRenderer.js +++ b/packages/virtualized-lists/Lists/VirtualizedListCellRenderer.js @@ -8,13 +8,15 @@ * @format */ -import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; -import type {FocusEvent, LayoutEvent} from '../Types/CoreEventTypes'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import type { + FocusEvent, + LayoutEvent, +} from 'react-native/Libraries/Types/CoreEventTypes'; import type FillRateHelper from './FillRateHelper'; import type {RenderItemType} from './VirtualizedListProps'; -import View from '../Components/View/View'; -import StyleSheet from '../StyleSheet/StyleSheet'; +import {View, StyleSheet} from 'react-native'; import {VirtualizedListCellContextProvider} from './VirtualizedListContext.js'; import invariant from 'invariant'; import * as React from 'react'; diff --git a/packages/virtualized-lists/Lists/VirtualizedListContext.js b/packages/virtualized-lists/Lists/VirtualizedListContext.js new file mode 100644 index 00000000000000..bca5724498a356 --- /dev/null +++ b/packages/virtualized-lists/Lists/VirtualizedListContext.js @@ -0,0 +1,116 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import typeof VirtualizedList from './VirtualizedList'; + +import * as React from 'react'; +import {useContext, useMemo} from 'react'; + +type Context = $ReadOnly<{ + cellKey: ?string, + getScrollMetrics: () => { + contentLength: number, + dOffset: number, + dt: number, + offset: number, + timestamp: number, + velocity: number, + visibleLength: number, + zoomScale: number, + }, + horizontal: ?boolean, + getOutermostParentListRef: () => React.ElementRef, + registerAsNestedChild: ({ + cellKey: string, + ref: React.ElementRef, + }) => void, + unregisterAsNestedChild: ({ + ref: React.ElementRef, + }) => void, +}>; + +export const VirtualizedListContext: React.Context = + React.createContext(null); +if (__DEV__) { + VirtualizedListContext.displayName = 'VirtualizedListContext'; +} + +/** + * Resets the context. Intended for use by portal-like components (e.g. Modal). + */ +export function VirtualizedListContextResetter({ + children, +}: { + children: React.Node, +}): React.Node { + return ( + + {children} + + ); +} + +/** + * Sets the context with memoization. Intended to be used by `VirtualizedList`. + */ +export function VirtualizedListContextProvider({ + children, + value, +}: { + children: React.Node, + value: Context, +}): React.Node { + // Avoid setting a newly created context object if the values are identical. + const context = useMemo( + () => ({ + cellKey: null, + getScrollMetrics: value.getScrollMetrics, + horizontal: value.horizontal, + getOutermostParentListRef: value.getOutermostParentListRef, + registerAsNestedChild: value.registerAsNestedChild, + unregisterAsNestedChild: value.unregisterAsNestedChild, + }), + [ + value.getScrollMetrics, + value.horizontal, + value.getOutermostParentListRef, + value.registerAsNestedChild, + value.unregisterAsNestedChild, + ], + ); + return ( + + {children} + + ); +} + +/** + * Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell. + */ +export function VirtualizedListCellContextProvider({ + cellKey, + children, +}: { + cellKey: string, + children: React.Node, +}): React.Node { + // Avoid setting a newly created context object if the values are identical. + const currContext = useContext(VirtualizedListContext); + const context = useMemo( + () => (currContext == null ? null : {...currContext, cellKey}), + [currContext, cellKey], + ); + return ( + + {children} + + ); +} diff --git a/Libraries/Lists/VirtualizedListProps.js b/packages/virtualized-lists/Lists/VirtualizedListProps.js similarity index 98% rename from Libraries/Lists/VirtualizedListProps.js rename to packages/virtualized-lists/Lists/VirtualizedListProps.js index f4d497b1d467a8..bfc59673270a57 100644 --- a/Libraries/Lists/VirtualizedListProps.js +++ b/packages/virtualized-lists/Lists/VirtualizedListProps.js @@ -8,8 +8,8 @@ * @format */ -import typeof ScrollView from '../Components/ScrollView/ScrollView'; -import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; +import {typeof ScrollView} from 'react-native'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type { ViewabilityConfig, ViewabilityConfigCallbackPair, diff --git a/packages/virtualized-lists/Lists/VirtualizedSectionList.js b/packages/virtualized-lists/Lists/VirtualizedSectionList.js new file mode 100644 index 00000000000000..61519bca4dc866 --- /dev/null +++ b/packages/virtualized-lists/Lists/VirtualizedSectionList.js @@ -0,0 +1,617 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +import type {ViewToken} from './ViewabilityHelper'; + +import {View} from 'react-native'; +import VirtualizedList from './VirtualizedList'; +import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; +import invariant from 'invariant'; +import * as React from 'react'; + +type Item = any; + +export type SectionBase = { + /** + * The data for rendering items in this section. + */ + data: $ReadOnlyArray, + /** + * Optional key to keep track of section re-ordering. If you don't plan on re-ordering sections, + * the array index will be used by default. + */ + key?: string, + // Optional props will override list-wide props just for this section. + renderItem?: ?(info: { + item: SectionItemT, + index: number, + section: SectionBase, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... + }, + ... + }) => null | React.Element, + ItemSeparatorComponent?: ?React.ComponentType, + keyExtractor?: (item: SectionItemT, index?: ?number) => string, + ... +}; + +type RequiredProps> = {| + sections: $ReadOnlyArray, +|}; + +type OptionalProps> = {| + /** + * Default renderer for every item in every section. + */ + renderItem?: (info: { + item: Item, + index: number, + section: SectionT, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... + }, + ... + }) => null | React.Element, + /** + * Rendered at the top of each section. These stick to the top of the `ScrollView` by default on + * iOS. See `stickySectionHeadersEnabled`. + */ + renderSectionHeader?: ?(info: { + section: SectionT, + ... + }) => null | React.Element, + /** + * Rendered at the bottom of each section. + */ + renderSectionFooter?: ?(info: { + section: SectionT, + ... + }) => null | React.Element, + /** + * Rendered at the top and bottom of each section (note this is different from + * `ItemSeparatorComponent` which is only rendered between items). These are intended to separate + * sections from the headers above and below and typically have the same highlight response as + * `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`, + * and any custom props from `separators.updateProps`. + */ + SectionSeparatorComponent?: ?React.ComponentType, + /** + * Makes section headers stick to the top of the screen until the next one pushes it off. Only + * enabled by default on iOS because that is the platform standard there. + */ + stickySectionHeadersEnabled?: boolean, + onEndReached?: ?({distanceFromEnd: number, ...}) => void, +|}; + +type VirtualizedListProps = React.ElementConfig; + +export type Props = {| + ...RequiredProps, + ...OptionalProps, + ...$Diff< + VirtualizedListProps, + { + renderItem: $PropertyType, + data: $PropertyType, + ... + }, + >, +|}; +export type ScrollToLocationParamsType = {| + animated?: ?boolean, + itemIndex: number, + sectionIndex: number, + viewOffset?: number, + viewPosition?: number, +|}; + +type State = {childProps: VirtualizedListProps, ...}; + +/** + * Right now this just flattens everything into one list and uses VirtualizedList under the + * hood. The only operation that might not scale well is concatting the data arrays of all the + * sections when new props are received, which should be plenty fast for up to ~10,000 items. + */ +class VirtualizedSectionList< + SectionT: SectionBase, +> extends React.PureComponent, State> { + scrollToLocation(params: ScrollToLocationParamsType) { + let index = params.itemIndex; + for (let i = 0; i < params.sectionIndex; i++) { + index += this.props.getItemCount(this.props.sections[i].data) + 2; + } + let viewOffset = params.viewOffset || 0; + if (this._listRef == null) { + return; + } + if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) { + const frame = this._listRef.__getFrameMetricsApprox( + index - params.itemIndex, + this._listRef.props, + ); + viewOffset += frame.length; + } + const toIndexParams = { + ...params, + viewOffset, + index, + }; + // $FlowFixMe[incompatible-use] + this._listRef.scrollToIndex(toIndexParams); + } + + getListRef(): ?React.ElementRef { + return this._listRef; + } + + render(): React.Node { + const { + ItemSeparatorComponent, // don't pass through, rendered with renderItem + SectionSeparatorComponent, + renderItem: _renderItem, + renderSectionFooter, + renderSectionHeader, + sections: _sections, + stickySectionHeadersEnabled, + ...passThroughProps + } = this.props; + + const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0; + + const stickyHeaderIndices = this.props.stickySectionHeadersEnabled + ? ([]: Array) + : undefined; + + let itemCount = 0; + for (const section of this.props.sections) { + // Track the section header indices + if (stickyHeaderIndices != null) { + stickyHeaderIndices.push(itemCount + listHeaderOffset); + } + + // Add two for the section header and footer. + itemCount += 2; + itemCount += this.props.getItemCount(section.data); + } + const renderItem = this._renderItem(itemCount); + + return ( + + this._getItem(this.props, sections, index) + } + getItemCount={() => itemCount} + onViewableItemsChanged={ + this.props.onViewableItemsChanged + ? this._onViewableItemsChanged + : undefined + } + ref={this._captureRef} + /> + ); + } + + _getItem( + props: Props, + sections: ?$ReadOnlyArray, + index: number, + ): ?Item { + if (!sections) { + return null; + } + let itemIdx = index - 1; + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + const sectionData = section.data; + const itemCount = props.getItemCount(sectionData); + if (itemIdx === -1 || itemIdx === itemCount) { + // We intend for there to be overflow by one on both ends of the list. + // This will be for headers and footers. When returning a header or footer + // item the section itself is the item. + return section; + } else if (itemIdx < itemCount) { + // If we are in the bounds of the list's data then return the item. + return props.getItem(sectionData, itemIdx); + } else { + itemIdx -= itemCount + 2; // Add two for the header and footer + } + } + return null; + } + + // $FlowFixMe[missing-local-annot] + _keyExtractor = (item: Item, index: number) => { + const info = this._subExtractor(index); + return (info && info.key) || String(index); + }; + + _subExtractor(index: number): ?{ + section: SectionT, + // Key of the section or combined key for section + item + key: string, + // Relative index within the section + index: ?number, + // True if this is the section header + header?: ?boolean, + leadingItem?: ?Item, + leadingSection?: ?SectionT, + trailingItem?: ?Item, + trailingSection?: ?SectionT, + ... + } { + let itemIndex = index; + const {getItem, getItemCount, keyExtractor, sections} = this.props; + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + const sectionData = section.data; + const key = section.key || String(i); + itemIndex -= 1; // The section adds an item for the header + if (itemIndex >= getItemCount(sectionData) + 1) { + itemIndex -= getItemCount(sectionData) + 1; // The section adds an item for the footer. + } else if (itemIndex === -1) { + return { + section, + key: key + ':header', + index: null, + header: true, + trailingSection: sections[i + 1], + }; + } else if (itemIndex === getItemCount(sectionData)) { + return { + section, + key: key + ':footer', + index: null, + header: false, + trailingSection: sections[i + 1], + }; + } else { + const extractor = + section.keyExtractor || keyExtractor || defaultKeyExtractor; + return { + section, + key: + key + ':' + extractor(getItem(sectionData, itemIndex), itemIndex), + index: itemIndex, + leadingItem: getItem(sectionData, itemIndex - 1), + leadingSection: sections[i - 1], + trailingItem: getItem(sectionData, itemIndex + 1), + trailingSection: sections[i + 1], + }; + } + } + } + + _convertViewable = (viewable: ViewToken): ?ViewToken => { + invariant(viewable.index != null, 'Received a broken ViewToken'); + const info = this._subExtractor(viewable.index); + if (!info) { + return null; + } + const keyExtractorWithNullableIndex = info.section.keyExtractor; + const keyExtractorWithNonNullableIndex = + this.props.keyExtractor || defaultKeyExtractor; + const key = + keyExtractorWithNullableIndex != null + ? keyExtractorWithNullableIndex(viewable.item, info.index) + : keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0); + + return { + ...viewable, + index: info.index, + key, + section: info.section, + }; + }; + + _onViewableItemsChanged = ({ + viewableItems, + changed, + }: { + viewableItems: Array, + changed: Array, + ... + }) => { + const onViewableItemsChanged = this.props.onViewableItemsChanged; + if (onViewableItemsChanged != null) { + onViewableItemsChanged({ + viewableItems: viewableItems + .map(this._convertViewable, this) + .filter(Boolean), + changed: changed.map(this._convertViewable, this).filter(Boolean), + }); + } + }; + + _renderItem = + (listItemCount: number): $FlowFixMe => + // eslint-disable-next-line react/no-unstable-nested-components + ({item, index}: {item: Item, index: number, ...}) => { + const info = this._subExtractor(index); + if (!info) { + return null; + } + const infoIndex = info.index; + if (infoIndex == null) { + const {section} = info; + if (info.header === true) { + const {renderSectionHeader} = this.props; + return renderSectionHeader ? renderSectionHeader({section}) : null; + } else { + const {renderSectionFooter} = this.props; + return renderSectionFooter ? renderSectionFooter({section}) : null; + } + } else { + const renderItem = info.section.renderItem || this.props.renderItem; + const SeparatorComponent = this._getSeparatorComponent( + index, + info, + listItemCount, + ); + invariant(renderItem, 'no renderItem!'); + return ( + + ); + } + }; + + _updatePropsFor = (cellKey: string, value: any) => { + const updateProps = this._updatePropsMap[cellKey]; + if (updateProps != null) { + updateProps(value); + } + }; + + _updateHighlightFor = (cellKey: string, value: boolean) => { + const updateHighlight = this._updateHighlightMap[cellKey]; + if (updateHighlight != null) { + updateHighlight(value); + } + }; + + _setUpdateHighlightFor = ( + cellKey: string, + updateHighlightFn: ?(boolean) => void, + ) => { + if (updateHighlightFn != null) { + this._updateHighlightMap[cellKey] = updateHighlightFn; + } else { + // $FlowFixMe[prop-missing] + delete this._updateHighlightFor[cellKey]; + } + }; + + _setUpdatePropsFor = (cellKey: string, updatePropsFn: ?(boolean) => void) => { + if (updatePropsFn != null) { + this._updatePropsMap[cellKey] = updatePropsFn; + } else { + delete this._updatePropsMap[cellKey]; + } + }; + + _getSeparatorComponent( + index: number, + info?: ?Object, + listItemCount: number, + ): ?React.ComponentType { + info = info || this._subExtractor(index); + if (!info) { + return null; + } + const ItemSeparatorComponent = + info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; + const {SectionSeparatorComponent} = this.props; + const isLastItemInList = index === listItemCount - 1; + const isLastItemInSection = + info.index === this.props.getItemCount(info.section.data) - 1; + if (SectionSeparatorComponent && isLastItemInSection) { + return SectionSeparatorComponent; + } + if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { + return ItemSeparatorComponent; + } + return null; + } + + _updateHighlightMap: {[string]: (boolean) => void} = {}; + _updatePropsMap: {[string]: void | (boolean => void)} = {}; + _listRef: ?React.ElementRef; + _captureRef = (ref: null | React$ElementRef>) => { + this._listRef = ref; + }; +} + +type ItemWithSeparatorCommonProps = $ReadOnly<{| + leadingItem: ?Item, + leadingSection: ?Object, + section: Object, + trailingItem: ?Item, + trailingSection: ?Object, +|}>; + +type ItemWithSeparatorProps = $ReadOnly<{| + ...ItemWithSeparatorCommonProps, + LeadingSeparatorComponent: ?React.ComponentType, + SeparatorComponent: ?React.ComponentType, + cellKey: string, + index: number, + item: Item, + setSelfHighlightCallback: ( + cellKey: string, + updateFn: ?(boolean) => void, + ) => void, + setSelfUpdatePropsCallback: ( + cellKey: string, + updateFn: ?(boolean) => void, + ) => void, + prevCellKey?: ?string, + updateHighlightFor: (prevCellKey: string, value: boolean) => void, + updatePropsFor: (prevCellKey: string, value: Object) => void, + renderItem: Function, + inverted: boolean, +|}>; + +function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node { + const { + LeadingSeparatorComponent, + // this is the trailing separator and is associated with this item + SeparatorComponent, + cellKey, + prevCellKey, + setSelfHighlightCallback, + updateHighlightFor, + setSelfUpdatePropsCallback, + updatePropsFor, + item, + index, + section, + inverted, + } = props; + + const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] = + React.useState(false); + + const [separatorHighlighted, setSeparatorHighlighted] = React.useState(false); + + const [leadingSeparatorProps, setLeadingSeparatorProps] = React.useState({ + leadingItem: props.leadingItem, + leadingSection: props.leadingSection, + section: props.section, + trailingItem: props.item, + trailingSection: props.trailingSection, + }); + const [separatorProps, setSeparatorProps] = React.useState({ + leadingItem: props.item, + leadingSection: props.leadingSection, + section: props.section, + trailingItem: props.trailingItem, + trailingSection: props.trailingSection, + }); + + React.useEffect(() => { + setSelfHighlightCallback(cellKey, setSeparatorHighlighted); + // $FlowFixMe[incompatible-call] + setSelfUpdatePropsCallback(cellKey, setSeparatorProps); + + return () => { + setSelfUpdatePropsCallback(cellKey, null); + setSelfHighlightCallback(cellKey, null); + }; + }, [ + cellKey, + setSelfHighlightCallback, + setSeparatorProps, + setSelfUpdatePropsCallback, + ]); + + const separators = { + highlight: () => { + setLeadingSeparatorHighlighted(true); + setSeparatorHighlighted(true); + if (prevCellKey != null) { + updateHighlightFor(prevCellKey, true); + } + }, + unhighlight: () => { + setLeadingSeparatorHighlighted(false); + setSeparatorHighlighted(false); + if (prevCellKey != null) { + updateHighlightFor(prevCellKey, false); + } + }, + updateProps: ( + select: 'leading' | 'trailing', + newProps: $Shape, + ) => { + if (select === 'leading') { + if (LeadingSeparatorComponent != null) { + setLeadingSeparatorProps({...leadingSeparatorProps, ...newProps}); + } else if (prevCellKey != null) { + // update the previous item's separator + updatePropsFor(prevCellKey, {...leadingSeparatorProps, ...newProps}); + } + } else if (select === 'trailing' && SeparatorComponent != null) { + setSeparatorProps({...separatorProps, ...newProps}); + } + }, + }; + const element = props.renderItem({ + item, + index, + section, + separators, + }); + const leadingSeparator = LeadingSeparatorComponent != null && ( + + ); + const separator = SeparatorComponent != null && ( + + ); + return leadingSeparator || separator ? ( + + {inverted === false ? leadingSeparator : separator} + {element} + {inverted === false ? separator : leadingSeparator} + + ) : ( + element + ); +} + +/* $FlowFixMe[class-object-subtyping] added when improving typing for this + * parameters */ +// $FlowFixMe[method-unbinding] +module.exports = (VirtualizedSectionList: React.AbstractComponent< + React.ElementConfig, + $ReadOnly<{ + getListRef: () => ?React.ElementRef, + scrollToLocation: (params: ScrollToLocationParamsType) => void, + ... + }>, +>); diff --git a/Libraries/Lists/__tests__/CellRenderMask-test.js b/packages/virtualized-lists/Lists/__tests__/CellRenderMask-test.js similarity index 100% rename from Libraries/Lists/__tests__/CellRenderMask-test.js rename to packages/virtualized-lists/Lists/__tests__/CellRenderMask-test.js diff --git a/Libraries/Lists/__tests__/FillRateHelper-test.js b/packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js similarity index 100% rename from Libraries/Lists/__tests__/FillRateHelper-test.js rename to packages/virtualized-lists/Lists/__tests__/FillRateHelper-test.js diff --git a/Libraries/Lists/__tests__/ViewabilityHelper-test.js b/packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js similarity index 100% rename from Libraries/Lists/__tests__/ViewabilityHelper-test.js rename to packages/virtualized-lists/Lists/__tests__/ViewabilityHelper-test.js diff --git a/Libraries/Lists/__tests__/VirtualizeUtils-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js similarity index 100% rename from Libraries/Lists/__tests__/VirtualizeUtils-test.js rename to packages/virtualized-lists/Lists/__tests__/VirtualizeUtils-test.js diff --git a/Libraries/Lists/__tests__/VirtualizedList-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js similarity index 100% rename from Libraries/Lists/__tests__/VirtualizedList-test.js rename to packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js diff --git a/Libraries/Lists/__tests__/VirtualizedSectionList-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js similarity index 100% rename from Libraries/Lists/__tests__/VirtualizedSectionList-test.js rename to packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js diff --git a/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap b/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap similarity index 100% rename from Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap rename to packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap diff --git a/Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap b/packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap similarity index 100% rename from Libraries/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap rename to packages/virtualized-lists/Lists/__tests__/__snapshots__/VirtualizedSectionList-test.js.snap diff --git a/Libraries/Utilities/__tests__/clamp-test.js b/packages/virtualized-lists/Utilities/__tests__/clamp-test.js similarity index 100% rename from Libraries/Utilities/__tests__/clamp-test.js rename to packages/virtualized-lists/Utilities/__tests__/clamp-test.js diff --git a/Libraries/Utilities/clamp.js b/packages/virtualized-lists/Utilities/clamp.js similarity index 100% rename from Libraries/Utilities/clamp.js rename to packages/virtualized-lists/Utilities/clamp.js diff --git a/packages/virtualized-lists/Utilities/infoLog.js b/packages/virtualized-lists/Utilities/infoLog.js new file mode 100644 index 00000000000000..6cb6df8d414971 --- /dev/null +++ b/packages/virtualized-lists/Utilities/infoLog.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +'use strict'; + +/** + * Intentional info-level logging for clear separation from ad-hoc console debug logging. + */ +function infoLog(...args: Array): void { + return console.log(...args); +} + +module.exports = infoLog; diff --git a/packages/virtualized-lists/index.d.ts b/packages/virtualized-lists/index.d.ts new file mode 100644 index 00000000000000..c66fc20521e439 --- /dev/null +++ b/packages/virtualized-lists/index.d.ts @@ -0,0 +1,10 @@ +/** + * 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 + */ + +export * from './Lists/VirtualizedList'; diff --git a/packages/virtualized-lists/index.js b/packages/virtualized-lists/index.js new file mode 100644 index 00000000000000..8516f7ccf73289 --- /dev/null +++ b/packages/virtualized-lists/index.js @@ -0,0 +1,57 @@ +/** + * 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 + */ + +'use strict'; + +import {keyExtractor} from './Lists/VirtualizeUtils'; + +import typeof VirtualizedList from './Lists/VirtualizedList'; +import typeof VirtualizedSectionList from './Lists/VirtualizedSectionList'; +import {typeof VirtualizedListContextResetter} from './Lists/VirtualizedListContext'; +import typeof ViewabilityHelper from './Lists/ViewabilityHelper'; +import typeof FillRateHelper from './Lists/FillRateHelper'; + +export type { + ViewToken, + ViewabilityConfig, + ViewabilityConfigCallbackPair, +} from './Lists/ViewabilityHelper'; +export type { + RenderItemProps, + RenderItemType, + Separators, +} from './Lists/VirtualizedListProps'; +export type { + Props as VirtualizedSectionListProps, + ScrollToLocationParamsType, + SectionBase, +} from './Lists/VirtualizedSectionList'; +export type {FillRateInfo} from './Lists/FillRateHelper'; + +module.exports = { + keyExtractor, + + get VirtualizedList(): VirtualizedList { + return require('./Lists/VirtualizedList'); + }, + get VirtualizedSectionList(): VirtualizedSectionList { + return require('./Lists/VirtualizedSectionList'); + }, + get VirtualizedListContextResetter(): VirtualizedListContextResetter { + const VirtualizedListContext = require('./Lists/VirtualizedListContext'); + return VirtualizedListContext.VirtualizedListContextResetter; + }, + get ViewabilityHelper(): ViewabilityHelper { + return require('./Lists/ViewabilityHelper'); + }, + get FillRateHelper(): FillRateHelper { + return require('./Lists/FillRateHelper'); + }, +}; diff --git a/packages/virtualized-lists/package.json b/packages/virtualized-lists/package.json new file mode 100644 index 00000000000000..8b19cd2c199f7a --- /dev/null +++ b/packages/virtualized-lists/package.json @@ -0,0 +1,21 @@ +{ + "name": "@react-native/virtualized-lists", + "version": "0.72.0", + "description": "Virtualized lists for React Native.", + "repository": { + "type": "git", + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/virtualized-lists" + }, + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "devDependencies": { + "react-test-renderer": "18.2.0" + }, + "peerDependencies": { + "react-native": "*", + "react-test-renderer": "18.2.0" + } +} diff --git a/types/index.d.ts b/types/index.d.ts index 97b34d5d2321db..349efc65a9fc8c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -115,7 +115,7 @@ export * from '../Libraries/LayoutAnimation/LayoutAnimation'; export * from '../Libraries/Linking/Linking'; export * from '../Libraries/Lists/FlatList'; export * from '../Libraries/Lists/SectionList'; -export * from '../Libraries/Lists/VirtualizedList'; +export * from '@react-native/virtualized-lists'; export * from '../Libraries/LogBox/LogBox'; export * from '../Libraries/Modal/Modal'; export * as Systrace from '../Libraries/Performance/Systrace'; From ecf967a51d358f3759acbf7795675f52d7190280 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 7 Feb 2023 02:54:06 -0800 Subject: [PATCH 51/65] Add fabric support for maintainVisibleContentPosition on iOS (#35319) Summary: This adds support for the `maintainVisibleContentPosition` in iOS fabric. This was previously only implemented in the old renderer. The implementation is very similar to what we currently have. ## Changelog [iOS] [Added] - Add fabric support for maintainVisibleContentPosition on iOS Pull Request resolved: https://github.com/facebook/react-native/pull/35319 Test Plan: Test in RN tester example. https://user-images.githubusercontent.com/2677334/201484543-f7944e34-6cb7-48d6-aa28-e2a7ccdfa666.mov Reviewed By: sammy-SC Differential Revision: D41273822 Pulled By: jacdebug fbshipit-source-id: 7900898f28280ff01619a4af609d2a37437c7240 --- .../ScrollView/RCTScrollViewComponentView.mm | 96 +++++++++++++++++++ .../components/scrollview/ScrollViewProps.cpp | 14 +++ .../components/scrollview/ScrollViewProps.h | 4 + .../components/scrollview/conversions.h | 35 +++++++ .../components/scrollview/primitives.h | 17 ++++ .../renderer/debug/DebugStringConvertible.h | 9 ++ .../examples/ScrollView/ScrollViewExample.js | 2 +- 7 files changed, 176 insertions(+), 1 deletion(-) diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 8c95cdbc0b9e18..1a243af970120f 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -22,6 +22,7 @@ #import "RCTConversions.h" #import "RCTEnhancedScrollView.h" #import "RCTFabricComponentsPlugins.h" +#import "RCTPullToRefreshViewComponentView.h" using namespace facebook::react; @@ -99,6 +100,11 @@ @implementation RCTScrollViewComponentView { BOOL _shouldUpdateContentInsetAdjustmentBehavior; CGPoint _contentOffsetWhenClipped; + + __weak UIView *_contentView; + + CGRect _prevFirstVisibleFrame; + __weak UIView *_firstVisibleView; } + (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view @@ -148,10 +154,17 @@ - (void)dealloc #pragma mark - RCTMountingTransactionObserving +- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction + withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry +{ + [self _prepareForMaintainVisibleScrollPosition]; +} + - (void)mountingTransactionDidMount:(MountingTransaction const &)transaction withSurfaceTelemetry:(facebook::react::SurfaceTelemetry const &)surfaceTelemetry { [self _remountChildren]; + [self _adjustForMaintainVisibleContentPosition]; } #pragma mark - RCTComponentViewProtocol @@ -336,11 +349,23 @@ - (void)_preserveContentOffsetIfNeededWithBlock:(void (^)())block - (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { [_containerView insertSubview:childComponentView atIndex:index]; + if ([childComponentView isKindOfClass:RCTPullToRefreshViewComponentView.class]) { + // Ignore the pull to refresh component. + } else { + RCTAssert(_contentView == nil, @"RCTScrollView may only contain a single subview."); + _contentView = childComponentView; + } } - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { [childComponentView removeFromSuperview]; + if ([childComponentView isKindOfClass:RCTPullToRefreshViewComponentView.class]) { + // Ignore the pull to refresh component. + } else { + RCTAssert(_contentView == childComponentView, @"Attempted to remove non-existent subview"); + _contentView = nil; + } } /* @@ -403,6 +428,9 @@ - (void)prepareForRecycle CGRect oldFrame = self.frame; self.frame = CGRectZero; self.frame = oldFrame; + _contentView = nil; + _prevFirstVisibleFrame = CGRectZero; + _firstVisibleView = nil; [super prepareForRecycle]; } @@ -683,6 +711,74 @@ - (void)removeScrollListener:(NSObject *)scrollListener [self.scrollViewDelegateSplitter removeDelegate:scrollListener]; } +#pragma mark - Maintain visible content position + +- (void)_prepareForMaintainVisibleScrollPosition +{ + const auto &props = *std::static_pointer_cast(_props); + if (!props.maintainVisibleContentPosition) { + return; + } + + BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width; + int minIdx = props.maintainVisibleContentPosition.value().minIndexForVisible; + for (NSUInteger ii = minIdx; ii < _contentView.subviews.count; ++ii) { + // Find the first entirely visible view. + UIView *subview = _contentView.subviews[ii]; + BOOL hasNewView = NO; + if (horizontal) { + hasNewView = subview.frame.origin.x > _scrollView.contentOffset.x; + } else { + hasNewView = subview.frame.origin.y > _scrollView.contentOffset.y; + } + if (hasNewView || ii == _contentView.subviews.count - 1) { + _prevFirstVisibleFrame = subview.frame; + _firstVisibleView = subview; + break; + } + } +} + +- (void)_adjustForMaintainVisibleContentPosition +{ + const auto &props = *std::static_pointer_cast(_props); + if (!props.maintainVisibleContentPosition) { + return; + } + + std::optional autoscrollThreshold = props.maintainVisibleContentPosition.value().autoscrollToTopThreshold; + BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width; + // TODO: detect and handle/ignore re-ordering + if (horizontal) { + CGFloat deltaX = _firstVisibleView.frame.origin.x - _prevFirstVisibleFrame.origin.x; + if (ABS(deltaX) > 0.5) { + CGFloat x = _scrollView.contentOffset.x; + [self _forceDispatchNextScrollEvent]; + _scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x + deltaX, _scrollView.contentOffset.y); + if (autoscrollThreshold) { + // If the offset WAS within the threshold of the start, animate to the start. + if (x <= autoscrollThreshold.value()) { + [self scrollToOffset:CGPointMake(0, _scrollView.contentOffset.y) animated:YES]; + } + } + } + } else { + CGRect newFrame = _firstVisibleView.frame; + CGFloat deltaY = newFrame.origin.y - _prevFirstVisibleFrame.origin.y; + if (ABS(deltaY) > 0.5) { + CGFloat y = _scrollView.contentOffset.y; + [self _forceDispatchNextScrollEvent]; + _scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x, _scrollView.contentOffset.y + deltaY); + if (autoscrollThreshold) { + // If the offset WAS within the threshold of the start, animate to the start. + if (y <= autoscrollThreshold.value()) { + [self scrollToOffset:CGPointMake(_scrollView.contentOffset.x, 0) animated:YES]; + } + } + } + } +} + @end Class RCTScrollViewCls(void) diff --git a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp index 4f0d53929c9602..ef2cee93e9f0e6 100644 --- a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp +++ b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp @@ -127,6 +127,15 @@ ScrollViewProps::ScrollViewProps( "keyboardDismissMode", sourceProps.keyboardDismissMode, {})), + maintainVisibleContentPosition( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.maintainVisibleContentPosition + : convertRawProp( + context, + rawProps, + "maintainVisibleContentPosition", + sourceProps.maintainVisibleContentPosition, + {})), maximumZoomScale( CoreFeatures::enablePropIteratorSetter ? sourceProps.maximumZoomScale @@ -337,6 +346,7 @@ void ScrollViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(directionalLockEnabled); RAW_SET_PROP_SWITCH_CASE_BASIC(indicatorStyle); RAW_SET_PROP_SWITCH_CASE_BASIC(keyboardDismissMode); + RAW_SET_PROP_SWITCH_CASE_BASIC(maintainVisibleContentPosition); RAW_SET_PROP_SWITCH_CASE_BASIC(maximumZoomScale); RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale); RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled); @@ -413,6 +423,10 @@ SharedDebugStringConvertibleList ScrollViewProps::getDebugProps() const { "keyboardDismissMode", keyboardDismissMode, defaultScrollViewProps.keyboardDismissMode), + debugStringConvertibleItem( + "maintainVisibleContentPosition", + maintainVisibleContentPosition, + defaultScrollViewProps.maintainVisibleContentPosition), debugStringConvertibleItem( "maximumZoomScale", maximumZoomScale, diff --git a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.h b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.h index bd53bc4bb22def..c76c99edddab6f 100644 --- a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.h +++ b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.h @@ -11,6 +11,8 @@ #include #include +#include + namespace facebook { namespace react { @@ -43,6 +45,8 @@ class ScrollViewProps final : public ViewProps { bool directionalLockEnabled{}; ScrollViewIndicatorStyle indicatorStyle{}; ScrollViewKeyboardDismissMode keyboardDismissMode{}; + std::optional + maintainVisibleContentPosition{}; Float maximumZoomScale{1.0f}; Float minimumZoomScale{1.0f}; bool scrollEnabled{true}; diff --git a/ReactCommon/react/renderer/components/scrollview/conversions.h b/ReactCommon/react/renderer/components/scrollview/conversions.h index 4605f08ea203dd..3c888b4813268b 100644 --- a/ReactCommon/react/renderer/components/scrollview/conversions.h +++ b/ReactCommon/react/renderer/components/scrollview/conversions.h @@ -10,6 +10,7 @@ #include #include #include +#include namespace facebook { namespace react { @@ -98,6 +99,26 @@ inline void fromRawValue( abort(); } +inline void fromRawValue( + const PropsParserContext &context, + const RawValue &value, + ScrollViewMaintainVisibleContentPosition &result) { + auto map = (butter::map)value; + + auto minIndexForVisible = map.find("minIndexForVisible"); + if (minIndexForVisible != map.end()) { + fromRawValue( + context, minIndexForVisible->second, result.minIndexForVisible); + } + auto autoscrollToTopThreshold = map.find("autoscrollToTopThreshold"); + if (autoscrollToTopThreshold != map.end()) { + fromRawValue( + context, + autoscrollToTopThreshold->second, + result.autoscrollToTopThreshold); + } +} + inline std::string toString(const ScrollViewSnapToAlignment &value) { switch (value) { case ScrollViewSnapToAlignment::Start: @@ -109,6 +130,8 @@ inline std::string toString(const ScrollViewSnapToAlignment &value) { } } +#if RN_DEBUG_STRING_CONVERTIBLE + inline std::string toString(const ScrollViewIndicatorStyle &value) { switch (value) { case ScrollViewIndicatorStyle::Default: @@ -144,5 +167,17 @@ inline std::string toString(const ContentInsetAdjustmentBehavior &value) { } } +inline std::string toString( + const std::optional &value) { + if (!value) { + return "null"; + } + return "{minIndexForVisible: " + toString(value.value().minIndexForVisible) + + ", autoscrollToTopThreshold: " + + toString(value.value().autoscrollToTopThreshold) + "}"; +} + +#endif + } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/components/scrollview/primitives.h b/ReactCommon/react/renderer/components/scrollview/primitives.h index fe8a60e21d7c0d..e627d767fa2958 100644 --- a/ReactCommon/react/renderer/components/scrollview/primitives.h +++ b/ReactCommon/react/renderer/components/scrollview/primitives.h @@ -7,6 +7,8 @@ #pragma once +#include + namespace facebook { namespace react { @@ -23,5 +25,20 @@ enum class ContentInsetAdjustmentBehavior { Always }; +class ScrollViewMaintainVisibleContentPosition final { + public: + int minIndexForVisible{0}; + std::optional autoscrollToTopThreshold{}; + + bool operator==(const ScrollViewMaintainVisibleContentPosition &rhs) const { + return std::tie(this->minIndexForVisible, this->autoscrollToTopThreshold) == + std::tie(rhs.minIndexForVisible, rhs.autoscrollToTopThreshold); + } + + bool operator!=(const ScrollViewMaintainVisibleContentPosition &rhs) const { + return !(*this == rhs); + } +}; + } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/debug/DebugStringConvertible.h b/ReactCommon/react/renderer/debug/DebugStringConvertible.h index a9a1ef02b4e349..7df17f01e39aec 100644 --- a/ReactCommon/react/renderer/debug/DebugStringConvertible.h +++ b/ReactCommon/react/renderer/debug/DebugStringConvertible.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -98,6 +99,14 @@ std::string toString(float const &value); std::string toString(double const &value); std::string toString(void const *value); +template +std::string toString(const std::optional &value) { + if (!value) { + return "null"; + } + return toString(value.value()); +} + /* * *Informal* `DebugStringConvertible` interface. * diff --git a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js index ec3b0d7a461446..a7d7a8f8258812 100644 --- a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js +++ b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js @@ -76,7 +76,7 @@ class AppendingList extends React.Component< Date: Tue, 7 Feb 2023 03:45:17 -0800 Subject: [PATCH 52/65] fix(script): handle patch versions after the .0 for set version (#36020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: A small backport to main of a local fix done in 0.71 to account for the logic for releases 0.Y.1,2,3-prerelease (meaning, not just strictly 0). I could have done like the other logics and just remove the check for patch, but decided to at least make sure it's a digit 😅 ## Changelog [INTERNAL] [FIXED] - handle patch versions after the .0 for set version Pull Request resolved: https://github.com/facebook/react-native/pull/36020 Test Plan: Tested in 0.71-stable, without it we can't test RNTestProject. Reviewed By: jacdebug, cortinico Differential Revision: D42924375 Pulled By: cipolleschi fbshipit-source-id: b003d884cc45a2602adbc14fa8b66d3f1e0c94a6 --- scripts/__tests__/version-utils-test.js | 27 ------------------------- scripts/version-utils.js | 2 +- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/scripts/__tests__/version-utils-test.js b/scripts/__tests__/version-utils-test.js index da5b67402a4810..ad6d958ebff313 100644 --- a/scripts/__tests__/version-utils-test.js +++ b/scripts/__tests__/version-utils-test.js @@ -141,15 +141,6 @@ describe('version-utils', () => { expect(prerelease).toBe('rc.4'); }); - it('should reject pre-release version with patch != 0', () => { - function testInvalidVersion() { - parseVersion('0.66.3-rc.4', 'release'); - } - expect(testInvalidVersion).toThrowErrorMatchingInlineSnapshot( - `"Version 0.66.3-rc.4 is not valid for Release"`, - ); - }); - it('should reject pre-release version from tag with random prerelease pattern', () => { function testInvalidVersion() { parseVersion('v0.66.0-something_invalid', 'release'); @@ -233,15 +224,6 @@ describe('version-utils', () => { expect(prerelease).toBe('rc.0'); }); - it('should reject dryrun with prerelease . version with patch different from 0', () => { - function testInvalidFunction() { - parseVersion('0.20.3-rc.0', 'dry-run'); - } - expect(testInvalidFunction).toThrowErrorMatchingInlineSnapshot( - `"Version 0.20.3-rc.0 is not valid for dry-runs"`, - ); - }); - it('should parse dryrun with prerelease - version', () => { const {version, major, minor, patch, prerelease} = parseVersion( '0.20.0-rc-0', @@ -254,15 +236,6 @@ describe('version-utils', () => { expect(prerelease).toBe('rc-0'); }); - it('should reject dryrun with prerelease - version with patch different from 0', () => { - function testInvalidFunction() { - parseVersion('0.20.3-rc-0', 'dry-run'); - } - expect(testInvalidFunction).toThrowErrorMatchingInlineSnapshot( - `"Version 0.20.3-rc-0 is not valid for dry-runs"`, - ); - }); - it('should parse dryrun with main version', () => { const {version, major, minor, patch, prerelease} = parseVersion( '1000.0.0', diff --git a/scripts/version-utils.js b/scripts/version-utils.js index 661eccb97c733f..826050ac0d0f4a 100644 --- a/scripts/version-utils.js +++ b/scripts/version-utils.js @@ -131,7 +131,7 @@ function isStablePrerelease(version) { return ( version.major === '0' && version.minor !== '0' && - version.patch === '0' && + version.patch.match(/^\d+$/) && version.prerelease != null && (version.prerelease.startsWith('rc.') || version.prerelease.startsWith('rc-') || From 9af3c9654ae8e2cb10d49770dd3438aec038fcef Mon Sep 17 00:00:00 2001 From: Xun Sun Date: Tue, 7 Feb 2023 06:16:07 -0800 Subject: [PATCH 53/65] switch from `@types/jest` to `@jest/globals` (#36068) Summary: Originally proposed in https://github.com/react-native-community/discussions-and-proposals/discussions/592 Main changes are: - Explicitly importing the global APIs in `App.test.tsx` - Drop `types/jest` from the devDependency list Benefits of these changes will be: - Keep in line with Jest's recommended practice - Remove a dev-dependency to make dependencies slimmer References: - https://github.com/facebook/jest/pull/13133 - https://github.com/DefinitelyTyped/DefinitelyTyped/pull/62037 ## Changelog [GENERAL] [CHANGED] - Switch from `types/jest` to `jest/globals` for new react-native projects Pull Request resolved: https://github.com/facebook/react-native/pull/36068 Test Plan: 1. Create a new RN project from the modified template 2. Ensure yarn test passes 3. Ensure IDEs do not complain about `App.test.tsx` Reviewed By: cipolleschi Differential Revision: D43080151 Pulled By: cortinico fbshipit-source-id: c9161cb930c78f3a1eaa08ccb6483aa38d52fa3c --- template/__tests__/App.test.tsx | 3 +++ template/package.json | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/template/__tests__/App.test.tsx b/template/__tests__/App.test.tsx index 178476699b6055..3413ac1c403683 100644 --- a/template/__tests__/App.test.tsx +++ b/template/__tests__/App.test.tsx @@ -6,6 +6,9 @@ import 'react-native'; import React from 'react'; import App from '../App'; +// Note: import explicitly to use the types shiped with jest. +import {it} from '@jest/globals'; + // Note: test renderer must be required after react-native. import renderer from 'react-test-renderer'; diff --git a/template/package.json b/template/package.json index 3ac504d528f1e8..cf3b10d19e9e0e 100644 --- a/template/package.json +++ b/template/package.json @@ -19,7 +19,6 @@ "@babel/runtime": "^7.12.5", "@react-native/eslint-config": "^0.72.1", "@tsconfig/react-native": "^2.0.2", - "@types/jest": "^29.2.1", "@types/react": "^18.0.24", "@types/react-test-renderer": "^18.0.0", "babel-jest": "^29.2.1", From 0a4dcb0309fdc8f4529ed7599c4170341b42c9b1 Mon Sep 17 00:00:00 2001 From: Birkir Gudjonsson Date: Tue, 7 Feb 2023 06:23:39 -0800 Subject: [PATCH 54/65] Add Appearance.setColorScheme support (#35989) Summary: Both Android and iOS allow you to set application specific user interface style, which is useful for applications that support both light and dark mode. With the newly added `Appearance.setColorScheme`, you can natively manage the application's user interface style rather than keeping that preference in JavaScript. The benefit is that native dialogs like alert, keyboard, action sheets and more will also be affected by this change. Implemented using Android X [AppCompatDelegate.setDefaultNightMode](https://developer.android.com/reference/androidx/appcompat/app/AppCompatDelegate#setDefaultNightMode(int)) and iOS 13+ [overrideUserInterfaceStyle](https://developer.apple.com/documentation/uikit/uiview/3238086-overrideuserinterfacestyle?language=objc) ## Changelog [GENERAL] [ADDED] - Added `setColorScheme` to `Appearance` module Pull Request resolved: https://github.com/facebook/react-native/pull/35989 Test Plan: This is a void function so testing is rather limited. ```tsx // Lets assume a given device is set to **dark** mode. Appearance.getColorScheme(); // `dark` // Set the app's user interface to `light` Appearance.setColorScheme('light'); Appearance.getColorScheme(); // `light` // Set the app's user interface to `unspecified` Appearance.setColorScheme(null); Appearance.getColorScheme() // `dark` ``` Reviewed By: NickGerleman Differential Revision: D42801094 Pulled By: jacdebug fbshipit-source-id: ede810fe9ee98f313fd3fbbb16b60c84ef8c7204 --- Libraries/Utilities/Appearance.d.ts | 10 +++++++ Libraries/Utilities/Appearance.js | 11 ++++++++ Libraries/Utilities/NativeAppearance.js | 1 + React/CoreModules/RCTAppearance.h | 1 + React/CoreModules/RCTAppearance.mm | 26 +++++++++++++++++++ .../modules/appearance/AppearanceModule.java | 12 +++++++++ .../facebook/react/modules/appearance/BUCK | 1 + .../main/java/com/facebook/react/shell/BUCK | 1 + 8 files changed, 63 insertions(+) diff --git a/Libraries/Utilities/Appearance.d.ts b/Libraries/Utilities/Appearance.d.ts index 7d2faf69d8cec4..2b8428e0d7d1de 100644 --- a/Libraries/Utilities/Appearance.d.ts +++ b/Libraries/Utilities/Appearance.d.ts @@ -28,6 +28,16 @@ export namespace Appearance { */ export function getColorScheme(): ColorSchemeName; + /** + * Set the color scheme preference. This is useful for overriding the default + * color scheme preference for the app. Note that this will not change the + * appearance of the system UI, only the appearance of the app. + * Only available on iOS 13+ and Android 10+. + */ + export function setColorScheme( + scheme: ColorSchemeName | null | undefined, + ): void; + /** * Add an event handler that is fired when appearance preferences change. */ diff --git a/Libraries/Utilities/Appearance.js b/Libraries/Utilities/Appearance.js index 54d03dd18200b2..69817442085124 100644 --- a/Libraries/Utilities/Appearance.js +++ b/Libraries/Utilities/Appearance.js @@ -85,6 +85,17 @@ module.exports = { return nativeColorScheme; }, + setColorScheme(colorScheme: ?ColorSchemeName): void { + const nativeColorScheme = colorScheme == null ? 'unspecified' : colorScheme; + + invariant( + colorScheme === 'dark' || colorScheme === 'light' || colorScheme == null, + "Unrecognized color scheme. Did you mean 'dark', 'light' or null?", + ); + + NativeAppearance?.setColorScheme?.(nativeColorScheme); + }, + /** * Add an event handler that is fired when appearance preferences change. */ diff --git a/Libraries/Utilities/NativeAppearance.js b/Libraries/Utilities/NativeAppearance.js index cb8688f14e2967..786790f5987140 100644 --- a/Libraries/Utilities/NativeAppearance.js +++ b/Libraries/Utilities/NativeAppearance.js @@ -26,6 +26,7 @@ export interface Spec extends TurboModule { // types. /* 'light' | 'dark' */ +getColorScheme: () => ?string; + +setColorScheme?: (colorScheme: string) => void; // RCTEventEmitter +addListener: (eventName: string) => void; diff --git a/React/CoreModules/RCTAppearance.h b/React/CoreModules/RCTAppearance.h index d8bb18b89ac32b..caa842d72f6a1b 100644 --- a/React/CoreModules/RCTAppearance.h +++ b/React/CoreModules/RCTAppearance.h @@ -8,6 +8,7 @@ #import #import +#import #import RCT_EXTERN void RCTEnableAppearancePreference(BOOL enabled); diff --git a/React/CoreModules/RCTAppearance.mm b/React/CoreModules/RCTAppearance.mm index 71259d4a98d119..72257c8fa1bfdc 100644 --- a/React/CoreModules/RCTAppearance.mm +++ b/React/CoreModules/RCTAppearance.mm @@ -10,6 +10,7 @@ #import #import #import +#import #import "CoreModulesPlugins.h" @@ -68,6 +69,20 @@ void RCTOverrideAppearancePreference(NSString *const colorSchemeOverride) return RCTAppearanceColorSchemeLight; } +@implementation RCTConvert (UIUserInterfaceStyle) + +RCT_ENUM_CONVERTER( + UIUserInterfaceStyle, + (@{ + @"light" : @(UIUserInterfaceStyleLight), + @"dark" : @(UIUserInterfaceStyleDark), + @"unspecified" : @(UIUserInterfaceStyleUnspecified) + }), + UIUserInterfaceStyleUnspecified, + integerValue); + +@end + @interface RCTAppearance () @end @@ -92,6 +107,17 @@ - (dispatch_queue_t)methodQueue return std::make_shared(params); } +RCT_EXPORT_METHOD(setColorScheme : (NSString *)style) +{ + UIUserInterfaceStyle userInterfaceStyle = [RCTConvert UIUserInterfaceStyle:style]; + NSArray<__kindof UIWindow *> *windows = RCTSharedApplication().windows; + if (@available(iOS 13.0, *)) { + for (UIWindow *window in windows) { + window.overrideUserInterfaceStyle = userInterfaceStyle; + } + } +} + RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getColorScheme) { if (_currentColorScheme == nil) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java index e64bbe122c6df6..301a8deff1bc05 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java @@ -11,6 +11,7 @@ import android.content.Context; import android.content.res.Configuration; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDelegate; import com.facebook.fbreact.specs.NativeAppearanceSpec; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; @@ -66,6 +67,17 @@ private String colorSchemeForCurrentConfiguration(Context context) { return "light"; } + @Override + public void setColorScheme(String style) { + if (style == "dark") { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } else if (style == "light") { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } else if (style == "unspecified") { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + } + } + @Override public String getColorScheme() { // Attempt to use the Activity context first in order to get the most up to date diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK index 4cb4e081946767..044b4c8d0941a1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK @@ -14,6 +14,7 @@ rn_android_library( deps = [ react_native_dep("third-party/java/jsr-305:jsr-305"), react_native_dep("third-party/android/androidx:annotation"), + react_native_dep("third-party/android/androidx:appcompat"), react_native_target("java/com/facebook/react/bridge:bridge"), react_native_target("java/com/facebook/react/common:common"), react_native_target("java/com/facebook/react/module/annotations:annotations"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index b64122b9d3975e..5621dd31762a68 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -15,6 +15,7 @@ rn_android_library( react_native_dep("libraries/fresco/fresco-react-native:imagepipeline"), react_native_dep("libraries/soloader/java/com/facebook/soloader:soloader"), react_native_dep("third-party/android/androidx:annotation"), + react_native_dep("third-party/android/androidx:appcompat"), react_native_dep("third-party/android/androidx:core"), react_native_dep("third-party/android/androidx:fragment"), react_native_dep("third-party/android/androidx:legacy-support-core-utils"), From f6a4e4f20f0d3b5fe2aad171cded9aba06d3c8f8 Mon Sep 17 00:00:00 2001 From: Raouf Date: Tue, 7 Feb 2023 07:00:14 -0800 Subject: [PATCH 55/65] update `asdf-vm` setup to use `$ASDF_DIR` to work if user defined custom directory (#36043) Summary: Update setup of sourcing `asdf-vm` in `find-node-for-xcode.sh` in case of user has custom defined of `$ASDF_DIR` by default `$ASDF_DIR` point to `$HOME/.asdf`, but if user has custom directory (like XDG convention) this script wont work without this change. ## Changelog [GENERAL] [CHANGED] - Find node binary when using asdf as the node version manager with custom `$ASDF_DIR` Pull Request resolved: https://github.com/facebook/react-native/pull/36043 Test Plan: use a defualt/custom `$ASDF_DIR` while using `asdf-vm` as node version manager then make iOS build. Reviewed By: cortinico Differential Revision: D42990407 Pulled By: cipolleschi fbshipit-source-id: 1fe3fdc786bddf741ff422e7bec55a6c9cc8ed83 --- scripts/find-node-for-xcode.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/find-node-for-xcode.sh b/scripts/find-node-for-xcode.sh index 0a49e25ce6d0b8..e928cd8e31be52 100644 --- a/scripts/find-node-for-xcode.sh +++ b/scripts/find-node-for-xcode.sh @@ -58,7 +58,10 @@ if [[ ! -x node && -d ${HOME}/.anyenv/bin ]]; then fi # Set up asdf-vm if present -if [[ -f "$HOME/.asdf/asdf.sh" ]]; then +if [[ -f "$ASDF_DIR/asdf.sh" ]]; then + # shellcheck source=/dev/null + . "$ASDF_DIR/asdf.sh" +elif [[ -f "$HOME/.asdf/asdf.sh" ]]; then # shellcheck source=/dev/null . "$HOME/.asdf/asdf.sh" elif [[ -x "$(command -v brew)" && -f "$(brew --prefix asdf)/asdf.sh" ]]; then From 2607602b8ab4dde22fab4182f7c4ed9af810189f Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Tue, 7 Feb 2023 07:22:30 -0800 Subject: [PATCH 56/65] Reduce flakyness by not downloading extra packages (#36077) Summary: Yesterday CircleCI was extremely flaky due to us trying to `apt install` extra packages. This mitigates one scenario where we try to redownload `jq` and `shellcheck`. I've moved to use a container which contains those packages already ## Changelog [INTERNAL] - Reduce flakyness by not downloading extra packages Pull Request resolved: https://github.com/facebook/react-native/pull/36077 Test Plan: Will wait for a green CI Reviewed By: cipolleschi Differential Revision: D43080349 Pulled By: cortinico fbshipit-source-id: 6527c5ad129f47d8b5f02bf207e1af67a095afa1 --- .circleci/Dockerfiles/Dockerfile.android | 2 +- .circleci/config.yml | 17 +---------------- package.json | 2 +- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/.circleci/Dockerfiles/Dockerfile.android b/.circleci/Dockerfiles/Dockerfile.android index 6b54b14815ca70..4038101a28bf4f 100644 --- a/.circleci/Dockerfiles/Dockerfile.android +++ b/.circleci/Dockerfiles/Dockerfile.android @@ -14,7 +14,7 @@ # and build a Android application that can be used to run the # tests specified in the scripts/ directory. # -FROM reactnativecommunity/react-native-android:6.2 +FROM reactnativecommunity/react-native-android:7.0 LABEL Description="React Native Android Test Image" LABEL maintainer="Héctor Ramos " diff --git a/.circleci/config.yml b/.circleci/config.yml index c86f1ce8fafa9d..768337f5995bd5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,7 +111,7 @@ executors: reactnativeandroid: <<: *defaults docker: - - image: reactnativecommunity/react-native-android:6.2 + - image: reactnativecommunity/react-native-android:7.0 resource_class: "xlarge" environment: - TERM: "dumb" @@ -448,24 +448,9 @@ jobs: steps: - checkout - run_yarn - - - # Note: The yarn gpg key needs to be refreshed to work around https://github.com/yarnpkg/yarn/issues/7866 - - run: - name: Install additional GitHub bot dependencies - # TEMP: Added workaround from https://github.com/nodesource/distributions/issues/1266#issuecomment-932583579 - command: | - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - - apt update && apt install -y shellcheck jq - apt-get -y install openssl ca-certificates - update-ca-certificates - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - - apt update && apt install -y shellcheck jq - - run: name: Run linters against modified files (analysis-bot) command: GITHUB_TOKEN="$PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A""$PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B" yarn lint-ci - when: always # ------------------------- # JOBS: Analyze Code diff --git a/package.json b/package.json index 225346a4a1d73d..6119bd30689bfd 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "prettier": "prettier --write \"./**/*.{js,md,yml,ts,tsx}\"", "format-check": "prettier --list-different \"./**/*.{js,md,yml,ts,tsx}\"", "update-lock": "npx yarn-deduplicate", - "docker-setup-android": "docker pull reactnativecommunity/react-native-android:6.2", + "docker-setup-android": "docker pull reactnativecommunity/react-native-android:7.0", "docker-build-android": "docker build -t reactnativeci/android -f .circleci/Dockerfiles/Dockerfile.android .", "test-android-run-instrumentation": "docker run --cap-add=SYS_ADMIN -it reactnativeci/android bash .circleci/Dockerfiles/scripts/run-android-docker-instrumentation-tests.sh", "test-android-run-unit": "docker run --cap-add=SYS_ADMIN -it reactnativeci/android bash .circleci/Dockerfiles/scripts/run-android-docker-unit-tests.sh", From bb02ccf13f76f46b8572e2a85d578fd8d4fd9467 Mon Sep 17 00:00:00 2001 From: shivenmian Date: Tue, 7 Feb 2023 07:29:17 -0800 Subject: [PATCH 57/65] RNGP - fix: use relative paths for gradle exec invocations (#36080) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36080 For Android release builds on Windows, gradle release build fails if there are spaces in path (https://github.com/facebook/react-native/issues/34878). This is due to gradle improperly handling arguments with spaces (this is also [an open issue](https://github.com/gradle/gradle/issues/6072) on Gradle). Since the Hermes compilation and other Gradle exec invocations involve arguments which will contain spaces (if there are spaces in your path), this also means it is hard to get around this by simply escaping the spaces (eg: by using double quotes), since these arguments are not properly handled by Gradle itself. As a workaround, this PR uses relative paths for all Gradle commands invoked for Android. As long as there aren't any spaces in the react-native directory structure (i.e this repo), this fix should work. ## Changelog [Android][Fixed] - Used relative paths for gradle commands Pull Request resolved: https://github.com/facebook/react-native/pull/36076 Test Plan: `npx react-native run-android` builds and runs the app successfully on Android device, when run inside an RN0711 project with a path containing spaces (and with the changes in this PR applied) on Windows. This includes release builds (i.e with the `--variant=release` flag). Reviewed By: cipolleschi Differential Revision: D43080177 Pulled By: cortinico fbshipit-source-id: 7625f3502af47e9b28c6fc7dfe1459d7c7f1362d --- .../facebook/react/tasks/BundleHermesCTask.kt | 50 ++++---- .../tasks/GenerateCodegenArtifactsTask.kt | 8 +- .../react/tasks/GenerateCodegenSchemaTask.kt | 8 +- .../kotlin/com/facebook/react/utils/Os.kt | 15 ++- .../com/facebook/react/utils/PathUtils.kt | 5 +- .../react/tasks/BundleHermesCTaskTest.kt | 119 +++++++++++++++++- .../tasks/GenerateCodegenArtifactsTaskTest.kt | 43 ++++++- .../tasks/GenerateCodegenSchemaTaskTest.kt | 43 ++++++- .../kotlin/com/facebook/react/utils/OsTest.kt | 27 ++++ 9 files changed, 279 insertions(+), 39 deletions(-) diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt index 2801d3aea63413..2eb989143b80a2 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt @@ -7,6 +7,7 @@ package com.facebook.react.tasks +import com.facebook.react.utils.Os.cliPath import com.facebook.react.utils.detectOSAwareHermesCommand import com.facebook.react.utils.moveTo import com.facebook.react.utils.windowsAwareCommandLine @@ -135,8 +136,9 @@ abstract class BundleHermesCTask : DefaultTask() { internal fun getBundleCommand(bundleFile: File, sourceMapFile: File): List = windowsAwareCommandLine( buildList { + val rootFile = root.get().asFile addAll(nodeExecutableAndArgs.get()) - add(cliFile.get().asFile.absolutePath) + add(cliFile.get().asFile.cliPath(rootFile)) add(bundleCommand.get()) add("--platform") add("android") @@ -144,16 +146,16 @@ abstract class BundleHermesCTask : DefaultTask() { add(devEnabled.get().toString()) add("--reset-cache") add("--entry-file") - add(entryFile.get().asFile.toString()) + add(entryFile.get().asFile.cliPath(rootFile)) add("--bundle-output") - add(bundleFile.toString()) + add(bundleFile.cliPath(rootFile)) add("--assets-dest") - add(resourcesDir.get().asFile.toString()) + add(resourcesDir.get().asFile.cliPath(rootFile)) add("--sourcemap-output") - add(sourceMapFile.toString()) + add(sourceMapFile.cliPath(rootFile)) if (bundleConfig.isPresent) { add("--config") - add(bundleConfig.get().asFile.absolutePath) + add(bundleConfig.get().asFile.cliPath(rootFile)) } add("--minify") add(minifyEnabled.get().toString()) @@ -165,26 +167,30 @@ abstract class BundleHermesCTask : DefaultTask() { hermesCommand: String, bytecodeFile: File, bundleFile: File - ): List = - windowsAwareCommandLine( - hermesCommand, - "-emit-binary", - "-out", - bytecodeFile.absolutePath, - bundleFile.absolutePath, - *hermesFlags.get().toTypedArray()) + ): List { + val rootFile = root.get().asFile + return windowsAwareCommandLine( + hermesCommand, + "-emit-binary", + "-out", + bytecodeFile.cliPath(rootFile), + bundleFile.cliPath(rootFile), + *hermesFlags.get().toTypedArray()) + } internal fun getComposeSourceMapsCommand( composeScript: File, packagerSourceMap: File, compilerSourceMap: File, outputSourceMap: File - ): List = - windowsAwareCommandLine( - *nodeExecutableAndArgs.get().toTypedArray(), - composeScript.absolutePath, - packagerSourceMap.toString(), - compilerSourceMap.toString(), - "-o", - outputSourceMap.toString()) + ): List { + val rootFile = root.get().asFile + return windowsAwareCommandLine( + *nodeExecutableAndArgs.get().toTypedArray(), + composeScript.cliPath(rootFile), + packagerSourceMap.cliPath(rootFile), + compilerSourceMap.cliPath(rootFile), + "-o", + outputSourceMap.cliPath(rootFile)) + } } diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt index b6003eeb5e14e6..30f92ff7d4749d 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt @@ -8,6 +8,7 @@ package com.facebook.react.tasks import com.facebook.react.utils.JsonUtils +import com.facebook.react.utils.Os.cliPath import com.facebook.react.utils.windowsAwareCommandLine import org.gradle.api.file.Directory import org.gradle.api.file.DirectoryProperty @@ -63,16 +64,17 @@ abstract class GenerateCodegenArtifactsTask : Exec() { } internal fun setupCommandLine(libraryName: String, codegenJavaPackageName: String) { + val workingDir = project.projectDir commandLine( windowsAwareCommandLine( *nodeExecutableAndArgs.get().toTypedArray(), - reactNativeDir.file("scripts/generate-specs-cli.js").get().asFile.absolutePath, + reactNativeDir.file("scripts/generate-specs-cli.js").get().asFile.cliPath(workingDir), "--platform", "android", "--schemaPath", - generatedSchemaFile.get().asFile.absolutePath, + generatedSchemaFile.get().asFile.cliPath(workingDir), "--outputDir", - generatedSrcDir.get().asFile.absolutePath, + generatedSrcDir.get().asFile.cliPath(workingDir), "--libraryName", libraryName, "--javaPackageName", diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTask.kt index d72bc0a379f214..3f724eb5ff222d 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTask.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTask.kt @@ -7,6 +7,7 @@ package com.facebook.react.tasks +import com.facebook.react.utils.Os.cliPath import com.facebook.react.utils.windowsAwareCommandLine import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFile @@ -63,6 +64,7 @@ abstract class GenerateCodegenSchemaTask : Exec() { } internal fun setupCommandLine() { + val workingDir = project.projectDir commandLine( windowsAwareCommandLine( *nodeExecutableAndArgs.get().toTypedArray(), @@ -70,11 +72,11 @@ abstract class GenerateCodegenSchemaTask : Exec() { .file("lib/cli/combine/combine-js-to-schema-cli.js") .get() .asFile - .absolutePath, + .cliPath(workingDir), "--platform", "android", - generatedSchemaFile.get().asFile.absolutePath, - jsRootDir.asFile.get().absolutePath, + generatedSchemaFile.get().asFile.cliPath(workingDir), + jsRootDir.asFile.get().cliPath(workingDir), )) } } diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/Os.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/Os.kt index c4dfad637fc531..4ca00699b84170 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/Os.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/Os.kt @@ -7,7 +7,9 @@ package com.facebook.react.utils -object Os { +import java.io.File + +internal object Os { fun isWindows(): Boolean = System.getProperty("os.name")?.lowercase()?.contains("windows") ?: false @@ -28,4 +30,15 @@ object Os { it } } + + /** + * As Gradle doesn't support well path with spaces on Windows, we need to return relative path on + * Win. On Linux & Mac we'll default to return absolute path. + */ + fun File.cliPath(base: File): String = + if (isWindows()) { + this.relativeTo(base).path + } else { + this.absolutePath + } } diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt index 552a611d5225c0..0710f04b994e07 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt @@ -11,6 +11,7 @@ package com.facebook.react.utils import com.facebook.react.ReactExtension import com.facebook.react.model.ModelPackageJson +import com.facebook.react.utils.Os.cliPath import java.io.File import org.gradle.api.Project @@ -130,7 +131,7 @@ internal fun detectOSAwareHermesCommand(projectRoot: File, hermesCommand: String val builtHermesc = getBuiltHermescFile(projectRoot, System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) if (builtHermesc.exists()) { - return builtHermesc.absolutePath + return builtHermesc.cliPath(projectRoot) } // 3. If the react-native contains a pre-built hermesc, use it. @@ -142,7 +143,7 @@ internal fun detectOSAwareHermesCommand(projectRoot: File, hermesCommand: String val prebuiltHermes = File(projectRoot, prebuiltHermesPath) if (prebuiltHermes.exists()) { - return prebuiltHermes.absolutePath + return prebuiltHermes.cliPath(projectRoot) } error( diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/BundleHermesCTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/BundleHermesCTaskTest.kt index fbe7fed6304946..3e07a78b36e1b4 100644 --- a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/BundleHermesCTaskTest.kt +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/BundleHermesCTaskTest.kt @@ -7,7 +7,9 @@ package com.facebook.react.tasks +import com.facebook.react.tests.OS import com.facebook.react.tests.OsRule +import com.facebook.react.tests.WithOs import com.facebook.react.tests.createTestTask import java.io.File import org.junit.Assert.* @@ -205,6 +207,7 @@ class BundleHermesCTaskTest { val task = createTestTask { it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2")) + it.root.set(tempFolder.root) it.cliFile.set(cliFile) it.bundleCommand.set("bundle") it.devEnabled.set(true) @@ -244,6 +247,60 @@ class BundleHermesCTaskTest { assertEquals(24, bundleCommand.size) } + @Test + @WithOs(OS.WIN) + fun getBundleCommand_onWindows_returnsWinValidCommandsPaths() { + val entryFile = tempFolder.newFile("index.js") + val cliFile = tempFolder.newFile("cli.js") + val bundleFile = tempFolder.newFile("bundle.js") + val sourceMapFile = tempFolder.newFile("bundle.js.map") + val resourcesDir = tempFolder.newFolder("res") + val bundleConfig = tempFolder.newFile("bundle.config") + val task = + createTestTask { + it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2")) + it.root.set(tempFolder.root) + it.cliFile.set(cliFile) + it.bundleCommand.set("bundle") + it.devEnabled.set(true) + it.entryFile.set(entryFile) + it.resourcesDir.set(resourcesDir) + it.bundleConfig.set(bundleConfig) + it.minifyEnabled.set(true) + it.extraPackagerArgs.set(listOf("--read-global-cache")) + } + + val bundleCommand = task.getBundleCommand(bundleFile, sourceMapFile) + + assertEquals("cmd", bundleCommand[0]) + assertEquals("/c", bundleCommand[1]) + assertEquals("node", bundleCommand[2]) + assertEquals("arg1", bundleCommand[3]) + assertEquals("arg2", bundleCommand[4]) + assertEquals(cliFile.relativeTo(tempFolder.root).path, bundleCommand[5]) + assertEquals("bundle", bundleCommand[6]) + assertEquals("--platform", bundleCommand[7]) + assertEquals("android", bundleCommand[8]) + assertEquals("--dev", bundleCommand[9]) + assertEquals("true", bundleCommand[10]) + assertEquals("--reset-cache", bundleCommand[11]) + assertEquals("--entry-file", bundleCommand[12]) + assertEquals(entryFile.relativeTo(tempFolder.root).path, bundleCommand[13]) + assertEquals("--bundle-output", bundleCommand[14]) + assertEquals(bundleFile.relativeTo(tempFolder.root).path, bundleCommand[15]) + assertEquals("--assets-dest", bundleCommand[16]) + assertEquals(resourcesDir.relativeTo(tempFolder.root).path, bundleCommand[17]) + assertEquals("--sourcemap-output", bundleCommand[18]) + assertEquals(sourceMapFile.relativeTo(tempFolder.root).path, bundleCommand[19]) + assertEquals("--config", bundleCommand[20]) + assertEquals(bundleConfig.relativeTo(tempFolder.root).path, bundleCommand[21]) + assertEquals("--minify", bundleCommand[22]) + assertEquals("true", bundleCommand[23]) + assertEquals("--read-global-cache", bundleCommand[24]) + assertEquals("--verbose", bundleCommand[25]) + assertEquals(26, bundleCommand.size) + } + @Test fun getBundleCommand_withoutConfig_returnsCommandWithoutConfig() { val entryFile = tempFolder.newFile("index.js") @@ -254,6 +311,7 @@ class BundleHermesCTaskTest { val task = createTestTask { it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2")) + it.root.set(tempFolder.root) it.cliFile.set(cliFile) it.bundleCommand.set("bundle") it.devEnabled.set(true) @@ -274,7 +332,10 @@ class BundleHermesCTaskTest { val bytecodeFile = tempFolder.newFile("bundle.js.hbc") val bundleFile = tempFolder.newFile("bundle.js") val task = - createTestTask { it.hermesFlags.set(listOf("my-custom-hermes-flag")) } + createTestTask { + it.root.set(tempFolder.root) + it.hermesFlags.set(listOf("my-custom-hermes-flag")) + } val hermesCommand = task.getHermescCommand(customHermesc, bytecodeFile, bundleFile) @@ -287,6 +348,31 @@ class BundleHermesCTaskTest { assertEquals(6, hermesCommand.size) } + @Test + @WithOs(OS.WIN) + fun getHermescCommand_onWindows_returnsRelativePaths() { + val customHermesc = "hermesc" + val bytecodeFile = tempFolder.newFile("bundle.js.hbc") + val bundleFile = tempFolder.newFile("bundle.js") + val task = + createTestTask { + it.root.set(tempFolder.root) + it.hermesFlags.set(listOf("my-custom-hermes-flag")) + } + + val hermesCommand = task.getHermescCommand(customHermesc, bytecodeFile, bundleFile) + + assertEquals("cmd", hermesCommand[0]) + assertEquals("/c", hermesCommand[1]) + assertEquals(customHermesc, hermesCommand[2]) + assertEquals("-emit-binary", hermesCommand[3]) + assertEquals("-out", hermesCommand[4]) + assertEquals(bytecodeFile.relativeTo(tempFolder.root).path, hermesCommand[5]) + assertEquals(bundleFile.relativeTo(tempFolder.root).path, hermesCommand[6]) + assertEquals("my-custom-hermes-flag", hermesCommand[7]) + assertEquals(8, hermesCommand.size) + } + @Test fun getComposeSourceMapsCommand_returnsCorrectCommand() { val packagerMap = tempFolder.newFile("bundle.js.packager.map") @@ -296,6 +382,7 @@ class BundleHermesCTaskTest { val composeSourceMapsFile = File(reactNativeDir, "scripts/compose-source-maps.js") val task = createTestTask { + it.root.set(tempFolder.root) it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2")) } @@ -312,4 +399,34 @@ class BundleHermesCTaskTest { assertEquals(outputMap.absolutePath, composeSourcemapCommand[7]) assertEquals(8, composeSourcemapCommand.size) } + + @Test + @WithOs(OS.WIN) + fun getComposeSourceMapsCommand_onWindows_returnsRelativePaths() { + val packagerMap = tempFolder.newFile("bundle.js.packager.map") + val compilerMap = tempFolder.newFile("bundle.js.compiler.map") + val outputMap = tempFolder.newFile("bundle.js.map") + val reactNativeDir = tempFolder.newFolder("node_modules/react-native") + val composeSourceMapsFile = File(reactNativeDir, "scripts/compose-source-maps.js") + val task = + createTestTask { + it.root.set(tempFolder.root) + it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2")) + } + + val composeSourcemapCommand = + task.getComposeSourceMapsCommand(composeSourceMapsFile, packagerMap, compilerMap, outputMap) + + assertEquals("cmd", composeSourcemapCommand[0]) + assertEquals("/c", composeSourcemapCommand[1]) + assertEquals("node", composeSourcemapCommand[2]) + assertEquals("arg1", composeSourcemapCommand[3]) + assertEquals("arg2", composeSourcemapCommand[4]) + assertEquals(composeSourceMapsFile.relativeTo(tempFolder.root).path, composeSourcemapCommand[5]) + assertEquals(packagerMap.relativeTo(tempFolder.root).path, composeSourcemapCommand[6]) + assertEquals(compilerMap.relativeTo(tempFolder.root).path, composeSourcemapCommand[7]) + assertEquals("-o", composeSourcemapCommand[8]) + assertEquals(outputMap.relativeTo(tempFolder.root).path, composeSourcemapCommand[9]) + assertEquals(10, composeSourcemapCommand.size) + } } diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTaskTest.kt index e188e47c849525..e2bf1ad7b9b4a7 100644 --- a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTaskTest.kt +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTaskTest.kt @@ -7,9 +7,8 @@ package com.facebook.react.tasks -import com.facebook.react.tests.OS -import com.facebook.react.tests.OsRule -import com.facebook.react.tests.WithOs +import com.facebook.react.tests.* +import com.facebook.react.tests.createProject import com.facebook.react.tests.createTestTask import java.io.File import org.junit.Assert.assertEquals @@ -97,6 +96,44 @@ class GenerateCodegenArtifactsTaskTest { task.commandLine.toMutableList()) } + @Test + @WithOs(OS.WIN) + fun setupCommandLine_onWindows_willSetupCorrectly() { + val reactNativeDir = tempFolder.newFolder("node_modules/react-native/") + val outputDir = tempFolder.newFolder("output") + + val project = createProject() + val task = + createTestTask(project) { + it.reactNativeDir.set(reactNativeDir) + it.generatedSrcDir.set(outputDir) + it.nodeExecutableAndArgs.set(listOf("--verbose")) + } + + task.setupCommandLine("example-test", "com.example.test") + + assertEquals( + listOf( + "cmd", + "/c", + "--verbose", + File(reactNativeDir, "scripts/generate-specs-cli.js") + .relativeTo(project.projectDir) + .path, + "--platform", + "android", + "--schemaPath", + File(outputDir, "schema.json").relativeTo(project.projectDir).path, + "--outputDir", + outputDir.relativeTo(project.projectDir).path, + "--libraryName", + "example-test", + "--javaPackageName", + "com.example.test", + ), + task.commandLine.toMutableList()) + } + @Test fun resolveTaskParameters_withConfigInPackageJson_usesIt() { val packageJsonFile = diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTaskTest.kt index 98e2b39795c1cb..abe77f0dd15527 100644 --- a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTaskTest.kt +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTaskTest.kt @@ -7,9 +7,8 @@ package com.facebook.react.tasks -import com.facebook.react.tests.OS -import com.facebook.react.tests.OsRule -import com.facebook.react.tests.WithOs +import com.facebook.react.tests.* +import com.facebook.react.tests.createProject import com.facebook.react.tests.createTestTask import java.io.File import org.junit.Assert.* @@ -140,13 +139,14 @@ class GenerateCodegenSchemaTaskTest { it.codegenDir.set(codegenDir) it.jsRootDir.set(jsRootDir) it.generatedSrcDir.set(outputDir) - it.nodeExecutableAndArgs.set(listOf("--verbose")) + it.nodeExecutableAndArgs.set(listOf("node", "--verbose")) } task.setupCommandLine() assertEquals( listOf( + "node", "--verbose", File(codegenDir, "lib/cli/combine/combine-js-to-schema-cli.js").toString(), "--platform", @@ -156,4 +156,39 @@ class GenerateCodegenSchemaTaskTest { ), task.commandLine.toMutableList()) } + + @Test + @WithOs(OS.WIN) + fun setupCommandLine_onWindows_willSetupCorrectly() { + val codegenDir = tempFolder.newFolder("codegen") + val jsRootDir = tempFolder.newFolder("js") + val outputDir = tempFolder.newFolder("output") + + val project = createProject() + val task = + createTestTask(project) { + it.codegenDir.set(codegenDir) + it.jsRootDir.set(jsRootDir) + it.generatedSrcDir.set(outputDir) + it.nodeExecutableAndArgs.set(listOf("node", "--verbose")) + } + + task.setupCommandLine() + + assertEquals( + listOf( + "cmd", + "/c", + "node", + "--verbose", + File(codegenDir, "lib/cli/combine/combine-js-to-schema-cli.js") + .relativeTo(project.projectDir) + .path, + "--platform", + "android", + File(outputDir, "schema.json").relativeTo(project.projectDir).path, + jsRootDir.relativeTo(project.projectDir).path, + ), + task.commandLine.toMutableList()) + } } diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/OsTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/OsTest.kt index fd37f2474f77e3..b58a2e067f8dc5 100644 --- a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/OsTest.kt +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/OsTest.kt @@ -10,14 +10,17 @@ package com.facebook.react.utils import com.facebook.react.tests.OS import com.facebook.react.tests.OsRule import com.facebook.react.tests.WithOs +import com.facebook.react.utils.Os.cliPath import com.facebook.react.utils.Os.unixifyPath import org.junit.Assert.* import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder class OsTest { @get:Rule val osRule = OsRule() + @get:Rule val tempFolder = TemporaryFolder() @Test @WithOs(OS.LINUX, "amd64") @@ -56,4 +59,28 @@ class OsTest { assertEquals("/D/just/a/windows/path/", aWindowsPath.unixifyPath()) } + + @Test + @WithOs(OS.WIN) + fun cliPath_onWindows_returnsRelativePath() { + val tempFile = tempFolder.newFile("test.txt").apply { createNewFile() } + + assertEquals(tempFile.relativeTo(tempFolder.root).path, tempFile.cliPath(tempFolder.root)) + } + + @Test + @WithOs(OS.LINUX) + fun cliPath_onLinux_returnsAbsolutePath() { + val tempFile = tempFolder.newFile("test.txt").apply { createNewFile() } + + assertEquals(tempFile.absolutePath, tempFile.cliPath(tempFolder.root)) + } + + @Test + @WithOs(OS.MAC) + fun cliPath_onMac_returnsAbsolutePath() { + val tempFile = tempFolder.newFile("test.txt").apply { createNewFile() } + + assertEquals(tempFile.absolutePath, tempFile.cliPath(tempFolder.root)) + } } From bbed15d4ae6df23bab2f0730562396ef61f0bc59 Mon Sep 17 00:00:00 2001 From: "Zihan Chen (MSFT)" <53799235+ZihanChen-MSFT@users.noreply.github.com> Date: Tue, 7 Feb 2023 07:41:37 -0800 Subject: [PATCH 58/65] Turbo Module supports intersection type for TypeScript (#36037) Summary: As [requested from community](https://github.com/reactwg/react-native-new-architecture/discussions/91#discussioncomment-4282384), intersection type is supported for component but no in turbo module. This PR fixed it. ## Changelog [GENERAL] [CHANGED] - Turbo Module supports intersection type for TypeScript Pull Request resolved: https://github.com/facebook/react-native/pull/36037 Test Plan: `yarn jest react-native-codegen` passed Reviewed By: christophpurrer Differential Revision: D42960338 Pulled By: cipolleschi fbshipit-source-id: d317d3155cb96643bf549d0ac3d77f503685edbc --- .../__tests__/checkModuleSnaps-test.js | 1 + .../modules/__test_fixtures__/fixtures.js | 45 +++++ ...script-module-parser-snapshot-test.js.snap | 108 +++++++++++ .../src/parsers/typescript/modules/index.js | 167 +++++++++++------- 4 files changed, 255 insertions(+), 66 deletions(-) diff --git a/packages/react-native-codegen/src/parsers/consistency/__tests__/checkModuleSnaps-test.js b/packages/react-native-codegen/src/parsers/consistency/__tests__/checkModuleSnaps-test.js index e90d3d22d30c0e..1e1fe3909c103f 100644 --- a/packages/react-native-codegen/src/parsers/consistency/__tests__/checkModuleSnaps-test.js +++ b/packages/react-native-codegen/src/parsers/consistency/__tests__/checkModuleSnaps-test.js @@ -22,6 +22,7 @@ const tsExtraCases = [ 'NATIVE_MODULE_WITH_ARRAY2_WITH_UNION_AND_TOUPLE', 'NATIVE_MODULE_WITH_BASIC_ARRAY2', 'NATIVE_MODULE_WITH_COMPLEX_ARRAY2', + 'NATIVE_MODULE_WITH_INTERSECTION_TYPES', 'NATIVE_MODULE_WITH_NESTED_INTERFACES', ]; const ignoredCases = []; diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js index ead2dde77b13d8..5f6377869afb1d 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js @@ -239,6 +239,50 @@ export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULE_WITH_INTERSECTION_TYPES = ` +/** + * 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 {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type Bar = { + z: number +}; + +type Base1 = { + bar1: Bar, +} + +type Base2 = { + bar2: Bar, +} + +type Base3 = Base2 & { + bar3: Bar, +} + +type Foo = Base1 & Base3 & { + bar4: Bar, +}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => Foo; + foo2: (x: Foo) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + const NATIVE_MODULE_WITH_FLOAT_AND_INT32 = ` /** * Copyright (c) Meta Platforms, Inc. and affiliates. @@ -803,6 +847,7 @@ module.exports = { NATIVE_MODULE_WITH_ALIASES, NATIVE_MODULE_WITH_NESTED_ALIASES, NATIVE_MODULE_WITH_NESTED_INTERFACES, + NATIVE_MODULE_WITH_INTERSECTION_TYPES, NATIVE_MODULE_WITH_PROMISE, NATIVE_MODULE_WITH_COMPLEX_OBJECTS, NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY, diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap index eed47982636b24..6f7033c94407af 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap @@ -1572,6 +1572,114 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_FL }" `; +exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_INTERSECTION_TYPES 1`] = ` +"{ + 'modules': { + 'NativeSampleTurboModule': { + 'type': 'NativeModule', + 'aliasMap': { + 'Bar': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'z', + 'optional': false, + 'typeAnnotation': { + 'type': 'NumberTypeAnnotation' + } + } + ] + }, + 'Foo': { + 'type': 'ObjectTypeAnnotation', + 'properties': [ + { + 'name': 'bar1', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Bar' + } + }, + { + 'name': 'bar2', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Bar' + } + }, + { + 'name': 'bar3', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Bar' + } + }, + { + 'name': 'bar4', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Bar' + } + } + ] + } + }, + 'enumMap': {}, + 'spec': { + 'properties': [ + { + 'name': 'foo1', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Foo' + }, + 'params': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Foo' + } + } + ] + } + }, + { + 'name': 'foo2', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'VoidTypeAnnotation' + }, + 'params': [ + { + 'name': 'x', + 'optional': false, + 'typeAnnotation': { + 'type': 'TypeAliasTypeAnnotation', + 'name': 'Foo' + } + } + ] + } + } + ] + }, + 'moduleName': 'SampleTurboModule' + } + } +}" +`; + exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_NESTED_ALIASES 1`] = ` "{ 'modules': { diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/index.js b/packages/react-native-codegen/src/parsers/typescript/modules/index.js index 18c9ddf13492af..64cd08459c0858 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -22,7 +22,12 @@ import type { } from '../../../CodegenSchema'; import type {Parser} from '../../parser'; -import type {ParserErrorCapturer, TypeDeclarationMap} from '../../utils'; +import type { + ParserErrorCapturer, + TypeResolutionStatus, + TypeDeclarationMap, +} from '../../utils'; +const {flattenIntersectionType} = require('../parseTopLevelType'); const {flattenProperties} = require('../components/componentsUtils'); const {visit, isModuleRegistryCall, verifyPlatforms} = require('../../utils'); @@ -74,6 +79,64 @@ const { const language = 'TypeScript'; +function translateObjectTypeAnnotation( + hasteModuleName: string, + /** + * TODO(T108222691): Use flow-types for @babel/parser + */ + nullable: boolean, + objectMembers: $ReadOnlyArray<$FlowFixMe>, + typeResolutionStatus: TypeResolutionStatus, + baseTypes: $ReadOnlyArray, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + parser: Parser, +): Nullable { + // $FlowFixMe[missing-type-arg] + const properties = objectMembers + .map>>(property => { + return tryParse(() => { + return parseObjectProperty( + property, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + translateTypeAnnotation, + parser, + ); + }); + }) + .filter(Boolean); + + let objectTypeAnnotation; + if (baseTypes.length === 0) { + objectTypeAnnotation = { + type: 'ObjectTypeAnnotation', + properties, + }; + } else { + objectTypeAnnotation = { + type: 'ObjectTypeAnnotation', + properties, + baseTypes, + }; + } + + return typeAliasResolution( + typeResolutionStatus, + objectTypeAnnotation, + aliasMap, + nullable, + ); +} + function translateTypeAnnotation( hasteModuleName: string, /** @@ -246,46 +309,36 @@ function translateTypeAnnotation( ); } - let objectTypeAnnotation = { - type: 'ObjectTypeAnnotation', - // $FlowFixMe[missing-type-arg] - properties: (flattenProperties( - [typeAnnotation], - types, - ): $ReadOnlyArray<$FlowFixMe>) - .map>>( - property => { - return tryParse(() => { - return parseObjectProperty( - property, - hasteModuleName, - types, - aliasMap, - enumMap, - tryParse, - cxxOnly, - nullable, - translateTypeAnnotation, - parser, - ); - }); - }, - ) - .filter(Boolean), - baseTypes, - }; - - if (objectTypeAnnotation.baseTypes.length === 0) { - // The flow checker does not allow adding a member after an object literal is created - // so here I do it in a reverse way - delete objectTypeAnnotation.baseTypes; - } - - return typeAliasResolution( + return translateObjectTypeAnnotation( + hasteModuleName, + nullable, + flattenProperties([typeAnnotation], types), typeResolutionStatus, - objectTypeAnnotation, + baseTypes, + types, aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + case 'TSIntersectionType': { + return translateObjectTypeAnnotation( + hasteModuleName, nullable, + flattenProperties( + flattenIntersectionType(typeAnnotation, types), + types, + ), + typeResolutionStatus, + [], + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, ); } case 'TSTypeLiteral': { @@ -313,36 +366,18 @@ function translateTypeAnnotation( } } - const objectTypeAnnotation = { - type: 'ObjectTypeAnnotation', - // $FlowFixMe[missing-type-arg] - properties: (typeAnnotation.members: Array<$FlowFixMe>) - .map>>( - property => { - return tryParse(() => { - return parseObjectProperty( - property, - hasteModuleName, - types, - aliasMap, - enumMap, - tryParse, - cxxOnly, - nullable, - translateTypeAnnotation, - parser, - ); - }); - }, - ) - .filter(Boolean), - }; - - return typeAliasResolution( + return translateObjectTypeAnnotation( + hasteModuleName, + nullable, + typeAnnotation.members, typeResolutionStatus, - objectTypeAnnotation, + [], + types, aliasMap, - nullable, + enumMap, + tryParse, + cxxOnly, + parser, ); } case 'TSEnumDeclaration': { From 7e02b41630777650f2bdda56f2f20089d4ad36e0 Mon Sep 17 00:00:00 2001 From: Xin Chen Date: Tue, 7 Feb 2023 09:22:58 -0800 Subject: [PATCH 59/65] Remove useOverflowInset flag as we rolled out 100% to public for over three months Summary: This was shipped in D36990986 (https://github.com/facebook/react-native/commit/df80ed40c7cde6cf81a0974d78ef0a7cfcf19e5c) but backed out last year in D37074879 (https://github.com/facebook/react-native/commit/59476d06f3be2e1c764c21447b4cf2ea712dd172), as we want to wait for some performance comparison results to come out. Remove overflow inset optimization flags as they've been rolled out 100% to public. Changelog: [Android][Internal] - Clean up feature flags for overflowInset Reviewed By: javache Differential Revision: D43070494 fbshipit-source-id: dbf5aed9b2b5d3db1ad351bc208cb2016dc62e40 --- .../com/facebook/react/config/ReactFeatureFlags.java | 3 --- .../com/facebook/react/uimanager/TouchTargetHelper.java | 2 -- .../src/main/jni/react/fabric/FabricMountingManager.cpp | 9 +++------ .../src/main/jni/react/fabric/FabricMountingManager.h | 1 - 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 44dbc60da5b8c6..a4df8b3565d6a9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -67,9 +67,6 @@ public class ReactFeatureFlags { /** Enables or disables calculation of Transformed Frames */ public static boolean calculateTransformedFramesEnabled = false; - /** Feature Flag to use overflowInset values provided by Yoga */ - public static boolean useOverflowInset = false; - public static boolean dispatchPointerEvents = false; /** Feature Flag to enable the pending event queue in fabric before mounting views */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java index 4b1a518f10dd7b..cff46d4dcbd27e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.java @@ -19,7 +19,6 @@ import androidx.annotation.Nullable; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.touch.ReactHitSlopView; import com.facebook.react.uimanager.common.ViewUtil; import java.util.ArrayList; @@ -192,7 +191,6 @@ private static View findTouchTargetView( // If the touch point is outside of the overflowinset for the view, we can safely ignore // it. if (ViewUtil.getUIManagerType(view.getId()) == FABRIC - && ReactFeatureFlags.useOverflowInset && !isTouchPointInViewWithOverflowInset(eventCoords[0], eventCoords[1], view)) { return null; } diff --git a/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index b1097967515bfd..345523cf54be56 100644 --- a/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -40,7 +40,6 @@ FabricMountingManager::FabricMountingManager( std::shared_ptr &config, global_ref &javaUIManager) : javaUIManager_(javaUIManager), - useOverflowInset_(getFeatureFlagValue("useOverflowInset")), reduceDeleteCreateMutation_( getFeatureFlagValue("reduceDeleteCreateMutation")) { CoreFeatures::enableMapBuffer = getFeatureFlagValue("useMapBufferProps"); @@ -404,8 +403,7 @@ void FabricMountingManager::executeMount( // children of the current view. The layout of current view may not // change, and we separate this part from layout mount items to not // pack too much data there. - if (useOverflowInset_ && - (oldChildShadowView.layoutMetrics.overflowInset != + if ((oldChildShadowView.layoutMetrics.overflowInset != newChildShadowView.layoutMetrics.overflowInset)) { cppUpdateOverflowInsetMountItems.push_back( CppMountItem::UpdateOverflowInsetMountItem( @@ -461,9 +459,8 @@ void FabricMountingManager::executeMount( // children of the current view. The layout of current view may not // change, and we separate this part from layout mount items to not // pack too much data there. - if (useOverflowInset_ && - newChildShadowView.layoutMetrics.overflowInset != - EdgeInsets::ZERO) { + if (newChildShadowView.layoutMetrics.overflowInset != + EdgeInsets::ZERO) { cppUpdateOverflowInsetMountItems.push_back( CppMountItem::UpdateOverflowInsetMountItem( newChildShadowView)); diff --git a/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h b/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h index 37440bf50198b0..c098e661b66632 100644 --- a/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h +++ b/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h @@ -69,7 +69,6 @@ class FabricMountingManager final { butter::map> allocatedViewRegistry_{}; std::recursive_mutex allocatedViewsMutex_; - bool const useOverflowInset_{false}; bool const reduceDeleteCreateMutation_{false}; jni::local_ref getProps( From a232decbb1252ade0247a352f887ca4d97ee273c Mon Sep 17 00:00:00 2001 From: Kudo Chien Date: Tue, 7 Feb 2023 09:33:44 -0800 Subject: [PATCH 60/65] update jsc-android to ndk r23 based (#36062) Summary: the current jsc-android is still built based on ndk r21, and react-native is now built based on ndk r23. the unwinder between r21 and r23 is incompatible (libgcc vs libunwind). if there's exceptions throwing from jsc, other react native libraries cannot catch these exceptions and cause runtime crash. this pr updates jsc-android to 235231.0.0 which is the same webkitgtk version as 235230.2.1 but only built by ndk r23. the jsc-android pr is from https://github.com/react-native-community/jsc-android-buildscripts/pull/179. note that the jsc is based on ndk r23c and react-native is based on ndk r23b. the reason is that i cannot get jsc building successfully on r23b. hopefully r23b and r23c are abi safe. there is another crash from libjscexecutor when testing the new jsc-android. to fix the issue, i have to explicitly link libunwind.a from libjscexecutor.so. supposedly ndk r23 should help to link libunwind under the hood, i still not figure out why it doesn't. but after linking libunwind.a, i can get new jsc-android work successfully. ``` E/art ( 2669): dlopen("/data/app/com.test-1/lib/x86_64/libjscexecutor.so", RTLD_LAZY) failed: dlopen failed: cannot locate symbol "_Unwind_Resume" referenced by "/data/app/com.test-1/lib/x86_64/libjscexecutor.so"... W/System.err( 2669): java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "_Unwind_Resume" referenced by "/data/app/com.test-1/lib/x86_64/libjscexecutor.so"... W/System.err( 2669): at java.lang.Runtime.load(Runtime.java:331) W/System.err( 2669): at java.lang.System.load(System.java:982) W/System.err( 2669): at com.facebook.soloader.SoLoader$1.load(SoLoader.java:558) W/System.err( 2669): at com.facebook.soloader.DirectorySoSource.loadLibraryFrom(DirectorySoSource.java:110) W/System.err( 2669): at com.facebook.soloader.DirectorySoSource.loadLibrary(DirectorySoSource.java:63) W/System.err( 2669): at com.facebook.soloader.ApplicationSoSource.loadLibrary(ApplicationSoSource.java:91) W/System.err( 2669): at com.facebook.soloader.SoLoader.doLoadLibraryBySoName(SoLoader.java:1067) W/System.err( 2669): at com.facebook.soloader.SoLoader.loadLibraryBySoNameImpl(SoLoader.java:943) W/System.err( 2669): at com.facebook.soloader.SoLoader.loadLibraryBySoName(SoLoader.java:855) W/System.err( 2669): at com.facebook.soloader.SoLoader.loadLibrary(SoLoader.java:802) W/System.err( 2669): at com.facebook.soloader.SoLoader.loadLibrary(SoLoader.java:772) W/System.err( 2669): at com.facebook.react.jscexecutor.JSCExecutor.loadLibrary(JSCExecutor.java:24) W/System.err( 2669): at com.facebook.react.jscexecutor.JSCExecutor.(JSCExecutor.java:20) W/System.err( 2669): at com.facebook.react.ReactInstanceManagerBuilder.getDefaultJSExecutorFactory(ReactInstanceManagerBuilder.java:363) W/System.err( 2669): at com.facebook.react.ReactInstanceManagerBuilder.build(ReactInstanceManagerBuilder.java:316) W/System.err( 2669): at com.facebook.react.ReactNativeHost.createReactInstanceManager(ReactNativeHost.java:94) W/System.err( 2669): at com.facebook.react.ReactNativeHost.getReactInstanceManager(ReactNativeHost.java:41) W/System.err( 2669): at com.test.MainApplication.onCreate(MainApplication.java:60) W/System.err( 2669): at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1011) W/System.err( 2669): at androidx.test.runner.MonitoringInstrumentation.callApplicationOnCreate(MonitoringInstrumentation.java:483) W/System.err( 2669): at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4518) W/System.err( 2669): at android.app.ActivityThread.access$1500(ActivityThread.java:144) W/System.err( 2669): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1339) W/System.err( 2669): at android.os.Handler.dispatchMessage(Handler.java:102) W/System.err( 2669): at android.os.Looper.loop(Looper.java:135) W/System.err( 2669): at android.app.ActivityThread.main(ActivityThread.java:5221) W/System.err( 2669): at java.lang.reflect.Method.invoke(Native Method) W/System.err( 2669): at java.lang.reflect.Method.invoke(Method.java:372) W/System.err( 2669): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899) W/System.err( 2669): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694) ``` fixes https://github.com/facebook/react-native/issues/36052 ## Changelog [ANDROID][FIXED] - Fixed jscexecutor crash on Android which is caused from NDK incompatibility Pull Request resolved: https://github.com/facebook/react-native/pull/36062 Test Plan: tested on [jsc-android instrumented test](https://github.com/react-native-community/jsc-android-buildscripts/tree/2.26.1/test) (based on react-native 0.71.2) Reviewed By: cipolleschi Differential Revision: D43040295 Pulled By: cortinico fbshipit-source-id: e0e5b8fb7faa8ee5654d4cde5f274bef4b517376 --- .../src/main/jni/react/jscexecutor/CMakeLists.txt | 13 +++++++++++++ package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/ReactAndroid/src/main/jni/react/jscexecutor/CMakeLists.txt b/ReactAndroid/src/main/jni/react/jscexecutor/CMakeLists.txt index 464aa19ccef868..67858803b3cff7 100644 --- a/ReactAndroid/src/main/jni/react/jscexecutor/CMakeLists.txt +++ b/ReactAndroid/src/main/jni/react/jscexecutor/CMakeLists.txt @@ -13,9 +13,22 @@ add_library(jscexecutor SHARED ${jscexecutor_SRC}) target_include_directories(jscexecutor PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +# Patch from Expo: https://github.com/expo/react-native/blob/02714ab44d1e206fa80e81aef618e61017cccdc1/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/CMakeLists.txt#L16-L25 +# Explicitly link libgcc.a to prevent undefined `_Unwind_Resume` symbol and crash from throwing c++ exceptions even someone tries to catch the exceptions. +# according to https://android.googlesource.com/platform/ndk/+/master/docs/BuildSystemMaintainers.md#unwinding, +# we should put the unwinder between static libs and shared libs. +# +# TODO(ncor): we don't need this patch anymore after upgrading to ndk r23 +if(ANDROID_NDK_REVISION VERSION_LESS "23.0.0") + set(LIB_UNWIND gcc) +else() + set(LIB_UNWIND unwind) +endif() + target_link_libraries(jscexecutor jsireact jscruntime + ${LIB_UNWIND} fb fbjni folly_runtime diff --git a/package.json b/package.json index 6119bd30689bfd..e17aa48a77c965 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "event-target-shim": "^5.0.1", "invariant": "^2.2.4", "jest-environment-node": "^29.2.1", - "jsc-android": "^250230.2.1", + "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-react-native-babel-transformer": "0.73.5", "metro-runtime": "0.73.5", diff --git a/yarn.lock b/yarn.lock index 640749212e5e5c..742db74a6ba207 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5732,10 +5732,10 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsc-android@^250230.2.1: - version "250230.2.1" - resolved "https://registry.yarnpkg.com/jsc-android/-/jsc-android-250230.2.1.tgz#3790313a970586a03ab0ad47defbc84df54f1b83" - integrity sha512-KmxeBlRjwoqCnBBKGsihFtvsBHyUFlBxJPK4FzeYcIuBfdjv6jFys44JITAgSTbQD+vIdwMEfyZklsuQX0yI1Q== +jsc-android@^250231.0.0: + version "250231.0.0" + resolved "https://registry.yarnpkg.com/jsc-android/-/jsc-android-250231.0.0.tgz#91720f8df382a108872fa4b3f558f33ba5e95262" + integrity sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw== jscodeshift@^0.14.0: version "0.14.0" From 25f585302eca6c2151c01c8363c39a2b00cc2486 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 7 Feb 2023 11:01:44 -0800 Subject: [PATCH 61/65] Pass RuntimeScheduler to Binding from Venice on Android Summary: Changelog: [internal] Pass RuntimeScheduler to Binding in Venice. This is needed for two reasons: - support "callImmediates". This is a workaround to bridge the gap in scheduling until microtasks in RN are shipped. - To block paint in case there is a state update in useLayoutEffect. Used later in this diff stack Reviewed By: sshic Differential Revision: D43088186 fbshipit-source-id: 8537234db5f72cbf057ad1861ca2c37a5c3dbd8b --- .../src/main/java/com/facebook/react/fabric/Binding.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java index e6cad3c21595b3..19caa193e40bd7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java @@ -9,7 +9,6 @@ import android.annotation.SuppressLint; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.facebook.jni.HybridData; import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.react.bridge.NativeMap; @@ -86,7 +85,7 @@ public native ReadableNativeMap getInspectorDataForInstance( public void register( @NonNull RuntimeExecutor runtimeExecutor, - @Nullable RuntimeScheduler runtimeScheduler, + @NonNull RuntimeScheduler runtimeScheduler, @NonNull FabricUIManager fabricUIManager, @NonNull EventBeatManager eventBeatManager, @NonNull ComponentFactory componentFactory, From d31670b4d975d7f6830d2934bfbac484211c4b94 Mon Sep 17 00:00:00 2001 From: Matthijs Mullender Date: Tue, 7 Feb 2023 12:42:34 -0800 Subject: [PATCH 62/65] do not strip JavascriptException Summary: Changelog: [internal] - DescriptionSome tooling breaks when the JavascriptException is obfuscated. This change prevents the exception from getting obfuscated, allowing tools to detect them without symbolicating. Differential Revision: D43091424 fbshipit-source-id: aae4768397bd78433a1d496ecac4a1442422d912 --- .../java/com/facebook/react/common/JavascriptException.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/JavascriptException.java b/ReactAndroid/src/main/java/com/facebook/react/common/JavascriptException.java index d1aaca03123ee3..b3945ee2508057 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/JavascriptException.java +++ b/ReactAndroid/src/main/java/com/facebook/react/common/JavascriptException.java @@ -7,12 +7,14 @@ package com.facebook.react.common; +import com.facebook.proguard.annotations.DoNotStrip; import javax.annotation.Nullable; /** * A JS exception that was propagated to native. In debug mode, these exceptions are normally shown * to developers in a redbox. */ +@DoNotStrip public class JavascriptException extends RuntimeException implements HasJavascriptExceptionMetadata { From 9d98e2cb07afd3f68945ec7d7125188f4b6367fa Mon Sep 17 00:00:00 2001 From: Deepak Jacob Date: Tue, 7 Feb 2023 13:06:38 -0800 Subject: [PATCH 63/65] Revert D41273822: Add fabric support for maintainVisibleContentPosition on iOS Differential Revision: D41273822 (https://github.com/facebook/react-native/commit/ecf967a51d358f3759acbf7795675f52d7190280) Original commit changeset: 7900898f2828 Original Phabricator Diff: D41273822 (https://github.com/facebook/react-native/commit/ecf967a51d358f3759acbf7795675f52d7190280) fbshipit-source-id: 0f4b9695c805407b9a41b0fec63784bd0e84be43 --- .../ScrollView/RCTScrollViewComponentView.mm | 96 ------------------- .../components/scrollview/ScrollViewProps.cpp | 14 --- .../components/scrollview/ScrollViewProps.h | 4 - .../components/scrollview/conversions.h | 35 ------- .../components/scrollview/primitives.h | 17 ---- .../renderer/debug/DebugStringConvertible.h | 9 -- .../examples/ScrollView/ScrollViewExample.js | 2 +- 7 files changed, 1 insertion(+), 176 deletions(-) diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 1a243af970120f..8c95cdbc0b9e18 100644 --- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -22,7 +22,6 @@ #import "RCTConversions.h" #import "RCTEnhancedScrollView.h" #import "RCTFabricComponentsPlugins.h" -#import "RCTPullToRefreshViewComponentView.h" using namespace facebook::react; @@ -100,11 +99,6 @@ @implementation RCTScrollViewComponentView { BOOL _shouldUpdateContentInsetAdjustmentBehavior; CGPoint _contentOffsetWhenClipped; - - __weak UIView *_contentView; - - CGRect _prevFirstVisibleFrame; - __weak UIView *_firstVisibleView; } + (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view @@ -154,17 +148,10 @@ - (void)dealloc #pragma mark - RCTMountingTransactionObserving -- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction - withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry -{ - [self _prepareForMaintainVisibleScrollPosition]; -} - - (void)mountingTransactionDidMount:(MountingTransaction const &)transaction withSurfaceTelemetry:(facebook::react::SurfaceTelemetry const &)surfaceTelemetry { [self _remountChildren]; - [self _adjustForMaintainVisibleContentPosition]; } #pragma mark - RCTComponentViewProtocol @@ -349,23 +336,11 @@ - (void)_preserveContentOffsetIfNeededWithBlock:(void (^)())block - (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { [_containerView insertSubview:childComponentView atIndex:index]; - if ([childComponentView isKindOfClass:RCTPullToRefreshViewComponentView.class]) { - // Ignore the pull to refresh component. - } else { - RCTAssert(_contentView == nil, @"RCTScrollView may only contain a single subview."); - _contentView = childComponentView; - } } - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { [childComponentView removeFromSuperview]; - if ([childComponentView isKindOfClass:RCTPullToRefreshViewComponentView.class]) { - // Ignore the pull to refresh component. - } else { - RCTAssert(_contentView == childComponentView, @"Attempted to remove non-existent subview"); - _contentView = nil; - } } /* @@ -428,9 +403,6 @@ - (void)prepareForRecycle CGRect oldFrame = self.frame; self.frame = CGRectZero; self.frame = oldFrame; - _contentView = nil; - _prevFirstVisibleFrame = CGRectZero; - _firstVisibleView = nil; [super prepareForRecycle]; } @@ -711,74 +683,6 @@ - (void)removeScrollListener:(NSObject *)scrollListener [self.scrollViewDelegateSplitter removeDelegate:scrollListener]; } -#pragma mark - Maintain visible content position - -- (void)_prepareForMaintainVisibleScrollPosition -{ - const auto &props = *std::static_pointer_cast(_props); - if (!props.maintainVisibleContentPosition) { - return; - } - - BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width; - int minIdx = props.maintainVisibleContentPosition.value().minIndexForVisible; - for (NSUInteger ii = minIdx; ii < _contentView.subviews.count; ++ii) { - // Find the first entirely visible view. - UIView *subview = _contentView.subviews[ii]; - BOOL hasNewView = NO; - if (horizontal) { - hasNewView = subview.frame.origin.x > _scrollView.contentOffset.x; - } else { - hasNewView = subview.frame.origin.y > _scrollView.contentOffset.y; - } - if (hasNewView || ii == _contentView.subviews.count - 1) { - _prevFirstVisibleFrame = subview.frame; - _firstVisibleView = subview; - break; - } - } -} - -- (void)_adjustForMaintainVisibleContentPosition -{ - const auto &props = *std::static_pointer_cast(_props); - if (!props.maintainVisibleContentPosition) { - return; - } - - std::optional autoscrollThreshold = props.maintainVisibleContentPosition.value().autoscrollToTopThreshold; - BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width; - // TODO: detect and handle/ignore re-ordering - if (horizontal) { - CGFloat deltaX = _firstVisibleView.frame.origin.x - _prevFirstVisibleFrame.origin.x; - if (ABS(deltaX) > 0.5) { - CGFloat x = _scrollView.contentOffset.x; - [self _forceDispatchNextScrollEvent]; - _scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x + deltaX, _scrollView.contentOffset.y); - if (autoscrollThreshold) { - // If the offset WAS within the threshold of the start, animate to the start. - if (x <= autoscrollThreshold.value()) { - [self scrollToOffset:CGPointMake(0, _scrollView.contentOffset.y) animated:YES]; - } - } - } - } else { - CGRect newFrame = _firstVisibleView.frame; - CGFloat deltaY = newFrame.origin.y - _prevFirstVisibleFrame.origin.y; - if (ABS(deltaY) > 0.5) { - CGFloat y = _scrollView.contentOffset.y; - [self _forceDispatchNextScrollEvent]; - _scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x, _scrollView.contentOffset.y + deltaY); - if (autoscrollThreshold) { - // If the offset WAS within the threshold of the start, animate to the start. - if (y <= autoscrollThreshold.value()) { - [self scrollToOffset:CGPointMake(_scrollView.contentOffset.x, 0) animated:YES]; - } - } - } - } -} - @end Class RCTScrollViewCls(void) diff --git a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp index ef2cee93e9f0e6..4f0d53929c9602 100644 --- a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp +++ b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp @@ -127,15 +127,6 @@ ScrollViewProps::ScrollViewProps( "keyboardDismissMode", sourceProps.keyboardDismissMode, {})), - maintainVisibleContentPosition( - CoreFeatures::enablePropIteratorSetter - ? sourceProps.maintainVisibleContentPosition - : convertRawProp( - context, - rawProps, - "maintainVisibleContentPosition", - sourceProps.maintainVisibleContentPosition, - {})), maximumZoomScale( CoreFeatures::enablePropIteratorSetter ? sourceProps.maximumZoomScale @@ -346,7 +337,6 @@ void ScrollViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(directionalLockEnabled); RAW_SET_PROP_SWITCH_CASE_BASIC(indicatorStyle); RAW_SET_PROP_SWITCH_CASE_BASIC(keyboardDismissMode); - RAW_SET_PROP_SWITCH_CASE_BASIC(maintainVisibleContentPosition); RAW_SET_PROP_SWITCH_CASE_BASIC(maximumZoomScale); RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale); RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled); @@ -423,10 +413,6 @@ SharedDebugStringConvertibleList ScrollViewProps::getDebugProps() const { "keyboardDismissMode", keyboardDismissMode, defaultScrollViewProps.keyboardDismissMode), - debugStringConvertibleItem( - "maintainVisibleContentPosition", - maintainVisibleContentPosition, - defaultScrollViewProps.maintainVisibleContentPosition), debugStringConvertibleItem( "maximumZoomScale", maximumZoomScale, diff --git a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.h b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.h index c76c99edddab6f..bd53bc4bb22def 100644 --- a/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.h +++ b/ReactCommon/react/renderer/components/scrollview/ScrollViewProps.h @@ -11,8 +11,6 @@ #include #include -#include - namespace facebook { namespace react { @@ -45,8 +43,6 @@ class ScrollViewProps final : public ViewProps { bool directionalLockEnabled{}; ScrollViewIndicatorStyle indicatorStyle{}; ScrollViewKeyboardDismissMode keyboardDismissMode{}; - std::optional - maintainVisibleContentPosition{}; Float maximumZoomScale{1.0f}; Float minimumZoomScale{1.0f}; bool scrollEnabled{true}; diff --git a/ReactCommon/react/renderer/components/scrollview/conversions.h b/ReactCommon/react/renderer/components/scrollview/conversions.h index 3c888b4813268b..4605f08ea203dd 100644 --- a/ReactCommon/react/renderer/components/scrollview/conversions.h +++ b/ReactCommon/react/renderer/components/scrollview/conversions.h @@ -10,7 +10,6 @@ #include #include #include -#include namespace facebook { namespace react { @@ -99,26 +98,6 @@ inline void fromRawValue( abort(); } -inline void fromRawValue( - const PropsParserContext &context, - const RawValue &value, - ScrollViewMaintainVisibleContentPosition &result) { - auto map = (butter::map)value; - - auto minIndexForVisible = map.find("minIndexForVisible"); - if (minIndexForVisible != map.end()) { - fromRawValue( - context, minIndexForVisible->second, result.minIndexForVisible); - } - auto autoscrollToTopThreshold = map.find("autoscrollToTopThreshold"); - if (autoscrollToTopThreshold != map.end()) { - fromRawValue( - context, - autoscrollToTopThreshold->second, - result.autoscrollToTopThreshold); - } -} - inline std::string toString(const ScrollViewSnapToAlignment &value) { switch (value) { case ScrollViewSnapToAlignment::Start: @@ -130,8 +109,6 @@ inline std::string toString(const ScrollViewSnapToAlignment &value) { } } -#if RN_DEBUG_STRING_CONVERTIBLE - inline std::string toString(const ScrollViewIndicatorStyle &value) { switch (value) { case ScrollViewIndicatorStyle::Default: @@ -167,17 +144,5 @@ inline std::string toString(const ContentInsetAdjustmentBehavior &value) { } } -inline std::string toString( - const std::optional &value) { - if (!value) { - return "null"; - } - return "{minIndexForVisible: " + toString(value.value().minIndexForVisible) + - ", autoscrollToTopThreshold: " + - toString(value.value().autoscrollToTopThreshold) + "}"; -} - -#endif - } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/components/scrollview/primitives.h b/ReactCommon/react/renderer/components/scrollview/primitives.h index e627d767fa2958..fe8a60e21d7c0d 100644 --- a/ReactCommon/react/renderer/components/scrollview/primitives.h +++ b/ReactCommon/react/renderer/components/scrollview/primitives.h @@ -7,8 +7,6 @@ #pragma once -#include - namespace facebook { namespace react { @@ -25,20 +23,5 @@ enum class ContentInsetAdjustmentBehavior { Always }; -class ScrollViewMaintainVisibleContentPosition final { - public: - int minIndexForVisible{0}; - std::optional autoscrollToTopThreshold{}; - - bool operator==(const ScrollViewMaintainVisibleContentPosition &rhs) const { - return std::tie(this->minIndexForVisible, this->autoscrollToTopThreshold) == - std::tie(rhs.minIndexForVisible, rhs.autoscrollToTopThreshold); - } - - bool operator!=(const ScrollViewMaintainVisibleContentPosition &rhs) const { - return !(*this == rhs); - } -}; - } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/debug/DebugStringConvertible.h b/ReactCommon/react/renderer/debug/DebugStringConvertible.h index 7df17f01e39aec..a9a1ef02b4e349 100644 --- a/ReactCommon/react/renderer/debug/DebugStringConvertible.h +++ b/ReactCommon/react/renderer/debug/DebugStringConvertible.h @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -99,14 +98,6 @@ std::string toString(float const &value); std::string toString(double const &value); std::string toString(void const *value); -template -std::string toString(const std::optional &value) { - if (!value) { - return "null"; - } - return toString(value.value()); -} - /* * *Informal* `DebugStringConvertible` interface. * diff --git a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js index a7d7a8f8258812..ec3b0d7a461446 100644 --- a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js +++ b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js @@ -76,7 +76,7 @@ class AppendingList extends React.Component< Date: Tue, 7 Feb 2023 13:10:43 -0800 Subject: [PATCH 64/65] Convert Bridge-only checks to overridable functions Summary: Convert Bridge-only checks to overridable functions and make Bridgeless override them in ReactSurfaceView so that the checks will work for Bridgeless as well. Issue fixed: https://fb.workplace.com/groups/rn.support/permalink/24231137939841493/ Changelog: [Android][Changed] - Convert Bridge-only calls to overridable functions Reviewed By: javache Differential Revision: D43063348 fbshipit-source-id: a1c181d27c1669f6033f3fb783c5a668b7c2585b --- .../com/facebook/react/ReactRootView.java | 82 ++++++++++--------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 9f4339bf490389..1ac3df2b2c08f5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -179,7 +179,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mWasMeasured = true; // Check if we were waiting for onMeasure to attach the root view. - if (mReactInstanceManager != null && !mIsAttachedToInstance) { + if (hasActiveReactInstance() && !isViewAttachedToReactInstance()) { attachToReactInstanceManager(); } else if (measureSpecsUpdated || mLastWidth != width || mLastHeight != height) { updateRootLayoutSpecs(true, mWidthMeasureSpec, mHeightMeasureSpec); @@ -203,7 +203,7 @@ public void onChildStartedNativeGesture(View childView, MotionEvent ev) { if (!isDispatcherReady()) { return; } - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + ReactContext reactContext = getCurrentReactContext(); UIManager uiManager = UIManagerHelper.getUIManager(reactContext, getUIManagerType()); if (uiManager != null) { @@ -220,7 +220,7 @@ public void onChildEndedNativeGesture(View childView, MotionEvent ev) { if (!isDispatcherReady()) { return; } - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + ReactContext reactContext = getCurrentReactContext(); UIManager uiManager = UIManagerHelper.getUIManager(reactContext, getUIManagerType()); if (uiManager != null) { @@ -233,9 +233,7 @@ public void onChildEndedNativeGesture(View childView, MotionEvent ev) { } private boolean isDispatcherReady() { - if (mReactInstanceManager == null - || !mIsAttachedToInstance - || mReactInstanceManager.getCurrentReactContext() == null) { + if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) { FLog.w(TAG, "Unable to dispatch touch to JS as the catalyst instance has not been attached"); return false; } @@ -303,9 +301,7 @@ protected void dispatchDraw(Canvas canvas) { @Override public boolean dispatchKeyEvent(KeyEvent ev) { - if (mReactInstanceManager == null - || !mIsAttachedToInstance - || mReactInstanceManager.getCurrentReactContext() == null) { + if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) { FLog.w(TAG, "Unable to handle key event as the catalyst instance has not been attached"); return super.dispatchKeyEvent(ev); } @@ -315,9 +311,7 @@ public boolean dispatchKeyEvent(KeyEvent ev) { @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - if (mReactInstanceManager == null - || !mIsAttachedToInstance - || mReactInstanceManager.getCurrentReactContext() == null) { + if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) { FLog.w( TAG, "Unable to handle focus changed event as the catalyst instance has not been attached"); @@ -330,9 +324,7 @@ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyF @Override public void requestChildFocus(View child, View focused) { - if (mReactInstanceManager == null - || !mIsAttachedToInstance - || mReactInstanceManager.getCurrentReactContext() == null) { + if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) { FLog.w( TAG, "Unable to handle child focus changed event as the catalyst instance has not been attached"); @@ -344,9 +336,7 @@ public void requestChildFocus(View child, View focused) { } protected void dispatchJSPointerEvent(MotionEvent event, boolean isCapture) { - if (mReactInstanceManager == null - || !mIsAttachedToInstance - || mReactInstanceManager.getCurrentReactContext() == null) { + if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) { FLog.w(TAG, "Unable to dispatch touch to JS as the catalyst instance has not been attached"); return; } @@ -357,7 +347,7 @@ protected void dispatchJSPointerEvent(MotionEvent event, boolean isCapture) { FLog.w(TAG, "Unable to dispatch pointer events to JS before the dispatcher is available"); return; } - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + ReactContext reactContext = getCurrentReactContext(); UIManager uiManager = UIManagerHelper.getUIManager(reactContext, getUIManagerType()); if (uiManager != null) { @@ -367,9 +357,7 @@ protected void dispatchJSPointerEvent(MotionEvent event, boolean isCapture) { } protected void dispatchJSTouchEvent(MotionEvent event) { - if (mReactInstanceManager == null - || !mIsAttachedToInstance - || mReactInstanceManager.getCurrentReactContext() == null) { + if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) { FLog.w(TAG, "Unable to dispatch touch to JS as the catalyst instance has not been attached"); return; } @@ -377,7 +365,7 @@ protected void dispatchJSTouchEvent(MotionEvent event) { FLog.w(TAG, "Unable to dispatch touch to JS before the dispatcher is available"); return; } - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + ReactContext reactContext = getCurrentReactContext(); UIManager uiManager = UIManagerHelper.getUIManager(reactContext, getUIManagerType()); if (uiManager != null) { @@ -412,7 +400,7 @@ private boolean isFabric() { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - if (mIsAttachedToInstance) { + if (isViewAttachedToReactInstance()) { removeOnGlobalLayoutListener(); getViewTreeObserver().addOnGlobalLayoutListener(getCustomGlobalLayoutListener()); } @@ -421,7 +409,7 @@ protected void onAttachedToWindow() { @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - if (mIsAttachedToInstance) { + if (isViewAttachedToReactInstance()) { removeOnGlobalLayoutListener(); } } @@ -571,7 +559,7 @@ public AtomicInteger getState() { private void updateRootLayoutSpecs( boolean measureSpecsChanged, final int widthMeasureSpec, final int heightMeasureSpec) { ReactMarker.logMarker(ReactMarkerConstants.ROOT_VIEW_UPDATE_LAYOUT_SPECS_START); - if (mReactInstanceManager == null) { + if (!hasActiveReactInstance()) { ReactMarker.logMarker(ReactMarkerConstants.ROOT_VIEW_UPDATE_LAYOUT_SPECS_END); FLog.w(TAG, "Unable to update root layout specs for uninitialized ReactInstanceManager"); return; @@ -585,7 +573,7 @@ private void updateRootLayoutSpecs( return; } - final ReactContext reactApplicationContext = mReactInstanceManager.getCurrentReactContext(); + final ReactContext reactApplicationContext = getCurrentReactContext(); if (reactApplicationContext != null) { @Nullable @@ -629,8 +617,8 @@ public void unmountReactApplication() { // to be committed via the Scheduler, which will cause mounting instructions // to be queued up and synchronously executed to delete and remove // all the views in the hierarchy. - if (mReactInstanceManager != null) { - final ReactContext reactApplicationContext = mReactInstanceManager.getCurrentReactContext(); + if (hasActiveReactInstance()) { + final ReactContext reactApplicationContext = getCurrentReactContext(); if (reactApplicationContext != null && isFabric()) { @Nullable UIManager uiManager = @@ -729,11 +717,11 @@ public void setAppProperties(@Nullable Bundle appProperties) { public void runApplication() { Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "ReactRootView.runApplication"); try { - if (mReactInstanceManager == null || !mIsAttachedToInstance) { + if (!hasActiveReactInstance() || !isViewAttachedToReactInstance()) { return; } - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + ReactContext reactContext = getCurrentReactContext(); if (reactContext == null) { return; } @@ -853,12 +841,12 @@ public void setRootViewTag(int rootViewTag) { @Override public void handleException(final Throwable t) { - if (mReactInstanceManager == null || mReactInstanceManager.getCurrentReactContext() == null) { + if (!hasActiveReactContext()) { throw new RuntimeException(t); } Exception e = new IllegalViewOperationException(t.getMessage(), this, t); - mReactInstanceManager.getCurrentReactContext().handleException(e); + getCurrentReactContext().handleException(e); } public void setIsFabric(boolean isFabric) { @@ -876,14 +864,30 @@ public ReactInstanceManager getReactInstanceManager() { } /* package */ void sendEvent(String eventName, @Nullable WritableMap params) { - if (mReactInstanceManager != null) { - mReactInstanceManager - .getCurrentReactContext() + if (hasActiveReactInstance()) { + getCurrentReactContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); } } + public boolean hasActiveReactInstance() { + return mReactInstanceManager != null; + } + + public boolean hasActiveReactContext() { + return mReactInstanceManager != null && mReactInstanceManager.getCurrentReactContext() != null; + } + + @Nullable + public ReactContext getCurrentReactContext() { + return mReactInstanceManager.getCurrentReactContext(); + } + + public boolean isViewAttachedToReactInstance() { + return mIsAttachedToInstance; + } + private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener { private final Rect mVisibleViewArea; private final int mMinKeyboardHeightDetected; @@ -900,9 +904,7 @@ private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLay @Override public void onGlobalLayout() { - if (mReactInstanceManager == null - || !mIsAttachedToInstance - || mReactInstanceManager.getCurrentReactContext() == null) { + if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) { return; } @@ -1061,7 +1063,7 @@ private void emitOrientationChanged(final int newRotation) { private void emitUpdateDimensionsEvent() { DeviceInfoModule deviceInfo = - mReactInstanceManager.getCurrentReactContext().getNativeModule(DeviceInfoModule.class); + getCurrentReactContext().getNativeModule(DeviceInfoModule.class); if (deviceInfo != null) { deviceInfo.emitUpdateDimensionsEvent(); From cab865be797b724d2fda5441e0ef23559180f722 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 7 Feb 2023 18:25:28 -0800 Subject: [PATCH 65/65] Fix VirtualizedList usage of potentially stale state on cell focus Summary: State updates can be batched together idependent of `this.state`, so we should do any calculation deriving state from state within a `setState()` callback. This fixes a bug where we were relying on potentially stale state, a RenderMask derived from `this.state` instead of the `state` callback parameter, when triggering updates from focus. Note that this is not exercised on Android/iOS, but it on desktop/web. I noticed this a while back while making another change, but that change got abandoned, so this is the independent fix. Changelog: [General][Fixed] - Calculate VirtualizedList render mask for focused cell during batched state updates Reviewed By: javache Differential Revision: D43073415 fbshipit-source-id: dee4197ec925a6d8d427b63fb063aa4e3b58c595 --- packages/virtualized-lists/Lists/VirtualizedList.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js index b93878c5658295..638daf14895558 100644 --- a/packages/virtualized-lists/Lists/VirtualizedList.js +++ b/packages/virtualized-lists/Lists/VirtualizedList.js @@ -1229,18 +1229,7 @@ class VirtualizedList extends StateSafePureComponent { _onCellFocusCapture(cellKey: string) { this._lastFocusedCellKey = cellKey; - const renderMask = VirtualizedList._createRenderMask( - this.props, - this.state.cellsAroundViewport, - this._getNonViewportRenderRegions(this.props), - ); - - this.setState(state => { - if (!renderMask.equals(state.renderMask)) { - return {renderMask}; - } - return null; - }); + this._updateCellsToRender(); } _onCellUnmount = (cellKey: string) => {