Add ReactNativeFeatureFlagsDynamicProvider to allow configuration in C++ using dynamic values (#48093)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48093

Changelog: [internal]

We're going to add support for specifying feature flags in Fantom tests in pragmas. E.g.:

```
/**
 * fantom_flags commonTestFlag:true
 */
```

Users will be able to specify any feature flags in their tests, so we need a way to pass that information from the test file to the runner, and the runner has to be able to apply this dynamic configuration.

Because the API is statically typed in C++, we need to define a method for every possible feature flag configurable through this API. We could do it in userland, but we'd have to manually add a method every time there was a new feature flag we wanted to support.

Instead of doing that, this introduces a new abstraction in the feature flag system that codegens it for you.

The API is basically:

```
  folly::dynamic values = folly::dynamic::object();

  values["commonTestFlag"] = true;

  ReactNativeFeatureFlags::override(
      std::make_unique<ReactNativeFeatureFlagsDynamicProvider>(values));

  EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true);
```

Then we can use this abstraction in Fantom to pass all the configured flags as `folly::dynamic` through this API.

Reviewed By: javache

Differential Revision: D66760118

fbshipit-source-id: c32329e5ca76923c3e0b9c0eb1fe8c3268e1f57b
This commit is contained in:
Rubén Norte
2024-12-05 17:06:11 -08:00
committed by Facebook GitHub Bot
parent da6d089305
commit db70b791ba
7 changed files with 669 additions and 11 deletions
@@ -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)
@@ -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 = "../.."
@@ -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<<a3ad884be5b18b3cfad650941abfa751>>
*/
/**
* 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 <folly/dynamic.h>
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
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
@@ -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 <gtest/gtest.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h>
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<ReactNativeFeatureFlagsDynamicProvider>(
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<ReactNativeFeatureFlagsDynamicProvider>(values));
EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true);
}
TEST_F(
ReactNativeFeatureFlagsDynamicProviderTest,
throwsWithIncorrectFlagTypes) {
folly::dynamic values = folly::dynamic::object();
values["commonTestFlag"] = 12;
ReactNativeFeatureFlags::override(
std::make_unique<ReactNativeFeatureFlagsDynamicProvider>(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<ReactNativeFeatureFlagsDynamicProvider>(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
@@ -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),
};
}
@@ -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 <folly/dynamic.h>
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
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
`);
}
+15 -11
View File
@@ -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 =