Skip to content

Commit

Permalink
Implement new feature flag system (#42430)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #42430

This PR creates a new internal feature flags system for React Native. This is only meant to be used internally within the framework, but we might expose it externally in some form in the future to allow customizing specific feature flags in frameworks and applications.

Features:
* 2 types of flags:
  * Common: can be overridden from native and are accessible from all layers of the stack (Objective-C/Swift, Java/Kotlin, C++ and JavaScript).
  * JS-only: flags that can only be defined and accessed from JS (to allow things like hot reloading without a native build).
* 1 source of truth for each flag.
* Feature flags are application/process scoped (using C++ singletons).

See the `README.md` file in this PR for additional information.

This also adds modifies `run-ci-javascript-tests` to run a new check to make sure that the generate files are in sync with the JSON file that contains the definitions.

Changelog: [internal]

Reviewed By: huntie

Differential Revision: D52806730

fbshipit-source-id: 0ba95803f61ec2f05266ee535921321bf6d3dc6a
  • Loading branch information
rubennorte authored and facebook-github-bot committed Jan 25, 2024
1 parent dc2ce9e commit 705c675
Show file tree
Hide file tree
Showing 73 changed files with 3,126 additions and 4 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/React-Core.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/ReactAndroid/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ android {
"runtimeexecutor",
"react_codegen_rncore",
"react_debug",
"react_featureflags",
"react_utils",
"react_render_componentregistry",
"react_newarchdefaults",
Expand All @@ -540,6 +541,7 @@ android {
"jsi",
"glog",
"fabricjni",
"featureflagsjni",
"react_render_mapbuffer",
"yoga",
"folly_runtime",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<<c201b2b577ab4aa4bf6071d6b99b43c9>>
*/

/**
* 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()
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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<<c2e41ac8c3d9471b4cb79f6147cc2bf2>>
*/

/**
* 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()
}
Original file line number Diff line number Diff line change
@@ -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<<aa1eaeee7b715e5b1d3cbcf9b7a7062e>>
*/

/**
* 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
}
Original file line number Diff line number Diff line change
@@ -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() })
}
}
Original file line number Diff line number Diff line change
@@ -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<String>()

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.
}
}
Original file line number Diff line number Diff line change
@@ -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<<babb2b7b32c88b1767ac53ae97dddf10>>
*/

/**
* 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ target_link_libraries(
mapbufferjni
react_codegen_rncore
react_debug
react_featureflags
react_render_animations
react_render_attributedstring
react_render_componentregistry
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Loading

0 comments on commit 705c675

Please sign in to comment.