diff --git a/packages/react-native/ReactCommon/react/featureflags/CMakeLists.txt b/packages/react-native/ReactCommon/react/featureflags/CMakeLists.txt index bd42dd9d805..4613a85e11c 100644 --- a/packages/react-native/ReactCommon/react/featureflags/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/featureflags/CMakeLists.txt @@ -18,3 +18,6 @@ file(GLOB react_featureflags_SRC CONFIGURE_DEPENDS *.cpp) add_library(react_featureflags OBJECT ${react_featureflags_SRC}) target_include_directories(react_featureflags PUBLIC ${REACT_COMMON_DIR}) + +target_link_libraries(react_featureflags + folly_runtime) diff --git a/packages/react-native/ReactCommon/react/featureflags/React-featureflags.podspec b/packages/react-native/ReactCommon/react/featureflags/React-featureflags.podspec index db1c074ede7..c0ba2a35dc2 100644 --- a/packages/react-native/ReactCommon/react/featureflags/React-featureflags.podspec +++ b/packages/react-native/ReactCommon/react/featureflags/React-featureflags.podspec @@ -22,6 +22,10 @@ if ENV['USE_FRAMEWORKS'] header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../..\"" # this is needed to allow the feature flags access its own files end +folly_config = get_folly_config() +folly_compiler_flags = folly_config[:compiler_flags] +folly_version = folly_config[:version] + Pod::Spec.new do |s| s.name = "React-featureflags" s.version = version @@ -32,11 +36,14 @@ Pod::Spec.new do |s| s.platforms = min_supported_versions s.source = source s.source_files = "*.{cpp,h}" + s.compiler_flags = folly_compiler_flags s.header_dir = "react/featureflags" s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(), "HEADER_SEARCH_PATHS" => header_search_paths.join(' '), "DEFINES_MODULE" => "YES" } + s.dependency "RCT-Folly", folly_version + if ENV['USE_FRAMEWORKS'] s.module_name = "React_featureflags" s.header_mappings_dir = "../.." diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h new file mode 100644 index 00000000000..1e19aa78a10 --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -0,0 +1,472 @@ +/* + * 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. + * + * @generated SignedSource<> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags --update + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +/** + * This class is a ReactNativeFeatureFlags provider that takes the values for + * feature flags from a folly::dynamic object (e.g. from a JSON object), if + * they are defined. For the flags not defined in the object, it falls back to + * the default values defined in ReactNativeFeatureFlagsDefaults. + * + * The API is strict about typing. It ignores null values from the + * folly::dynamic object, but if the key is defined, the value must have the + * correct type or otherwise throws an exception. + */ +class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDefaults { + private: + folly::dynamic values_; + + public: + ReactNativeFeatureFlagsDynamicProvider(folly::dynamic values): values_(std::move(values)) { + if (!values_.isObject()) { + throw std::invalid_argument("ReactNativeFeatureFlagsDynamicProvider: values must be an object"); + } + } + + bool commonTestFlag() override { + auto value = values_["commonTestFlag"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::commonTestFlag(); + } + + bool completeReactInstanceCreationOnBgThreadOnAndroid() override { + auto value = values_["completeReactInstanceCreationOnBgThreadOnAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::completeReactInstanceCreationOnBgThreadOnAndroid(); + } + + bool disableEventLoopOnBridgeless() override { + auto value = values_["disableEventLoopOnBridgeless"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::disableEventLoopOnBridgeless(); + } + + bool disableMountItemReorderingAndroid() override { + auto value = values_["disableMountItemReorderingAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::disableMountItemReorderingAndroid(); + } + + bool enableAlignItemsBaselineOnFabricIOS() override { + auto value = values_["enableAlignItemsBaselineOnFabricIOS"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableAlignItemsBaselineOnFabricIOS(); + } + + bool enableAndroidLineHeightCentering() override { + auto value = values_["enableAndroidLineHeightCentering"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableAndroidLineHeightCentering(); + } + + bool enableBridgelessArchitecture() override { + auto value = values_["enableBridgelessArchitecture"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableBridgelessArchitecture(); + } + + bool enableCppPropsIteratorSetter() override { + auto value = values_["enableCppPropsIteratorSetter"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableCppPropsIteratorSetter(); + } + + bool enableDeletionOfUnmountedViews() override { + auto value = values_["enableDeletionOfUnmountedViews"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableDeletionOfUnmountedViews(); + } + + bool enableEagerRootViewAttachment() override { + auto value = values_["enableEagerRootViewAttachment"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableEagerRootViewAttachment(); + } + + bool enableEventEmitterRetentionDuringGesturesOnAndroid() override { + auto value = values_["enableEventEmitterRetentionDuringGesturesOnAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableEventEmitterRetentionDuringGesturesOnAndroid(); + } + + bool enableFabricLogs() override { + auto value = values_["enableFabricLogs"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableFabricLogs(); + } + + bool enableFabricRenderer() override { + auto value = values_["enableFabricRenderer"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableFabricRenderer(); + } + + bool enableFabricRendererExclusively() override { + auto value = values_["enableFabricRendererExclusively"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableFabricRendererExclusively(); + } + + bool enableFixForViewCommandRace() override { + auto value = values_["enableFixForViewCommandRace"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableFixForViewCommandRace(); + } + + bool enableGranularShadowTreeStateReconciliation() override { + auto value = values_["enableGranularShadowTreeStateReconciliation"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableGranularShadowTreeStateReconciliation(); + } + + bool enableIOSViewClipToPaddingBox() override { + auto value = values_["enableIOSViewClipToPaddingBox"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableIOSViewClipToPaddingBox(); + } + + bool enableImagePrefetchingAndroid() override { + auto value = values_["enableImagePrefetchingAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableImagePrefetchingAndroid(); + } + + bool enableLayoutAnimationsOnAndroid() override { + auto value = values_["enableLayoutAnimationsOnAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableLayoutAnimationsOnAndroid(); + } + + bool enableLayoutAnimationsOnIOS() override { + auto value = values_["enableLayoutAnimationsOnIOS"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableLayoutAnimationsOnIOS(); + } + + bool enableLongTaskAPI() override { + auto value = values_["enableLongTaskAPI"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableLongTaskAPI(); + } + + bool enableNewBackgroundAndBorderDrawables() override { + auto value = values_["enableNewBackgroundAndBorderDrawables"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableNewBackgroundAndBorderDrawables(); + } + + bool enablePreciseSchedulingForPremountItemsOnAndroid() override { + auto value = values_["enablePreciseSchedulingForPremountItemsOnAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enablePreciseSchedulingForPremountItemsOnAndroid(); + } + + bool enablePropsUpdateReconciliationAndroid() override { + auto value = values_["enablePropsUpdateReconciliationAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enablePropsUpdateReconciliationAndroid(); + } + + bool enableReportEventPaintTime() override { + auto value = values_["enableReportEventPaintTime"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableReportEventPaintTime(); + } + + bool enableSynchronousStateUpdates() override { + auto value = values_["enableSynchronousStateUpdates"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableSynchronousStateUpdates(); + } + + bool enableUIConsistency() override { + auto value = values_["enableUIConsistency"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableUIConsistency(); + } + + bool enableViewRecycling() override { + auto value = values_["enableViewRecycling"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableViewRecycling(); + } + + bool excludeYogaFromRawProps() override { + auto value = values_["excludeYogaFromRawProps"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::excludeYogaFromRawProps(); + } + + bool fixDifferentiatorEmittingUpdatesWithWrongParentTag() override { + auto value = values_["fixDifferentiatorEmittingUpdatesWithWrongParentTag"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fixDifferentiatorEmittingUpdatesWithWrongParentTag(); + } + + bool fixMappingOfEventPrioritiesBetweenFabricAndReact() override { + auto value = values_["fixMappingOfEventPrioritiesBetweenFabricAndReact"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fixMappingOfEventPrioritiesBetweenFabricAndReact(); + } + + bool fixMountingCoordinatorReportedPendingTransactionsOnAndroid() override { + auto value = values_["fixMountingCoordinatorReportedPendingTransactionsOnAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fixMountingCoordinatorReportedPendingTransactionsOnAndroid(); + } + + bool fuseboxEnabledDebug() override { + auto value = values_["fuseboxEnabledDebug"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fuseboxEnabledDebug(); + } + + bool fuseboxEnabledRelease() override { + auto value = values_["fuseboxEnabledRelease"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fuseboxEnabledRelease(); + } + + bool initEagerTurboModulesOnNativeModulesQueueAndroid() override { + auto value = values_["initEagerTurboModulesOnNativeModulesQueueAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::initEagerTurboModulesOnNativeModulesQueueAndroid(); + } + + bool lazyAnimationCallbacks() override { + auto value = values_["lazyAnimationCallbacks"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::lazyAnimationCallbacks(); + } + + bool loadVectorDrawablesOnImages() override { + auto value = values_["loadVectorDrawablesOnImages"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::loadVectorDrawablesOnImages(); + } + + bool traceTurboModulePromiseRejectionsOnAndroid() override { + auto value = values_["traceTurboModulePromiseRejectionsOnAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::traceTurboModulePromiseRejectionsOnAndroid(); + } + + bool useAlwaysAvailableJSErrorHandling() override { + auto value = values_["useAlwaysAvailableJSErrorHandling"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useAlwaysAvailableJSErrorHandling(); + } + + bool useFabricInterop() override { + auto value = values_["useFabricInterop"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useFabricInterop(); + } + + bool useImmediateExecutorInAndroidBridgeless() override { + auto value = values_["useImmediateExecutorInAndroidBridgeless"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useImmediateExecutorInAndroidBridgeless(); + } + + bool useNativeViewConfigsInBridgelessMode() override { + auto value = values_["useNativeViewConfigsInBridgelessMode"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useNativeViewConfigsInBridgelessMode(); + } + + bool useOptimisedViewPreallocationOnAndroid() override { + auto value = values_["useOptimisedViewPreallocationOnAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useOptimisedViewPreallocationOnAndroid(); + } + + bool useOptimizedEventBatchingOnAndroid() override { + auto value = values_["useOptimizedEventBatchingOnAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useOptimizedEventBatchingOnAndroid(); + } + + bool useRuntimeShadowNodeReferenceUpdate() override { + auto value = values_["useRuntimeShadowNodeReferenceUpdate"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useRuntimeShadowNodeReferenceUpdate(); + } + + bool useTurboModuleInterop() override { + auto value = values_["useTurboModuleInterop"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useTurboModuleInterop(); + } + + bool useTurboModules() override { + auto value = values_["useTurboModules"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useTurboModules(); + } +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/featureflags/tests/ReactNativeFeatureFlagsDynamicProviderTest.cpp b/packages/react-native/ReactCommon/react/featureflags/tests/ReactNativeFeatureFlagsDynamicProviderTest.cpp new file mode 100644 index 00000000000..9d390d4ddd1 --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/tests/ReactNativeFeatureFlagsDynamicProviderTest.cpp @@ -0,0 +1,78 @@ +/* + * 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. + */ + +#include +#include +#include + +namespace facebook::react { + +class ReactNativeFeatureFlagsDynamicProviderTest : public testing::Test { + protected: + void TearDown() override { + ReactNativeFeatureFlags::dangerouslyReset(); + } +}; + +TEST_F(ReactNativeFeatureFlagsDynamicProviderTest, providesDefaults) { + auto values = folly::dynamic::object(); + + ReactNativeFeatureFlags::override( + std::make_unique( + std::move(values))); + + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), false); +} + +TEST_F(ReactNativeFeatureFlagsDynamicProviderTest, providesDynamicOverrides) { + folly::dynamic values = folly::dynamic::object(); + + values["commonTestFlag"] = true; + + ReactNativeFeatureFlags::override( + std::make_unique(values)); + + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true); +} + +TEST_F( + ReactNativeFeatureFlagsDynamicProviderTest, + throwsWithIncorrectFlagTypes) { + folly::dynamic values = folly::dynamic::object(); + + values["commonTestFlag"] = 12; + + ReactNativeFeatureFlags::override( + std::make_unique(values)); + + try { + ReactNativeFeatureFlags::commonTestFlag(); + FAIL() + << "Expected ReactNativeFeatureFlags::commonTestFlag() to throw an exception"; + } catch (const std::runtime_error& e) { + EXPECT_STREQ( + "TypeError: expected dynamic type 'boolean', but had type 'int64'", + e.what()); + } +} + +TEST_F(ReactNativeFeatureFlagsDynamicProviderTest, throwsWithNonObjectValues) { + folly::dynamic values = folly::dynamic("string"); + + try { + auto provider = + std::make_unique(values); + FAIL() + << "Expected ReactNativeFeatureFlagsDynamicProvider constructor to throw an exception"; + } catch (const std::invalid_argument& e) { + EXPECT_STREQ( + "ReactNativeFeatureFlagsDynamicProvider: values must be an object", + e.what()); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/scripts/featureflags/generateCommonCxxModules.js b/packages/react-native/scripts/featureflags/generateCommonCxxModules.js index 7329bf8abbd..8f12bca1ddf 100644 --- a/packages/react-native/scripts/featureflags/generateCommonCxxModules.js +++ b/packages/react-native/scripts/featureflags/generateCommonCxxModules.js @@ -15,6 +15,7 @@ import ReactNativeFeatureFlagsH from './templates/common-cxx/ReactNativeFeatureF import ReactNativeFeatureFlagsAccessorCPP from './templates/common-cxx/ReactNativeFeatureFlagsAccessor.cpp-template'; import ReactNativeFeatureFlagsAccessorH from './templates/common-cxx/ReactNativeFeatureFlagsAccessor.h-template'; import ReactNativeFeatureFlagsDefaultsH from './templates/common-cxx/ReactNativeFeatureFlagsDefaults.h-template'; +import ReactNativeFeatureFlagsDynamicProviderH from './templates/common-cxx/ReactNativeFeatureFlagsDynamicProvider.h-template'; import ReactNativeFeatureFlagsProviderH from './templates/common-cxx/ReactNativeFeatureFlagsProvider.h-template'; import path from 'path'; @@ -36,5 +37,7 @@ export default function generateCommonCxxModules( ReactNativeFeatureFlagsDefaultsH(featureFlagDefinitions), [path.join(commonCxxPath, 'ReactNativeFeatureFlagsProvider.h')]: ReactNativeFeatureFlagsProviderH(featureFlagDefinitions), + [path.join(commonCxxPath, 'ReactNativeFeatureFlagsDynamicProvider.h')]: + ReactNativeFeatureFlagsDynamicProviderH(featureFlagDefinitions), }; } diff --git a/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsDynamicProvider.h-template.js b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsDynamicProvider.h-template.js new file mode 100644 index 00000000000..e4b0563ba6e --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsDynamicProvider.h-template.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +import type { + CommonFeatureFlagConfig, + FeatureFlagDefinitions, +} from '../../types'; + +import {DO_NOT_MODIFY_COMMENT, getCxxTypeFromDefaultValue} from '../../utils'; +import signedsource from 'signedsource'; + +function getFollyDynamicAccessor(config: CommonFeatureFlagConfig): string { + switch (typeof config.defaultValue) { + case 'boolean': + return 'getBool'; + case 'number': + return 'getInt'; + case 'string': + return 'getString'; + default: + throw new Error(`Unsupported type: ${typeof config.defaultValue}`); + } +} + +export default function (definitions: FeatureFlagDefinitions): string { + return signedsource.signFile(`/* + * 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. + * + * ${signedsource.getSigningToken()} + */ + +${DO_NOT_MODIFY_COMMENT} + +#pragma once + +#include +#include + +namespace facebook::react { + +/** + * This class is a ReactNativeFeatureFlags provider that takes the values for + * feature flags from a folly::dynamic object (e.g. from a JSON object), if + * they are defined. For the flags not defined in the object, it falls back to + * the default values defined in ReactNativeFeatureFlagsDefaults. + * + * The API is strict about typing. It ignores null values from the + * folly::dynamic object, but if the key is defined, the value must have the + * correct type or otherwise throws an exception. + */ +class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDefaults { + private: + folly::dynamic values_; + + public: + ReactNativeFeatureFlagsDynamicProvider(folly::dynamic values): values_(std::move(values)) { + if (!values_.isObject()) { + throw std::invalid_argument("ReactNativeFeatureFlagsDynamicProvider: values must be an object"); + } + } + +${Object.entries(definitions.common) + .map( + ([flagName, flagConfig]) => + ` ${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} ${flagName}() override { + auto value = values_["${flagName}"]; + if (!value.isNull()) { + return value.${getFollyDynamicAccessor(flagConfig)}(); + } + + return ReactNativeFeatureFlagsDefaults::${flagName}(); + }`, + ) + .join('\n\n')} +}; + +} // namespace facebook::react +`); +} diff --git a/packages/react-native/scripts/featureflags/types.js b/packages/react-native/scripts/featureflags/types.js index 9316c9487f4..48a5bed11a0 100644 --- a/packages/react-native/scripts/featureflags/types.js +++ b/packages/react-native/scripts/featureflags/types.js @@ -15,21 +15,25 @@ export type FeatureFlagDefinitions = $ReadOnly<{ jsOnly: JsOnlyFeatureFlagList, }>; +export type CommonFeatureFlagConfig = $ReadOnly<{ + defaultValue: FeatureFlagValue, + metadata: FeatureFlagMetadata, + // Indicates if this API should only be defined in JavaScript, only to + // preserve backwards compatibility with existing native code temporarily. + skipNativeAPI?: true, +}>; + export type CommonFeatureFlagList = $ReadOnly<{ - [flagName: string]: $ReadOnly<{ - defaultValue: FeatureFlagValue, - metadata: FeatureFlagMetadata, - // Indicates if this API should only be defined in JavaScript, only to - // preserve backwards compatibility with existing native code temporarily. - skipNativeAPI?: true, - }>, + [flagName: string]: CommonFeatureFlagConfig, +}>; + +export type JsOnlyFeatureFlagConfig = $ReadOnly<{ + defaultValue: FeatureFlagValue, + metadata: FeatureFlagMetadata, }>; export type JsOnlyFeatureFlagList = $ReadOnly<{ - [flagName: string]: $ReadOnly<{ - defaultValue: FeatureFlagValue, - metadata: FeatureFlagMetadata, - }>, + [flagName: string]: JsOnlyFeatureFlagConfig, }>; export type FeatureFlagMetadata =