diff --git a/package.json b/package.json index 68bfdde88089f5..4999fe6b773eca 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "flow": "flow", "format-check": "prettier --list-different \"./**/*.{js,md,yml,ts,tsx}\"", "format": "npm run prettier && npm run clang-format", + "featureflags-check": "cd packages/react-native && yarn featureflags-check", + "featureflags-update": "cd packages/react-native && yarn featureflags-update", "lint-ci": "./scripts/circleci/analyze_code.sh && yarn shellcheck", "lint-java": "node ./scripts/lint-java.js", "lint": "eslint .", diff --git a/packages/react-native/React-Core.podspec b/packages/react-native/React-Core.podspec index 5de8cef1c9e6ea..80ed4672f59919 100644 --- a/packages/react-native/React-Core.podspec +++ b/packages/react-native/React-Core.podspec @@ -129,6 +129,7 @@ Pod::Spec.new do |s| s.dependency "React-jsi" s.dependency "React-jsiexecutor" s.dependency "React-utils" + s.dependency "React-featureflags" s.dependency "SocketRocket", socket_rocket_version s.dependency "React-runtimescheduler" s.dependency "Yoga" diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 9365583ce63859..58ab6ad457af44 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -526,6 +526,7 @@ android { "runtimeexecutor", "react_codegen_rncore", "react_debug", + "react_featureflags", "react_utils", "react_render_componentregistry", "react_newarchdefaults", @@ -540,6 +541,7 @@ android { "jsi", "glog", "fabricjni", + "featureflagsjni", "react_render_mapbuffer", "yoga", "folly_runtime", diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt new file mode 100644 index 00000000000000..1707a7591b1ced --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -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. + * + * @generated SignedSource<> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +package com.facebook.react.internal.featureflags + +/** + * This object provides access to internal React Native feature flags. + * + * All the methods are thread-safe if you handle `override` correctly. + */ +object ReactNativeFeatureFlags { + private var accessorProvider: () -> ReactNativeFeatureFlagsAccessor = { ReactNativeFeatureFlagsCxxAccessor() } + private var accessor: ReactNativeFeatureFlagsAccessor = accessorProvider() + + /** + * Common flag for testing. Do NOT modify. + */ + fun commonTestFlag() = accessor.commonTestFlag() + + /** + * Overrides the feature flags with the ones provided by the given provider + * (generally one that extends `ReactNativeFeatureFlagsDefaults`). + * + * This method must be called before you initialize the React Native runtime. + * + * @example + * + * ``` + * ReactNativeFeatureFlags.override(object : ReactNativeFeatureFlagsDefaults() { + * override fun someFlag(): Boolean = true // or a dynamic value + * }) + * ``` + */ + fun override(provider: ReactNativeFeatureFlagsProvider) = accessor.override(provider) + + /** + * Removes the overridden feature flags and makes the API return default + * values again. + * + * This should only be called if you destroy the React Native runtime and + * need to create a new one with different overrides. In that case, + * call `dangerouslyReset` after destroying the runtime and `override` + * again before initializing the new one. + */ + fun dangerouslyReset() { + // This is necessary when the accessor interops with C++ and we need to + // remove the overrides set there. + accessor.dangerouslyReset() + + // This discards the cached values and the overrides set in the JVM. + accessor = accessorProvider() + } + + /** + * This is just used to replace the default ReactNativeFeatureFlagsCxxAccessor + * that uses JNI with a version that doesn't, to simplify testing. + */ + internal fun setAccessorProvider(newAccessorProvider: () -> ReactNativeFeatureFlagsAccessor) { + accessorProvider = newAccessorProvider + accessor = accessorProvider() + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsAccessor.kt new file mode 100644 index 00000000000000..5104a79aedb2a1 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsAccessor.kt @@ -0,0 +1,14 @@ +/* + * 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. + */ + +package com.facebook.react.internal.featureflags + +interface ReactNativeFeatureFlagsAccessor : ReactNativeFeatureFlagsProvider { + fun override(provider: ReactNativeFeatureFlagsProvider) + + fun dangerouslyReset() +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt new file mode 100644 index 00000000000000..c3521e833ebced --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -0,0 +1,38 @@ +/* + * 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<<961f1fb0a7ad802a492437f15b1f2dcb>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +package com.facebook.react.internal.featureflags + +class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccessor { + private var commonTestFlagCache: Boolean? = null + + override fun commonTestFlag(): Boolean { + var cached = commonTestFlagCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.commonTestFlag() + commonTestFlagCache = cached + } + return cached + } + + override fun override(provider: ReactNativeFeatureFlagsProvider) = + ReactNativeFeatureFlagsCxxInterop.override(provider as Any) + + override fun dangerouslyReset() = ReactNativeFeatureFlagsCxxInterop.dangerouslyReset() +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt new file mode 100644 index 00000000000000..cb65ae352fba59 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +package com.facebook.react.internal.featureflags + +import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.soloader.SoLoader + +@DoNotStrip +object ReactNativeFeatureFlagsCxxInterop { + init { + SoLoader.loadLibrary("reactfeatureflagsjni") + } + + @DoNotStrip @JvmStatic external fun commonTestFlag(): Boolean + + @DoNotStrip @JvmStatic external fun override(provider: Any) + + @DoNotStrip @JvmStatic external fun dangerouslyReset() +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt new file mode 100644 index 00000000000000..363a9990b44a06 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +package com.facebook.react.internal.featureflags + +open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvider { + // We could use JNI to get the defaults from C++, + // but that is more expensive than just duplicating the defaults here. + + override fun commonTestFlag(): Boolean = false +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsForTests.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsForTests.kt new file mode 100644 index 00000000000000..e18b900d901e8e --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsForTests.kt @@ -0,0 +1,14 @@ +/* + * 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. + */ + +package com.facebook.react.internal.featureflags + +object ReactNativeFeatureFlagsForTests { + fun setUp() { + ReactNativeFeatureFlags.setAccessorProvider({ ReactNativeFeatureFlagsLocalAccessor() }) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt new file mode 100644 index 00000000000000..bc622a3d70955c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -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. + * + * @generated SignedSource<<8bbd7e8cc2c50cfbf44ba6671d095f23>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +package com.facebook.react.internal.featureflags + +class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAccessor { + private var currentProvider: ReactNativeFeatureFlagsProvider = ReactNativeFeatureFlagsDefaults() + + private val accessedFeatureFlags = mutableSetOf() + + private var commonTestFlagCache: Boolean? = null + + override fun commonTestFlag(): Boolean { + var cached = commonTestFlagCache + if (cached == null) { + cached = currentProvider.commonTestFlag() + accessedFeatureFlags.add("commonTestFlag") + commonTestFlagCache = cached + } + return cached + } + + override fun override(provider: ReactNativeFeatureFlagsProvider) { + if (accessedFeatureFlags.isNotEmpty()) { + val accessedFeatureFlagsStr = accessedFeatureFlags.joinToString(separator = ", ") { it } + throw IllegalStateException( + "Feature flags were accessed before being overridden: $accessedFeatureFlagsStr") + } + currentProvider = provider + } + + override fun dangerouslyReset() { + // We don't need to do anything here because `ReactNativeFeatureFlags` will + // just create a new instance of this class. + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt new file mode 100644 index 00000000000000..23cd91d0da34dc --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +package com.facebook.react.internal.featureflags + +interface ReactNativeFeatureFlagsProvider { + fun commonTestFlag(): Boolean +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 0736ec47faf1f9..59f047310fff76 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -70,6 +70,7 @@ add_react_common_subdir(hermes/inspector-modern) add_react_common_subdir(react/renderer/runtimescheduler) add_react_common_subdir(react/debug) add_react_common_subdir(react/config) +add_react_common_subdir(react/featureflags) add_react_common_subdir(react/renderer/animations) add_react_common_subdir(react/renderer/attributedstring) add_react_common_subdir(react/renderer/componentregistry) @@ -118,6 +119,7 @@ add_react_android_subdir(src/main/jni/react/uimanager) add_react_android_subdir(src/main/jni/react/mapbuffer) add_react_android_subdir(src/main/jni/react/reactnativeblob) add_react_android_subdir(src/main/jni/react/fabric) +add_react_android_subdir(src/main/jni/react/featureflags) add_react_android_subdir(src/main/jni/react/newarchdefaults) add_react_android_subdir(src/main/jni/react/hermes/reactexecutor) add_react_android_subdir(src/main/jni/react/hermes/instrumentation/) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt index afaca0d1916e52..38dfe037936abd 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt @@ -26,6 +26,7 @@ target_link_libraries( mapbufferjni react_codegen_rncore react_debug + react_featureflags react_render_animations react_render_attributedstring react_render_componentregistry diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/CMakeLists.txt new file mode 100644 index 00000000000000..859891bc984251 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/CMakeLists.txt @@ -0,0 +1,34 @@ +# 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) + +file(GLOB featureflagsjni_SRCS CONFIGURE_DEPENDS *.cpp) + +add_library( + featureflagsjni + SHARED + ${featureflagsjni_SRCS} +) + +target_include_directories(featureflagsjni PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries( + featureflagsjni + fb + fbjni + react_featureflags + reactnativejni +) + +target_compile_options( + featureflagsjni + PRIVATE + -DLOG_TAG=\"ReactNative\" + -fexceptions + -frtti + -std=c++20 + -Wall +) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp new file mode 100644 index 00000000000000..ea67eac8841533 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#include "JReactNativeFeatureFlagsCxxInterop.h" +#include +#include + +namespace facebook::react { + +bool JReactNativeFeatureFlagsCxxInterop::commonTestFlag( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::commonTestFlag(); +} + +void JReactNativeFeatureFlagsCxxInterop::override( + facebook::jni::alias_ref /*unused*/, + jni::alias_ref provider) { + ReactNativeFeatureFlags::override( + std::make_unique(provider)); +} + +void JReactNativeFeatureFlagsCxxInterop::dangerouslyReset( + facebook::jni::alias_ref /*unused*/) { + ReactNativeFeatureFlags::dangerouslyReset(); +} + +void JReactNativeFeatureFlagsCxxInterop::registerNatives() { + javaClassLocal()->registerNatives({ + makeNativeMethod( + "override", JReactNativeFeatureFlagsCxxInterop::override), + makeNativeMethod("dangerouslyReset", JReactNativeFeatureFlagsCxxInterop::dangerouslyReset), + makeNativeMethod( + "commonTestFlag", + JReactNativeFeatureFlagsCxxInterop::commonTestFlag), + }); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h new file mode 100644 index 00000000000000..0c02ca2f5ef89b --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class JReactNativeFeatureFlagsCxxInterop + : public jni::JavaClass { + public: + constexpr static auto kJavaDescriptor = + "Lcom/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop;"; + + static bool commonTestFlag( + facebook::jni::alias_ref); + + static void override( + facebook::jni::alias_ref, + jni::alias_ref provider); + + static void dangerouslyReset( + facebook::jni::alias_ref); + + static void registerNatives(); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/OnLoad.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/OnLoad.cpp new file mode 100644 index 00000000000000..40ff2f89d76257 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/OnLoad.cpp @@ -0,0 +1,16 @@ +/* + * 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 "JReactNativeFeatureFlagsCxxInterop.h" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return facebook::jni::initialize(vm, [] { + facebook::react::JReactNativeFeatureFlagsCxxInterop::registerNatives(); + }); +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/ReactNativeFeatureFlagsProviderHolder.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/ReactNativeFeatureFlagsProviderHolder.cpp new file mode 100644 index 00000000000000..99ded3d88d1a60 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/ReactNativeFeatureFlagsProviderHolder.cpp @@ -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. + * + * @generated SignedSource<<7a019bd967a22f93cd9e2e0ddb5201e3>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#include "ReactNativeFeatureFlagsProviderHolder.h" + +namespace facebook::react { + +bool ReactNativeFeatureFlagsProviderHolder::commonTestFlag() { + static const auto method = + facebook::jni::findClassStatic( + "com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider") + ->getMethod("commonTestFlag"); + return method(javaProvider_); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/ReactNativeFeatureFlagsProviderHolder.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/ReactNativeFeatureFlagsProviderHolder.h new file mode 100644 index 00000000000000..6b22df569e8862 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/ReactNativeFeatureFlagsProviderHolder.h @@ -0,0 +1,44 @@ +/* + * 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<<9a2d162cbd83f3b5122d0eb86f6f9177>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +/** + * Implementation of ReactNativeFeatureFlagsProvider that wraps a + * ReactNativeFeatureFlagsProvider Java object. + */ +class ReactNativeFeatureFlagsProviderHolder + : public ReactNativeFeatureFlagsProvider { + public: + explicit ReactNativeFeatureFlagsProviderHolder( + jni::alias_ref javaProvider) + : javaProvider_(make_global(javaProvider)){}; + + bool commonTestFlag() override; + + private: + jni::global_ref javaProvider_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt index b928ca15839865..e2f42ccae1937e 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/newarchdefaults/CMakeLists.txt @@ -17,6 +17,7 @@ target_include_directories(react_newarchdefaults PUBLIC .) target_link_libraries(react_newarchdefaults fbjni fabricjni + featureflagsjni react_nativemodule_core react_codegen_rncore react_cxxreactpackage diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/CMakeLists.txt index ab59671367e982..6073f255135dcb 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/CMakeLists.txt @@ -23,6 +23,7 @@ target_include_directories(rninstance PUBLIC .) target_link_libraries( rninstance fabricjni + featureflagsjni turbomodulejsijni fb jsi diff --git a/packages/react-native/ReactCommon/react/featureflags/CMakeLists.txt b/packages/react-native/ReactCommon/react/featureflags/CMakeLists.txt new file mode 100644 index 00000000000000..757b0ea1ff8ece --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/CMakeLists.txt @@ -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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -DLOG_TAG=\"ReactNative\") + +file(GLOB react_featureflags_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_featureflags SHARED ${react_featureflags_SRC}) + +target_include_directories(react_featureflags PUBLIC ${REACT_COMMON_DIR}) diff --git a/packages/react-native/ReactCommon/react/featureflags/React-featureflags.podspec b/packages/react-native/ReactCommon/react/featureflags/React-featureflags.podspec new file mode 100644 index 00000000000000..0cefca1a4ba800 --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/React-featureflags.podspec @@ -0,0 +1,44 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "..", "..", "..", "package.json"))) +version = package['version'] + +source = { :git => 'https://github.com/facebook/react-native.git' } +if version == '1000.0.0' + # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") +else + source[:tag] = "v#{version}" +end + +header_search_paths = [] + +if ENV['USE_FRAMEWORKS'] + header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../..\"" # this is needed to allow the feature flags access its own files +end + +Pod::Spec.new do |s| + s.name = "React-featureflags" + s.version = version + s.summary = "React Native internal feature flags" + s.homepage = "https://reactnative.dev/" + s.license = package["license"] + s.author = "Meta Platforms, Inc. and its affiliates" + s.platforms = min_supported_versions + s.source = source + s.source_files = "*.{cpp,h}" + s.header_dir = "react/featureflags" + s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + "HEADER_SEARCH_PATHS" => header_search_paths.join(' '), + "DEFINES_MODULE" => "YES" } + + if ENV['USE_FRAMEWORKS'] + s.module_name = "React_featureflags" + s.header_mappings_dir = "../.." + end +end diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp new file mode 100644 index 00000000000000..5325717c0f38b9 --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<<85960128b845159e7de70d0e85910dd9>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#include "ReactNativeFeatureFlags.h" + +namespace facebook::react { + +bool ReactNativeFeatureFlags::commonTestFlag() { + return getAccessor().commonTestFlag(); +} + +void ReactNativeFeatureFlags::override( + std::unique_ptr provider) { + getAccessor().override(std::move(provider)); +} + +void ReactNativeFeatureFlags::dangerouslyReset() { + getAccessor(true); +} + +ReactNativeFeatureFlagsAccessor& ReactNativeFeatureFlags::getAccessor( + bool reset) { + static std::unique_ptr accessor; + if (accessor == nullptr || reset) { + accessor = std::make_unique(); + } + return *accessor; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h new file mode 100644 index 00000000000000..68de969ae7e63a --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +/** + * This class provides access to internal React Native feature flags. + * + * All the methods are thread-safe if you handle `override` correctly. + */ +class ReactNativeFeatureFlags { + public: + /** + * Common flag for testing. Do NOT modify. + */ + static bool commonTestFlag(); + + /** + * Overrides the feature flags with the ones provided by the given provider + * (generally one that extends `ReactNativeFeatureFlagsDefaults`). + * + * This method must be called before you initialize the React Native runtime. + * + * @example + * + * ``` + * class MyReactNativeFeatureFlags : public ReactNativeFeatureFlagsDefaults { + * public: + * bool someFlag() override; + * }; + * + * ReactNativeFeatureFlags.override( + * std::make_unique()); + * ``` + */ + static void override( + std::unique_ptr provider); + + /** + * Removes the overridden feature flags and makes the API return default + * values again. + * + * This is **dangerous**. Use it only if you really understand the + * implications of this method. + * + * This should only be called if you destroy the React Native runtime and + * need to create a new one with different overrides. In that case, + * call `dangerouslyReset` after destroying the runtime and `override` again + * before initializing the new one. + */ + static void dangerouslyReset(); + + private: + ReactNativeFeatureFlags() = delete; + static ReactNativeFeatureFlagsAccessor& getAccessor(bool reset = false); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp new file mode 100644 index 00000000000000..28bd0bc1a4502d --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -0,0 +1,69 @@ +/* + * 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<<7f8b2ae5c0b18aceeaac0ee60e53bdbb>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#include +#include +#include +#include +#include "ReactNativeFeatureFlags.h" + +namespace facebook::react { + +ReactNativeFeatureFlagsAccessor::ReactNativeFeatureFlagsAccessor() + : currentProvider_(std::make_unique()) {} + +bool ReactNativeFeatureFlagsAccessor::commonTestFlag() { + if (!commonTestFlag_.has_value()) { + // Mark the flag as accessed. + static const char* flagName = "commonTestFlag"; + if (std::find( + accessedFeatureFlags_.begin(), + accessedFeatureFlags_.end(), + flagName) == accessedFeatureFlags_.end()) { + accessedFeatureFlags_.push_back(flagName); + } + + commonTestFlag_.emplace(currentProvider_->commonTestFlag()); + } + + return commonTestFlag_.value(); +} + +void ReactNativeFeatureFlagsAccessor::override( + std::unique_ptr provider) { + if (!accessedFeatureFlags_.empty()) { + std::ostringstream featureFlagListBuilder; + for (const auto& featureFlagName : accessedFeatureFlags_) { + featureFlagListBuilder << featureFlagName << ", "; + } + std::string accessedFeatureFlagNames = featureFlagListBuilder.str(); + if (!accessedFeatureFlagNames.empty()) { + accessedFeatureFlagNames = accessedFeatureFlagNames.substr( + 0, accessedFeatureFlagNames.size() - 2); + } + + throw std::runtime_error( + "Feature flags were accessed before being overridden: " + + accessedFeatureFlagNames); + } + + currentProvider_ = std::move(provider); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h new file mode 100644 index 00000000000000..8c869a3c5c4c45 --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -0,0 +1,44 @@ +/* + * 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<<3fa0171b372cf6aae150b2ec159fc41e>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class ReactNativeFeatureFlagsAccessor { + public: + ReactNativeFeatureFlagsAccessor(); + + bool commonTestFlag(); + + void override(std::unique_ptr provider); + + private: + std::unique_ptr currentProvider_; + std::vector accessedFeatureFlags_; + + std::optional commonTestFlag_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h new file mode 100644 index 00000000000000..5b196a5a5d073c --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<<76033e9fb87174da88e0ee922df28701>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#pragma once + +#include + +namespace facebook::react { + +class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { + public: + ReactNativeFeatureFlagsDefaults() = default; + + bool commonTestFlag() override { + return false; + } +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h new file mode 100644 index 00000000000000..5c1c30c24d5825 --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<<20dfc971dddc23a6d0cc55938b0d65b7>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#pragma once + +namespace facebook::react { + +class ReactNativeFeatureFlagsProvider { + public: + virtual ~ReactNativeFeatureFlagsProvider() = default; + + virtual bool commonTestFlag() = 0; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/featureflags/tests/ReactNativeFeatureFlagsTest.cpp b/packages/react-native/ReactCommon/react/featureflags/tests/ReactNativeFeatureFlagsTest.cpp new file mode 100644 index 00000000000000..c6ea0528ecf10e --- /dev/null +++ b/packages/react-native/ReactCommon/react/featureflags/tests/ReactNativeFeatureFlagsTest.cpp @@ -0,0 +1,101 @@ +/* + * 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 +#include + +namespace facebook::react { + +uint overrideAccessCount = 0; + +class ReactNativeFeatureFlagsTestOverrides + : public ReactNativeFeatureFlagsDefaults { + public: + bool commonTestFlag() override { + overrideAccessCount++; + return true; + } +}; + +class ReactNativeFeatureFlagsTest : public testing::Test { + protected: + void SetUp() override { + overrideAccessCount = 0; + } +}; + +TEST_F(ReactNativeFeatureFlagsTest, providesDefaults) { + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), false); +} + +TEST_F(ReactNativeFeatureFlagsTest, providesOverriddenValues) { + ReactNativeFeatureFlags::override( + std::make_unique()); + + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true); +} + +TEST_F(ReactNativeFeatureFlagsTest, preventsOverridingAfterAccess) { + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), false); + + try { + ReactNativeFeatureFlags::override( + std::make_unique()); + FAIL() + << "Expected ReactNativeFeatureFlags::override() to throw an exception"; + } catch (const std::runtime_error& e) { + EXPECT_STREQ( + "Feature flags were accessed before being overridden: commonTestFlag", + e.what()); + } + + // Overrides shouldn't be applied after they've been accessed + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), false); +} + +TEST_F(ReactNativeFeatureFlagsTest, cachesValuesFromOverride) { + ReactNativeFeatureFlags::override( + std::make_unique()); + + EXPECT_EQ(overrideAccessCount, 0); + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true); + EXPECT_EQ(overrideAccessCount, 1); + + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true); + EXPECT_EQ(overrideAccessCount, 1); +} + +TEST_F( + ReactNativeFeatureFlagsTest, + providesDefaulValuesAgainWhenResettingAfterAnOverride) { + ReactNativeFeatureFlags::override( + std::make_unique()); + + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true); + + ReactNativeFeatureFlags::dangerouslyReset(); + + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), false); +} + +TEST_F(ReactNativeFeatureFlagsTest, allowsOverridingAgainAfterReset) { + ReactNativeFeatureFlags::override( + std::make_unique()); + + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true); + + ReactNativeFeatureFlags::dangerouslyReset(); + + ReactNativeFeatureFlags::override( + std::make_unique()); + + EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp new file mode 100644 index 00000000000000..63bd0852c036a8 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#include "NativeReactNativeFeatureFlags.h" +#include + +#include "Plugins.h" + +std::shared_ptr +NativeReactNativeFeatureFlagsModuleProvider( + std::shared_ptr jsInvoker) { + return std::make_shared( + std::move(jsInvoker)); +} + +namespace facebook::react { + +NativeReactNativeFeatureFlags::NativeReactNativeFeatureFlags( + std::shared_ptr jsInvoker) + : NativeReactNativeFeatureFlagsCxxSpec(std::move(jsInvoker)) {} + +bool NativeReactNativeFeatureFlags::commonTestFlag( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::commonTestFlag(); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h new file mode 100644 index 00000000000000..44bb7771143fa9 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @generated SignedSource<<18a3543b75c44e00bdf73ca2f12d230c>> + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +#pragma once + +#include + +namespace facebook::react { + +class NativeReactNativeFeatureFlags + : public NativeReactNativeFeatureFlagsCxxSpec< + NativeReactNativeFeatureFlags>, + std::enable_shared_from_this { + public: + NativeReactNativeFeatureFlags(std::shared_ptr jsInvoker); + + bool commonTestFlag(jsi::Runtime& runtime); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/runtime/CMakeLists.txt b/packages/react-native/ReactCommon/react/runtime/CMakeLists.txt index 3ab0252454390a..85cd27912630aa 100644 --- a/packages/react-native/ReactCommon/react/runtime/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/runtime/CMakeLists.txt @@ -30,6 +30,7 @@ target_link_libraries( bridgeless jserrorhandler fabricjni + featureflagsjni turbomodulejsijni fb jsi diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 51b2e8a35968c3..86db951b093ff6 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -89,7 +89,9 @@ "types" ], "scripts": { - "prepack": "cp ../../README.md ." + "prepack": "cp ../../README.md .", + "featureflags-check": "node ./scripts/featureflags/update.js --verify-unchanged", + "featureflags-update": "node ./scripts/featureflags/update.js" }, "peerDependencies": { "react": "18.2.0" diff --git a/packages/react-native/scripts/cocoapods/__tests__/codegen_utils-test.rb b/packages/react-native/scripts/cocoapods/__tests__/codegen_utils-test.rb index cdb7474dbbf63f..194faddaa88bda 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/codegen_utils-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/codegen_utils-test.rb @@ -556,6 +556,7 @@ def get_podspec_fabric_and_script_phases(script_phases) 'React-Fabric': [], 'React-FabricImage': [], 'React-utils': [], + 'React-featureflags': [], 'React-debug': [], 'React-rendererdebug': [], }) @@ -569,13 +570,14 @@ def get_podspec_when_use_frameworks specs = get_podspec_no_fabric_no_script() specs["pod_target_xcconfig"]["FRAMEWORK_SEARCH_PATHS"].concat([]) - specs["pod_target_xcconfig"]["HEADER_SEARCH_PATHS"].concat(" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-FabricImage/React_FabricImage.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTFabric/RCTFabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-debug/React_debug.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-rendererdebug/React_rendererdebug.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-utils/React_utils.framework/Headers\"") + specs["pod_target_xcconfig"]["HEADER_SEARCH_PATHS"].concat(" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-FabricImage/React_FabricImage.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTFabric/RCTFabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-debug/React_debug.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-rendererdebug/React_rendererdebug.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-utils/React_utils.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-featureflags/React_featureflags.framework/Headers\"") specs[:dependencies].merge!({ 'React-graphics': [], 'React-Fabric': [], 'React-FabricImage': [], 'React-utils': [], + 'React-featureflags': [], 'React-debug': [], 'React-rendererdebug': [], }) diff --git a/packages/react-native/scripts/cocoapods/__tests__/new_architecture-test.rb b/packages/react-native/scripts/cocoapods/__tests__/new_architecture-test.rb index 2fd12ff5b535eb..f86cb8e3ef50bb 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/new_architecture-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/new_architecture-test.rb @@ -151,7 +151,7 @@ def test_installModulesDependencies_whenNewArchEnabledAndNewArchAndNoSearchPaths folly_compiler_flags = folly_config[:compiler_flags] assert_equal(spec.compiler_flags, NewArchitectureHelper.folly_compiler_flags) - assert_equal(spec.pod_target_xcconfig["HEADER_SEARCH_PATHS"], "\"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/Headers/Private/Yoga\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/fmt/include\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-FabricImage/React_FabricImage.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTFabric/RCTFabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-utils/React_utils.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-debug/React_debug.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-ImageManager/React_ImageManager.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-rendererdebug/React_rendererdebug.framework/Headers\"") + assert_equal(spec.pod_target_xcconfig["HEADER_SEARCH_PATHS"], "\"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/Headers/Private/Yoga\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/fmt/include\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-FabricImage/React_FabricImage.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTFabric/RCTFabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-utils/React_utils.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-featureflags/React_featureflags.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-debug/React_debug.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-ImageManager/React_ImageManager.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-rendererdebug/React_rendererdebug.framework/Headers\"") assert_equal(spec.pod_target_xcconfig["CLANG_CXX_LANGUAGE_STANDARD"], "c++20") assert_equal(spec.pod_target_xcconfig["OTHER_CPLUSPLUSFLAGS"], "$(inherited) -DRCT_NEW_ARCH_ENABLED=1 "+ folly_compiler_flags) assert_equal( @@ -171,6 +171,7 @@ def test_installModulesDependencies_whenNewArchEnabledAndNewArchAndNoSearchPaths { :dependency_name => "React-Fabric" }, { :dependency_name => "React-graphics" }, { :dependency_name => "React-utils" }, + { :dependency_name => "React-featureflags" }, { :dependency_name => "React-debug" }, { :dependency_name => "React-ImageManager" }, { :dependency_name => "React-rendererdebug" }, @@ -193,7 +194,7 @@ def test_installModulesDependencies_whenNewArchDisabledAndSearchPathsAndCompiler # Assert assert_equal(Helpers::Constants.folly_config[:compiler_flags], "#{NewArchitectureHelper.folly_compiler_flags}") - assert_equal(spec.pod_target_xcconfig["HEADER_SEARCH_PATHS"], "#{other_flags} \"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/Headers/Private/Yoga\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/fmt/include\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-FabricImage/React_FabricImage.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTFabric/RCTFabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-utils/React_utils.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-debug/React_debug.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-ImageManager/React_ImageManager.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-rendererdebug/React_rendererdebug.framework/Headers\"") + assert_equal(spec.pod_target_xcconfig["HEADER_SEARCH_PATHS"], "#{other_flags} \"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/Headers/Private/Yoga\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/fmt/include\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-FabricImage/React_FabricImage.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-RCTFabric/RCTFabric.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-utils/React_utils.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-featureflags/React_featureflags.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-debug/React_debug.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-ImageManager/React_ImageManager.framework/Headers\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-rendererdebug/React_rendererdebug.framework/Headers\"") assert_equal(spec.pod_target_xcconfig["CLANG_CXX_LANGUAGE_STANDARD"], "c++20") assert_equal( spec.dependencies, @@ -212,6 +213,7 @@ def test_installModulesDependencies_whenNewArchDisabledAndSearchPathsAndCompiler { :dependency_name => "React-Fabric" }, { :dependency_name => "React-graphics" }, { :dependency_name => "React-utils" }, + { :dependency_name => "React-featureflags" }, { :dependency_name => "React-debug" }, { :dependency_name => "React-ImageManager" }, { :dependency_name => "React-rendererdebug" }, diff --git a/packages/react-native/scripts/cocoapods/codegen_utils.rb b/packages/react-native/scripts/cocoapods/codegen_utils.rb index 8b92268fb2c8d3..ebdc01e9fef7ee 100644 --- a/packages/react-native/scripts/cocoapods/codegen_utils.rb +++ b/packages/react-native/scripts/cocoapods/codegen_utils.rb @@ -101,6 +101,7 @@ def get_react_codegen_spec(package_json_file, folly_version: get_folly_config()[ .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-debug", "React_debug", [])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-rendererdebug", "React_rendererdebug", [])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-utils", "React_utils", [])) + .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-featureflags", "React_featureflags", [])) .each { |search_path| header_search_paths << "\"#{search_path}\"" } @@ -140,6 +141,7 @@ def get_react_codegen_spec(package_json_file, folly_version: get_folly_config()[ 'React-FabricImage': [], 'React-debug': [], 'React-utils': [], + 'React-featureflags': [], } } diff --git a/packages/react-native/scripts/cocoapods/new_architecture.rb b/packages/react-native/scripts/cocoapods/new_architecture.rb index 95a1221deb827c..a37662bff600fc 100644 --- a/packages/react-native/scripts/cocoapods/new_architecture.rb +++ b/packages/react-native/scripts/cocoapods/new_architecture.rb @@ -90,6 +90,7 @@ def self.install_modules_dependencies(spec, new_arch_enabled, folly_version = ge .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-NativeModulesApple", "React_NativeModulesApple", [])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-RCTFabric", "RCTFabric", [])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-utils", "React_utils", [])) + .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-featureflags", "React_featureflags", [])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-debug", "React_debug", [])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-ImageManager", "React_ImageManager", [])) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-rendererdebug", "React_rendererdebug", [])) @@ -124,6 +125,7 @@ def self.install_modules_dependencies(spec, new_arch_enabled, folly_version = ge spec.dependency "React-Fabric" spec.dependency "React-graphics" spec.dependency "React-utils" + spec.dependency "React-featureflags" spec.dependency "React-debug" spec.dependency "React-ImageManager" spec.dependency "React-rendererdebug" diff --git a/packages/react-native/scripts/featureflags/README.md b/packages/react-native/scripts/featureflags/README.md new file mode 100644 index 00000000000000..42e18adba8e7e0 --- /dev/null +++ b/packages/react-native/scripts/featureflags/README.md @@ -0,0 +1,146 @@ +# Feature Flags + +Feature flags are values that determine the behavior of specific parts of React +Native. This directory contains the configuration for those values, and scripts +to generate files for different languages to access and customize them. + +There are 2 types of feature flags: +* Common: can be accessed from any language and they provide consistent values +everywhere. +* JS-only: they can only be accessed and customized from JavaScript. + +## Definition + +The source of truth for the definition of the flags is the file `ReactNativeFeatureFlags.json` +in this directory. That JSON file should have the following structure: + +```flow +type Config = { + common: FeatureFlagsList, + jsOnly: FeatureFlagsList, +}; + +type FeatureFlagsList = { + [flagName: string]: { + description: string, + defaultValue: boolean | number | string, + }, +}; +``` + +Example: +```json +{ + "common": { + "enableMicrotasks": { + "description": "Enable the use of microtasks in the JS runtime.", + "defaultValue": false + } + }, + "jsOnly": { + "enableAccessToHostTreeInFabric": { + "description": "Enables access to the host tree in Fabric using DOM-compatible APIs.", + "defaultValue": false + } + } +} +``` + +After any changes to this definitions, the code that provides access to them +must be regenerated executing the `update` script in this directory. + +## Access + +### C++ / Objective-C + +```c++ +#include + +if (ReactNativeFeatureFlags::enableMicrotasks()) { + // do something +} +``` + +### Kotlin + +```kotlin +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags + +fun someMethod() { + if (ReactNativeFeatureFlags.enableMicrotasks()) { + // do something + } +} +``` + +### JavaScript + +```javascript +import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags'; + +if (ReactNativeFeatureFlags.enableMicrotasks()) { + // Native flag +} + +if (ReactNativeFeatureFlags.enableAccessToHostTreeInFabric()) { + // JS-only flag +} +``` + +## Customization + +### C++/Objective-C + +```c++ +#include +#include + +class CustomReactNativeFeatureFlags : public ReactNativeFeatureFlagsDefaults { + public: + CustomReactNativeFeatureFlags(); + + bool enableMicrotasks() override { + return true; + } +} + +ReactNativeFeatureFlags::override(std::make_unique()); +``` + +### Kotlin + +```kotlin +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults + +fun overrideFeatureFlags() { + ReactNativeFeatureFlags.override(object : ReactNativeFeatureFlagsDefaults() { + override fun useMicrotasks(): Boolean = true + }) +} +``` + +### JavaScript + +```javascript +import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags'; + +ReactNativeFeatureFlags.override({ + enableAccessToHostTreeInFabric: () => true, +}); +``` + +## Architecture + +The architecture of this feature flags system can be described as follows: +* A shared C++ core, where we provide access to the flags and allow +customizations. +* A Kotlin/Java interface that allows accessing and customizing the values in +the C++ core (via JNI). +* A JavaScript interface that allows accessing the common values (via a native +module) and accessing and customizing the JS-only values. + +![Diagram of the architecture of feature flags in React Native](./assets/react-native-feature-flags-architecture.excalidraw-embedded.png) + +_This image has an embedded [Excalidraw](https://www.excalidraw.com) diagram, +so you can upload it there if you need to make further modifications._ diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json new file mode 100644 index 00000000000000..974c2f6fce5c13 --- /dev/null +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json @@ -0,0 +1,14 @@ +{ + "common": { + "commonTestFlag": { + "description": "Common flag for testing. Do NOT modify.", + "defaultValue": false + } + }, + "jsOnly": { + "jsOnlyTestFlag": { + "description": "JS-only flag for testing. Do NOT modify.", + "defaultValue": false + } + } +} diff --git a/packages/react-native/scripts/featureflags/assets/react-native-feature-flags-architecture.excalidraw-embedded.png b/packages/react-native/scripts/featureflags/assets/react-native-feature-flags-architecture.excalidraw-embedded.png new file mode 100644 index 00000000000000..cfebac31b929d0 Binary files /dev/null and b/packages/react-native/scripts/featureflags/assets/react-native-feature-flags-architecture.excalidraw-embedded.png differ diff --git a/packages/react-native/scripts/featureflags/generateAndroidModules.js b/packages/react-native/scripts/featureflags/generateAndroidModules.js new file mode 100644 index 00000000000000..edf0ce966a2ee5 --- /dev/null +++ b/packages/react-native/scripts/featureflags/generateAndroidModules.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. + * + * @format + */ + +'use strict'; + +const JReactNativeFeatureFlagsCxxInteropCPP = require('./templates/android/JReactNativeFeatureFlagsCxxInterop.cpp-template'); +const JReactNativeFeatureFlagsCxxInteropH = require('./templates/android/JReactNativeFeatureFlagsCxxInterop.h-template'); +const ReactNativeFeatureFlagsKt = require('./templates/android/ReactNativeFeatureFlags.kt-template'); +const ReactNativeFeatureFlagsCxxAccessorKt = require('./templates/android/ReactNativeFeatureFlagsCxxAccessor.kt-template'); +const ReactNativeFeatureFlagsCxxInteropKt = require('./templates/android/ReactNativeFeatureFlagsCxxInterop.kt-template'); +const ReactNativeFeatureFlagsDefaultsKt = require('./templates/android/ReactNativeFeatureFlagsDefaults.kt-template'); +const ReactNativeFeatureFlagsLocalAccessorKt = require('./templates/android/ReactNativeFeatureFlagsLocalAccessor.kt-template'); +const ReactNativeFeatureFlagsProviderKt = require('./templates/android/ReactNativeFeatureFlagsProvider.kt-template'); +const ReactNativeFeatureFlagsProviderHolderCPP = require('./templates/android/ReactNativeFeatureFlagsProviderHolder.cpp-template'); +const ReactNativeFeatureFlagsProviderHolderH = require('./templates/android/ReactNativeFeatureFlagsProviderHolder.h-template'); +const path = require('path'); + +module.exports = function generateandroidModules( + generatorConfig, + featureFlagsConfig, +) { + return { + [path.join(generatorConfig.androidPath, 'ReactNativeFeatureFlags.kt')]: + ReactNativeFeatureFlagsKt(featureFlagsConfig), + [path.join( + generatorConfig.androidPath, + 'ReactNativeFeatureFlagsCxxAccessor.kt', + )]: ReactNativeFeatureFlagsCxxAccessorKt(featureFlagsConfig), + [path.join( + generatorConfig.androidPath, + 'ReactNativeFeatureFlagsLocalAccessor.kt', + )]: ReactNativeFeatureFlagsLocalAccessorKt(featureFlagsConfig), + [path.join( + generatorConfig.androidPath, + 'ReactNativeFeatureFlagsCxxInterop.kt', + )]: ReactNativeFeatureFlagsCxxInteropKt(featureFlagsConfig), + [path.join( + generatorConfig.androidPath, + 'ReactNativeFeatureFlagsDefaults.kt', + )]: ReactNativeFeatureFlagsDefaultsKt(featureFlagsConfig), + [path.join( + generatorConfig.androidPath, + 'ReactNativeFeatureFlagsProvider.kt', + )]: ReactNativeFeatureFlagsProviderKt(featureFlagsConfig), + [path.join( + generatorConfig.androidJniPath, + 'ReactNativeFeatureFlagsProviderHolder.h', + )]: ReactNativeFeatureFlagsProviderHolderH(featureFlagsConfig), + [path.join( + generatorConfig.androidJniPath, + 'ReactNativeFeatureFlagsProviderHolder.cpp', + )]: ReactNativeFeatureFlagsProviderHolderCPP(featureFlagsConfig), + [path.join( + generatorConfig.androidJniPath, + 'JReactNativeFeatureFlagsCxxInterop.h', + )]: JReactNativeFeatureFlagsCxxInteropH(featureFlagsConfig), + [path.join( + generatorConfig.androidJniPath, + 'JReactNativeFeatureFlagsCxxInterop.cpp', + )]: JReactNativeFeatureFlagsCxxInteropCPP(featureFlagsConfig), + }; +}; diff --git a/packages/react-native/scripts/featureflags/generateCommonCxxModules.js b/packages/react-native/scripts/featureflags/generateCommonCxxModules.js new file mode 100644 index 00000000000000..4c021fc1d6445f --- /dev/null +++ b/packages/react-native/scripts/featureflags/generateCommonCxxModules.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const ReactNativeFeatureFlagsCPP = require('./templates/common-cxx/ReactNativeFeatureFlags.cpp-template'); +const ReactNativeFeatureFlagsH = require('./templates/common-cxx/ReactNativeFeatureFlags.h-template'); +const ReactNativeFeatureFlagsAccessorCPP = require('./templates/common-cxx/ReactNativeFeatureFlagsAccessor.cpp-template'); +const ReactNativeFeatureFlagsAccessorH = require('./templates/common-cxx/ReactNativeFeatureFlagsAccessor.h-template'); +const ReactNativeFeatureFlagsDefaultsH = require('./templates/common-cxx/ReactNativeFeatureFlagsDefaults.h-template'); +const ReactNativeFeatureFlagsProviderH = require('./templates/common-cxx/ReactNativeFeatureFlagsProvider.h-template'); +const path = require('path'); + +module.exports = function generateCommonCxxModules( + generatorConfig, + featureFlagsConfig, +) { + return { + [path.join(generatorConfig.commonCxxPath, 'ReactNativeFeatureFlags.h')]: + ReactNativeFeatureFlagsH(featureFlagsConfig), + [path.join(generatorConfig.commonCxxPath, 'ReactNativeFeatureFlags.cpp')]: + ReactNativeFeatureFlagsCPP(featureFlagsConfig), + [path.join( + generatorConfig.commonCxxPath, + 'ReactNativeFeatureFlagsAccessor.h', + )]: ReactNativeFeatureFlagsAccessorH(featureFlagsConfig), + [path.join( + generatorConfig.commonCxxPath, + 'ReactNativeFeatureFlagsAccessor.cpp', + )]: ReactNativeFeatureFlagsAccessorCPP(featureFlagsConfig), + [path.join( + generatorConfig.commonCxxPath, + 'ReactNativeFeatureFlagsDefaults.h', + )]: ReactNativeFeatureFlagsDefaultsH(featureFlagsConfig), + [path.join( + generatorConfig.commonCxxPath, + 'ReactNativeFeatureFlagsProvider.h', + )]: ReactNativeFeatureFlagsProviderH(featureFlagsConfig), + }; +}; diff --git a/packages/react-native/scripts/featureflags/generateFiles.js b/packages/react-native/scripts/featureflags/generateFiles.js new file mode 100644 index 00000000000000..91a974354931f5 --- /dev/null +++ b/packages/react-native/scripts/featureflags/generateFiles.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +const generateAndroidModules = require('./generateAndroidModules'); +const generateCommonCxxModules = require('./generateCommonCxxModules'); +const generateJavaScriptModules = require('./generateJavaScriptModules'); +const fs = require('fs'); + +module.exports = function generateFiles(generatorConfig, generatorOptions) { + const userDefinedFeatureFlagsConfig = JSON.parse( + fs.readFileSync(generatorConfig.configPath, 'utf8'), + ); + + const featureFlagsConfig = Object.assign( + {jsOnly: {}, common: {}}, + userDefinedFeatureFlagsConfig, + ); + + fs.mkdirSync(generatorConfig.jsPath, {recursive: true}); + fs.mkdirSync(generatorConfig.commonCxxPath, {recursive: true}); + fs.mkdirSync(generatorConfig.commonNativeModuleCxxPath, {recursive: true}); + fs.mkdirSync(generatorConfig.androidPath, {recursive: true}); + fs.mkdirSync(generatorConfig.androidJniPath, {recursive: true}); + + const jsModules = generateJavaScriptModules( + generatorConfig, + featureFlagsConfig, + ); + + const commonCxxModules = generateCommonCxxModules( + generatorConfig, + featureFlagsConfig, + ); + + const androidModules = generateAndroidModules( + generatorConfig, + featureFlagsConfig, + ); + + const generatedFiles = {...jsModules, ...commonCxxModules, ...androidModules}; + + if (generatorOptions.verifyUnchanged) { + const existingModules = {}; + for (const moduleName of Object.keys(generatedFiles)) { + const existingModule = fs.readFileSync(moduleName, 'utf8'); + existingModules[moduleName] = existingModule; + } + + const changedFiles = []; + for (const moduleName of Object.keys(generatedFiles)) { + const module = generatedFiles[moduleName]; + const existingModule = existingModules[moduleName]; + + if (module !== existingModule) { + changedFiles.push(moduleName); + } + } + + if (changedFiles.length > 0) { + const changedFilesStr = changedFiles + .map(changedFile => ' ' + changedFile) + .join('\n'); + + throw new Error( + `Detected changes in generated files for feature flags:\n${changedFilesStr}\n\n` + + 'Please rerun `yarn featureflags-update` and commit the changes.', + ); + } + + return; + } + + for (const [modulePath, moduleContents] of Object.entries(generatedFiles)) { + fs.writeFileSync(modulePath, moduleContents, 'utf8'); + } +}; diff --git a/packages/react-native/scripts/featureflags/generateJavaScriptModules.js b/packages/react-native/scripts/featureflags/generateJavaScriptModules.js new file mode 100644 index 00000000000000..63b11b912eb0ec --- /dev/null +++ b/packages/react-native/scripts/featureflags/generateJavaScriptModules.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const NativeReactNativeFeatureFlagsCPP = require('./templates/js/NativeReactNativeFeatureFlags.cpp-template'); +const NativeReactNativeFeatureFlagsH = require('./templates/js/NativeReactNativeFeatureFlags.h-template'); +const NativeReactNativeFeatureFlagsJS = require('./templates/js/NativeReactNativeFeatureFlags.js-template'); +const ReactNativeFeatureFlagsJS = require('./templates/js/ReactNativeFeatureFlags.js-template'); +const path = require('path'); + +module.exports = function generateCommonCxxModules( + generatorConfig, + featureFlagsConfig, +) { + return { + [path.join(generatorConfig.jsPath, 'ReactNativeFeatureFlags.js')]: + ReactNativeFeatureFlagsJS(featureFlagsConfig), + [path.join(generatorConfig.jsPath, 'NativeReactNativeFeatureFlags.js')]: + NativeReactNativeFeatureFlagsJS(featureFlagsConfig), + [path.join( + generatorConfig.commonNativeModuleCxxPath, + 'NativeReactNativeFeatureFlags.h', + )]: NativeReactNativeFeatureFlagsH(featureFlagsConfig), + [path.join( + generatorConfig.commonNativeModuleCxxPath, + 'NativeReactNativeFeatureFlags.cpp', + )]: NativeReactNativeFeatureFlagsCPP(featureFlagsConfig), + }; +}; diff --git a/packages/react-native/scripts/featureflags/templates/android/JReactNativeFeatureFlagsCxxInterop.cpp-template.js b/packages/react-native/scripts/featureflags/templates/android/JReactNativeFeatureFlagsCxxInterop.cpp-template.js new file mode 100644 index 00000000000000..35e95e17fbf20e --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/JReactNativeFeatureFlagsCxxInterop.cpp-template.js @@ -0,0 +1,77 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +#include "JReactNativeFeatureFlagsCxxInterop.h" +#include +#include + +namespace facebook::react { + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + `${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} JReactNativeFeatureFlagsCxxInterop::${flagName}( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::${flagName}(); +}`, + ) + .join('\n\n')} + +void JReactNativeFeatureFlagsCxxInterop::override( + facebook::jni::alias_ref /*unused*/, + jni::alias_ref provider) { + ReactNativeFeatureFlags::override( + std::make_unique(provider)); +} + +void JReactNativeFeatureFlagsCxxInterop::dangerouslyReset( + facebook::jni::alias_ref /*unused*/) { + ReactNativeFeatureFlags::dangerouslyReset(); +} + +void JReactNativeFeatureFlagsCxxInterop::registerNatives() { + javaClassLocal()->registerNatives({ + makeNativeMethod( + "override", JReactNativeFeatureFlagsCxxInterop::override), + makeNativeMethod("dangerouslyReset", JReactNativeFeatureFlagsCxxInterop::dangerouslyReset), +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` makeNativeMethod( + "${flagName}", + JReactNativeFeatureFlagsCxxInterop::${flagName}),`, + ) + .join('\n')} + }); +} + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/android/JReactNativeFeatureFlagsCxxInterop.h-template.js b/packages/react-native/scripts/featureflags/templates/android/JReactNativeFeatureFlagsCxxInterop.h-template.js new file mode 100644 index 00000000000000..580e47fcaab361 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/JReactNativeFeatureFlagsCxxInterop.h-template.js @@ -0,0 +1,64 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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 { + +class JReactNativeFeatureFlagsCxxInterop + : public jni::JavaClass { + public: + constexpr static auto kJavaDescriptor = + "Lcom/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop;"; + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` static ${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} ${flagName}( + facebook::jni::alias_ref);`, + ) + .join('\n\n')} + + static void override( + facebook::jni::alias_ref, + jni::alias_ref provider); + + static void dangerouslyReset( + facebook::jni::alias_ref); + + static void registerNatives(); +}; + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlags.kt-template.js b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlags.kt-template.js new file mode 100644 index 00000000000000..21bc02730a5db9 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlags.kt-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. + * + * @format + */ + +'use strict'; + +const {DO_NOT_MODIFY_COMMENT} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +package com.facebook.react.internal.featureflags + +/** + * This object provides access to internal React Native feature flags. + * + * All the methods are thread-safe if you handle \`override\` correctly. + */ +object ReactNativeFeatureFlags { + private var accessorProvider: () -> ReactNativeFeatureFlagsAccessor = { ReactNativeFeatureFlagsCxxAccessor() } + private var accessor: ReactNativeFeatureFlagsAccessor = accessorProvider() + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` /** + * ${flagConfig.description} + */ + fun ${flagName}() = accessor.${flagName}()`, + ) + .join('\n\n')} + + /** + * Overrides the feature flags with the ones provided by the given provider + * (generally one that extends \`ReactNativeFeatureFlagsDefaults\`). + * + * This method must be called before you initialize the React Native runtime. + * + * @example + * + * \`\`\` + * ReactNativeFeatureFlags.override(object : ReactNativeFeatureFlagsDefaults() { + * override fun someFlag(): Boolean = true // or a dynamic value + * }) + * \`\`\` + */ + fun override(provider: ReactNativeFeatureFlagsProvider) = accessor.override(provider) + + /** + * Removes the overridden feature flags and makes the API return default + * values again. + * + * This should only be called if you destroy the React Native runtime and + * need to create a new one with different overrides. In that case, + * call \`dangerouslyReset\` after destroying the runtime and \`override\` + * again before initializing the new one. + */ + fun dangerouslyReset() { + // This is necessary when the accessor interops with C++ and we need to + // remove the overrides set there. + accessor.dangerouslyReset() + + // This discards the cached values and the overrides set in the JVM. + accessor = accessorProvider() + } + + /** + * This is just used to replace the default ReactNativeFeatureFlagsCxxAccessor + * that uses JNI with a version that doesn't, to simplify testing. + */ + internal fun setAccessorProvider(newAccessorProvider: () -> ReactNativeFeatureFlagsAccessor) { + accessorProvider = newAccessorProvider + accessor = accessorProvider() + } +} +`); diff --git a/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsCxxAccessor.kt-template.js b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsCxxAccessor.kt-template.js new file mode 100644 index 00000000000000..a5fe55b8579d29 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsCxxAccessor.kt-template.js @@ -0,0 +1,60 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getKotlinTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +package com.facebook.react.internal.featureflags + +class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccessor { +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` private var ${flagName}Cache: ${getKotlinTypeFromDefaultValue( + flagConfig.defaultValue, + )}? = null`, + ) + .join('\n')} + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => ` override fun ${flagName}(): Boolean { + var cached = ${flagName}Cache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.${flagName}() + ${flagName}Cache = cached + } + return cached + }`, + ) + .join('\n\n')} + + override fun override(provider: ReactNativeFeatureFlagsProvider) = + ReactNativeFeatureFlagsCxxInterop.override(provider as Any) + + override fun dangerouslyReset() = ReactNativeFeatureFlagsCxxInterop.dangerouslyReset() +} +`); diff --git a/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsCxxInterop.kt-template.js b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsCxxInterop.kt-template.js new file mode 100644 index 00000000000000..0eb81df5190a9f --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsCxxInterop.kt-template.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getKotlinTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +package com.facebook.react.internal.featureflags + +import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.soloader.SoLoader + +@DoNotStrip +object ReactNativeFeatureFlagsCxxInterop { + init { + SoLoader.loadLibrary("reactfeatureflagsjni") + } + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` @DoNotStrip @JvmStatic external fun ${flagName}(): ${getKotlinTypeFromDefaultValue( + flagConfig.defaultValue, + )}`, + ) + .join('\n')} + + @DoNotStrip @JvmStatic external fun override(provider: Any) + + @DoNotStrip @JvmStatic external fun dangerouslyReset() +} +`); diff --git a/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsDefaults.kt-template.js b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsDefaults.kt-template.js new file mode 100644 index 00000000000000..e7f11b17fbc2d6 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsDefaults.kt-template.js @@ -0,0 +1,45 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getKotlinTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +package com.facebook.react.internal.featureflags + +open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvider { + // We could use JNI to get the defaults from C++, + // but that is more expensive than just duplicating the defaults here. + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` override fun ${flagName}(): ${getKotlinTypeFromDefaultValue( + flagConfig.defaultValue, + )} = ${JSON.stringify(flagConfig.defaultValue)}`, + ) + .join('\n')} +} +`); diff --git a/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsLocalAccessor.kt-template.js b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsLocalAccessor.kt-template.js new file mode 100644 index 00000000000000..3271ac92931575 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsLocalAccessor.kt-template.js @@ -0,0 +1,74 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getKotlinTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +package com.facebook.react.internal.featureflags + +class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAccessor { + private var currentProvider: ReactNativeFeatureFlagsProvider = ReactNativeFeatureFlagsDefaults() + + private val accessedFeatureFlags = mutableSetOf() + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` private var ${flagName}Cache: ${getKotlinTypeFromDefaultValue( + flagConfig.defaultValue, + )}? = null`, + ) + .join('\n')} + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => ` override fun ${flagName}(): Boolean { + var cached = ${flagName}Cache + if (cached == null) { + cached = currentProvider.${flagName}() + accessedFeatureFlags.add("${flagName}") + ${flagName}Cache = cached + } + return cached + }`, + ) + .join('\n\n')} + + override fun override(provider: ReactNativeFeatureFlagsProvider) { + if (accessedFeatureFlags.isNotEmpty()) { + val accessedFeatureFlagsStr = accessedFeatureFlags.joinToString(separator = ", ") { it } + throw IllegalStateException( + "Feature flags were accessed before being overridden: $accessedFeatureFlagsStr") + } + currentProvider = provider + } + + override fun dangerouslyReset() { + // We don't need to do anything here because \`ReactNativeFeatureFlags\` will + // just create a new instance of this class. + } +} +`); diff --git a/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsProvider.kt-template.js b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsProvider.kt-template.js new file mode 100644 index 00000000000000..76f472ee5e76c9 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsProvider.kt-template.js @@ -0,0 +1,42 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getKotlinTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +package com.facebook.react.internal.featureflags + +interface ReactNativeFeatureFlagsProvider { +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` fun ${flagName}(): ${getKotlinTypeFromDefaultValue( + flagConfig.defaultValue, + )}`, + ) + .join('\n')} +} +`); diff --git a/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsProviderHolder.cpp-template.js b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsProviderHolder.cpp-template.js new file mode 100644 index 00000000000000..abb4202f61a5bf --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsProviderHolder.cpp-template.js @@ -0,0 +1,50 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +#include "ReactNativeFeatureFlagsProviderHolder.h" + +namespace facebook::react { + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + `${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} ReactNativeFeatureFlagsProviderHolder::${flagName}() { + static const auto method = + facebook::jni::findClassStatic( + "com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider") + ->getMethod("${flagName}"); + return method(javaProvider_); +}`, + ) + .join('\n\n')} + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsProviderHolder.h-template.js b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsProviderHolder.h-template.js new file mode 100644 index 00000000000000..60e3272c90362c --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/android/ReactNativeFeatureFlagsProviderHolder.h-template.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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 { + +/** + * Implementation of ReactNativeFeatureFlagsProvider that wraps a + * ReactNativeFeatureFlagsProvider Java object. + */ +class ReactNativeFeatureFlagsProviderHolder + : public ReactNativeFeatureFlagsProvider { + public: + explicit ReactNativeFeatureFlagsProviderHolder( + jni::alias_ref javaProvider) + : javaProvider_(make_global(javaProvider)){}; + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` ${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} ${flagName}() override;`, + ) + .join('\n')} + + private: + jni::global_ref javaProvider_; +}; + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlags.cpp-template.js b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlags.cpp-template.js new file mode 100644 index 00000000000000..3bd2d55eddc9c5 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlags.cpp-template.js @@ -0,0 +1,64 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +#include "ReactNativeFeatureFlags.h" + +namespace facebook::react { + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + `${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} ReactNativeFeatureFlags::${flagName}() { + return getAccessor().${flagName}(); +}`, + ) + .join('\n\n')} + +void ReactNativeFeatureFlags::override( + std::unique_ptr provider) { + getAccessor().override(std::move(provider)); +} + +void ReactNativeFeatureFlags::dangerouslyReset() { + getAccessor(true); +} + +ReactNativeFeatureFlagsAccessor& ReactNativeFeatureFlags::getAccessor( + bool reset) { + static std::unique_ptr accessor; + if (accessor == nullptr || reset) { + accessor = std::make_unique(); + } + return *accessor; +} + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlags.h-template.js b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlags.h-template.js new file mode 100644 index 00000000000000..829f61822a4254 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlags.h-template.js @@ -0,0 +1,96 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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 +#include + +namespace facebook::react { + +/** + * This class provides access to internal React Native feature flags. + * + * All the methods are thread-safe if you handle \`override\` correctly. + */ +class ReactNativeFeatureFlags { + public: +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` /** + * ${flagConfig.description} + */ + static ${getCxxTypeFromDefaultValue(flagConfig.defaultValue)} ${flagName}();`, + ) + .join('\n\n')} + + /** + * Overrides the feature flags with the ones provided by the given provider + * (generally one that extends \`ReactNativeFeatureFlagsDefaults\`). + * + * This method must be called before you initialize the React Native runtime. + * + * @example + * + * \`\`\` + * class MyReactNativeFeatureFlags : public ReactNativeFeatureFlagsDefaults { + * public: + * bool someFlag() override; + * }; + * + * ReactNativeFeatureFlags.override( + * std::make_unique()); + * \`\`\` + */ + static void override( + std::unique_ptr provider); + + /** + * Removes the overridden feature flags and makes the API return default + * values again. + * + * This is **dangerous**. Use it only if you really understand the + * implications of this method. + * + * This should only be called if you destroy the React Native runtime and + * need to create a new one with different overrides. In that case, + * call \`dangerouslyReset\` after destroying the runtime and \`override\` again + * before initializing the new one. + */ + static void dangerouslyReset(); + + private: + ReactNativeFeatureFlags() = delete; + static ReactNativeFeatureFlagsAccessor& getAccessor(bool reset = false); +}; + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsAccessor.cpp-template.js b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsAccessor.cpp-template.js new file mode 100644 index 00000000000000..0c83a6b546045a --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsAccessor.cpp-template.js @@ -0,0 +1,87 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +#include +#include +#include +#include +#include "ReactNativeFeatureFlags.h" + +namespace facebook::react { + +ReactNativeFeatureFlagsAccessor::ReactNativeFeatureFlagsAccessor() + : currentProvider_(std::make_unique()) {} + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + `${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} ReactNativeFeatureFlagsAccessor::${flagName}() { + if (!${flagName}_.has_value()) { + // Mark the flag as accessed. + static const char* flagName = "${flagName}"; + if (std::find( + accessedFeatureFlags_.begin(), + accessedFeatureFlags_.end(), + flagName) == accessedFeatureFlags_.end()) { + accessedFeatureFlags_.push_back(flagName); + } + + ${flagName}_.emplace(currentProvider_->${flagName}()); + } + + return ${flagName}_.value(); +}`, + ) + .join('\n\n')} + +void ReactNativeFeatureFlagsAccessor::override( + std::unique_ptr provider) { + if (!accessedFeatureFlags_.empty()) { + std::ostringstream featureFlagListBuilder; + for (const auto& featureFlagName : accessedFeatureFlags_) { + featureFlagListBuilder << featureFlagName << ", "; + } + std::string accessedFeatureFlagNames = featureFlagListBuilder.str(); + if (!accessedFeatureFlagNames.empty()) { + accessedFeatureFlagNames = accessedFeatureFlagNames.substr( + 0, accessedFeatureFlagNames.size() - 2); + } + + throw std::runtime_error( + "Feature flags were accessed before being overridden: " + + accessedFeatureFlagNames); + } + + currentProvider_ = std::move(provider); +} + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsAccessor.h-template.js b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsAccessor.h-template.js new file mode 100644 index 00000000000000..e2cd096586da22 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsAccessor.h-template.js @@ -0,0 +1,67 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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 +#include +#include + +namespace facebook::react { + +class ReactNativeFeatureFlagsAccessor { + public: + ReactNativeFeatureFlagsAccessor(); + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` ${getCxxTypeFromDefaultValue(flagConfig.defaultValue)} ${flagName}();`, + ) + .join('\n')} + + void override(std::unique_ptr provider); + + private: + std::unique_ptr currentProvider_; + std::vector accessedFeatureFlags_; + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` std::optional<${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )}> ${flagName}_;`, + ) + .join('\n')} +}; + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsDefaults.h-template.js b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsDefaults.h-template.js new file mode 100644 index 00000000000000..eec42229555d3c --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsDefaults.h-template.js @@ -0,0 +1,53 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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 + +namespace facebook::react { + +class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { + public: + ReactNativeFeatureFlagsDefaults() = default; + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` ${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} ${flagName}() override { + return ${JSON.stringify(flagConfig.defaultValue)}; + }`, + ) + .join('\n\n')} +}; + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsProvider.h-template.js b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsProvider.h-template.js new file mode 100644 index 00000000000000..1f01590b07b304 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/common-cxx/ReactNativeFeatureFlagsProvider.h-template.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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 + +namespace facebook::react { + +class ReactNativeFeatureFlagsProvider { + public: + virtual ~ReactNativeFeatureFlagsProvider() = default; + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` virtual ${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} ${flagName}() = 0;`, + ) + .join('\n')} +}; + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/js/NativeReactNativeFeatureFlags.cpp-template.js b/packages/react-native/scripts/featureflags/templates/js/NativeReactNativeFeatureFlags.cpp-template.js new file mode 100644 index 00000000000000..9844c5a5b4a7dc --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/js/NativeReactNativeFeatureFlags.cpp-template.js @@ -0,0 +1,61 @@ +/** + * 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 + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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} + +#include "NativeReactNativeFeatureFlags.h" +#include + +#include "Plugins.h" + +std::shared_ptr +NativeReactNativeFeatureFlagsModuleProvider( + std::shared_ptr jsInvoker) { + return std::make_shared( + std::move(jsInvoker)); +} + +namespace facebook::react { + +NativeReactNativeFeatureFlags::NativeReactNativeFeatureFlags( + std::shared_ptr jsInvoker) + : NativeReactNativeFeatureFlagsCxxSpec(std::move(jsInvoker)) {} + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + `${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} NativeReactNativeFeatureFlags::${flagName}( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::${flagName}(); +}`, + ) + .join('\n\n')} + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/js/NativeReactNativeFeatureFlags.h-template.js b/packages/react-native/scripts/featureflags/templates/js/NativeReactNativeFeatureFlags.h-template.js new file mode 100644 index 00000000000000..0bf7a9b28dc17f --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/js/NativeReactNativeFeatureFlags.h-template.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const { + DO_NOT_MODIFY_COMMENT, + getCxxTypeFromDefaultValue, +} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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 + +namespace facebook::react { + +class NativeReactNativeFeatureFlags + : public NativeReactNativeFeatureFlagsCxxSpec< + NativeReactNativeFeatureFlags>, + std::enable_shared_from_this { + public: + NativeReactNativeFeatureFlags(std::shared_ptr jsInvoker); + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` ${getCxxTypeFromDefaultValue( + flagConfig.defaultValue, + )} ${flagName}(jsi::Runtime& runtime);`, + ) + .join('\n\n')} +}; + +} // namespace facebook::react +`); diff --git a/packages/react-native/scripts/featureflags/templates/js/NativeReactNativeFeatureFlags.js-template.js b/packages/react-native/scripts/featureflags/templates/js/NativeReactNativeFeatureFlags.js-template.js new file mode 100644 index 00000000000000..3bd1b05daac1ea --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/js/NativeReactNativeFeatureFlags.js-template.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const {DO_NOT_MODIFY_COMMENT} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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()} + * @flow strict-local + */ + +${DO_NOT_MODIFY_COMMENT} + +import type {TurboModule} from '../../../Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../../../Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` +${flagName}?: () => ${typeof flagConfig.defaultValue};`, + ) + .join('\n')} +} + +const NativeReactNativeFeatureFlags: ?Spec = TurboModuleRegistry.get( + 'NativeReactNativeFeatureFlagsCxx', +); + +export default NativeReactNativeFeatureFlags; +`); diff --git a/packages/react-native/scripts/featureflags/templates/js/ReactNativeFeatureFlags.js-template.js b/packages/react-native/scripts/featureflags/templates/js/ReactNativeFeatureFlags.js-template.js new file mode 100644 index 00000000000000..3d6251d3e7cd80 --- /dev/null +++ b/packages/react-native/scripts/featureflags/templates/js/ReactNativeFeatureFlags.js-template.js @@ -0,0 +1,85 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const {DO_NOT_MODIFY_COMMENT} = require('../../utils'); +const signedsource = require('signedsource'); + +module.exports = config => + 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()} + * @flow strict-local + */ + +${DO_NOT_MODIFY_COMMENT} + +import { + type Getter, + createJavaScriptFlagGetter, + createNativeFlagGetter, + setOverrides, +} from './ReactNativeFeatureFlagsBase'; + +export type ReactNativeFeatureFlagsJsOnly = { +${Object.entries(config.jsOnly) + .map( + ([flagName, flagConfig]) => + ` ${flagName}: Getter<${typeof flagConfig.defaultValue}>,`, + ) + .join('\n')} +}; + +export type ReactNativeFeatureFlagsJsOnlyOverrides = Partial; + +export type ReactNativeFeatureFlags = { + ...ReactNativeFeatureFlagsJsOnly, +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + ` ${flagName}: Getter<${typeof flagConfig.defaultValue}>,`, + ) + .join('\n')} +} + +${Object.entries(config.jsOnly) + .map( + ([flagName, flagConfig]) => + `/** + * ${flagConfig.description} + */ +export const ${flagName}: Getter<${typeof flagConfig.defaultValue}> = createJavaScriptFlagGetter('${flagName}', ${JSON.stringify( + flagConfig.defaultValue, + )});`, + ) + .join('\n\n')} + +${Object.entries(config.common) + .map( + ([flagName, flagConfig]) => + `/** + * ${flagConfig.description} + */ +export const ${flagName}: Getter<${typeof flagConfig.defaultValue}> = createNativeFlagGetter('${flagName}', ${JSON.stringify( + flagConfig.defaultValue, + )});`, + ) + .join('\n')} + +/** + * Overrides the feature flags with the provided methods. + * NOTE: Only JS-only flags can be overridden from JavaScript using this API. + */ +export const override = setOverrides; +`); diff --git a/packages/react-native/scripts/featureflags/update.js b/packages/react-native/scripts/featureflags/update.js new file mode 100644 index 00000000000000..dff625d3e8d284 --- /dev/null +++ b/packages/react-native/scripts/featureflags/update.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. + * + * @format + */ + +const generateFiles = require('./generateFiles'); +const path = require('path'); + +const REACT_NATIVE_PACKAGE_ROOT = path.join(__dirname, '..', '..'); + +function update() { + generateFiles( + { + configPath: path.join(__dirname, 'ReactNativeFeatureFlags.json'), + jsPath: path.join( + REACT_NATIVE_PACKAGE_ROOT, + 'src', + 'private', + 'featureflags', + ), + commonCxxPath: path.join( + REACT_NATIVE_PACKAGE_ROOT, + 'ReactCommon', + 'react', + 'featureflags', + ), + commonNativeModuleCxxPath: path.join( + REACT_NATIVE_PACKAGE_ROOT, + 'ReactCommon', + 'react', + 'nativemodule', + 'featureflags', + ), + androidPath: path.join( + REACT_NATIVE_PACKAGE_ROOT, + 'ReactAndroid', + 'src', + 'main', + 'java', + 'com', + 'facebook', + 'react', + 'internal', + 'featureflags', + ), + androidJniPath: path.join( + REACT_NATIVE_PACKAGE_ROOT, + 'ReactAndroid', + 'src', + 'main', + 'jni', + 'react', + 'featureflags', + ), + }, + { + verifyUnchanged: process.argv.includes('--verify-unchanged'), + }, + ); +} + +if (require.main === module) { + update(); +} diff --git a/packages/react-native/scripts/featureflags/utils.js b/packages/react-native/scripts/featureflags/utils.js new file mode 100644 index 00000000000000..a551227a6cb2d4 --- /dev/null +++ b/packages/react-native/scripts/featureflags/utils.js @@ -0,0 +1,50 @@ +/** + * 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 + */ + +module.exports = { + getCxxTypeFromDefaultValue: defaultValue => { + switch (typeof defaultValue) { + case 'boolean': + return 'bool'; + case 'number': + return 'int'; + case 'string': + return 'std::string'; + default: + throw new Error( + `Unsupported default value type: ${typeof defaultValue}`, + ); + } + }, + + getKotlinTypeFromDefaultValue: defaultValue => { + switch (typeof defaultValue) { + case 'boolean': + return 'Boolean'; + case 'number': + return 'Int'; + case 'string': + return 'String'; + default: + throw new Error( + `Unsupported default value type: ${typeof defaultValue}`, + ); + } + }, + + DO_NOT_MODIFY_COMMENT: `/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */`, +}; diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb index 91a241c2301379..0943e9029e9dc5 100644 --- a/packages/react-native/scripts/react_native_pods.rb +++ b/packages/react-native/scripts/react_native_pods.rb @@ -124,6 +124,7 @@ def use_react_native! ( pod 'React-cxxreact', :path => "#{prefix}/ReactCommon/cxxreact" pod 'React-debug', :path => "#{prefix}/ReactCommon/react/debug" pod 'React-utils', :path => "#{prefix}/ReactCommon/react/utils" + pod 'React-featureflags', :path => "#{prefix}/ReactCommon/react/featureflags" pod 'React-Mapbuffer', :path => "#{prefix}/ReactCommon" pod 'React-jserrorhandler', :path => "#{prefix}/ReactCommon/jserrorhandler" pod 'React-nativeconfig', :path => "#{prefix}/ReactCommon" diff --git a/packages/react-native/src/private/featureflags/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/NativeReactNativeFeatureFlags.js new file mode 100644 index 00000000000000..3132692c948d21 --- /dev/null +++ b/packages/react-native/src/private/featureflags/NativeReactNativeFeatureFlags.js @@ -0,0 +1,33 @@ +/** + * 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<> + * @flow strict-local + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +import type {TurboModule} from '../../../Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../../../Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +commonTestFlag?: () => boolean; +} + +const NativeReactNativeFeatureFlags: ?Spec = TurboModuleRegistry.get( + 'NativeReactNativeFeatureFlagsCxx', +); + +export default NativeReactNativeFeatureFlags; diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js new file mode 100644 index 00000000000000..8495e3af234525 --- /dev/null +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -0,0 +1,53 @@ +/** + * 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<> + * @flow strict-local + */ + +/** + * IMPORTANT: Do NOT modify this file directly. + * + * To change the definition of the flags, edit + * packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.json. + * + * To regenerate this code, run the following script from the repo root: + * yarn featureflags-update + */ + +import { + type Getter, + createJavaScriptFlagGetter, + createNativeFlagGetter, + setOverrides, +} from './ReactNativeFeatureFlagsBase'; + +export type ReactNativeFeatureFlagsJsOnly = { + jsOnlyTestFlag: Getter, +}; + +export type ReactNativeFeatureFlagsJsOnlyOverrides = Partial; + +export type ReactNativeFeatureFlags = { + ...ReactNativeFeatureFlagsJsOnly, + commonTestFlag: Getter, +} + +/** + * JS-only flag for testing. Do NOT modify. + */ +export const jsOnlyTestFlag: Getter = createJavaScriptFlagGetter('jsOnlyTestFlag', false); + +/** + * Common flag for testing. Do NOT modify. + */ +export const commonTestFlag: Getter = createNativeFlagGetter('commonTestFlag', false); + +/** + * Overrides the feature flags with the provided methods. + * NOTE: Only JS-only flags can be overridden from JavaScript using this API. + */ +export const override = setOverrides; diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlagsBase.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlagsBase.js new file mode 100644 index 00000000000000..a6fdee5d289f6c --- /dev/null +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlagsBase.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type { + ReactNativeFeatureFlagsJsOnly, + ReactNativeFeatureFlagsJsOnlyOverrides, +} from './ReactNativeFeatureFlags'; + +import NativeReactNativeFeatureFlags from './NativeReactNativeFeatureFlags'; + +const accessedFeatureFlags: Set = new Set(); +const overrides: ReactNativeFeatureFlagsJsOnlyOverrides = {}; + +export type Getter = () => T; + +function createGetter( + configName: string, + customValueGetter: Getter, + defaultValue: T, +): Getter { + let cachedValue: ?T; + + return () => { + if (cachedValue == null) { + accessedFeatureFlags.add(configName); + cachedValue = customValueGetter() ?? defaultValue; + } + return cachedValue; + }; +} + +export function createJavaScriptFlagGetter< + K: $Keys, +>( + configName: K, + defaultValue: ReturnType, +): Getter> { + return createGetter( + configName, + () => overrides[configName]?.(), + defaultValue, + ); +} + +type NativeFeatureFlags = $NonMaybeType; + +export function createNativeFlagGetter>( + configName: K, + defaultValue: ReturnType<$NonMaybeType>, +): Getter>> { + return createGetter( + configName, + () => NativeReactNativeFeatureFlags?.[configName]?.(), + defaultValue, + ); +} + +export function getOverrides(): ?ReactNativeFeatureFlagsJsOnlyOverrides { + return overrides; +} + +export function setOverrides( + newOverrides: ReactNativeFeatureFlagsJsOnlyOverrides, +): void { + if (accessedFeatureFlags.size > 0) { + const accessedFeatureFlagsStr = Array.from(accessedFeatureFlags).join(', '); + throw new Error( + `Feature flags were accessed before being overridden: ${accessedFeatureFlagsStr}`, + ); + } + + Object.assign(overrides, newOverrides); +} diff --git a/packages/react-native/src/private/featureflags/__tests__/ReactNativeFeatureFlags-test.js b/packages/react-native/src/private/featureflags/__tests__/ReactNativeFeatureFlags-test.js new file mode 100644 index 00000000000000..fef002a2bc5114 --- /dev/null +++ b/packages/react-native/src/private/featureflags/__tests__/ReactNativeFeatureFlags-test.js @@ -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. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +describe('ReactNativeFeatureFlags', () => { + beforeEach(() => { + jest.resetModules(); + }); + + it('should provide default values for common flags if the native module is NOT available', () => { + const ReactNativeFeatureFlags = require('../ReactNativeFeatureFlags'); + expect(ReactNativeFeatureFlags.commonTestFlag()).toBe(false); + }); + + it('should access and cache common flags from the native module if it is available', () => { + const commonTestFlagFn = jest.fn(() => true); + + jest.doMock('../NativeReactNativeFeatureFlags', () => ({ + __esModule: true, + default: { + commonTestFlag: commonTestFlagFn, + }, + })); + + const ReactNativeFeatureFlags = require('../ReactNativeFeatureFlags'); + + expect(commonTestFlagFn).toHaveBeenCalledTimes(0); + + expect(ReactNativeFeatureFlags.commonTestFlag()).toBe(true); + expect(commonTestFlagFn).toHaveBeenCalledTimes(1); + + expect(ReactNativeFeatureFlags.commonTestFlag()).toBe(true); + expect(commonTestFlagFn).toHaveBeenCalledTimes(1); + }); + + it('should provide default values for JS-only flags', () => { + const ReactNativeFeatureFlags = require('../ReactNativeFeatureFlags'); + expect(ReactNativeFeatureFlags.jsOnlyTestFlag()).toBe(false); + }); + + it('should access and cache overridden JS-only flags', () => { + const ReactNativeFeatureFlags = require('../ReactNativeFeatureFlags'); + + const jsOnlyTestFlagFn = jest.fn(() => true); + ReactNativeFeatureFlags.override({ + jsOnlyTestFlag: jsOnlyTestFlagFn, + }); + + expect(jsOnlyTestFlagFn).toHaveBeenCalledTimes(0); + + expect(ReactNativeFeatureFlags.jsOnlyTestFlag()).toBe(true); + expect(jsOnlyTestFlagFn).toHaveBeenCalledTimes(1); + + expect(ReactNativeFeatureFlags.jsOnlyTestFlag()).toBe(true); + expect(jsOnlyTestFlagFn).toHaveBeenCalledTimes(1); + }); + + it('should throw an error if any of the flags has been accessed before overridding', () => { + const ReactNativeFeatureFlags = require('../ReactNativeFeatureFlags'); + + ReactNativeFeatureFlags.commonTestFlag(); + + expect(() => + ReactNativeFeatureFlags.override({ + jsOnlyTestFlag: () => true, + }), + ).toThrow( + 'Feature flags were accessed before being overridden: commonTestFlag', + ); + }); +}); diff --git a/scripts/run-ci-javascript-tests.js b/scripts/run-ci-javascript-tests.js index 759deb2459f8fc..0aa402a16ad677 100644 --- a/scripts/run-ci-javascript-tests.js +++ b/scripts/run-ci-javascript-tests.js @@ -33,6 +33,13 @@ function describe(message) { try { echo('Executing JavaScript tests'); + describe('Test: feature flags codegen'); + if (exec(`${YARN_BINARY} run featureflags-check`).code) { + echo('Failed to run featureflags check.'); + exitCode = 1; + throw Error(exitCode); + } + describe('Test: eslint'); if (exec(`${YARN_BINARY} run lint`).code) { echo('Failed to run eslint.');