first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
# React Native Gradle Plugin
This plugin is used by React Native Apps to configure themselves.
NOTE: It's important that this folder is called `react-native-gradle-plugin` as it's used
by users in their `build.gradle` file as follows:
```gradle
buildscript {
// ...
dependencies {
classpath("com.facebook.react:react-native-gradle-plugin")
}
}
```
The name of the artifact is imposed by the folder name.

View File

@@ -0,0 +1,83 @@
/*
* 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.
*/
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.ktfmt)
id("java-gradle-plugin")
}
repositories {
google()
mavenCentral()
}
gradlePlugin {
plugins {
create("react") {
id = "com.facebook.react"
implementationClass = "com.facebook.react.ReactPlugin"
}
create("reactrootproject") {
id = "com.facebook.react.rootproject"
implementationClass = "com.facebook.react.ReactRootProjectPlugin"
}
}
}
group = "com.facebook.react"
dependencies {
implementation(project(":shared"))
implementation(gradleApi())
// The KGP/AGP version is defined by React Native Gradle plugin.
// Therefore we specify an implementation dep rather than a compileOnly.
implementation(libs.kotlin.gradle.plugin)
implementation(libs.android.gradle.plugin)
implementation(libs.gson)
implementation(libs.guava)
implementation(libs.javapoet)
testImplementation(libs.junit)
testImplementation(libs.assertj)
testImplementation(project(":shared-testutil"))
}
// We intentionally don't build for Java 17 as users will see a cryptic bytecode version
// error first. Instead we produce a Java 11-compatible Gradle Plugin, so that AGP can print their
// nice message showing that JDK 11 (or 17) is required first
java { targetCompatibility = JavaVersion.VERSION_11 }
kotlin { jvmToolchain(17) }
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
apiVersion.set(KotlinVersion.KOTLIN_1_8)
// See comment above on JDK 11 support
jvmTarget.set(JvmTarget.JVM_11)
allWarningsAsErrors.set(
project.properties["enableWarningsAsErrors"]?.toString()?.toBoolean() ?: false
)
}
}
tasks.withType<Test>().configureEach {
testLogging {
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
}
}

View File

@@ -0,0 +1,219 @@
/*
* 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
import com.facebook.react.utils.JsonUtils
import com.facebook.react.utils.projectPathToLibraryName
import java.io.File
import javax.inject.Inject
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
abstract class ReactExtension @Inject constructor(val project: Project) {
private val objects = project.objects
/**
* The path to the root of your project. This is the path to where the `package.json` lives. All
* the CLI commands will be invoked from this folder as working directory.
*
* Default: ${rootProject.dir}/../
*/
val root: DirectoryProperty =
objects.directoryProperty().convention(project.rootProject.layout.projectDirectory.dir("../"))
/**
* The path to the react-native NPM package folder.
*
* Default: ${rootProject.dir}/../node_modules/react-native
*/
val reactNativeDir: DirectoryProperty =
objects.directoryProperty().convention(root.dir("node_modules/react-native"))
/**
* The path to the JS entry file. If not specified, the plugin will try to resolve it using a list
* of known locations (e.g. `index.android.js`, `index.js`, etc.).
*/
val entryFile: RegularFileProperty = objects.fileProperty()
/**
* The reference to the React Native CLI. If not specified, the plugin will try to resolve it
* looking for `react-native` CLI inside `node_modules` in [root].
*/
val cliFile: RegularFileProperty =
objects.fileProperty().convention(reactNativeDir.file("cli.js"))
/**
* The path to the Node executable and extra args. By default it assumes that you have `node`
* installed and configured in your $PATH. Default: ["node"]
*/
val nodeExecutableAndArgs: ListProperty<String> =
objects.listProperty(String::class.java).convention(listOf("node"))
/** The command to use to invoke bundle. Default is `bundle` and will be invoked on [root]. */
val bundleCommand: Property<String> = objects.property(String::class.java).convention("bundle")
/**
* Custom configuration file for the [bundleCommand]. If provided, it will be passed over with a
* `--config` flag to the bundle command.
*/
val bundleConfig: RegularFileProperty = objects.fileProperty()
/**
* The Bundle Asset name. This name will be used also for deriving other bundle outputs such as
* the packager source map, the compiler source map and the output source map file.
*
* Default: index.android.bundle
*/
val bundleAssetName: Property<String> =
objects.property(String::class.java).convention("index.android.bundle")
/**
* Whether the Bundle Asset should be compressed when packaged into a `.apk`, or not. Disabling
* compression for the `.bundle` allows it to be directly memory-mapped to RAM, hence improving
* startup time - at the cost of a larger resulting `.apk` size.
*
* Default: false
*/
val enableBundleCompression: Property<Boolean> =
objects.property(Boolean::class.java).convention(false)
/**
* Toggles the .so Cleanup step. If enabled, we will clean up all the unnecessary files before the
* bundle task. If disabled, the developers will have to manually cleanup the files. Default: true
*/
val enableSoCleanup: Property<Boolean> = objects.property(Boolean::class.java).convention(true)
/** Extra args that will be passed to the [bundleCommand] Default: [] */
val extraPackagerArgs: ListProperty<String> =
objects.listProperty(String::class.java).convention(emptyList())
/**
* Allows to specify the debuggable variants (by default just 'debug'). Variants in this list will
* not be bundled (the bundle file will not be created and won't be copied over).
*
* Default: ['debug', 'debugOptimized']
*/
val debuggableVariants: ListProperty<String> =
objects.listProperty(String::class.java).convention(listOf("debug", "debugOptimized"))
/** Hermes Config */
/**
* The command to use to invoke hermesc (the hermes compiler). Default is "", the plugin will
* autodetect it.
*/
val hermesCommand: Property<String> = objects.property(String::class.java).convention("")
/**
* Whether to enable Hermes only on certain variants. If specified as a non-empty list, hermesc
* and the .so cleanup for Hermes will be executed only for variants in this list. An empty list
* assumes you're either using Hermes for all variants or not (see [enableHermes]).
*
* Default: []
*/
val enableHermesOnlyInVariants: ListProperty<String> =
objects.listProperty(String::class.java).convention(emptyList())
/** Flags to pass to Hermesc. Default: ["-O", "-output-source-map"] */
val hermesFlags: ListProperty<String> =
objects.listProperty(String::class.java).convention(listOf("-O", "-output-source-map"))
/** Codegen Config */
/**
* The path to the react-native-codegen NPM package folder.
*
* Default: ${rootProject.dir}/../node_modules/@react-native/codegen
*/
val codegenDir: DirectoryProperty =
objects.directoryProperty().convention(root.dir("node_modules/@react-native/codegen"))
/**
* The root directory for all JS files for the app.
*
* Default: the parent folder of the `/android` folder.
*/
val jsRootDir: DirectoryProperty = objects.directoryProperty()
/**
* The library name that will be used for the codegen artifacts.
*
* Default: <UpperCamelVersionOfProjectPath>Spec (e.g. for :example:project it will be
* ExampleProjectSpec).
*/
val libraryName: Property<String> =
objects.property(String::class.java).convention(projectPathToLibraryName(project.path))
/**
* Java package name to use for any codegen artifacts produced during build time. Default:
* com.facebook.fbreact.specs
*/
val codegenJavaPackageName: Property<String> =
objects.property(String::class.java).convention("com.facebook.fbreact.specs")
/** Auto-linking Utils */
/**
* Utility function to autolink libraries to the app.
*
* This function will read the autolinking configuration file and add Gradle dependencies to the
* app. This function should be invoked inside the react {} block in the app's build.gradle and is
* necessary for libraries to be linked correctly.
*/
fun autolinkLibrariesWithApp() {
val inputFile =
project.rootProject.layout.buildDirectory
.file("generated/autolinking/autolinking.json")
.get()
.asFile
val dependenciesToApply = getGradleDependenciesToApply(inputFile)
dependenciesToApply.forEach { (configuration, path) ->
project.dependencies.add(configuration, project.dependencies.project(mapOf("path" to path)))
}
}
companion object {
/**
* Util function to construct a list of Gradle Configuration <-> Project name pairs for
* autolinking. Pairs looks like: "implementation" -> ":react-native_oss-library-example"
*
* They will be applied to the Gradle project for linking the libraries.
*
* @param inputFile The file to read the autolinking configuration from.
* @return A list of Gradle Configuration <-> Project name pairs.
*/
internal fun getGradleDependenciesToApply(inputFile: File): MutableList<Pair<String, String>> {
val model = JsonUtils.fromAutolinkingConfigJson(inputFile)
val result = mutableListOf<Pair<String, String>>()
model
?.dependencies
?.values
?.filter { it.platforms?.android !== null }
?.filterNot { it.platforms?.android?.isPureCxxDependency == true }
?.forEach { deps ->
val nameCleansed = deps.nameCleansed
val dependencyConfiguration = deps.platforms?.android?.dependencyConfiguration
val buildTypes = deps.platforms?.android?.buildTypes ?: emptyList()
if (buildTypes.isEmpty()) {
result.add((dependencyConfiguration ?: "implementation") to ":$nameCleansed")
} else {
buildTypes.forEach { buildType ->
result.add(
(dependencyConfiguration ?: "${buildType}Implementation") to ":$nameCleansed"
)
}
}
}
return result
}
}
}

View File

@@ -0,0 +1,320 @@
/*
* 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
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.facebook.react.internal.PrivateReactExtension
import com.facebook.react.tasks.GenerateAutolinkingNewArchitecturesFileTask
import com.facebook.react.tasks.GenerateCodegenArtifactsTask
import com.facebook.react.tasks.GenerateCodegenSchemaTask
import com.facebook.react.tasks.GenerateEntryPointTask
import com.facebook.react.tasks.GeneratePackageListTask
import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForApp
import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForLibraries
import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildTypesForApp
import com.facebook.react.utils.AgpConfiguratorUtils.configureDevServerLocation
import com.facebook.react.utils.AgpConfiguratorUtils.configureNamespaceForLibraries
import com.facebook.react.utils.BackwardCompatUtils.configureBackwardCompatibilityReactMap
import com.facebook.react.utils.DependencyUtils.configureDependencies
import com.facebook.react.utils.DependencyUtils.configureRepositories
import com.facebook.react.utils.DependencyUtils.readVersionAndGroupStrings
import com.facebook.react.utils.JdkConfiguratorUtils.configureJavaToolChains
import com.facebook.react.utils.JsonUtils
import com.facebook.react.utils.NdkConfiguratorUtils.configureReactNativeNdk
import com.facebook.react.utils.ProjectUtils.isHermesV1Enabled
import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson
import com.facebook.react.utils.findPackageJsonFile
import java.io.File
import kotlin.system.exitProcess
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.Directory
import org.gradle.api.provider.Provider
import org.gradle.internal.jvm.Jvm
class ReactPlugin : Plugin<Project> {
override fun apply(project: Project) {
checkJvmVersion(project)
val extension = project.extensions.create("react", ReactExtension::class.java, project)
// We register a private extension on the rootProject so that project wide configs
// like codegen config can be propagated from app project to libraries.
val rootExtension =
project.rootProject.extensions.findByType(PrivateReactExtension::class.java)
?: project.rootProject.extensions.create(
"privateReact",
PrivateReactExtension::class.java,
project,
)
if (project.rootProject.isHermesV1Enabled) {
rootExtension.hermesV1Enabled.set(true)
}
// App Only Configuration
project.pluginManager.withPlugin("com.android.application") {
// We wire the root extension with the values coming from the app (either user populated or
// defaults).
rootExtension.root.set(extension.root)
rootExtension.reactNativeDir.set(extension.reactNativeDir)
rootExtension.codegenDir.set(extension.codegenDir)
rootExtension.nodeExecutableAndArgs.set(extension.nodeExecutableAndArgs)
project.afterEvaluate {
val reactNativeDir = extension.reactNativeDir.get().asFile
val propertiesFile = File(reactNativeDir, "ReactAndroid/gradle.properties")
val hermesVersionPropertiesFile =
File(reactNativeDir, "sdks/hermes-engine/version.properties")
val versionAndGroupStrings =
readVersionAndGroupStrings(propertiesFile, hermesVersionPropertiesFile)
val hermesV1Enabled = rootExtension.hermesV1Enabled.get()
configureDependencies(project, versionAndGroupStrings, hermesV1Enabled)
configureRepositories(project)
}
configureReactNativeNdk(project, extension)
configureBuildConfigFieldsForApp(project, extension)
configureDevServerLocation(project)
configureBackwardCompatibilityReactMap(project)
configureJavaToolChains(project)
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).apply {
onVariants(selector().all()) { variant ->
project.configureReactTasks(variant = variant, config = extension)
}
}
configureAutolinking(project, extension)
configureCodegen(project, extension, rootExtension, isLibrary = false)
configureResources(project, extension)
configureBuildTypesForApp(project)
}
// Library Only Configuration
configureBuildConfigFieldsForLibraries(project)
configureNamespaceForLibraries(project)
project.pluginManager.withPlugin("com.android.library") {
configureCodegen(project, extension, rootExtension, isLibrary = true)
}
}
private fun checkJvmVersion(project: Project) {
val jvmVersion = Jvm.current().javaVersion?.majorVersion
if ((jvmVersion?.toIntOrNull() ?: 0) <= 16) {
project.logger.error(
"""
********************************************************************************
ERROR: requires JDK17 or higher.
Incompatible major version detected: '$jvmVersion'
********************************************************************************
"""
.trimIndent()
)
exitProcess(1)
}
}
/** This function configures Android resources - in this case just the bundle */
private fun configureResources(project: Project, reactExtension: ReactExtension) {
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl {
ext ->
val bundleFileExtension = reactExtension.bundleAssetName.get().substringAfterLast('.', "")
if (!reactExtension.enableBundleCompression.get() && bundleFileExtension.isNotBlank()) {
ext.androidResources.noCompress.add(bundleFileExtension)
}
}
}
/** This function sets up `react-native-codegen` in our Gradle plugin. */
@Suppress("UnstableApiUsage")
private fun configureCodegen(
project: Project,
localExtension: ReactExtension,
rootExtension: PrivateReactExtension,
isLibrary: Boolean,
) {
// First, we set up the output dir for the codegen.
val generatedSrcDir: Provider<Directory> =
project.layout.buildDirectory.dir("generated/source/codegen")
// We specify the default value (convention) for jsRootDir.
// It's the root folder for apps (so ../../ from the Gradle project)
// and the package folder for library (so ../ from the Gradle project)
if (isLibrary) {
localExtension.jsRootDir.convention(project.layout.projectDirectory.dir("../"))
} else {
localExtension.jsRootDir.convention(localExtension.root)
}
// We create the task to produce schema from JS files.
val generateCodegenSchemaTask =
project.tasks.register(
"generateCodegenSchemaFromJavaScript",
GenerateCodegenSchemaTask::class.java,
) { it ->
it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
it.codegenDir.set(rootExtension.codegenDir)
it.generatedSrcDir.set(generatedSrcDir)
it.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
// We're reading the package.json at configuration time to properly feed
// the `jsRootDir` @Input property of this task & the onlyIf. Therefore, the
// parsePackageJson should be invoked inside this lambda.
val packageJson = findPackageJsonFile(project, rootExtension.root)
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir
val includesGeneratedCode =
parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
if (jsSrcsDirInPackageJson != null) {
it.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson))
} else {
it.jsRootDir.set(localExtension.jsRootDir)
}
it.jsInputFiles.set(
project.fileTree(it.jsRootDir) { tree ->
tree.include("**/*.js")
tree.include("**/*.jsx")
tree.include("**/*.ts")
tree.include("**/*.tsx")
tree.exclude("node_modules/**/*")
tree.exclude("**/*.d.ts")
// We want to exclude the build directory, to don't pick them up for execution
// avoidance.
tree.exclude("**/build/**/*")
}
)
val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root)
it.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
}
// We create the task to generate Java code from schema.
val generateCodegenArtifactsTask =
project.tasks.register(
"generateCodegenArtifactsFromSchema",
GenerateCodegenArtifactsTask::class.java,
) { task ->
task.dependsOn(generateCodegenSchemaTask)
task.reactNativeDir.set(rootExtension.reactNativeDir)
task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
task.generatedSrcDir.set(generatedSrcDir)
task.packageJsonFile.set(findPackageJsonFile(project, rootExtension.root))
task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName)
task.libraryName.set(localExtension.libraryName)
task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
// Please note that appNeedsCodegen is triggering a read of the package.json at
// configuration time as we need to feed the onlyIf condition of this task.
// Therefore, the appNeedsCodegen needs to be invoked inside this lambda.
val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root)
val packageJson = findPackageJsonFile(project, rootExtension.root)
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
val includesGeneratedCode =
parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
task.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
}
// We update the android configuration to include the generated sources.
// This equivalent to this DSL:
//
// android { sourceSets { main { java { srcDirs += "$generatedSrcDir/java" } } } }
if (isLibrary) {
project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).finalizeDsl { ext
->
ext.sourceSets.getByName("main").java.srcDir(generatedSrcDir.get().dir("java").asFile)
}
} else {
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl {
ext ->
ext.sourceSets.getByName("main").java.srcDir(generatedSrcDir.get().dir("java").asFile)
}
}
// `preBuild` is one of the base tasks automatically registered by AGP.
// This will invoke the codegen before compiling the entire project.
project.tasks.named("preBuild", Task::class.java).dependsOn(generateCodegenArtifactsTask)
}
/** This function sets up Autolinking for App users */
private fun configureAutolinking(
project: Project,
extension: ReactExtension,
) {
val generatedAutolinkingJavaDir: Provider<Directory> =
project.layout.buildDirectory.dir("generated/autolinking/src/main/java")
val generatedAutolinkingJniDir: Provider<Directory> =
project.layout.buildDirectory.dir("generated/autolinking/src/main/jni")
// The autolinking.json file is available in the root build folder as it's generated
// by ReactSettingsPlugin.kt
val rootGeneratedAutolinkingFile =
project.rootProject.layout.buildDirectory.file("generated/autolinking/autolinking.json")
// We add a task called generateAutolinkingPackageList to do not clash with the existing task
// called generatePackageList. This can to be renamed once we unlink the rn <-> cli
// dependency.
val generatePackageListTask =
project.tasks.register(
"generateAutolinkingPackageList",
GeneratePackageListTask::class.java,
) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
}
// We add a task called generateAutolinkingPackageList to do not clash with the existing task
// called generatePackageList. This can to be renamed once we unlink the rn <-> cli
// dependency.
val generateEntryPointTask =
project.tasks.register(
"generateReactNativeEntryPoint",
GenerateEntryPointTask::class.java,
) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
}
// We also need to generate code for C++ Autolinking
val generateAutolinkingNewArchitectureFilesTask =
project.tasks.register(
"generateAutolinkingNewArchitectureFiles",
GenerateAutolinkingNewArchitecturesFileTask::class.java,
) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJniDir)
}
project.tasks
.named("preBuild", Task::class.java)
.dependsOn(generateAutolinkingNewArchitectureFilesTask)
// We let generateAutolinkingPackageList and generateEntryPoint depend on the preBuild task so
// it's executed before
// everything else.
project.tasks
.named("preBuild", Task::class.java)
.dependsOn(generatePackageListTask, generateEntryPointTask)
// We tell Android Gradle Plugin that inside /build/generated/autolinking/src/main/java there
// are sources to be compiled as well.
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).apply {
onVariants(selector().all()) { variant ->
variant.sources.java?.addStaticSourceDirectory(
generatedAutolinkingJavaDir.get().asFile.absolutePath
)
}
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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
import com.facebook.react.utils.PropertyUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.extraProperties
/**
* Gradle plugin applied to the `android/build.gradle` file.
*
* This plugin allows to specify project wide configurations that can be applied to both apps and
* libraries before they're evaluated.
*/
class ReactRootProjectPlugin : Plugin<Project> {
override fun apply(project: Project) {
checkLegacyArchProperty(project)
project.subprojects { subproject ->
// As the :app project (i.e. ReactPlugin) configures both namespaces and JVM toolchains
// for libraries, its evaluation must happen before the libraries' evaluation.
// Eventually the configuration of namespace/JVM toolchain can be moved inside this plugin.
if (subproject.path != ":app") {
subproject.evaluationDependsOn(":app")
}
// We set the New Architecture properties to true for all subprojects. So that
// libraries don't need to be modified and can keep on using the isNewArchEnabled()
// function to check if property is set.
if (subproject.hasProperty(PropertyUtils.SCOPED_NEW_ARCH_ENABLED)) {
subproject.setProperty(PropertyUtils.SCOPED_NEW_ARCH_ENABLED, "true")
}
if (subproject.hasProperty(PropertyUtils.NEW_ARCH_ENABLED)) {
subproject.setProperty(PropertyUtils.NEW_ARCH_ENABLED, "true")
}
subproject.extraProperties.set(PropertyUtils.NEW_ARCH_ENABLED, "true")
subproject.extraProperties.set(PropertyUtils.SCOPED_NEW_ARCH_ENABLED, "true")
}
// We need to make sure that `:app:preBuild` task depends on all other subprojects' preBuild
// tasks. This is necessary in order to have all the codegen generated code before the CMake
// configuration build kicks in.
project.gradle.projectsEvaluated {
val appProject = project.rootProject.subprojects.find { it.name == "app" }
val appPreBuild = appProject?.tasks?.findByName("preBuild")
if (appPreBuild != null) {
// Find all other subprojects' preBuild tasks
val otherPreBuildTasks =
project.rootProject.subprojects
.filter { it != appProject }
.mapNotNull { it.tasks.findByName("preBuild") }
// Make :app:preBuild depend on all others
appPreBuild.dependsOn(otherPreBuildTasks)
}
}
}
private fun checkLegacyArchProperty(project: Project) {
if (
(project.hasProperty(PropertyUtils.NEW_ARCH_ENABLED) &&
!project.property(PropertyUtils.NEW_ARCH_ENABLED).toString().toBoolean()) ||
(project.hasProperty(PropertyUtils.SCOPED_NEW_ARCH_ENABLED) &&
!project.property(PropertyUtils.SCOPED_NEW_ARCH_ENABLED).toString().toBoolean())
) {
project.logger.error(
"""
********************************************************************************
WARNING: Setting `newArchEnabled=false` in your `gradle.properties` file is not
supported anymore since React Native 0.82.
You can remove the line from your `gradle.properties` file.
The application will run with the New Architecture enabled by default.
********************************************************************************
"""
.trimIndent()
)
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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
import com.android.build.api.variant.Variant
import com.facebook.react.tasks.BundleHermesCTask
import com.facebook.react.utils.BackwardCompatUtils.showJSCRemovalMessage
import com.facebook.react.utils.KotlinStdlibCompatUtils.capitalizeCompat
import com.facebook.react.utils.NdkConfiguratorUtils.configureJsEnginePackagingOptions
import com.facebook.react.utils.NdkConfiguratorUtils.configureNewArchPackagingOptions
import com.facebook.react.utils.ProjectUtils.isHermesEnabled
import com.facebook.react.utils.ProjectUtils.useThirdPartyJSC
import com.facebook.react.utils.detectedCliFile
import com.facebook.react.utils.detectedEntryFile
import java.io.File
import org.gradle.api.Project
@Suppress("SpreadOperator", "UnstableApiUsage")
internal fun Project.configureReactTasks(variant: Variant, config: ReactExtension) {
val targetName = variant.name.capitalizeCompat()
val targetPath = variant.name
val buildDir = layout.buildDirectory.get().asFile
// Resources: generated/assets/react/<variant>/index.android.bundle
val resourcesDir = File(buildDir, "generated/res/react/$targetPath")
// Bundle: generated/assets/react/<variant>/index.android.bundle
val jsBundleDir = File(buildDir, "generated/assets/react/$targetPath")
// Sourcemap: generated/sourcemaps/react/<variant>/index.android.bundle.map
val jsSourceMapsDir = File(buildDir, "generated/sourcemaps/react/$targetPath")
// Intermediate packager:
// intermediates/sourcemaps/react/<variant>/index.android.bundle.packager.map
// Intermediate compiler:
// intermediates/sourcemaps/react/<variant>/index.android.bundle.compiler.map
val jsIntermediateSourceMapsDir = File(buildDir, "intermediates/sourcemaps/react/$targetPath")
// The location of the cli.js file for React Native
val cliFile = detectedCliFile(config)
val isHermesEnabledInProject = project.isHermesEnabled
val isHermesEnabledInThisVariant =
if (config.enableHermesOnlyInVariants.get().isNotEmpty()) {
config.enableHermesOnlyInVariants.get().contains(variant.name) && isHermesEnabledInProject
} else {
isHermesEnabledInProject
}
val isDebuggableVariant =
config.debuggableVariants.get().any { it.equals(variant.name, ignoreCase = true) }
val useThirdPartyJSC = project.useThirdPartyJSC
configureNewArchPackagingOptions(project, config, variant)
configureJsEnginePackagingOptions(config, variant, isHermesEnabledInThisVariant, useThirdPartyJSC)
if (
!isHermesEnabledInThisVariant &&
!useThirdPartyJSC &&
rootProject.name != "react-native-github"
) {
showJSCRemovalMessage(project)
}
if (!isDebuggableVariant) {
val entryFileEnvVariable = System.getenv("ENTRY_FILE")
val bundleTask =
tasks.register("createBundle${targetName}JsAndAssets", BundleHermesCTask::class.java) { task
->
task.root.set(config.root)
task.nodeExecutableAndArgs.set(config.nodeExecutableAndArgs)
task.cliFile.set(cliFile)
task.bundleCommand.set(config.bundleCommand)
task.entryFile.set(detectedEntryFile(config, entryFileEnvVariable))
task.extraPackagerArgs.set(config.extraPackagerArgs)
task.bundleConfig.set(config.bundleConfig)
task.bundleAssetName.set(config.bundleAssetName)
task.jsBundleDir.set(jsBundleDir)
task.resourcesDir.set(resourcesDir)
task.hermesEnabled.set(isHermesEnabledInThisVariant)
task.minifyEnabled.set(!isHermesEnabledInThisVariant)
task.devEnabled.set(false)
task.jsIntermediateSourceMapsDir.set(jsIntermediateSourceMapsDir)
task.jsSourceMapsDir.set(jsSourceMapsDir)
task.hermesCommand.set(config.hermesCommand)
task.hermesFlags.set(config.hermesFlags)
task.reactNativeDir.set(config.reactNativeDir)
}
variant.sources.res?.addGeneratedSourceDirectory(bundleTask, BundleHermesCTask::resourcesDir)
variant.sources.assets?.addGeneratedSourceDirectory(bundleTask, BundleHermesCTask::jsBundleDir)
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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
import javax.inject.Inject
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
/**
* A private extension we set on the rootProject to make easier to share values at execution time
* between app project and library project.
*
* Specifically, the [codegenDir], [reactNativeDir] and other properties should be provided by apps
* (for setups like a monorepo which are app specific) and libraries should honor those values.
*
* Users are not supposed to access directly this extension from their build.gradle file.
*/
abstract class PrivateReactExtension @Inject constructor(project: Project) {
private val objects = project.objects
val root: DirectoryProperty =
objects
.directoryProperty()
.convention(
// This is the default for the project root if the users hasn't specified anything.
// If the project is called "react-native-github" or "react-native-build-from-source"
// - We're inside the Github Repo -> root is defined by RN Tester (so no default
// needed)
// - We're inside an includedBuild as we're performing a build from source
// (then we're inside `node_modules/react-native`, so default should be ../../)
// If the project is called in any other name
// - We're inside a user project, so inside the ./android folder. Default should be
// ../
// User can always override this default by setting a `root =` inside the template.
if (
project.rootProject.name == "react-native-github" ||
project.rootProject.name == "react-native-build-from-source"
) {
project.rootProject.layout.projectDirectory.dir("../../")
} else {
project.rootProject.layout.projectDirectory.dir("../")
}
)
val reactNativeDir: DirectoryProperty =
objects.directoryProperty().convention(root.dir("node_modules/react-native"))
val nodeExecutableAndArgs: ListProperty<String> =
objects.listProperty(String::class.java).convention(listOf("node"))
val codegenDir: DirectoryProperty =
objects.directoryProperty().convention(root.dir("node_modules/@react-native/codegen"))
val hermesV1Enabled: Property<Boolean> = objects.property(Boolean::class.java).convention(false)
}

View File

@@ -0,0 +1,210 @@
/*
* 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.tasks
import com.facebook.react.utils.Os.cliPath
import com.facebook.react.utils.detectOSAwareHermesCommand
import com.facebook.react.utils.moveTo
import com.facebook.react.utils.windowsAwareCommandLine
import java.io.File
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileTree
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
abstract class BundleHermesCTask : DefaultTask() {
init {
group = "react"
}
@get:Inject abstract val execOperations: ExecOperations
@get:Internal abstract val root: DirectoryProperty
@get:InputFiles
val sources: ConfigurableFileTree =
project.fileTree(root) { fileTree ->
fileTree.include("**/*.js")
fileTree.include("**/*.jsx")
fileTree.include("**/*.ts")
fileTree.include("**/*.tsx")
fileTree.exclude("**/android/**/*")
fileTree.exclude("**/ios/**/*")
fileTree.exclude("**/build/**/*")
fileTree.exclude("**/node_modules/**/*")
}
@get:Input abstract val nodeExecutableAndArgs: ListProperty<String>
@get:InputFile abstract val cliFile: RegularFileProperty
@get:Internal abstract val reactNativeDir: DirectoryProperty
@get:Input abstract val bundleCommand: Property<String>
@get:InputFile abstract val entryFile: RegularFileProperty
@get:InputFile @get:Optional abstract val bundleConfig: RegularFileProperty
@get:Input abstract val bundleAssetName: Property<String>
@get:Input abstract val minifyEnabled: Property<Boolean>
@get:Input abstract val hermesEnabled: Property<Boolean>
@get:Input abstract val devEnabled: Property<Boolean>
@get:Input abstract val extraPackagerArgs: ListProperty<String>
@get:Input abstract val hermesCommand: Property<String>
@get:Input abstract val hermesFlags: ListProperty<String>
@get:OutputDirectory abstract val jsBundleDir: DirectoryProperty
@get:OutputDirectory abstract val resourcesDir: DirectoryProperty
@get:OutputDirectory abstract val jsIntermediateSourceMapsDir: RegularFileProperty
@get:OutputDirectory abstract val jsSourceMapsDir: DirectoryProperty
@TaskAction
fun run() {
jsBundleDir.get().asFile.mkdirs()
resourcesDir.get().asFile.mkdirs()
jsIntermediateSourceMapsDir.get().asFile.mkdirs()
jsSourceMapsDir.get().asFile.mkdirs()
val bundleAssetFilename = bundleAssetName.get()
val bundleFile = File(jsBundleDir.get().asFile, bundleAssetFilename)
val packagerSourceMap = resolvePackagerSourceMapFile(bundleAssetFilename)
val bundleCommand = getBundleCommand(bundleFile, packagerSourceMap)
runCommand(bundleCommand)
if (hermesEnabled.get()) {
val detectedHermesCommand = detectOSAwareHermesCommand(root.get().asFile, hermesCommand.get())
val bytecodeFile = File("${bundleFile}.hbc")
val outputSourceMap = resolveOutputSourceMap(bundleAssetFilename)
val compilerSourceMap = resolveCompilerSourceMap(bundleAssetFilename)
val hermesCommand = getHermescCommand(detectedHermesCommand, bytecodeFile, bundleFile)
runCommand(hermesCommand)
bytecodeFile.moveTo(bundleFile)
if (hermesFlags.get().contains("-output-source-map")) {
val hermesTempSourceMapFile = File("$bytecodeFile.map")
hermesTempSourceMapFile.moveTo(compilerSourceMap)
val reactNativeDir = reactNativeDir.get().asFile
val composeScriptFile = File(reactNativeDir, "scripts/compose-source-maps.js")
val composeSourceMapsCommand =
getComposeSourceMapsCommand(
composeScriptFile,
packagerSourceMap,
compilerSourceMap,
outputSourceMap,
)
runCommand(composeSourceMapsCommand)
}
}
}
internal fun resolvePackagerSourceMapFile(bundleAssetName: String) =
if (hermesEnabled.get()) {
File(jsIntermediateSourceMapsDir.get().asFile, "$bundleAssetName.packager.map")
} else {
resolveOutputSourceMap(bundleAssetName)
}
internal fun resolveOutputSourceMap(bundleAssetName: String) =
File(jsSourceMapsDir.get().asFile, "$bundleAssetName.map")
internal fun resolveCompilerSourceMap(bundleAssetName: String) =
File(jsIntermediateSourceMapsDir.get().asFile, "$bundleAssetName.compiler.map")
private fun runCommand(command: List<Any>) {
execOperations.exec { exec ->
exec.workingDir(root.get().asFile)
exec.commandLine(command)
}
}
internal fun getBundleCommand(bundleFile: File, sourceMapFile: File): List<Any> {
val rootFile = root.get().asFile
val commandLine =
mutableListOf<String>().apply {
addAll(nodeExecutableAndArgs.get())
add(cliFile.get().asFile.cliPath(rootFile))
add(bundleCommand.get())
add("--platform")
add("android")
add("--dev")
add(devEnabled.get().toString())
add("--reset-cache")
add("--entry-file")
add(entryFile.get().asFile.cliPath(rootFile))
add("--bundle-output")
add(bundleFile.cliPath(rootFile))
add("--assets-dest")
add(resourcesDir.get().asFile.cliPath(rootFile))
add("--sourcemap-output")
add(sourceMapFile.cliPath(rootFile))
if (bundleConfig.isPresent) {
add("--config")
add(bundleConfig.get().asFile.cliPath(rootFile))
}
add("--minify")
add(minifyEnabled.get().toString())
addAll(extraPackagerArgs.get())
add("--verbose")
}
return windowsAwareCommandLine(commandLine)
}
internal fun getHermescCommand(
hermesCommand: String,
bytecodeFile: File,
bundleFile: File,
): List<Any> {
val rootFile = root.get().asFile
return windowsAwareCommandLine(
hermesCommand,
"-w",
"-emit-binary",
"-max-diagnostic-width=80",
"-out",
bytecodeFile.cliPath(rootFile),
bundleFile.cliPath(rootFile),
*hermesFlags.get().toTypedArray(),
)
}
internal fun getComposeSourceMapsCommand(
composeScript: File,
packagerSourceMap: File,
compilerSourceMap: File,
outputSourceMap: File,
): List<Any> {
val rootFile = root.get().asFile
return windowsAwareCommandLine(
*nodeExecutableAndArgs.get().toTypedArray(),
composeScript.cliPath(rootFile),
packagerSourceMap.cliPath(rootFile),
compilerSourceMap.cliPath(rootFile),
"-o",
outputSourceMap.cliPath(rootFile),
)
}
}

View File

@@ -0,0 +1,252 @@
/*
* 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.tasks
import com.facebook.react.model.ModelAutolinkingConfigJson
import com.facebook.react.model.ModelAutolinkingDependenciesPlatformAndroidJson
import com.facebook.react.utils.JsonUtils
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
abstract class GenerateAutolinkingNewArchitecturesFileTask : DefaultTask() {
init {
group = "react"
}
@get:InputFile abstract val autolinkInputFile: RegularFileProperty
@get:OutputDirectory abstract val generatedOutputDirectory: DirectoryProperty
@TaskAction
fun taskAction() {
val model = JsonUtils.fromAutolinkingConfigJson(autolinkInputFile.get().asFile)
val packages = filterAndroidPackages(model)
val cmakeFileContent = generateCmakeFileContent(packages)
val cppFileContent = generateCppFileContent(packages)
val outputDir = generatedOutputDirectory.get().asFile
outputDir.mkdirs()
File(outputDir, CMAKE_FILENAME).apply { writeText(cmakeFileContent) }
File(outputDir, CPP_FILENAME).apply { writeText(cppFileContent) }
File(outputDir, H_FILENAME).apply { writeText(hTemplate) }
}
internal fun filterAndroidPackages(
model: ModelAutolinkingConfigJson?
): List<ModelAutolinkingDependenciesPlatformAndroidJson> =
model?.dependencies?.values?.mapNotNull { it.platforms?.android } ?: emptyList()
internal fun generateCmakeFileContent(
packages: List<ModelAutolinkingDependenciesPlatformAndroidJson>
): String {
val libraryIncludes =
packages.joinToString("\n") { dep ->
var addDirectoryString = ""
val libraryName = dep.libraryName
val cmakeListsPath = dep.cmakeListsPath
val cxxModuleCMakeListsPath = dep.cxxModuleCMakeListsPath
if (libraryName != null && cmakeListsPath != null) {
// If user provided a custom cmakeListsPath, let's honor it.
val nativeFolderPath = sanitizeCmakeListsPath(cmakeListsPath)
addDirectoryString +=
"add_subdirectory(\"$nativeFolderPath\" ${libraryName}_autolinked_build)"
}
if (cxxModuleCMakeListsPath != null) {
// If user provided a custom cxxModuleCMakeListsPath, let's honor it.
val nativeFolderPath = sanitizeCmakeListsPath(cxxModuleCMakeListsPath)
addDirectoryString +=
"\nadd_subdirectory(\"$nativeFolderPath\" ${libraryName}_cxxmodule_autolinked_build)"
}
addDirectoryString
}
val libraryModules =
packages.joinToString("\n ") { dep ->
var autolinkedLibraries = ""
if (dep.libraryName != null) {
autolinkedLibraries += "$CODEGEN_LIB_PREFIX${dep.libraryName}"
}
if (dep.cxxModuleCMakeListsModuleName != null) {
autolinkedLibraries += "\n${dep.cxxModuleCMakeListsModuleName}"
}
autolinkedLibraries
}
return CMAKE_TEMPLATE.replace("{{ libraryIncludes }}", libraryIncludes)
.replace("{{ libraryModules }}", libraryModules)
}
internal fun generateCppFileContent(
packages: List<ModelAutolinkingDependenciesPlatformAndroidJson>
): String {
val packagesWithLibraryNames = packages.filter { android -> android.libraryName != null }
val cppIncludes =
packagesWithLibraryNames.joinToString("\n") { dep ->
var include = "#include <${dep.libraryName}.h>"
if (dep.componentDescriptors.isNotEmpty()) {
include +=
"\n#include <${COMPONENT_INCLUDE_PATH}/${dep.libraryName}/${COMPONENT_DESCRIPTOR_FILENAME}>"
}
if (dep.cxxModuleHeaderName != null) {
include += "\n#include <${dep.cxxModuleHeaderName}.h>"
}
include
}
val cppTurboModuleJavaProviders =
packagesWithLibraryNames.joinToString("\n") { dep ->
val libraryName = dep.libraryName
// language=cpp
"""
auto module_$libraryName = ${libraryName}_ModuleProvider(moduleName, params);
if (module_$libraryName != nullptr) {
return module_$libraryName;
}
"""
.trimIndent()
}
val cppTurboModuleCxxProviders =
packagesWithLibraryNames
.filter { it.cxxModuleHeaderName != null }
.joinToString("\n") { dep ->
val cxxModuleHeaderName = dep.cxxModuleHeaderName
// language=cpp
"""
if (moduleName == $cxxModuleHeaderName::kModuleName) {
return std::make_shared<$cxxModuleHeaderName>(jsInvoker);
}
"""
.trimIndent()
}
val cppComponentDescriptors =
packagesWithLibraryNames
.filter { it.componentDescriptors.isNotEmpty() }
.joinToString("\n") {
it.componentDescriptors.joinToString("\n") {
"providerRegistry->add(concreteComponentDescriptorProvider<$it>());"
}
}
return CPP_TEMPLATE.replace("{{ autolinkingCppIncludes }}", cppIncludes)
.replace("{{ autolinkingCppTurboModuleJavaProviders }}", cppTurboModuleJavaProviders)
.replace("{{ autolinkingCppTurboModuleCxxProviders }}", cppTurboModuleCxxProviders)
.replace("{{ autolinkingCppComponentDescriptors }}", cppComponentDescriptors)
}
companion object {
const val CMAKE_FILENAME = "Android-autolinking.cmake"
const val H_FILENAME = "autolinking.h"
const val CPP_FILENAME = "autolinking.cpp"
const val CODEGEN_LIB_PREFIX = "react_codegen_"
const val COMPONENT_DESCRIPTOR_FILENAME = "ComponentDescriptors.h"
const val COMPONENT_INCLUDE_PATH = "react/renderer/components"
internal fun sanitizeCmakeListsPath(cmakeListsPath: String): String =
cmakeListsPath.replace("CMakeLists.txt", "").replace(" ", "\\ ")
// language=cmake
val CMAKE_TEMPLATE =
"""
# This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin)
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
# We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so
# or link against a old prefab target (this is needed for React Native 0.76 on).
set(REACTNATIVE_MERGED_SO true)
{{ libraryIncludes }}
set(AUTOLINKED_LIBRARIES
{{ libraryModules }}
)
"""
.trimIndent()
// language=cpp
val CPP_TEMPLATE =
"""
/**
* This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
*/
#include "autolinking.h"
{{ autolinkingCppIncludes }}
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
{{ autolinkingCppTurboModuleJavaProviders }}
return nullptr;
}
std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker) {
{{ autolinkingCppTurboModuleCxxProviders }}
return nullptr;
}
void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
{{ autolinkingCppComponentDescriptors }}
return;
}
} // namespace react
} // namespace facebook
"""
.trimIndent()
// language=cpp
val hTemplate =
"""
/**
* This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
*/
#pragma once
#include <ReactCommon/CallInvoker.h>
#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params);
std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker);
void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry);
} // namespace react
} // namespace facebook
"""
.trimIndent()
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.tasks
import com.facebook.react.utils.JsonUtils
import com.facebook.react.utils.Os.cliPath
import com.facebook.react.utils.windowsAwareCommandLine
import java.io.File
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
abstract class GenerateCodegenArtifactsTask : Exec() {
@get:Internal abstract val reactNativeDir: DirectoryProperty
@get:Internal abstract val generatedSrcDir: DirectoryProperty
@get:InputFile abstract val packageJsonFile: RegularFileProperty
@get:Input abstract val nodeWorkingDir: Property<String>
@get:Input abstract val nodeExecutableAndArgs: ListProperty<String>
@get:Input abstract val codegenJavaPackageName: Property<String>
@get:Input abstract val libraryName: Property<String>
@get:InputFile
val generatedSchemaFile: Provider<RegularFile> = generatedSrcDir.file("schema.json")
@get:OutputDirectory val generatedJavaFiles: Provider<Directory> = generatedSrcDir.dir("java")
@get:OutputDirectory val generatedJniFiles: Provider<Directory> = generatedSrcDir.dir("jni")
override fun exec() {
val (resolvedLibraryName, resolvedCodegenJavaPackageName) = resolveTaskParameters()
setupCommandLine(resolvedLibraryName, resolvedCodegenJavaPackageName)
super.exec()
}
internal fun resolveTaskParameters(): Pair<String, String> {
val parsedPackageJson =
if (packageJsonFile.isPresent && packageJsonFile.get().asFile.exists()) {
JsonUtils.fromPackageJson(packageJsonFile.get().asFile)
} else {
null
}
val resolvedLibraryName = parsedPackageJson?.codegenConfig?.name ?: libraryName.get()
val resolvedCodegenJavaPackageName =
parsedPackageJson?.codegenConfig?.android?.javaPackageName ?: codegenJavaPackageName.get()
return resolvedLibraryName to resolvedCodegenJavaPackageName
}
internal fun setupCommandLine(libraryName: String, codegenJavaPackageName: String) {
val workingDir = File(nodeWorkingDir.get())
commandLine(
windowsAwareCommandLine(
*nodeExecutableAndArgs.get().toTypedArray(),
reactNativeDir.file("scripts/generate-specs-cli.js").get().asFile.cliPath(workingDir),
"--platform",
"android",
"--schemaPath",
generatedSchemaFile.get().asFile.cliPath(workingDir),
"--outputDir",
generatedSrcDir.get().asFile.cliPath(workingDir),
"--libraryName",
libraryName,
"--javaPackageName",
codegenJavaPackageName,
)
)
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.tasks
import com.facebook.react.utils.Os.cliPath
import com.facebook.react.utils.windowsAwareCommandLine
import java.io.File
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileTree
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
/**
* A task that will collect all the *.js files inside the provided [jsRootDir] and will run the
* `combine-js-to-schema-cli.js` on top of it (from `react-native-codegen`). The output is a
* `schema.json` file that contains an intermediate representation of the code to be generated.
*/
abstract class GenerateCodegenSchemaTask : Exec() {
@get:Internal abstract val jsRootDir: DirectoryProperty
@get:Internal abstract val codegenDir: DirectoryProperty
@get:Internal abstract val generatedSrcDir: DirectoryProperty
@get:Input abstract val nodeWorkingDir: Property<String>
@get:Input abstract val nodeExecutableAndArgs: ListProperty<String>
@get:InputFiles abstract val jsInputFiles: Property<FileTree>
@get:OutputFile
val generatedSchemaFile: Provider<RegularFile> = generatedSrcDir.file("schema.json")
override fun exec() {
wipeOutputDir()
setupCommandLine()
super.exec()
}
internal fun wipeOutputDir() {
generatedSrcDir.asFile.get().apply {
deleteRecursively()
mkdirs()
}
}
internal fun setupCommandLine() {
val workingDir = File(nodeWorkingDir.get())
commandLine(
windowsAwareCommandLine(
*nodeExecutableAndArgs.get().toTypedArray(),
codegenDir
.file("lib/cli/combine/combine-js-to-schema-cli.js")
.get()
.asFile
.cliPath(workingDir),
"--platform",
"android",
"--exclude",
"NativeSampleTurboModule",
generatedSchemaFile.get().asFile.cliPath(workingDir),
jsRootDir.asFile.get().cliPath(workingDir),
)
)
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.tasks
import com.facebook.react.utils.JsonUtils
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
abstract class GenerateEntryPointTask : DefaultTask() {
init {
group = "react"
}
@get:InputFile abstract val autolinkInputFile: RegularFileProperty
@get:OutputDirectory abstract val generatedOutputDirectory: DirectoryProperty
@TaskAction
fun taskAction() {
val model =
JsonUtils.fromAutolinkingConfigJson(autolinkInputFile.get().asFile)
?: error(
"""
RNGP - Autolinking: Could not parse autolinking config file:
${autolinkInputFile.get().asFile.absolutePath}
The file is either missing or not containing valid JSON so the build won't succeed.
"""
.trimIndent()
)
val packageName =
model.project?.android?.packageName
?: error(
"RNGP - Autolinking: Could not find project.android.packageName in react-native config output! Could not autolink packages without this field."
)
val generatedFileContents = composeFileContent(packageName)
val outputDir = generatedOutputDirectory.get().asFile
outputDir.mkdirs()
File(outputDir, GENERATED_FILENAME).apply {
parentFile.mkdirs()
writeText(generatedFileContents)
}
}
internal fun composeFileContent(packageName: String): String =
generatedFileContentsTemplate.replace("{{packageName}}", packageName)
companion object {
const val GENERATED_FILENAME = "com/facebook/react/ReactNativeApplicationEntryPoint.java"
// language=java
val generatedFileContentsTemplate =
"""
package com.facebook.react;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger;
import com.facebook.react.views.view.WindowUtilKt;
import com.facebook.react.soloader.OpenSourceMergedSoMapping;
import com.facebook.soloader.SoLoader;
import java.io.IOException;
/**
* This class is the entry point for loading React Native using the configuration
* that the users specifies in their .gradle files.
*
* The `loadReactNative(this)` method invocation should be called inside the
* application onCreate otherwise the app won't load correctly.
*/
public class ReactNativeApplicationEntryPoint {
public static void loadReactNative(Context context) {
try {
SoLoader.init(context, OpenSourceMergedSoMapping.INSTANCE);
} catch (IOException error) {
throw new RuntimeException(error);
}
if ({{packageName}}.BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
DefaultNewArchitectureEntryPoint.load();
}
if ({{packageName}}.BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
WindowUtilKt.setEdgeToEdgeFeatureFlagOn();
}
}
}
"""
.trimIndent()
}
}

View File

@@ -0,0 +1,204 @@
/*
* 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.tasks
import com.facebook.react.model.ModelAutolinkingConfigJson
import com.facebook.react.model.ModelAutolinkingDependenciesPlatformAndroidJson
import com.facebook.react.utils.JsonUtils
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
abstract class GeneratePackageListTask : DefaultTask() {
init {
group = "react"
}
@get:InputFile abstract val autolinkInputFile: RegularFileProperty
@get:OutputDirectory abstract val generatedOutputDirectory: DirectoryProperty
@TaskAction
fun taskAction() {
val model =
JsonUtils.fromAutolinkingConfigJson(autolinkInputFile.get().asFile)
?: error(
"""
RNGP - Autolinking: Could not parse autolinking config file:
${autolinkInputFile.get().asFile.absolutePath}
The file is either missing or not containing valid JSON so the build won't succeed.
"""
.trimIndent()
)
val packageName =
model.project?.android?.packageName
?: error(
"RNGP - Autolinking: Could not find project.android.packageName in react-native config output! Could not autolink packages without this field."
)
val androidPackages = filterAndroidPackages(model)
val packageImports = composePackageImports(packageName, androidPackages)
val packageClassInstance = composePackageInstance(packageName, androidPackages)
val generatedFileContents = composeFileContent(packageImports, packageClassInstance)
val outputDir = generatedOutputDirectory.get().asFile
outputDir.mkdirs()
File(outputDir, GENERATED_FILENAME).apply {
parentFile.mkdirs()
writeText(generatedFileContents)
}
}
internal fun composePackageImports(
packageName: String,
packages: Map<String, ModelAutolinkingDependenciesPlatformAndroidJson>,
) =
packages.entries.joinToString("\n") { (name, dep) ->
val packageImportPath =
requireNotNull(dep.packageImportPath) {
"RNGP - Autolinking: Missing `packageImportPath` in `config` for dependency $name. This is required to generate the autolinking package list."
}
"// $name\n${interpolateDynamicValues(packageImportPath, packageName)}"
}
internal fun composePackageInstance(
packageName: String,
packages: Map<String, ModelAutolinkingDependenciesPlatformAndroidJson>,
) =
if (packages.isEmpty()) {
""
} else {
",\n " +
packages.entries.joinToString(",\n ") { (name, dep) ->
val packageInstance =
requireNotNull(dep.packageInstance) {
"RNGP - Autolinking: Missing `packageInstance` in `config` for dependency $name. This is required to generate the autolinking package list."
}
interpolateDynamicValues(packageInstance, packageName)
}
}
internal fun filterAndroidPackages(
model: ModelAutolinkingConfigJson?
): Map<String, ModelAutolinkingDependenciesPlatformAndroidJson> {
val packages = model?.dependencies?.values ?: emptyList()
return packages
.filter { it.platforms?.android != null }
// The pure C++ dependencies won't have a .java/.kt file to import
.filterNot { it.platforms?.android?.isPureCxxDependency == true }
.associate { it.name to checkNotNull(it.platforms?.android) }
}
internal fun composeFileContent(packageImports: String, packageClassInstance: String): String =
generatedFileContentsTemplate
.replace("{{ packageImports }}", packageImports)
.replace("{{ packageClassInstances }}", packageClassInstance)
companion object {
const val GENERATED_FILENAME = "com/facebook/react/PackageList.java"
/**
* Before adding the package replacement mechanism, BuildConfig and R classes were imported
* automatically into the scope of the file. We want to replace all non-FQDN references to those
* classes with the package name of the MainApplication.
*
* We want to match "R" or "BuildConfig":
* - new Package(R.string…),
* - Module.configure(BuildConfig);
* ^ hence including (BuildConfig|R)
* but we don't want to match "R":
* - new Package(getResources…),
* - new PackageR…,
* - new Royal…,
* ^ hence excluding \w before and after matches
* and "BuildConfig" that has FQDN reference:
* - Module.configure(com.acme.BuildConfig);
* ^ hence excluding . before the match.
*/
internal fun interpolateDynamicValues(input: String, packageName: String): String =
input.replace(Regex("([^.\\w])(BuildConfig|R)(\\W)")) { match ->
val (prefix, className, suffix) = match.destructured
"${prefix}${packageName}.${className}${suffix}"
}
// language=java
val generatedFileContentsTemplate =
"""
package com.facebook.react;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainPackageConfig;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.ArrayList;
{{ packageImports }}
@SuppressWarnings("deprecation")
public class PackageList {
private Application application;
private ReactNativeHost reactNativeHost;
private MainPackageConfig mConfig;
public PackageList(ReactNativeHost reactNativeHost) {
this(reactNativeHost, null);
}
public PackageList(Application application) {
this(application, null);
}
public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
this.reactNativeHost = reactNativeHost;
mConfig = config;
}
public PackageList(Application application, MainPackageConfig config) {
this.reactNativeHost = null;
this.application = application;
mConfig = config;
}
private ReactNativeHost getReactNativeHost() {
return this.reactNativeHost;
}
private Resources getResources() {
return this.getApplication().getResources();
}
private Application getApplication() {
if (this.reactNativeHost == null) return this.application;
return this.reactNativeHost.getApplication();
}
private Context getApplicationContext() {
return this.getApplication().getApplicationContext();
}
public ArrayList<ReactPackage> getPackages() {
return new ArrayList<>(Arrays.<ReactPackage>asList(
new MainReactPackage(mConfig){{ packageClassInstances }}
));
}
}
"""
.trimIndent()
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.tasks.internal
import com.facebook.react.utils.Os.unixifyPath
import com.facebook.react.utils.windowsAwareBashCommandLine
import java.io.FileOutputStream
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileTree
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
/**
* A Task that will call the `scripts/oss/build.sh` script to trigger the creation of the codegen
* lib artifacts.
*
* NOTE: This task is required when using react-native-codegen from source, instead of npm.
*/
abstract class BuildCodegenCLITask : Exec() {
@get:Internal abstract val codegenDir: DirectoryProperty
@get:Internal abstract val bashWindowsHome: Property<String>
@get:Internal abstract val rootProjectName: Property<String>
@get:InputFiles abstract val inputFiles: Property<FileTree>
@get:OutputFiles abstract val outputFiles: Property<FileTree>
@get:OutputFile abstract val logFile: RegularFileProperty
override fun exec() {
// For build from source scenario, we don't need to build the codegen at all.
if (rootProjectName.get() == "react-native-build-from-source") {
return
}
val logFileConcrete =
logFile.get().asFile.apply {
parentFile.mkdirs()
if (exists()) {
delete()
}
createNewFile()
}
standardOutput = FileOutputStream(logFileConcrete)
commandLine(
windowsAwareBashCommandLine(
codegenDir.asFile.get().canonicalPath.unixifyPath().plus(BUILD_SCRIPT_PATH),
bashWindowsHome = bashWindowsHome.orNull,
)
)
super.exec()
}
companion object {
private const val BUILD_SCRIPT_PATH = "/scripts/oss/build.sh"
}
}

View File

@@ -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.
*/
package com.facebook.react.tasks.internal
import java.io.File
import java.io.FileOutputStream
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
/**
* A Task that will just expose an Exec-like task and that offers properties to configure the
* standard output and error.
*/
abstract class CustomExecTask : Exec() {
@get:OutputFile @get:Optional abstract val standardOutputFile: RegularFileProperty
@get:OutputFile @get:Optional abstract val errorOutputFile: RegularFileProperty
@get:Input @get:Optional abstract val onlyIfProvidedPathDoesNotExists: Property<String>
override fun exec() {
if (
onlyIfProvidedPathDoesNotExists.isPresent &&
File(onlyIfProvidedPathDoesNotExists.get()).exists()
) {
return
}
if (standardOutputFile.isPresent) {
standardOutput = FileOutputStream(standardOutputFile.get().asFile)
}
if (errorOutputFile.isPresent) {
errorOutput = FileOutputStream(errorOutputFile.get().asFile)
}
super.exec()
}
}

View File

@@ -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.
*/
package com.facebook.react.tasks.internal
import java.io.File
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
/**
* A task that takes care of extracting Boost from a source folder/zip and preparing it to be
* consumed by the NDK
*/
abstract class PrepareBoostTask : DefaultTask() {
@get:InputFiles abstract val boostPath: ConfigurableFileCollection
@get:InputDirectory abstract val boostThirdPartyJniPath: DirectoryProperty
@get:Input abstract val boostVersion: Property<String>
@get:OutputDirectory abstract val outputDir: DirectoryProperty
@get:Inject abstract val fs: FileSystemOperations
@TaskAction
fun taskAction() {
fs.copy { it ->
it.from(boostPath)
it.from(boostThirdPartyJniPath)
it.include(
"CMakeLists.txt",
"boost_${boostVersion.get()}/boost/**/*.hpp",
"boost/boost/**/*.hpp",
"asm/**/*.S",
)
it.includeEmptyDirs = false
it.into(outputDir)
}
File(outputDir.asFile.get(), "boost").apply {
renameTo(File(parentFile, "boost_${boostVersion.get()}"))
}
}
}

View File

@@ -0,0 +1,119 @@
/*
* 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.tasks.internal
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.CopySpec
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
/**
* A task that takes care of extracting gflags from a source folder/zip and preparing it to be
* consumed by the NDK. This task will also take care of applying the mapping for gflags parameters.
*/
abstract class PrepareGflagsTask : DefaultTask() {
@get:InputFiles abstract val gflagsPath: ConfigurableFileCollection
@get:InputDirectory abstract val gflagsThirdPartyPath: DirectoryProperty
@get:Input abstract val gflagsVersion: Property<String>
@get:OutputDirectory abstract val outputDir: DirectoryProperty
@get:Inject abstract val fs: FileSystemOperations
@TaskAction
fun taskAction() {
val commonCopyConfig: (action: CopySpec) -> Unit = { action ->
action.from(gflagsPath)
action.from(gflagsThirdPartyPath)
action.duplicatesStrategy = DuplicatesStrategy.INCLUDE
action.includeEmptyDirs = false
action.into(outputDir)
}
fs.copy { action ->
commonCopyConfig(action)
action.include(
"gflags-${gflagsVersion.get()}/src/*.h",
"gflags-${gflagsVersion.get()}/src/*.cc",
"CMakeLists.txt",
)
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.path = "gflags/${matchedFile.name}"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/gflags_declare.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line ->
// Replace all placeholders with appropriate values
// see https://github.com/gflags/gflags/blob/v2.2.0/src/gflags_declare.h.in
line
.replace(Regex("@GFLAGS_NAMESPACE@"), "gflags")
.replace(
Regex(
"@(HAVE_STDINT_H|HAVE_SYS_TYPES_H|HAVE_INTTYPES_H|GFLAGS_INTTYPES_FORMAT_C99)@"
),
"1",
)
.replace(Regex("@([A-Z0-9_]+)@"), "1")
}
matchedFile.path = "gflags/${matchedFile.name.removeSuffix(".in")}"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/config.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line -> line.replace(Regex("^#cmakedefine"), "//cmakedefine") }
matchedFile.path = "gflags/${matchedFile.name.removeSuffix(".in")}"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/gflags_ns.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line ->
line.replace(Regex("@ns@"), "google").replace(Regex("@NS@"), "google".uppercase())
}
matchedFile.path = "gflags/gflags_google.h"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/gflags.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line ->
line
.replace(Regex("@GFLAGS_ATTRIBUTE_UNUSED@"), "")
.replace(Regex("@INCLUDE_GFLAGS_NS_H@"), "#include \"gflags/gflags_google.h\"")
}
matchedFile.path = "gflags/${matchedFile.name.removeSuffix(".in")}"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/gflags_completions.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line -> line.replace(Regex("@GFLAGS_NAMESPACE@"), "gflags") }
matchedFile.path = "gflags/${matchedFile.name.removeSuffix(".in")}"
}
}
}
}

View File

@@ -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.
*/
package com.facebook.react.tasks.internal
import java.io.File
import javax.inject.Inject
import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
/**
* A task that takes care of extracting Glog from a source folder/zip and preparing it to be
* consumed by the NDK. This task will also take care of applying the mapping for Glog parameters.
*/
abstract class PrepareGlogTask : DefaultTask() {
@get:InputFiles abstract val glogPath: ConfigurableFileCollection
@get:InputDirectory abstract val glogThirdPartyJniPath: DirectoryProperty
@get:Input abstract val glogVersion: Property<String>
@get:OutputDirectory abstract val outputDir: DirectoryProperty
@get:Inject abstract val fs: FileSystemOperations
@TaskAction
fun taskAction() {
fs.copy { action ->
action.from(glogPath)
action.from(glogThirdPartyJniPath)
action.include("glog-${glogVersion.get()}/src/**/*", "CMakeLists.txt", "config.h")
action.duplicatesStrategy = DuplicatesStrategy.INCLUDE
action.includeEmptyDirs = false
action.filesMatching("**/*.h.in") { matchedFile ->
matchedFile.filter(
mapOf(
"tokens" to
mapOf(
"ac_cv_have_unistd_h" to "1",
"ac_cv_have_stdint_h" to "1",
"ac_cv_have_systypes_h" to "1",
"ac_cv_have_inttypes_h" to "1",
"ac_cv_have_libgflags" to "0",
"ac_google_start_namespace" to "namespace google {",
"ac_cv_have_uint16_t" to "1",
"ac_cv_have_u_int16_t" to "1",
"ac_cv_have___uint16" to "0",
"ac_google_end_namespace" to "}",
"ac_cv_have___builtin_expect" to "1",
"ac_google_namespace" to "google",
"ac_cv___attribute___noinline" to "__attribute__ ((noinline))",
"ac_cv___attribute___noreturn" to "__attribute__ ((noreturn))",
"ac_cv___attribute___printf_4_5" to
"__attribute__((__format__ (__printf__, 4, 5)))",
)
),
ReplaceTokens::class.java,
)
matchedFile.path = (matchedFile.name.removeSuffix(".in"))
}
action.into(outputDir)
}
val exportedDir = File(outputDir.asFile.get(), "exported/glog/").apply { mkdirs() }
fs.copy { action ->
action.from(outputDir)
action.include(
"stl_logging.h",
"logging.h",
"raw_logging.h",
"vlog_is_on.h",
"**/src/glog/log_severity.h",
)
action.eachFile { file -> file.path = file.name }
action.includeEmptyDirs = false
action.into(exportedDir)
}
}
}

View File

@@ -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.
*/
package com.facebook.react.tasks.internal
import com.facebook.react.tasks.internal.utils.PrefabPreprocessingEntry
import java.io.File
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
/**
* A task that takes care of copying headers and filtering them so that can be consumed by the
* Prefab protocol. This task handles also the header prefixes.
*
* It currently filters out some of the Boost headers as they're not used by React Native and are
* resulting in bigger .aar (250Mb+).
*
* You should provide in input a list fo [PrefabPreprocessingEntry] that will be used by this task
* to do the necessary copy operations.
*/
abstract class PreparePrefabHeadersTask : DefaultTask() {
@get:Input abstract val input: ListProperty<PrefabPreprocessingEntry>
@get:OutputDirectory abstract val outputDir: DirectoryProperty
@get:Inject abstract val fs: FileSystemOperations
@TaskAction
fun taskAction() {
input.get().forEach { (libraryName, pathToPrefixCouples) ->
val outputFolder: RegularFile = outputDir.file(libraryName).get()
pathToPrefixCouples.forEach { (headerPath, headerPrefix) ->
fs.copy { copySpec ->
copySpec.from(headerPath)
copySpec.include("**/*.h")
copySpec.exclude("**/*.cpp")
copySpec.exclude("**/*.txt")
// We don't want to copy all the boost headers as they are 250Mb+
copySpec.include("boost/config.hpp")
copySpec.include("boost/config/**/*.hpp")
copySpec.include("boost/core/*.hpp")
copySpec.include("boost/detail/workaround.hpp")
copySpec.include("boost/operators.hpp")
copySpec.include("boost/preprocessor/**/*.hpp")
// Headers needed for exposing rrc_text and rrc_textinput
copySpec.include("boost/container_hash/**/*.hpp")
copySpec.include("boost/detail/**/*.hpp")
copySpec.include("boost/intrusive/**/*.hpp")
copySpec.include("boost/iterator/**/*.hpp")
copySpec.include("boost/move/**/*.hpp")
copySpec.include("boost/mpl/**/*.hpp")
copySpec.include("boost/mp11/**/*.hpp")
copySpec.include("boost/describe/**/*.hpp")
copySpec.include("boost/type_traits/**/*.hpp")
copySpec.include("boost/utility/**/*.hpp")
copySpec.include("boost/assert.hpp")
copySpec.include("boost/static_assert.hpp")
copySpec.include("boost/cstdint.hpp")
copySpec.include("boost/utility.hpp")
copySpec.include("boost/version.hpp")
copySpec.into(File(outputFolder.asFile, headerPrefix))
}
}
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.tasks.internal.utils
import java.io.Serializable
/**
* This data class represents an entry that can be consumed by the [PreparePrefabHeadersTask].
*
* @param libraryName The name of the library that you're preparing for Prefab
* @param pathToPrefixCouples A list of pairs Path to Header prefix. You can use this list to supply
* a list of paths that you want to be considered for prefab. Each path can specify an header
* prefix that will be used by prefab to re-created the header layout.
*/
data class PrefabPreprocessingEntry(
val libraryName: String,
val pathToPrefixCouples: List<Pair<String, String>>,
) : Serializable {
constructor(
libraryName: String,
pathToPrefixCouple: Pair<String, String>,
) : this(libraryName, listOf(pathToPrefixCouple))
}

View File

@@ -0,0 +1,162 @@
/*
* 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.utils
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.LibraryExtension
import com.facebook.react.ReactExtension
import com.facebook.react.utils.ProjectUtils.isEdgeToEdgeEnabled
import com.facebook.react.utils.ProjectUtils.isHermesEnabled
import java.io.File
import java.net.Inet4Address
import java.net.NetworkInterface
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.plus
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.plugins.AppliedPlugin
import org.w3c.dom.Element
@Suppress("UnstableApiUsage")
internal object AgpConfiguratorUtils {
fun configureBuildTypesForApp(project: Project) {
val action =
Action<AppliedPlugin> {
project.extensions
.getByType(ApplicationAndroidComponentsExtension::class.java)
.finalizeDsl { ext ->
ext.buildTypes {
val debug =
getByName("debug").apply {
manifestPlaceholders["usesCleartextTraffic"] = "true"
}
getByName("release").apply {
manifestPlaceholders["usesCleartextTraffic"] = "false"
}
maybeCreate("debugOptimized").apply {
manifestPlaceholders["usesCleartextTraffic"] = "true"
initWith(debug)
matchingFallbacks += listOf("release")
externalNativeBuild { cmake { arguments("-DCMAKE_BUILD_TYPE=Release") } }
}
}
}
}
project.pluginManager.withPlugin("com.android.application", action)
}
fun configureBuildConfigFieldsForApp(project: Project, extension: ReactExtension) {
val action =
Action<AppliedPlugin> {
project.extensions
.getByType(ApplicationAndroidComponentsExtension::class.java)
.finalizeDsl { ext ->
ext.buildFeatures.buildConfig = true
ext.defaultConfig.buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true")
ext.defaultConfig.buildConfigField(
"boolean",
"IS_HERMES_ENABLED",
project.isHermesEnabled.toString(),
)
ext.defaultConfig.buildConfigField(
"boolean",
"IS_EDGE_TO_EDGE_ENABLED",
project.isEdgeToEdgeEnabled.toString(),
)
}
}
project.pluginManager.withPlugin("com.android.application", action)
project.pluginManager.withPlugin("com.android.library", action)
}
fun configureBuildConfigFieldsForLibraries(appProject: Project) {
appProject.rootProject.allprojects { subproject ->
subproject.pluginManager.withPlugin("com.android.library") {
subproject.extensions
.getByType(LibraryAndroidComponentsExtension::class.java)
.finalizeDsl { ext -> ext.buildFeatures.buildConfig = true }
}
}
}
fun configureDevServerLocation(project: Project) {
val devServerPort =
project.properties["reactNativeDevServerPort"]?.toString() ?: DEFAULT_DEV_SERVER_PORT
val action =
Action<AppliedPlugin> {
project.extensions
.getByType(ApplicationAndroidComponentsExtension::class.java)
.finalizeDsl { ext ->
ext.buildFeatures.resValues = true
ext.defaultConfig.resValue(
"string",
"react_native_dev_server_ip",
getHostIpAddress(),
)
ext.defaultConfig.resValue("integer", "react_native_dev_server_port", devServerPort)
}
}
project.pluginManager.withPlugin("com.android.application", action)
project.pluginManager.withPlugin("com.android.library", action)
}
fun configureNamespaceForLibraries(appProject: Project) {
appProject.rootProject.allprojects { subproject ->
subproject.pluginManager.withPlugin("com.android.library") {
subproject.extensions
.getByType(LibraryAndroidComponentsExtension::class.java)
.finalizeDsl { ext ->
if (ext.namespace == null) {
val android = subproject.extensions.getByType(LibraryExtension::class.java)
val manifestFile = android.sourceSets.getByName("main").manifest.srcFile
manifestFile
.takeIf { it.exists() }
?.let { file ->
getPackageNameFromManifest(file)?.let { packageName ->
ext.namespace = packageName
}
}
}
}
}
}
}
}
const val DEFAULT_DEV_SERVER_PORT = "8081"
fun getPackageNameFromManifest(manifest: File): String? {
val factory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
val builder: DocumentBuilder = factory.newDocumentBuilder()
try {
val xmlDocument = builder.parse(manifest)
val manifestElement = xmlDocument.getElementsByTagName("manifest").item(0) as? Element
val packageName = manifestElement?.getAttribute("package")
return if (packageName.isNullOrEmpty()) null else packageName
} catch (e: Exception) {
return null
}
}
internal fun getHostIpAddress(): String =
NetworkInterface.getNetworkInterfaces()
.asSequence()
.filter { it.isUp && !it.isLoopback }
.flatMap { it.inetAddresses.asSequence() }
.filter { it is Inet4Address && !it.isLoopbackAddress }
.map { it.hostAddress }
.firstOrNull() ?: "localhost"

View File

@@ -0,0 +1,71 @@
/*
* 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.utils
import java.util.*
import org.gradle.api.Project
internal object BackwardCompatUtils {
private var hasShownJSCRemovalMessage = false
fun configureBackwardCompatibilityReactMap(project: Project) {
if (project.extensions.extraProperties.has("react")) {
@Suppress("UNCHECKED_CAST")
val reactMap =
project.extensions.extraProperties.get("react") as? Map<String, Any?> ?: mapOf()
if (reactMap.isNotEmpty()) {
project.logger.error(
"""
********************************************************************************
ERROR: Using old project.ext.react configuration.
We identified that your project is using a old configuration block as:
project.ext.react = [
// ...
]
You should migrate to the new configuration:
react {
// ...
}
You can find documentation inside `android/app/build.gradle` on how to use it.
********************************************************************************
"""
.trimIndent()
)
}
}
// We set an empty react[] map so if a library is reading it, they will find empty values.
project.extensions.extraProperties.set("react", mapOf<String, String>())
}
fun showJSCRemovalMessage(project: Project) {
if (hasShownJSCRemovalMessage) {
return
}
val message =
"""
=============== JavaScriptCore is being moved ===============
JavaScriptCore has been extracted from react-native core
and will be removed in a future release. It can now be
installed from `@react-native-community/javascriptcore`
See: https://github.com/react-native-community/javascriptcore
=============================================================
"""
.trimIndent()
project.logger.warn(message)
hasShownJSCRemovalMessage = true
}
}

View File

@@ -0,0 +1,285 @@
/*
* 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.utils
import com.facebook.react.utils.PropertyUtils.DEFAULT_INTERNAL_HERMES_PUBLISHING_GROUP
import com.facebook.react.utils.PropertyUtils.DEFAULT_INTERNAL_REACT_PUBLISHING_GROUP
import com.facebook.react.utils.PropertyUtils.EXCLUSIVE_ENTEPRISE_REPOSITORY
import com.facebook.react.utils.PropertyUtils.INCLUDE_JITPACK_REPOSITORY
import com.facebook.react.utils.PropertyUtils.INCLUDE_JITPACK_REPOSITORY_DEFAULT
import com.facebook.react.utils.PropertyUtils.INTERNAL_HERMES_PUBLISHING_GROUP
import com.facebook.react.utils.PropertyUtils.INTERNAL_HERMES_V1_VERSION_NAME
import com.facebook.react.utils.PropertyUtils.INTERNAL_HERMES_VERSION_NAME
import com.facebook.react.utils.PropertyUtils.INTERNAL_REACT_NATIVE_MAVEN_LOCAL_REPO
import com.facebook.react.utils.PropertyUtils.INTERNAL_REACT_PUBLISHING_GROUP
import com.facebook.react.utils.PropertyUtils.INTERNAL_USE_HERMES_NIGHTLY
import com.facebook.react.utils.PropertyUtils.INTERNAL_VERSION_NAME
import com.facebook.react.utils.PropertyUtils.SCOPED_EXCLUSIVE_ENTEPRISE_REPOSITORY
import com.facebook.react.utils.PropertyUtils.SCOPED_INCLUDE_JITPACK_REPOSITORY
import java.io.File
import java.net.URI
import java.util.*
import org.gradle.api.Project
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
internal object DependencyUtils {
internal data class Coordinates(
val versionString: String,
val hermesVersionString: String,
val hermesV1VersionString: String,
val reactGroupString: String = DEFAULT_INTERNAL_REACT_PUBLISHING_GROUP,
val hermesGroupString: String = DEFAULT_INTERNAL_HERMES_PUBLISHING_GROUP,
)
/**
* This method takes care of configuring the repositories{} block for both the app and all the 3rd
* party libraries which are auto-linked.
*/
fun configureRepositories(project: Project) {
val exclusiveEnterpriseRepository = project.rootProject.exclusiveEnterpriseRepository()
if (exclusiveEnterpriseRepository != null) {
project.logger.lifecycle(
"Replacing ALL Maven Repositories with: $exclusiveEnterpriseRepository"
)
}
project.rootProject.allprojects { eachProject ->
with(eachProject) {
if (hasProperty(INTERNAL_REACT_NATIVE_MAVEN_LOCAL_REPO)) {
val mavenLocalRepoPath = property(INTERNAL_REACT_NATIVE_MAVEN_LOCAL_REPO) as String
mavenRepoFromURI(File(mavenLocalRepoPath).toURI()) { repo ->
repo.content { it.excludeGroup("org.webkit") }
}
}
if (exclusiveEnterpriseRepository != null) {
// We remove all previously set repositories and only configure the proxy provided by the
// user.
rootProject.repositories.clear()
mavenRepoFromUrl(exclusiveEnterpriseRepository)
// We return here as we don't want to configure other repositories as well.
return@allprojects
}
// We add the snapshot for users on nightlies.
mavenRepoFromUrl("https://central.sonatype.com/repository/maven-snapshots/") { repo ->
repo.content { it.excludeGroup("org.webkit") }
}
repositories.mavenCentral { repo ->
// We don't want to fetch JSC from Maven Central as there are older versions there.
repo.content { it.excludeGroup("org.webkit") }
// If the user provided a react.internal.mavenLocalRepo, do not attempt to load
// anything from Maven Central that is react related.
if (hasProperty(INTERNAL_REACT_NATIVE_MAVEN_LOCAL_REPO)) {
repo.content { it.excludeGroup("com.facebook.react") }
}
}
repositories.google { repo ->
repo.content {
// We don't want to fetch JSC or React from Google
it.excludeGroup("org.webkit")
it.excludeGroup("io.github.react-native-community")
it.excludeGroup("com.facebook.react")
}
}
if (shouldAddJitPack()) {
mavenRepoFromUrl("https://www.jitpack.io") { repo ->
repo.content { content ->
// We don't want to fetch JSC or React from JitPack
content.excludeGroup("org.webkit")
content.excludeGroup("io.github.react-native-community")
content.excludeGroup("com.facebook.react")
}
}
}
}
}
}
/**
* This method takes care of configuring the resolution strategy for both the app and all the 3rd
* party libraries which are auto-linked. Specifically it takes care of:
* - Forcing the react-android/hermes-android version to the one specified in the package.json
* - Substituting `react-native` with `react-android` and `hermes-engine` with `hermes-android`
* - Selecting between the classic Hermes and Hermes V1
*/
fun configureDependencies(
project: Project,
coordinates: Coordinates,
hermesV1Enabled: Boolean = false,
) {
if (
coordinates.versionString.isBlank() ||
(!hermesV1Enabled && coordinates.hermesVersionString.isBlank()) ||
(hermesV1Enabled && coordinates.hermesV1VersionString.isBlank())
)
return
project.rootProject.allprojects { eachProject ->
eachProject.configurations.all { configuration ->
// Here we set a dependencySubstitution for both react-native and hermes-engine as those
// coordinates are voided due to https://github.com/facebook/react-native/issues/35210
// This allows users to import libraries that are still using
// implementation("com.facebook.react:react-native:+") and resolve the right dependency.
configuration.resolutionStrategy.dependencySubstitution {
getDependencySubstitutions(coordinates, hermesV1Enabled).forEach { (module, dest, reason)
->
it.substitute(it.module(module)).using(it.module(dest)).because(reason)
}
}
configuration.resolutionStrategy.force(
"${coordinates.reactGroupString}:react-android:${coordinates.versionString}",
)
if (!(eachProject.findProperty(INTERNAL_USE_HERMES_NIGHTLY) as? String).toBoolean()) {
// Contributors only: The hermes-engine version is forced only if the user has
// not opted into using nightlies for local development.
configuration.resolutionStrategy.force(
"${coordinates.hermesGroupString}:hermes-android:${if (hermesV1Enabled) coordinates.hermesV1VersionString else coordinates.hermesVersionString}"
)
}
}
}
}
internal fun getDependencySubstitutions(
coordinates: Coordinates,
hermesV1Enabled: Boolean = false,
): List<Triple<String, String, String>> {
val dependencySubstitution = mutableListOf<Triple<String, String, String>>()
val hermesVersion =
if (hermesV1Enabled) coordinates.hermesV1VersionString else coordinates.hermesVersionString
val hermesVersionString = "${coordinates.hermesGroupString}:hermes-android:${hermesVersion}"
dependencySubstitution.add(
Triple(
"com.facebook.react:react-native",
"${coordinates.reactGroupString}:react-android:${coordinates.versionString}",
"The react-native artifact was deprecated in favor of react-android due to https://github.com/facebook/react-native/issues/35210.",
)
)
dependencySubstitution.add(
Triple(
"com.facebook.react:hermes-engine",
hermesVersionString,
"The hermes-engine artifact was deprecated in favor of hermes-android due to https://github.com/facebook/react-native/issues/35210.",
)
)
dependencySubstitution.add(
Triple(
"com.facebook.react:hermes-android",
hermesVersionString,
"The hermes-android artifact was moved to com.facebook.hermes publishing group.",
)
)
if (coordinates.reactGroupString != DEFAULT_INTERNAL_REACT_PUBLISHING_GROUP) {
dependencySubstitution.add(
Triple(
"com.facebook.react:react-android",
"${coordinates.reactGroupString}:react-android:${coordinates.versionString}",
"The react-android dependency was modified to use the correct Maven group.",
)
)
dependencySubstitution.add(
Triple(
"com.facebook.react:hermes-android",
hermesVersionString,
"The hermes-android dependency was modified to use the correct Maven group.",
)
)
}
if (coordinates.hermesGroupString != DEFAULT_INTERNAL_HERMES_PUBLISHING_GROUP) {
dependencySubstitution.add(
Triple(
"com.facebook.hermes:hermes-android",
hermesVersionString,
"The hermes-android dependency was modified to use the correct Maven group.",
)
)
}
return dependencySubstitution
}
fun readVersionAndGroupStrings(propertiesFile: File, hermesVersionFile: File): Coordinates {
val reactAndroidProperties = Properties()
propertiesFile.inputStream().use { reactAndroidProperties.load(it) }
val versionStringFromFile = (reactAndroidProperties[INTERNAL_VERSION_NAME] as? String).orEmpty()
// If on a nightly, we need to fetch the -SNAPSHOT artifact from Sonatype.
val versionString =
if (versionStringFromFile.startsWith("0.0.0") || "-nightly-" in versionStringFromFile) {
"$versionStringFromFile-SNAPSHOT"
} else {
versionStringFromFile
}
// Returns Maven group for repos using different group for Maven artifacts
val reactGroupString =
reactAndroidProperties[INTERNAL_REACT_PUBLISHING_GROUP] as? String
?: DEFAULT_INTERNAL_REACT_PUBLISHING_GROUP
val hermesGroupString =
reactAndroidProperties[INTERNAL_HERMES_PUBLISHING_GROUP] as? String
?: DEFAULT_INTERNAL_HERMES_PUBLISHING_GROUP
val hermesVersionProperties = Properties()
hermesVersionFile.inputStream().use { hermesVersionProperties.load(it) }
val hermesVersionString =
(hermesVersionProperties[INTERNAL_HERMES_VERSION_NAME] as? String).orEmpty()
val hermesVersion =
if (hermesVersionString.startsWith("0.0.0") || "-commitly-" in hermesVersionString) {
"$hermesVersionString-SNAPSHOT"
} else {
hermesVersionString
}
val hermesV1Version =
(hermesVersionProperties[INTERNAL_HERMES_V1_VERSION_NAME] as? String).orEmpty()
return Coordinates(
versionString,
hermesVersion,
hermesV1Version,
reactGroupString,
hermesGroupString,
)
}
fun Project.mavenRepoFromUrl(
url: String,
action: (MavenArtifactRepository) -> Unit = {},
): MavenArtifactRepository =
project.repositories.maven {
it.url = URI.create(url)
action(it)
}
fun Project.mavenRepoFromURI(
uri: URI,
action: (MavenArtifactRepository) -> Unit = {},
): MavenArtifactRepository =
project.repositories.maven {
it.url = uri
action(it)
}
internal fun Project.shouldAddJitPack() =
when {
hasProperty(SCOPED_INCLUDE_JITPACK_REPOSITORY) ->
property(SCOPED_INCLUDE_JITPACK_REPOSITORY).toString().toBoolean()
hasProperty(INCLUDE_JITPACK_REPOSITORY) ->
property(INCLUDE_JITPACK_REPOSITORY).toString().toBoolean()
else -> INCLUDE_JITPACK_REPOSITORY_DEFAULT
}
internal fun Project.exclusiveEnterpriseRepository() =
when {
hasProperty(SCOPED_EXCLUSIVE_ENTEPRISE_REPOSITORY) ->
property(SCOPED_EXCLUSIVE_ENTEPRISE_REPOSITORY).toString()
hasProperty(EXCLUSIVE_ENTEPRISE_REPOSITORY) ->
property(EXCLUSIVE_ENTEPRISE_REPOSITORY).toString()
else -> null
}
}

View File

@@ -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.
*/
package com.facebook.react.utils
import java.io.File
internal fun File.moveTo(destination: File) {
copyTo(destination, overwrite = true)
delete()
}
internal fun File.recreateDir() {
deleteRecursively()
mkdirs()
}

View File

@@ -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.
*/
package com.facebook.react.utils
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.facebook.react.utils.PropertyUtils.INTERNAL_DISABLE_JAVA_VERSION_ALIGNMENT
import org.gradle.api.Action
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.plugins.AppliedPlugin
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
internal object JdkConfiguratorUtils {
/**
* Function that takes care of configuring the JDK toolchain for all the projects projects. As we
* do decide the JDK version based on the AGP version that RNGP brings over, here we can safely
* configure the toolchain to 17.
*/
fun configureJavaToolChains(input: Project) {
// Check at the app level if react.internal.disableJavaVersionAlignment is set.
if (input.hasProperty(INTERNAL_DISABLE_JAVA_VERSION_ALIGNMENT)) {
return
}
input.rootProject.allprojects { project ->
// Allows every single module to set react.internal.disableJavaVersionAlignment also.
if (project.hasProperty(INTERNAL_DISABLE_JAVA_VERSION_ALIGNMENT)) {
return@allprojects
}
val applicationAction =
Action<AppliedPlugin> {
project.extensions
.getByType(ApplicationAndroidComponentsExtension::class.java)
.finalizeDsl { ext ->
ext.compileOptions.sourceCompatibility = JavaVersion.VERSION_17
ext.compileOptions.targetCompatibility = JavaVersion.VERSION_17
}
}
val libraryAction =
Action<AppliedPlugin> {
project.extensions
.getByType(LibraryAndroidComponentsExtension::class.java)
.finalizeDsl { ext ->
ext.compileOptions.sourceCompatibility = JavaVersion.VERSION_17
ext.compileOptions.targetCompatibility = JavaVersion.VERSION_17
}
}
project.pluginManager.withPlugin("com.android.application", applicationAction)
project.pluginManager.withPlugin("com.android.library", libraryAction)
project.pluginManager.withPlugin("org.jetbrains.kotlin.android") {
project.kotlinExtension.jvmToolchain(17)
}
project.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
project.kotlinExtension.jvmToolchain(17)
}
}
}
}

View File

@@ -0,0 +1,141 @@
/*
* 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.utils
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.Variant
import com.facebook.react.ReactExtension
import com.facebook.react.utils.ProjectUtils.getReactNativeArchitectures
import java.io.File
import org.gradle.api.Project
internal object NdkConfiguratorUtils {
@Suppress("UnstableApiUsage")
fun configureReactNativeNdk(project: Project, extension: ReactExtension) {
project.pluginManager.withPlugin("com.android.application") {
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl {
ext ->
// We enable prefab so users can consume .so/headers from ReactAndroid and hermes-engine
// .aar
ext.buildFeatures.prefab = true
// If the user has not provided a CmakeLists.txt path, let's provide
// the default one from the framework
if (ext.externalNativeBuild.cmake.path == null) {
ext.externalNativeBuild.cmake.path =
File(
extension.reactNativeDir.get().asFile,
"ReactAndroid/cmake-utils/default-app-setup/CMakeLists.txt",
)
}
// Parameters should be provided in an additive manner (do not override what
// the user provided, but allow for sensible defaults).
val cmakeArgs = ext.defaultConfig.externalNativeBuild.cmake.arguments
if (cmakeArgs.none { it.startsWith("-DPROJECT_BUILD_DIR") }) {
cmakeArgs.add("-DPROJECT_BUILD_DIR=${project.layout.buildDirectory.get().asFile}")
}
if (cmakeArgs.none { it.startsWith("-DPROJECT_ROOT_DIR") }) {
cmakeArgs.add("-DPROJECT_ROOT_DIR=${project.rootProject.layout.projectDirectory.asFile}")
}
if (cmakeArgs.none { it.startsWith("-DREACT_ANDROID_DIR") }) {
cmakeArgs.add(
"-DREACT_ANDROID_DIR=${extension.reactNativeDir.file("ReactAndroid").get().asFile}"
)
}
if (cmakeArgs.none { it.startsWith("-DANDROID_STL") }) {
cmakeArgs.add("-DANDROID_STL=c++_shared")
}
if (cmakeArgs.none { it.startsWith("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES") }) {
cmakeArgs.add("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON")
}
val architectures = project.getReactNativeArchitectures()
// abiFilters are split ABI are not compatible each other, so we set the abiFilters
// only if the user hasn't enabled the split abi feature.
if (architectures.isNotEmpty() && !ext.splits.abi.isEnable) {
ext.defaultConfig.ndk.abiFilters.addAll(architectures)
}
}
}
}
/**
* This method is used to configure the .so Packaging Options for the given variant. It will make
* sure we specify the correct .pickFirsts for all the .so files we are producing or that we're
* aware of as some of our dependencies are pulling them in.
*/
fun configureNewArchPackagingOptions(
project: Project,
extension: ReactExtension,
variant: Variant,
) {
// We set some packagingOptions { pickFirst ... } for our users for libraries we own.
variant.packaging.jniLibs.pickFirsts.addAll(
listOf(
// This is the .so provided by FBJNI via prefab
"**/libfbjni.so",
// Those are prefab libraries we distribute via ReactAndroid
// Due to a bug in AGP, they fire a warning on console as both the JNI
// and the prefab .so files gets considered.
"**/libreactnative.so",
"**/libjsi.so",
// AGP will give priority of libc++_shared coming from App modules.
"**/libc++_shared.so",
)
)
}
/**
* This method is used to configure the .so Cleanup for the given variant. It takes care of
* cleaning up the .so files that are not needed for Hermes or JSC, given a specific variant.
*/
fun configureJsEnginePackagingOptions(
config: ReactExtension,
variant: Variant,
hermesEnabled: Boolean,
useThirdPartyJSC: Boolean,
) {
if (config.enableSoCleanup.get()) {
val (excludes, includes) = getPackagingOptionsForVariant(hermesEnabled, useThirdPartyJSC)
variant.packaging.jniLibs.excludes.addAll(excludes)
variant.packaging.jniLibs.pickFirsts.addAll(includes)
}
}
fun getPackagingOptionsForVariant(
hermesEnabled: Boolean,
useThirdPartyJSC: Boolean,
): Pair<List<String>, List<String>> {
val excludes = mutableListOf<String>()
val includes = mutableListOf<String>()
// note: libjsctooling.so is kept here for backward compatibility.
when {
hermesEnabled -> {
excludes.add("**/libjsc.so")
excludes.add("**/libjsctooling.so")
includes.add("**/libhermesvm.so")
includes.add("**/libhermestooling.so")
}
useThirdPartyJSC -> {
excludes.add("**/libhermesvm.so")
excludes.add("**/libhermestooling.so")
excludes.add("**/libjsctooling.so")
includes.add("**/libjsc.so")
}
else -> {
excludes.add("**/libhermesvm.so")
excludes.add("**/libhermestooling.so")
includes.add("**/libjsc.so")
includes.add("**/libjsctooling.so")
}
}
return excludes to includes
}
}

View File

@@ -0,0 +1,242 @@
/*
* 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.
*/
@file:JvmName("PathUtils")
package com.facebook.react.utils
import com.facebook.react.ReactExtension
import com.facebook.react.model.ModelPackageJson
import com.facebook.react.utils.KotlinStdlibCompatUtils.capitalizeCompat
import com.facebook.react.utils.Os.cliPath
import java.io.File
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
/**
* Computes the entry file for React Native. The Algo follows this order:
* 1. The file pointed by the ENTRY_FILE env variable, if set.
* 2. The file provided by the `entryFile` config in the `reactApp` Gradle extension
* 3. The `index.android.js` file, if available.
* 4. Fallback to the `index.js` file.
*
* @param config The [ReactExtension] configured for this project
*/
internal fun detectedEntryFile(config: ReactExtension, envVariableOverride: String? = null): File =
detectEntryFile(
entryFile = config.entryFile.orNull?.asFile,
reactRoot = config.root.get().asFile,
envVariableOverride = envVariableOverride,
)
/**
* Computes the CLI file for React Native. The Algo follows this order:
* 1. The path provided by the `cliFile` config in the `react {}` Gradle extension
* 2. The output of `node --print "require.resolve('react-native/cli');"` if not failing.
* 3. The `node_modules/react-native/cli.js` file if exists
* 4. Fails otherwise
*/
internal fun detectedCliFile(config: ReactExtension): File =
detectCliFile(
reactNativeRoot = config.root.get().asFile,
preconfiguredCliFile = config.cliFile.asFile.orNull,
)
/**
* Computes the `hermesc` command location. The Algo follows this order:
* 1. The path provided by the `hermesCommand` config in the `react` Gradle extension
* 2. The file located in `node_modules/react-native/sdks/hermes/build/bin/hermesc`. This will be
* used if the user is building Hermes from source.
* 3. The file located in `node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc` where `%OS-BIN%`
* is substituted with the correct OS arch. This will be used if the user is using a precompiled
* hermes-engine package.
* 4. Fails otherwise
*/
internal fun detectedHermesCommand(config: ReactExtension): String =
detectOSAwareHermesCommand(config.root.get().asFile, config.hermesCommand.get())
private fun detectEntryFile(
entryFile: File?,
reactRoot: File,
envVariableOverride: String? = null,
): File =
when {
envVariableOverride != null -> File(reactRoot, envVariableOverride)
entryFile != null -> entryFile
File(reactRoot, "index.android.js").exists() -> File(reactRoot, "index.android.js")
else -> File(reactRoot, "index.js")
}
private fun detectCliFile(reactNativeRoot: File, preconfiguredCliFile: File?): File {
// 1. preconfigured path
if (preconfiguredCliFile != null) {
if (preconfiguredCliFile.exists()) {
return preconfiguredCliFile
}
}
// 2. node module path
val nodeProcess =
Runtime.getRuntime()
.exec(
arrayOf("node", "--print", "require.resolve('react-native/cli');"),
emptyArray(),
reactNativeRoot,
)
val nodeProcessOutput = nodeProcess.inputStream.use { it.bufferedReader().readText().trim() }
if (nodeProcessOutput.isNotEmpty()) {
val nodeModuleCliJs = File(nodeProcessOutput)
if (nodeModuleCliJs.exists()) {
return nodeModuleCliJs
}
}
// 3. cli.js in the root folder
val rootCliJs = File(reactNativeRoot, "node_modules/react-native/cli.js")
if (rootCliJs.exists()) {
return rootCliJs
}
error(
"""
Couldn't determine CLI location!
Please set `react { cliFile = file(...) }` inside your
build.gradle to the path of the react-native cli.js file.
This file typically resides in `node_modules/react-native/cli.js`
"""
.trimIndent()
)
}
/**
* Computes the `hermesc` command location. The Algo follows this order:
* 1. The path provided by the `hermesCommand` config in the `react` Gradle extension
* 2. The file located in `node_modules/react-native/sdks/hermes/build/bin/hermesc`. This will be
* used if the user is building Hermes from source.
* 3. The file located in `node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc` where `%OS-BIN%`
* is substituted with the correct OS arch. This will be used if the user is using a precompiled
* hermes-engine package. Or, if the user has opted in to use Hermes V1, the used file will be
* located in `node_modules/hermes-compiler/%OS-BIN%/hermesc` where `%OS-BIN%` is substituted
* with the correct OS arch.
* 4. Fails otherwise
*/
internal fun detectOSAwareHermesCommand(
projectRoot: File,
hermesCommand: String,
): String { // 1. If the project specifies a Hermes command, don't second guess it.
if (hermesCommand.isNotBlank()) {
val osSpecificHermesCommand =
if ("%OS-BIN%" in hermesCommand) {
hermesCommand.replace("%OS-BIN%", getHermesOSBin())
} else {
hermesCommand
}
return osSpecificHermesCommand
// Execution on Windows fails with / as separator
.replace('/', File.separatorChar)
}
// 2. If the project is building hermes-engine from source, use hermesc from there
val builtHermesc =
getBuiltHermescFile(projectRoot, System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR"))
if (builtHermesc.exists()) {
return builtHermesc.cliPath(projectRoot)
}
// 3. Use hermes-compiler from npm
val prebuiltHermesPath =
HERMES_COMPILER_NPM_DIR.plus(getHermesCBin())
.replace("%OS-BIN%", getHermesOSBin())
// Execution on Windows fails with / as separator
.replace('/', File.separatorChar)
val prebuiltHermes = File(projectRoot, prebuiltHermesPath)
if (prebuiltHermes.exists()) {
return prebuiltHermes.cliPath(projectRoot)
}
error(
"Couldn't determine Hermesc location. " +
"Please set `react.hermesCommand` to the path of the hermesc binary file. " +
"node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
)
}
/**
* Gets the location where Hermesc should be. If nothing is specified, built hermesc is assumed to
* be inside [HERMESC_BUILT_FROM_SOURCE_DIR]. Otherwise user can specify an override with
* [pathOverride], which is assumed to be an absolute path where Hermes source code is
* provided/built.
*
* @param projectRoot The root of the Project.
*/
internal fun getBuiltHermescFile(projectRoot: File, pathOverride: String?) =
if (!pathOverride.isNullOrBlank()) {
File(pathOverride, "build/bin/${getHermesCBin()}")
} else {
File(projectRoot, HERMESC_BUILT_FROM_SOURCE_DIR.plus(getHermesCBin()))
}
internal fun getHermesCBin() = if (Os.isWindows()) "hermesc.exe" else "hermesc"
internal fun getHermesOSBin(): String {
if (Os.isWindows()) return "win64-bin"
if (Os.isMac()) return "osx-bin"
if (Os.isLinuxAmd64()) return "linux64-bin"
error(
"OS not recognized. Please set project.react.hermesCommand " +
"to the path of a working Hermes compiler."
)
}
internal fun projectPathToLibraryName(projectPath: String): String =
projectPath
.split(':', '-', '_', '.')
.joinToString("") { token -> token.capitalizeCompat() }
.plus("Spec")
/**
* Function to look for the relevant `package.json`. We first look in the parent folder of this
* Gradle module (generally the case for library projects) or we fallback to looking into the `root`
* folder of a React Native project (generally the case for app projects).
*/
internal fun findPackageJsonFile(project: Project, rootProperty: DirectoryProperty): File? {
val inParent = project.file("../package.json")
if (inParent.exists()) {
return inParent
}
val fromExtension = rootProperty.file("package.json").orNull?.asFile
if (fromExtension?.exists() == true) {
return fromExtension
}
return null
}
/**
* Function to look for the `package.json` and parse it. It returns a [ModelPackageJson] if found or
* null others.
*
* Please note that this function access the [DirectoryProperty] parameter and calls .get() on them,
* so calling this during apply() of the ReactPlugin is not recommended. It should be invoked inside
* lazy lambdas or at execution time.
*/
internal fun readPackageJsonFile(
project: Project,
rootProperty: DirectoryProperty,
): ModelPackageJson? {
val packageJson = findPackageJsonFile(project, rootProperty)
return packageJson?.let { JsonUtils.fromPackageJson(it) }
}
private const val HERMES_COMPILER_NPM_DIR = "node_modules/hermes-compiler/hermesc/%OS-BIN%/"
private const val HERMESC_BUILT_FROM_SOURCE_DIR =
"node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/"

View File

@@ -0,0 +1,111 @@
/*
* 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.utils
import com.facebook.react.ReactExtension
import com.facebook.react.model.ModelPackageJson
import com.facebook.react.utils.KotlinStdlibCompatUtils.lowercaseCompat
import com.facebook.react.utils.KotlinStdlibCompatUtils.toBooleanStrictOrNullCompat
import com.facebook.react.utils.PropertyUtils.EDGE_TO_EDGE_ENABLED
import com.facebook.react.utils.PropertyUtils.HERMES_ENABLED
import com.facebook.react.utils.PropertyUtils.HERMES_V1_ENABLED
import com.facebook.react.utils.PropertyUtils.REACT_NATIVE_ARCHITECTURES
import com.facebook.react.utils.PropertyUtils.SCOPED_EDGE_TO_EDGE_ENABLED
import com.facebook.react.utils.PropertyUtils.SCOPED_HERMES_ENABLED
import com.facebook.react.utils.PropertyUtils.SCOPED_HERMES_V1_ENABLED
import com.facebook.react.utils.PropertyUtils.SCOPED_REACT_NATIVE_ARCHITECTURES
import com.facebook.react.utils.PropertyUtils.SCOPED_USE_THIRD_PARTY_JSC
import com.facebook.react.utils.PropertyUtils.USE_THIRD_PARTY_JSC
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.jetbrains.kotlin.gradle.plugin.extraProperties
internal object ProjectUtils {
const val HERMES_FALLBACK = true
internal fun Project.isNewArchEnabled(): Boolean = true
internal val Project.isHermesEnabled: Boolean
get() =
if (project.hasProperty(HERMES_ENABLED) || project.hasProperty(SCOPED_HERMES_ENABLED)) {
val propertyString =
if (project.hasProperty(HERMES_ENABLED)) {
HERMES_ENABLED
} else {
SCOPED_HERMES_ENABLED
}
project
.property(propertyString)
.toString()
.lowercaseCompat()
.toBooleanStrictOrNullCompat() ?: true
} else if (project.extensions.extraProperties.has("react")) {
@Suppress("UNCHECKED_CAST")
val reactMap = project.extensions.extraProperties.get("react") as? Map<String, Any?>
when (val enableHermesKey = reactMap?.get("enableHermes")) {
is Boolean -> enableHermesKey
is String -> enableHermesKey.lowercaseCompat().toBooleanStrictOrNullCompat() ?: true
else -> HERMES_FALLBACK
}
} else {
HERMES_FALLBACK
}
internal val Project.isEdgeToEdgeEnabled: Boolean
get() =
(project.hasProperty(EDGE_TO_EDGE_ENABLED) &&
project.property(EDGE_TO_EDGE_ENABLED).toString().toBoolean()) ||
(project.hasProperty(SCOPED_EDGE_TO_EDGE_ENABLED) &&
project.property(SCOPED_EDGE_TO_EDGE_ENABLED).toString().toBoolean())
internal val Project.useThirdPartyJSC: Boolean
get() =
(project.hasProperty(USE_THIRD_PARTY_JSC) &&
project.property(USE_THIRD_PARTY_JSC).toString().toBoolean()) ||
(project.hasProperty(SCOPED_USE_THIRD_PARTY_JSC) &&
project.property(SCOPED_USE_THIRD_PARTY_JSC).toString().toBoolean())
internal val Project.isHermesV1Enabled: Boolean
get() =
(project.hasProperty(HERMES_V1_ENABLED) &&
project.property(HERMES_V1_ENABLED).toString().toBoolean()) ||
(project.hasProperty(SCOPED_HERMES_V1_ENABLED) &&
project.property(SCOPED_HERMES_V1_ENABLED).toString().toBoolean()) ||
(project.extraProperties.has(HERMES_V1_ENABLED) &&
project.extraProperties.get(HERMES_V1_ENABLED).toString().toBoolean()) ||
(project.extraProperties.has(SCOPED_HERMES_V1_ENABLED) &&
project.extraProperties.get(SCOPED_HERMES_V1_ENABLED).toString().toBoolean())
internal fun Project.needsCodegenFromPackageJson(rootProperty: DirectoryProperty): Boolean {
val parsedPackageJson = readPackageJsonFile(this, rootProperty)
return needsCodegenFromPackageJson(parsedPackageJson)
}
internal fun Project.needsCodegenFromPackageJson(model: ModelPackageJson?): Boolean {
return model?.codegenConfig != null
}
internal fun Project.getReactNativeArchitectures(): List<String> {
val architectures = mutableListOf<String>()
if (project.hasProperty(REACT_NATIVE_ARCHITECTURES)) {
val architecturesString = project.property(REACT_NATIVE_ARCHITECTURES).toString()
architectures.addAll(architecturesString.split(",").filter { it.isNotBlank() })
} else if (project.hasProperty(SCOPED_REACT_NATIVE_ARCHITECTURES)) {
val architecturesString = project.property(SCOPED_REACT_NATIVE_ARCHITECTURES).toString()
architectures.addAll(architecturesString.split(",").filter { it.isNotBlank() })
}
return architectures
}
internal fun Project.reactNativeDir(extension: ReactExtension): String =
extension.reactNativeDir.get().asFile.absolutePath
internal fun Project.hasPropertySetToFalse(property: String): Boolean =
this.hasProperty(property) && this.property(property).toString().toBoolean() == false
}

View File

@@ -0,0 +1,89 @@
/*
* 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.utils
/** Collection of all the Gradle Properties that are accepted by React Native Gradle Plugin. */
object PropertyUtils {
/** Public property that toggles the New Architecture */
const val NEW_ARCH_ENABLED = "newArchEnabled"
const val SCOPED_NEW_ARCH_ENABLED = "react.newArchEnabled"
/** Public property that toggles Hermes */
const val HERMES_ENABLED = "hermesEnabled"
const val SCOPED_HERMES_ENABLED = "react.hermesEnabled"
/** Public property that toggles Hermes V1 */
const val HERMES_V1_ENABLED = "hermesV1Enabled"
const val SCOPED_HERMES_V1_ENABLED = "react.hermesV1Enabled"
/** Public property that toggles edge-to-edge */
const val EDGE_TO_EDGE_ENABLED = "edgeToEdgeEnabled"
const val SCOPED_EDGE_TO_EDGE_ENABLED = "react.edgeToEdgeEnabled"
/** Public property that excludes jsctooling from core */
const val USE_THIRD_PARTY_JSC = "useThirdPartyJSC"
const val SCOPED_USE_THIRD_PARTY_JSC = "react.useThirdPartyJSC"
/** Public property that allows to control which architectures to build for React Native. */
const val REACT_NATIVE_ARCHITECTURES = "reactNativeArchitectures"
const val SCOPED_REACT_NATIVE_ARCHITECTURES = "react.nativeArchitectures"
/** Public property that allows to control whether the JitPack repository is included or not */
const val INCLUDE_JITPACK_REPOSITORY = "includeJitpackRepository"
const val SCOPED_INCLUDE_JITPACK_REPOSITORY = "react.includeJitpackRepository"
/**
* Public property that allows to configure an enterprise repository proxy as exclusive repository
*/
const val EXCLUSIVE_ENTEPRISE_REPOSITORY = "exclusiveEnterpriseRepository"
const val SCOPED_EXCLUSIVE_ENTEPRISE_REPOSITORY = "react.exclusiveEnterpriseRepository"
/** By default we include JitPack to avoid breaking user builds */
internal const val INCLUDE_JITPACK_REPOSITORY_DEFAULT = true
/**
* Internal Property that acts as a killswitch to configure the JDK version and align it for app
* and all the libraries.
*/
const val INTERNAL_DISABLE_JAVA_VERSION_ALIGNMENT = "react.internal.disableJavaVersionAlignment"
/**
* Internal Property that allows to specify a local Maven repository to use for React Native
* artifacts It's used on CI to test templates against a version of React Native built on the fly.
*/
const val INTERNAL_REACT_NATIVE_MAVEN_LOCAL_REPO = "react.internal.mavenLocalRepo"
/**
* Internal property used to specify where the Windows Bash executable is located. This is useful
* for contributors who are running Windows on their machine.
*/
const val INTERNAL_REACT_WINDOWS_BASH = "react.internal.windowsBashPath"
/**
* Internal property to force the build to use Hermes from the latest nightly. This speeds up the
* build at the cost of not testing the latest integration against Hermes.
*/
const val INTERNAL_USE_HERMES_NIGHTLY = "react.internal.useHermesNightly"
/** Internal property used to override the publishing group for the React Native artifacts. */
const val INTERNAL_REACT_PUBLISHING_GROUP = "react.internal.publishingGroup"
const val INTERNAL_HERMES_PUBLISHING_GROUP = "react.internal.hermesPublishingGroup"
const val DEFAULT_INTERNAL_REACT_PUBLISHING_GROUP = "com.facebook.react"
const val DEFAULT_INTERNAL_HERMES_PUBLISHING_GROUP = "com.facebook.hermes"
/** Internal property used to control the version name of React Native */
const val INTERNAL_VERSION_NAME = "VERSION_NAME"
/**
* Internal properties, shared with iOS, used to control the version name of Hermes Engine. They
* are stored in sdks/hermes-engine/version.properties
*/
const val INTERNAL_HERMES_VERSION_NAME = "HERMES_VERSION_NAME"
const val INTERNAL_HERMES_V1_VERSION_NAME = "HERMES_V1_VERSION_NAME"
}

View File

@@ -0,0 +1,241 @@
/*
* 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
import com.facebook.react.ReactExtension.Companion.getGradleDependenciesToApply
import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class ReactExtensionTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun getGradleDependenciesToApply_withEmptyFile_returnsEmptyMap() {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0"
}
"""
.trimIndent()
)
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).isEmpty()
}
@Test
fun getGradleDependenciesToApply_withOneDependency_returnsValidDep() {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
}
}
}
"""
.trimIndent()
)
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).containsExactly("implementation" to ":react-native_oss-library-example")
}
@Test
fun getGradleDependenciesToApply_withDependencyConfiguration_returnsValidConfiguration() {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react",
"dependencyConfiguration": "compileOnly"
}
}
}
}
}
"""
.trimIndent()
)
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).containsExactly("compileOnly" to ":react-native_oss-library-example")
}
@Test
fun getGradleDependenciesToApply_withBuildTypes_returnsValidConfiguration() {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react",
"buildTypes": ["debug", "release"]
}
}
}
}
}
"""
.trimIndent()
)
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps)
.containsExactly(
"debugImplementation" to ":react-native_oss-library-example",
"releaseImplementation" to ":react-native_oss-library-example",
)
}
@Test
fun getGradleDependenciesToApply_withMultipleDependencies_returnsValidConfiguration() {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
},
"@react-native/another-library-for-testing": {
"root": "./node_modules/@react-native/another-library-for-testing",
"name": "@react-native/another-library-for-testing",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
}
}
}
"""
.trimIndent()
)
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps)
.containsExactly(
"implementation" to ":react-native_oss-library-example",
"implementation" to ":react-native_another-library-for-testing",
)
}
@Test
fun getGradleDependenciesToApply_withiOSOnlyLibrary_returnsEmptyDepsMap() {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"ios": {
"podspecPath": "./node_modules/@react-native/oss-library-example/oss-library-example.podspec",
"version": "0.0.0",
"configurations": [],
"scriptPhases": []
},
"android": null
}
}
}
}
"""
.trimIndent()
)
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).isEmpty()
}
@Test
fun getGradleDependenciesToApply_withIsPureCxxDeps_filtersCorrectly() {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/android-example",
"name": "@react-native/android-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
},
"@react-native/another-library-for-testing": {
"root": "./node_modules/@react-native/cxx-testing",
"name": "@react-native/cxx-testing",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react",
"isPureCxxDependency": true
}
}
}
}
}
"""
.trimIndent()
)
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).containsExactly("implementation" to ":react-native_android-example")
}
private fun createJsonFile(@Language("JSON") input: String) =
tempFolder.newFile().apply { writeText(input) }
}

View File

@@ -0,0 +1,12 @@
/*
* 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
import org.gradle.api.Project
class TestReactExtension(project: Project) : ReactExtension(project)

View File

@@ -0,0 +1,48 @@
/*
* 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.model
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class ModelAutolinkingDependenciesJsonTest {
@Test
fun nameCleansed_withoutScope() {
assertThat(ModelAutolinkingDependenciesJson("", "name", null).nameCleansed).isEqualTo("name")
assertThat(ModelAutolinkingDependenciesJson("", "react~native", null).nameCleansed)
.isEqualTo("react_native")
assertThat(ModelAutolinkingDependenciesJson("", "react*native", null).nameCleansed)
.isEqualTo("react_native")
assertThat(ModelAutolinkingDependenciesJson("", "react!native", null).nameCleansed)
.isEqualTo("react_native")
assertThat(ModelAutolinkingDependenciesJson("", "react'native", null).nameCleansed)
.isEqualTo("react_native")
assertThat(ModelAutolinkingDependenciesJson("", "react(native", null).nameCleansed)
.isEqualTo("react_native")
assertThat(ModelAutolinkingDependenciesJson("", "react)native", null).nameCleansed)
.isEqualTo("react_native")
assertThat(ModelAutolinkingDependenciesJson("", "react~*!'()native", null).nameCleansed)
.isEqualTo("react_native")
}
@Test
fun nameCleansed_withScope() {
assertThat(ModelAutolinkingDependenciesJson("", "@react-native/package", null).nameCleansed)
.isEqualTo("react-native_package")
assertThat(
ModelAutolinkingDependenciesJson(
"",
"@this*is~a(more)complicated/example!of~weird)packages",
null,
)
.nameCleansed
)
.isEqualTo("this_is_a_more_complicated_example_of_weird_packages")
}
}

View File

@@ -0,0 +1,444 @@
/*
* 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.tasks
import com.facebook.react.tests.OS
import com.facebook.react.tests.OsRule
import com.facebook.react.tests.WithOs
import com.facebook.react.tests.createTestTask
import java.io.File
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class BundleHermesCTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@get:Rule val osRule = OsRule()
@Test
fun bundleTask_groupIsSetCorrectly() {
val task = createTestTask<BundleHermesCTask> {}
assertThat(task.group).isEqualTo("react")
}
@Test
fun bundleTask_inputFiles_areSetCorrectly() {
val rootDir =
tempFolder.newFolder("js").apply {
File(this, "file.js").createNewFile()
File(this, "file.jsx").createNewFile()
File(this, "file.ts").createNewFile()
File(this, "file.tsx").createNewFile()
}
val task = createTestTask<BundleHermesCTask> { it.root.set(rootDir) }
assertThat(task.sources.files.size).isEqualTo(4)
assertThat(task.sources.files)
.containsExactlyInAnyOrder(
File(rootDir, "file.js"),
File(rootDir, "file.jsx"),
File(rootDir, "file.ts"),
File(rootDir, "file.tsx"),
)
}
@Test
fun bundleTask_inputFilesInExcludedPath_areExcluded() {
fun File.createFileAndPath() {
parentFile.mkdirs()
createNewFile()
}
val rootDir =
tempFolder.newFolder("js").apply {
File(this, "afolder/includedfile.js").createFileAndPath()
// Those files should be excluded due to their filepath
File(this, "android/excludedfile.js").createFileAndPath()
File(this, "ios/excludedfile.js").createFileAndPath()
File(this, "build/excludedfile.js").createFileAndPath()
File(this, "node_modules/react-native/excludedfile.js").createFileAndPath()
}
val task = createTestTask<BundleHermesCTask> { it.root.set(rootDir) }
assertThat(task.sources.excludes)
.containsExactlyInAnyOrder(
"**/android/**/*",
"**/ios/**/*",
"**/build/**/*",
"**/node_modules/**/*",
)
assertThat(task.sources.files.size).isEqualTo(1)
assertThat(task.sources.files).containsExactly(File(rootDir, "afolder/includedfile.js"))
}
@Test
fun bundleTask_staticInputs_areSetCorrectly() {
val task =
createTestTask<BundleHermesCTask> {
it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2"))
it.bundleCommand.set("bundle")
it.bundleAssetName.set("myassetname")
it.minifyEnabled.set(true)
it.hermesEnabled.set(true)
it.devEnabled.set(true)
it.extraPackagerArgs.set(listOf("extra", "arg"))
it.hermesCommand.set("./my-hermesc")
it.hermesFlags.set(listOf("flag1", "flag2"))
}
assertThat(task.nodeExecutableAndArgs.get()).isEqualTo(listOf("node", "arg1", "arg2"))
assertThat(task.bundleCommand.get()).isEqualTo("bundle")
assertThat(task.bundleAssetName.get()).isEqualTo("myassetname")
assertThat(task.minifyEnabled.get()).isTrue()
assertThat(task.hermesEnabled.get()).isTrue()
assertThat(task.devEnabled.get()).isTrue()
assertThat(task.extraPackagerArgs.get()).isEqualTo(listOf("extra", "arg"))
assertThat(task.hermesCommand.get()).isEqualTo("./my-hermesc")
assertThat(task.hermesFlags.get()).isEqualTo(listOf("flag1", "flag2"))
}
@Test
fun bundleTask_filesInput_areSetCorrectly() {
val entryFile = tempFolder.newFile("entry.js")
val cliFile = tempFolder.newFile("cli.js")
val jsBundleDir = tempFolder.newFolder("jsbundle")
val resourcesDir = tempFolder.newFolder("resources")
val jsIntermediateSourceMapsDir = tempFolder.newFolder("jsIntermediateSourceMaps")
val jsSourceMapsDir = tempFolder.newFolder("jsSourceMaps")
val bundleConfig = tempFolder.newFile("bundle.config")
val reactNativeDir = tempFolder.newFolder("node_modules/react-native")
val task =
createTestTask<BundleHermesCTask> {
it.entryFile.set(entryFile)
it.cliFile.set(cliFile)
it.jsBundleDir.set(jsBundleDir)
it.resourcesDir.set(resourcesDir)
it.jsIntermediateSourceMapsDir.set(jsIntermediateSourceMapsDir)
it.jsSourceMapsDir.set(jsSourceMapsDir)
it.bundleConfig.set(bundleConfig)
it.reactNativeDir.set(reactNativeDir)
}
assertThat(task.entryFile.get().asFile).isEqualTo(entryFile)
assertThat(task.cliFile.get().asFile).isEqualTo(cliFile)
assertThat(task.jsBundleDir.get().asFile).isEqualTo(jsBundleDir)
assertThat(task.resourcesDir.get().asFile).isEqualTo(resourcesDir)
assertThat(task.jsIntermediateSourceMapsDir.get().asFile).isEqualTo(jsIntermediateSourceMapsDir)
assertThat(task.jsSourceMapsDir.get().asFile).isEqualTo(jsSourceMapsDir)
assertThat(task.bundleConfig.get().asFile).isEqualTo(bundleConfig)
assertThat(task.reactNativeDir.get().asFile).isEqualTo(reactNativeDir)
}
@Test
fun resolvePackagerSourceMapFile_withHermesEnabled_returnsCorrectFile() {
val jsIntermediateSourceMapsDir = tempFolder.newFolder("jsIntermediateSourceMaps")
val bundleAssetName = "myassetname"
val task =
createTestTask<BundleHermesCTask> {
it.jsIntermediateSourceMapsDir.set(jsIntermediateSourceMapsDir)
it.hermesEnabled.set(true)
it.bundleAssetName.set(bundleAssetName)
}
assertThat(task.resolvePackagerSourceMapFile(bundleAssetName))
.isEqualTo(File(jsIntermediateSourceMapsDir, "myassetname.packager.map"))
}
@Test
fun resolvePackagerSourceMapFile_withHermesDisabled_returnsCorrectFile() {
val jsSourceMapsDir = tempFolder.newFolder("jsSourceMaps")
val bundleAssetName = "myassetname"
val task =
createTestTask<BundleHermesCTask> {
it.jsSourceMapsDir.set(jsSourceMapsDir)
it.hermesEnabled.set(false)
}
assertThat(task.resolvePackagerSourceMapFile(bundleAssetName))
.isEqualTo(File(jsSourceMapsDir, "myassetname.map"))
}
@Test
fun resolveOutputSourceMap_returnsCorrectFile() {
val jsSourceMapsDir = tempFolder.newFolder("jsSourceMaps")
val bundleAssetName = "myassetname"
val task = createTestTask<BundleHermesCTask> { it.jsSourceMapsDir.set(jsSourceMapsDir) }
assertThat(task.resolveOutputSourceMap(bundleAssetName))
.isEqualTo(File(jsSourceMapsDir, "myassetname.map"))
}
@Test
fun resolveCompilerSourceMap_returnsCorrectFile() {
val jsIntermediateSourceMapsDir = tempFolder.newFolder("jsIntermediateSourceMaps")
val bundleAssetName = "myassetname"
val task =
createTestTask<BundleHermesCTask> {
it.jsIntermediateSourceMapsDir.set(jsIntermediateSourceMapsDir)
}
assertThat(task.resolveCompilerSourceMap(bundleAssetName))
.isEqualTo(File(jsIntermediateSourceMapsDir, "myassetname.compiler.map"))
}
@Test
fun getBundleCommand_returnsCorrectCommand() {
val entryFile = tempFolder.newFile("index.js")
val cliFile = tempFolder.newFile("cli.js")
val bundleFile = tempFolder.newFile("bundle.js")
val sourceMapFile = tempFolder.newFile("bundle.js.map")
val resourcesDir = tempFolder.newFolder("res")
val bundleConfig = tempFolder.newFile("bundle.config")
val task =
createTestTask<BundleHermesCTask> {
it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2"))
it.root.set(tempFolder.root)
it.cliFile.set(cliFile)
it.bundleCommand.set("bundle")
it.devEnabled.set(true)
it.entryFile.set(entryFile)
it.resourcesDir.set(resourcesDir)
it.bundleConfig.set(bundleConfig)
it.minifyEnabled.set(true)
it.extraPackagerArgs.set(listOf("--read-global-cache"))
}
val bundleCommand = task.getBundleCommand(bundleFile, sourceMapFile)
assertThat(bundleCommand)
.containsExactly(
"node",
"arg1",
"arg2",
cliFile.absolutePath,
"bundle",
"--platform",
"android",
"--dev",
"true",
"--reset-cache",
"--entry-file",
entryFile.absolutePath,
"--bundle-output",
bundleFile.absolutePath,
"--assets-dest",
resourcesDir.absolutePath,
"--sourcemap-output",
sourceMapFile.absolutePath,
"--config",
bundleConfig.absolutePath,
"--minify",
"true",
"--read-global-cache",
"--verbose",
)
}
@Test
@WithOs(OS.WIN)
fun getBundleCommand_onWindows_returnsWinValidCommandsPaths() {
val entryFile = tempFolder.newFile("index.js")
val cliFile = tempFolder.newFile("cli.js")
val bundleFile = tempFolder.newFile("bundle.js")
val sourceMapFile = tempFolder.newFile("bundle.js.map")
val resourcesDir = tempFolder.newFolder("res")
val bundleConfig = tempFolder.newFile("bundle.config")
val task =
createTestTask<BundleHermesCTask> {
it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2"))
it.root.set(tempFolder.root)
it.cliFile.set(cliFile)
it.bundleCommand.set("bundle")
it.devEnabled.set(true)
it.entryFile.set(entryFile)
it.resourcesDir.set(resourcesDir)
it.bundleConfig.set(bundleConfig)
it.minifyEnabled.set(true)
it.extraPackagerArgs.set(listOf("--read-global-cache"))
}
val bundleCommand = task.getBundleCommand(bundleFile, sourceMapFile)
assertThat(bundleCommand)
.containsExactly(
"cmd",
"/c",
"node",
"arg1",
"arg2",
cliFile.relativeTo(tempFolder.root).path,
"bundle",
"--platform",
"android",
"--dev",
"true",
"--reset-cache",
"--entry-file",
entryFile.relativeTo(tempFolder.root).path,
"--bundle-output",
bundleFile.relativeTo(tempFolder.root).path,
"--assets-dest",
resourcesDir.relativeTo(tempFolder.root).path,
"--sourcemap-output",
sourceMapFile.relativeTo(tempFolder.root).path,
"--config",
bundleConfig.relativeTo(tempFolder.root).path,
"--minify",
"true",
"--read-global-cache",
"--verbose",
)
}
@Test
fun getBundleCommand_withoutConfig_returnsCommandWithoutConfig() {
val entryFile = tempFolder.newFile("index.js")
val cliFile = tempFolder.newFile("cli.js")
val bundleFile = tempFolder.newFile("bundle.js")
val sourceMapFile = tempFolder.newFile("bundle.js.map")
val resourcesDir = tempFolder.newFolder("res")
val task =
createTestTask<BundleHermesCTask> {
it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2"))
it.root.set(tempFolder.root)
it.cliFile.set(cliFile)
it.bundleCommand.set("bundle")
it.devEnabled.set(true)
it.entryFile.set(entryFile)
it.resourcesDir.set(resourcesDir)
it.minifyEnabled.set(true)
it.extraPackagerArgs.set(listOf("--read-global-cache"))
}
val bundleCommand = task.getBundleCommand(bundleFile, sourceMapFile)
assertThat(bundleCommand).doesNotContain("--config")
}
@Test
fun getHermescCommand_returnsCorrectCommand() {
val customHermesc = "hermesc"
val bytecodeFile = tempFolder.newFile("bundle.js.hbc")
val bundleFile = tempFolder.newFile("bundle.js")
val task =
createTestTask<BundleHermesCTask> {
it.root.set(tempFolder.root)
it.hermesFlags.set(listOf("my-custom-hermes-flag"))
}
val hermesCommand = task.getHermescCommand(customHermesc, bytecodeFile, bundleFile)
assertThat(hermesCommand)
.containsExactly(
customHermesc,
"-w",
"-emit-binary",
"-max-diagnostic-width=80",
"-out",
bytecodeFile.absolutePath,
bundleFile.absolutePath,
"my-custom-hermes-flag",
)
}
@Test
@WithOs(OS.WIN)
fun getHermescCommand_onWindows_returnsRelativePaths() {
val customHermesc = "hermesc"
val bytecodeFile = tempFolder.newFile("bundle.js.hbc")
val bundleFile = tempFolder.newFile("bundle.js")
val task =
createTestTask<BundleHermesCTask> {
it.root.set(tempFolder.root)
it.hermesFlags.set(listOf("my-custom-hermes-flag"))
}
val hermesCommand = task.getHermescCommand(customHermesc, bytecodeFile, bundleFile)
assertThat(hermesCommand)
.containsExactly(
"cmd",
"/c",
customHermesc,
"-w",
"-emit-binary",
"-max-diagnostic-width=80",
"-out",
bytecodeFile.relativeTo(tempFolder.root).path,
bundleFile.relativeTo(tempFolder.root).path,
"my-custom-hermes-flag",
)
}
@Test
fun getComposeSourceMapsCommand_returnsCorrectCommand() {
val packagerMap = tempFolder.newFile("bundle.js.packager.map")
val compilerMap = tempFolder.newFile("bundle.js.compiler.map")
val outputMap = tempFolder.newFile("bundle.js.map")
val reactNativeDir = tempFolder.newFolder("node_modules/react-native")
val composeSourceMapsFile = File(reactNativeDir, "scripts/compose-source-maps.js")
val task =
createTestTask<BundleHermesCTask> {
it.root.set(tempFolder.root)
it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2"))
}
val composeSourcemapCommand =
task.getComposeSourceMapsCommand(composeSourceMapsFile, packagerMap, compilerMap, outputMap)
assertThat(composeSourcemapCommand)
.containsExactly(
"node",
"arg1",
"arg2",
composeSourceMapsFile.absolutePath,
packagerMap.absolutePath,
compilerMap.absolutePath,
"-o",
outputMap.absolutePath,
)
}
@Test
@WithOs(OS.WIN)
fun getComposeSourceMapsCommand_onWindows_returnsRelativePaths() {
val packagerMap = tempFolder.newFile("bundle.js.packager.map")
val compilerMap = tempFolder.newFile("bundle.js.compiler.map")
val outputMap = tempFolder.newFile("bundle.js.map")
val reactNativeDir = tempFolder.newFolder("node_modules/react-native")
val composeSourceMapsFile = File(reactNativeDir, "scripts/compose-source-maps.js")
val task =
createTestTask<BundleHermesCTask> {
it.root.set(tempFolder.root)
it.nodeExecutableAndArgs.set(listOf("node", "arg1", "arg2"))
}
val composeSourcemapCommand =
task.getComposeSourceMapsCommand(composeSourceMapsFile, packagerMap, compilerMap, outputMap)
assertThat(composeSourcemapCommand)
.containsExactly(
"cmd",
"/c",
"node",
"arg1",
"arg2",
composeSourceMapsFile.relativeTo(tempFolder.root).path,
packagerMap.relativeTo(tempFolder.root).path,
compilerMap.relativeTo(tempFolder.root).path,
"-o",
outputMap.relativeTo(tempFolder.root).path,
)
}
}

View File

@@ -0,0 +1,316 @@
/*
* 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.tasks
import com.facebook.react.model.ModelAutolinkingConfigJson
import com.facebook.react.model.ModelAutolinkingDependenciesJson
import com.facebook.react.model.ModelAutolinkingDependenciesPlatformAndroidJson
import com.facebook.react.model.ModelAutolinkingDependenciesPlatformJson
import com.facebook.react.tasks.GenerateAutolinkingNewArchitecturesFileTask.Companion.sanitizeCmakeListsPath
import com.facebook.react.tests.createTestTask
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class GenerateAutolinkingNewArchitecturesFileTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun generatePackageListTask_groupIsSetCorrectly() {
val task = createTestTask<GenerateAutolinkingNewArchitecturesFileTask> {}
assertThat(task.group).isEqualTo("react")
}
@Test
fun generatePackageListTask_staticInputs_areSetCorrectly() {
val outputFolder = tempFolder.newFolder("build")
val inputFile = tempFolder.newFile("config.json")
val task =
createTestTask<GenerateAutolinkingNewArchitecturesFileTask> { task ->
task.generatedOutputDirectory.set(outputFolder)
task.autolinkInputFile.set(inputFile)
}
assertThat(task.generatedOutputDirectory.get().asFile).isEqualTo(outputFolder)
assertThat(task.generatedOutputDirectory.get().asFile).isEqualTo(outputFolder)
}
@Test
fun filterAndroidPackages_withNull_returnsEmpty() {
val task = createTestTask<GenerateAutolinkingNewArchitecturesFileTask>()
val result = task.filterAndroidPackages(null)
assertThat(result).isEmpty()
}
@Test
fun filterAndroidPackages_withEmptyObject_returnsEmpty() {
val task = createTestTask<GenerateAutolinkingNewArchitecturesFileTask>()
val result = task.filterAndroidPackages(ModelAutolinkingConfigJson("1000.0.0", null, null))
assertThat(result).isEmpty()
}
@Test
fun filterAndroidPackages_withNoAndroidObject_returnsEmpty() {
val task = createTestTask<GenerateAutolinkingNewArchitecturesFileTask>()
val result =
task.filterAndroidPackages(
ModelAutolinkingConfigJson(
reactNativeVersion = "1000.0.0",
dependencies =
mapOf(
"a-dependency" to
ModelAutolinkingDependenciesJson(
root = "./a/directory",
name = "a-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = null),
)
),
project = null,
)
)
assertThat(result).isEmpty()
}
@Test
fun filterAndroidPackages_withValidAndroidObject_returnsIt() {
val task = createTestTask<GenerateAutolinkingNewArchitecturesFileTask>()
val android =
ModelAutolinkingDependenciesPlatformAndroidJson(
sourceDir = "./a/directory/android",
packageImportPath = "import com.facebook.react.aPackage;",
packageInstance = "new APackage()",
buildTypes = emptyList(),
)
val result =
task.filterAndroidPackages(
ModelAutolinkingConfigJson(
reactNativeVersion = "1000.0.0",
dependencies =
mapOf(
"a-dependency" to
ModelAutolinkingDependenciesJson(
root = "./a/directory",
name = "a-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = android),
)
),
project = null,
)
)
assertThat(result).containsExactly(android)
}
@Test
fun generateCmakeFileContent_withNoPackages_returnsEmpty() {
val output =
createTestTask<GenerateAutolinkingNewArchitecturesFileTask>()
.generateCmakeFileContent(emptyList())
// language=cmake
assertThat(output)
.isEqualTo(
"""
# This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin)
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
# We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so
# or link against a old prefab target (this is needed for React Native 0.76 on).
set(REACTNATIVE_MERGED_SO true)
set(AUTOLINKED_LIBRARIES
)
"""
.trimIndent()
)
}
@Test
fun generateCmakeFileContent_withPackages_returnsImportCorrectly() {
val output =
createTestTask<GenerateAutolinkingNewArchitecturesFileTask>()
.generateCmakeFileContent(testDependencies)
// language=cmake
assertThat(output)
.isEqualTo(
"""
# This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin)
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
# We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so
# or link against a old prefab target (this is needed for React Native 0.76 on).
set(REACTNATIVE_MERGED_SO true)
add_subdirectory("./a/directory/" aPackage_autolinked_build)
add_subdirectory("./another/directory/with\ spaces/" anotherPackage_autolinked_build)
add_subdirectory("./another/directory/cxx/" anotherPackage_cxxmodule_autolinked_build)
set(AUTOLINKED_LIBRARIES
react_codegen_aPackage
react_codegen_anotherPackage
another_cxxModule
)
"""
.trimIndent()
)
}
@Test
fun generateCppFileContent_withNoPackages_returnsEmpty() {
val output =
createTestTask<GenerateAutolinkingNewArchitecturesFileTask>()
.generateCppFileContent(emptyList())
// language=cpp
assertThat(output)
.isEqualTo(
"""
/**
* This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
*/
#include "autolinking.h"
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
return nullptr;
}
std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker) {
return nullptr;
}
void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
return;
}
} // namespace react
} // namespace facebook
"""
.trimIndent()
)
}
@Test
fun generateCppFileContent_withPackages_returnsImportCorrectly() {
val output =
createTestTask<GenerateAutolinkingNewArchitecturesFileTask>()
.generateCppFileContent(testDependencies)
// language=cpp
assertThat(output)
.isEqualTo(
"""
/**
* This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
*/
#include "autolinking.h"
#include <aPackage.h>
#include <anotherPackage.h>
#include <react/renderer/components/anotherPackage/ComponentDescriptors.h>
#include <AnotherCxxModule.h>
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
auto module_aPackage = aPackage_ModuleProvider(moduleName, params);
if (module_aPackage != nullptr) {
return module_aPackage;
}
auto module_anotherPackage = anotherPackage_ModuleProvider(moduleName, params);
if (module_anotherPackage != nullptr) {
return module_anotherPackage;
}
return nullptr;
}
std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker) {
if (moduleName == AnotherCxxModule::kModuleName) {
return std::make_shared<AnotherCxxModule>(jsInvoker);
}
return nullptr;
}
void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
providerRegistry->add(concreteComponentDescriptorProvider<AnotherPackageComponentDescriptor>());
return;
}
} // namespace react
} // namespace facebook
"""
.trimIndent()
)
}
@Test
fun sanitizeCmakeListsPath_withPathEndingWithFileName_removesFilename() {
val input = "./a/directory/CMakeLists.txt"
assertThat(sanitizeCmakeListsPath(input)).isEqualTo("./a/directory/")
}
@Test
fun sanitizeCmakeListsPath_withSpaces_removesSpaces() {
val input = "./a/dir ectory/with spaces/"
assertThat(sanitizeCmakeListsPath(input)).isEqualTo("./a/dir\\ ectory/with\\ spaces/")
}
@Test
fun sanitizeCmakeListsPath_withPathEndingWithFileNameAndSpaces_sanitizesIt() {
val input = "./a/dir ectory/CMakeLists.txt"
assertThat(sanitizeCmakeListsPath(input)).isEqualTo("./a/dir\\ ectory/")
}
private val testDependencies =
listOf(
ModelAutolinkingDependenciesPlatformAndroidJson(
sourceDir = "./a/directory",
packageImportPath = "import com.facebook.react.aPackage;",
packageInstance = "new APackage()",
buildTypes = emptyList(),
libraryName = "aPackage",
componentDescriptors = emptyList(),
cmakeListsPath = "./a/directory/CMakeLists.txt",
),
ModelAutolinkingDependenciesPlatformAndroidJson(
sourceDir = "./another/directory",
packageImportPath = "import com.facebook.react.anotherPackage;",
packageInstance = "new AnotherPackage()",
buildTypes = emptyList(),
libraryName = "anotherPackage",
componentDescriptors = listOf("AnotherPackageComponentDescriptor"),
cmakeListsPath = "./another/directory/with spaces/CMakeLists.txt",
cxxModuleCMakeListsPath = "./another/directory/cxx/CMakeLists.txt",
cxxModuleHeaderName = "AnotherCxxModule",
cxxModuleCMakeListsModuleName = "another_cxxModule",
),
)
}

View File

@@ -0,0 +1,212 @@
/*
* 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.tasks
import com.facebook.react.tests.*
import com.facebook.react.tests.createProject
import com.facebook.react.tests.createTestTask
import java.io.File
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class GenerateCodegenArtifactsTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@get:Rule val osRule = OsRule()
@Test
fun generateCodegenSchema_inputFiles_areSetCorrectly() {
val outputDir = tempFolder.newFolder("output")
val task = createTestTask<GenerateCodegenArtifactsTask> { it.generatedSrcDir.set(outputDir) }
assertThat(task.generatedSchemaFile.get().asFile).isEqualTo(File(outputDir, "schema.json"))
}
@Test
fun generateCodegenSchema_outputFile_isSetCorrectly() {
val outputDir = tempFolder.newFolder("output")
val task = createTestTask<GenerateCodegenArtifactsTask> { it.generatedSrcDir.set(outputDir) }
assertThat(task.generatedJavaFiles.get().asFile).isEqualTo(File(outputDir, "java"))
assertThat(task.generatedJniFiles.get().asFile).isEqualTo(File(outputDir, "jni"))
}
@Test
fun generateCodegenSchema_simpleProperties_areInsideInput() {
val packageJsonFile = tempFolder.newFile("package.json")
val task =
createTestTask<GenerateCodegenArtifactsTask> {
it.nodeExecutableAndArgs.set(listOf("npm", "help"))
it.codegenJavaPackageName.set("com.example.test")
it.libraryName.set("example-test")
it.packageJsonFile.set(packageJsonFile)
}
assertThat(task.nodeExecutableAndArgs.get()).isEqualTo(listOf("npm", "help"))
assertThat(task.codegenJavaPackageName.get()).isEqualTo("com.example.test")
assertThat(task.libraryName.get()).isEqualTo("example-test")
assertThat(task.inputs.properties)
.containsKeys("nodeExecutableAndArgs", "codegenJavaPackageName", "libraryName")
}
@Test
@WithOs(OS.LINUX)
fun setupCommandLine_willSetupCorrectly() {
val reactNativeDir = tempFolder.newFolder("node_modules/react-native/")
val outputDir = tempFolder.newFolder("output")
val task =
createTestTask<GenerateCodegenArtifactsTask> { task ->
task.reactNativeDir.set(reactNativeDir)
task.generatedSrcDir.set(outputDir)
task.nodeExecutableAndArgs.set(listOf("--verbose"))
task.nodeWorkingDir.set(tempFolder.root.absolutePath)
}
task.setupCommandLine("example-test", "com.example.test")
assertThat(task.commandLine)
.containsExactly(
"--verbose",
File(reactNativeDir, "scripts/generate-specs-cli.js").toString(),
"--platform",
"android",
"--schemaPath",
File(outputDir, "schema.json").toString(),
"--outputDir",
outputDir.toString(),
"--libraryName",
"example-test",
"--javaPackageName",
"com.example.test",
)
}
@Test
@WithOs(OS.WIN)
fun setupCommandLine_onWindows_willSetupCorrectly() {
val reactNativeDir = tempFolder.newFolder("node_modules/react-native/")
val outputDir = tempFolder.newFolder("output")
val project = createProject()
val task =
createTestTask<GenerateCodegenArtifactsTask>(project) { task ->
task.reactNativeDir.set(reactNativeDir)
task.generatedSrcDir.set(outputDir)
task.nodeExecutableAndArgs.set(listOf("--verbose"))
task.nodeWorkingDir.set(project.projectDir.absolutePath)
}
task.setupCommandLine("example-test", "com.example.test")
assertThat(task.commandLine)
.containsExactly(
"cmd",
"/c",
"--verbose",
File(reactNativeDir, "scripts/generate-specs-cli.js")
.relativeTo(project.projectDir)
.path,
"--platform",
"android",
"--schemaPath",
File(outputDir, "schema.json").relativeTo(project.projectDir).path,
"--outputDir",
outputDir.relativeTo(project.projectDir).path,
"--libraryName",
"example-test",
"--javaPackageName",
"com.example.test",
)
}
@Test
fun resolveTaskParameters_withConfigInPackageJson_usesIt() {
val packageJsonFile =
tempFolder.newFile("package.json").apply {
// language=JSON
writeText(
"""
{
"name": "@a/library",
"codegenConfig": {
"name": "an-awesome-library",
"android": {
"javaPackageName": "com.awesome.package"
}
}
}
"""
.trimIndent()
)
}
val task =
createTestTask<GenerateCodegenArtifactsTask> {
it.packageJsonFile.set(packageJsonFile)
it.codegenJavaPackageName.set("com.example.ignored")
it.libraryName.set("a-library-name-that-is-ignored")
}
val (libraryName, javaPackageName) = task.resolveTaskParameters()
assertThat(libraryName).isEqualTo("an-awesome-library")
assertThat(javaPackageName).isEqualTo("com.awesome.package")
}
@Test
fun resolveTaskParameters_withConfigMissingInPackageJson_usesGradleOne() {
val packageJsonFile =
tempFolder.newFile("package.json").apply {
// language=JSON
writeText(
"""
{
"name": "@a/library",
"codegenConfig": {
}
}
"""
.trimIndent()
)
}
val task =
createTestTask<GenerateCodegenArtifactsTask> {
it.packageJsonFile.set(packageJsonFile)
it.codegenJavaPackageName.set("com.example.test")
it.libraryName.set("a-library-name-from-gradle")
}
val (libraryName, javaPackageName) = task.resolveTaskParameters()
assertThat(libraryName).isEqualTo("a-library-name-from-gradle")
assertThat(javaPackageName).isEqualTo("com.example.test")
}
@Test
fun resolveTaskParameters_withMissingPackageJson_usesGradleOne() {
val task =
createTestTask<GenerateCodegenArtifactsTask> { task ->
task.packageJsonFile.set(File(tempFolder.root, "package.json"))
task.codegenJavaPackageName.set("com.example.test")
task.libraryName.set("a-library-name-from-gradle")
}
val (libraryName, javaPackageName) = task.resolveTaskParameters()
assertThat(libraryName).isEqualTo("a-library-name-from-gradle")
assertThat(javaPackageName).isEqualTo("com.example.test")
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.tasks
import com.facebook.react.tests.*
import com.facebook.react.tests.createProject
import com.facebook.react.tests.createTestTask
import java.io.File
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class GenerateCodegenSchemaTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@get:Rule val osRule = OsRule()
@Test
fun generateCodegenSchema_outputFile_isSetCorrectly() {
val outputDir = tempFolder.newFolder("output")
val task = createTestTask<GenerateCodegenSchemaTask> { it.generatedSrcDir.set(outputDir) }
assertThat(task.generatedSchemaFile.get().asFile).isEqualTo(File(outputDir, "schema.json"))
}
@Test
fun generateCodegenSchema_nodeExecutablesArgs_areInsideInput() {
val task =
createTestTask<GenerateCodegenSchemaTask> {
it.nodeExecutableAndArgs.set(listOf("npm", "help"))
}
assertThat(task.nodeExecutableAndArgs.get()).isEqualTo(listOf("npm", "help"))
assertThat(task.inputs.properties).containsKey("nodeExecutableAndArgs")
}
@Test
fun wipeOutputDir_willCreateOutputDir() {
val task =
createTestTask<GenerateCodegenSchemaTask> {
it.generatedSrcDir.set(File(tempFolder.root, "output"))
}
task.wipeOutputDir()
assertThat(File(tempFolder.root, "output")).exists()
assertThat(File(tempFolder.root, "output").listFiles()).isEmpty()
}
@Test
fun wipeOutputDir_willWipeOutputDir() {
val outputDir =
tempFolder.newFolder("output").apply { File(this, "some-generated-file").createNewFile() }
val task = createTestTask<GenerateCodegenSchemaTask> { it.generatedSrcDir.set(outputDir) }
task.wipeOutputDir()
assertThat(outputDir.exists()).isTrue()
assertThat(outputDir.listFiles()).isEmpty()
}
@Test
@WithOs(OS.LINUX)
fun setupCommandLine_willSetupCorrectly() {
val codegenDir = tempFolder.newFolder("codegen")
val jsRootDir = tempFolder.newFolder("js")
val outputDir = tempFolder.newFolder("output")
val workingDir = jsRootDir
val task =
createTestTask<GenerateCodegenSchemaTask> { task ->
task.codegenDir.set(codegenDir)
task.jsRootDir.set(jsRootDir)
task.generatedSrcDir.set(outputDir)
task.nodeExecutableAndArgs.set(listOf("node", "--verbose"))
task.nodeWorkingDir.set(workingDir.absolutePath)
}
task.setupCommandLine()
assertThat(task.commandLine)
.containsExactly(
"node",
"--verbose",
File(codegenDir, "lib/cli/combine/combine-js-to-schema-cli.js").toString(),
"--platform",
"android",
"--exclude",
"NativeSampleTurboModule",
File(outputDir, "schema.json").toString(),
jsRootDir.toString(),
)
}
@Test
@WithOs(OS.WIN)
fun setupCommandLine_onWindows_willSetupCorrectly() {
val codegenDir = tempFolder.newFolder("codegen")
val jsRootDir = tempFolder.newFolder("js")
val outputDir = tempFolder.newFolder("output")
val project = createProject()
val task =
createTestTask<GenerateCodegenSchemaTask>(project) { task ->
task.codegenDir.set(codegenDir)
task.jsRootDir.set(jsRootDir)
task.generatedSrcDir.set(outputDir)
task.nodeExecutableAndArgs.set(listOf("node", "--verbose"))
task.nodeWorkingDir.set(project.rootDir.absolutePath)
}
task.setupCommandLine()
assertThat(task.commandLine)
.containsExactly(
"cmd",
"/c",
"node",
"--verbose",
File(codegenDir, "lib/cli/combine/combine-js-to-schema-cli.js")
.relativeTo(project.projectDir)
.path,
"--platform",
"android",
"--exclude",
"NativeSampleTurboModule",
File(outputDir, "schema.json").relativeTo(project.projectDir).path,
jsRootDir.relativeTo(project.projectDir).path,
)
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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.tasks
import com.facebook.react.tests.createTestTask
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class GenerateEntryPointTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun generatePackageListTask_groupIsSetCorrectly() {
val task = createTestTask<GenerateEntryPointTask> {}
assertThat(task.group).isEqualTo("react")
}
@Test
fun generatePackageListTask_staticInputs_areSetCorrectly() {
val outputFolder = tempFolder.newFolder("build")
val inputFile = tempFolder.newFile("config.json")
val task =
createTestTask<GenerateEntryPointTask> { task ->
task.generatedOutputDirectory.set(outputFolder)
task.autolinkInputFile.set(inputFile)
}
assertThat(task.inputs.files.singleFile).isEqualTo(inputFile)
assertThat(task.outputs.files.singleFile).isEqualTo(outputFolder)
}
@Test
fun composeFileContent_withNoPackages_returnsValidFile() {
val task = createTestTask<GenerateEntryPointTask>()
val packageName = "com.facebook.react"
val result = task.composeFileContent(packageName)
// language=java
assertThat(result)
.isEqualTo(
"""
package com.facebook.react;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger;
import com.facebook.react.views.view.WindowUtilKt;
import com.facebook.react.soloader.OpenSourceMergedSoMapping;
import com.facebook.soloader.SoLoader;
import java.io.IOException;
/**
* This class is the entry point for loading React Native using the configuration
* that the users specifies in their .gradle files.
*
* The `loadReactNative(this)` method invocation should be called inside the
* application onCreate otherwise the app won't load correctly.
*/
public class ReactNativeApplicationEntryPoint {
public static void loadReactNative(Context context) {
try {
SoLoader.init(context, OpenSourceMergedSoMapping.INSTANCE);
} catch (IOException error) {
throw new RuntimeException(error);
}
if (com.facebook.react.BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
DefaultNewArchitectureEntryPoint.load();
}
if (com.facebook.react.BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
WindowUtilKt.setEdgeToEdgeFeatureFlagOn();
}
}
}
"""
.trimIndent()
)
}
}

View File

@@ -0,0 +1,410 @@
/*
* 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.tasks
import com.facebook.react.model.ModelAutolinkingConfigJson
import com.facebook.react.model.ModelAutolinkingDependenciesJson
import com.facebook.react.model.ModelAutolinkingDependenciesPlatformAndroidJson
import com.facebook.react.model.ModelAutolinkingDependenciesPlatformJson
import com.facebook.react.tests.createTestTask
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class GeneratePackageListTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun generatePackageListTask_groupIsSetCorrectly() {
val task = createTestTask<GeneratePackageListTask> {}
assertThat(task.group).isEqualTo("react")
}
@Test
fun generatePackageListTask_staticInputs_areSetCorrectly() {
val outputFolder = tempFolder.newFolder("build")
val inputFile = tempFolder.newFile("config.json")
val task =
createTestTask<GeneratePackageListTask> { testTask ->
testTask.generatedOutputDirectory.set(outputFolder)
testTask.autolinkInputFile.set(inputFile)
}
assertThat(task.inputs.files.singleFile).isEqualTo(inputFile)
assertThat(task.outputs.files.singleFile).isEqualTo(outputFolder)
}
@Test
fun composePackageImports_withNoPackages_returnsEmpty() {
val task = createTestTask<GeneratePackageListTask>()
val packageName = "com.facebook.react"
val result = task.composePackageImports(packageName, emptyMap())
assertThat(result).isEqualTo("")
}
@Test
fun composePackageImports_withPackages_returnsImportCorrectly() {
val task = createTestTask<GeneratePackageListTask>()
val packageName = "com.facebook.react"
val result = task.composePackageImports(packageName, testDependencies)
assertThat(result)
.isEqualTo(
"""
// @react-native/a-package
import com.facebook.react.aPackage;
// @react-native/another-package
import com.facebook.react.anotherPackage;
"""
.trimIndent()
)
}
@Test
fun composePackageInstance_withNoPackages_returnsEmpty() {
val task = createTestTask<GeneratePackageListTask>()
val packageName = "com.facebook.react"
val result = task.composePackageInstance(packageName, emptyMap())
assertThat(result).isEqualTo("")
}
@Test
fun composePackageInstance_withPackages_returnsImportCorrectly() {
val task = createTestTask<GeneratePackageListTask>()
val packageName = "com.facebook.react"
val result = task.composePackageInstance(packageName, testDependencies)
assertThat(result)
.isEqualTo(
"""
,
new APackage(),
new AnotherPackage()
"""
.trimIndent()
)
}
@Test
fun interpolateDynamicValues_withNoBuildConfigOrROccurrencies_doesNothing() {
val packageName = "com.facebook.react"
val input = "com.facebook.react.aPackage"
val output = GeneratePackageListTask.interpolateDynamicValues(input, packageName)
assertThat(output).isEqualTo(input)
}
@Test
fun interpolateDynamicValues_withR_doesQualifyThem() {
val packageName = "com.facebook.react"
val input = "new APackageWithR(R.string.value)"
val output = GeneratePackageListTask.interpolateDynamicValues(input, packageName)
assertThat(output).isEqualTo("new APackageWithR(com.facebook.react.R.string.value)")
}
@Test
fun interpolateDynamicValues_withBuildConfig_doesQualifyThem() {
val packageName = "com.facebook.react"
val input = "new APackageWithBuildConfigInTheName(BuildConfig.VALUE)"
val output = GeneratePackageListTask.interpolateDynamicValues(input, packageName)
assertThat(output)
.isEqualTo("new APackageWithBuildConfigInTheName(com.facebook.react.BuildConfig.VALUE)")
}
@Test
fun filterAndroidPackages_withNull_returnsEmpty() {
val task = createTestTask<GeneratePackageListTask>()
val result = task.filterAndroidPackages(null)
assertThat(result)
.isEqualTo(emptyMap<String, ModelAutolinkingDependenciesPlatformAndroidJson>())
}
@Test
fun filterAndroidPackages_withEmptyObject_returnsEmpty() {
val task = createTestTask<GeneratePackageListTask>()
val result = task.filterAndroidPackages(ModelAutolinkingConfigJson("1000.0.0", null, null))
assertThat(result)
.isEqualTo(emptyMap<String, ModelAutolinkingDependenciesPlatformAndroidJson>())
}
@Test
fun filterAndroidPackages_withNoAndroidObject_returnsEmpty() {
val task = createTestTask<GeneratePackageListTask>()
val result =
task.filterAndroidPackages(
ModelAutolinkingConfigJson(
reactNativeVersion = "1000.0.0",
dependencies =
mapOf(
"a-dependency" to
ModelAutolinkingDependenciesJson(
root = "./a/directory",
name = "a-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = null),
)
),
project = null,
)
)
assertThat(result)
.isEqualTo(emptyMap<String, ModelAutolinkingDependenciesPlatformAndroidJson>())
}
@Test
fun filterAndroidPackages_withValidAndroidObject_returnsIt() {
val task = createTestTask<GeneratePackageListTask>()
val android =
ModelAutolinkingDependenciesPlatformAndroidJson(
sourceDir = "./a/directory/android",
packageImportPath = "import com.facebook.react.aPackage;",
packageInstance = "new APackage()",
buildTypes = emptyList(),
)
val result =
task.filterAndroidPackages(
ModelAutolinkingConfigJson(
reactNativeVersion = "1000.0.0",
dependencies =
mapOf(
"a-dependency" to
ModelAutolinkingDependenciesJson(
root = "./a/directory",
name = "a-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = android),
)
),
project = null,
)
)
assertThat(result.entries.size).isEqualTo(1)
assertThat(result["a-dependency"]).isEqualTo(android)
}
@Test
fun filterAndroidPackages_withIsPureCxxDependencyObject_returnsIt() {
val task = createTestTask<GeneratePackageListTask>()
val android =
ModelAutolinkingDependenciesPlatformAndroidJson(
sourceDir = "./a/directory/android",
packageImportPath = "import com.facebook.react.aPackage;",
packageInstance = "new APackage()",
buildTypes = emptyList(),
isPureCxxDependency = true,
)
val result =
task.filterAndroidPackages(
ModelAutolinkingConfigJson(
reactNativeVersion = "1000.0.0",
dependencies =
mapOf(
"a-pure-cxx-dependency" to
ModelAutolinkingDependenciesJson(
root = "./a/directory",
name = "a-pure-cxx-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = android),
)
),
project = null,
)
)
assertThat(result)
.isEqualTo(emptyMap<String, ModelAutolinkingDependenciesPlatformAndroidJson>())
}
@Test
fun composeFileContent_withNoPackages_returnsValidFile() {
val task = createTestTask<GeneratePackageListTask>()
val packageName = "com.facebook.react"
val imports = task.composePackageImports(packageName, emptyMap())
val instance = task.composePackageInstance(packageName, emptyMap())
val result = task.composeFileContent(imports, instance)
// language=java
assertThat(result)
.isEqualTo(
"""
package com.facebook.react;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainPackageConfig;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.ArrayList;
@SuppressWarnings("deprecation")
public class PackageList {
private Application application;
private ReactNativeHost reactNativeHost;
private MainPackageConfig mConfig;
public PackageList(ReactNativeHost reactNativeHost) {
this(reactNativeHost, null);
}
public PackageList(Application application) {
this(application, null);
}
public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
this.reactNativeHost = reactNativeHost;
mConfig = config;
}
public PackageList(Application application, MainPackageConfig config) {
this.reactNativeHost = null;
this.application = application;
mConfig = config;
}
private ReactNativeHost getReactNativeHost() {
return this.reactNativeHost;
}
private Resources getResources() {
return this.getApplication().getResources();
}
private Application getApplication() {
if (this.reactNativeHost == null) return this.application;
return this.reactNativeHost.getApplication();
}
private Context getApplicationContext() {
return this.getApplication().getApplicationContext();
}
public ArrayList<ReactPackage> getPackages() {
return new ArrayList<>(Arrays.<ReactPackage>asList(
new MainReactPackage(mConfig)
));
}
}
"""
.trimIndent()
)
}
@Test
fun composeFileContent_withPackages_returnsValidFile() {
val task = createTestTask<GeneratePackageListTask>()
val packageName = "com.facebook.react"
val imports = task.composePackageImports(packageName, testDependencies)
val instance = task.composePackageInstance(packageName, testDependencies)
val result = task.composeFileContent(imports, instance)
// language=java
assertThat(result)
.isEqualTo(
"""
package com.facebook.react;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainPackageConfig;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.ArrayList;
// @react-native/a-package
import com.facebook.react.aPackage;
// @react-native/another-package
import com.facebook.react.anotherPackage;
@SuppressWarnings("deprecation")
public class PackageList {
private Application application;
private ReactNativeHost reactNativeHost;
private MainPackageConfig mConfig;
public PackageList(ReactNativeHost reactNativeHost) {
this(reactNativeHost, null);
}
public PackageList(Application application) {
this(application, null);
}
public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
this.reactNativeHost = reactNativeHost;
mConfig = config;
}
public PackageList(Application application, MainPackageConfig config) {
this.reactNativeHost = null;
this.application = application;
mConfig = config;
}
private ReactNativeHost getReactNativeHost() {
return this.reactNativeHost;
}
private Resources getResources() {
return this.getApplication().getResources();
}
private Application getApplication() {
if (this.reactNativeHost == null) return this.application;
return this.reactNativeHost.getApplication();
}
private Context getApplicationContext() {
return this.getApplication().getApplicationContext();
}
public ArrayList<ReactPackage> getPackages() {
return new ArrayList<>(Arrays.<ReactPackage>asList(
new MainReactPackage(mConfig),
new APackage(),
new AnotherPackage()
));
}
}
"""
.trimIndent()
)
}
private val testDependencies =
mapOf(
"@react-native/a-package" to
ModelAutolinkingDependenciesPlatformAndroidJson(
sourceDir = "./a/directory",
packageImportPath = "import com.facebook.react.aPackage;",
packageInstance = "new APackage()",
buildTypes = emptyList(),
libraryName = "aPackage",
componentDescriptors = emptyList(),
cmakeListsPath = "./a/directory/CMakeLists.txt",
),
"@react-native/another-package" to
ModelAutolinkingDependenciesPlatformAndroidJson(
sourceDir = "./another/directory",
packageImportPath = "import com.facebook.react.anotherPackage;",
packageInstance = "new AnotherPackage()",
buildTypes = emptyList(),
libraryName = "anotherPackage",
componentDescriptors = emptyList(),
cmakeListsPath = "./another/directory/CMakeLists.txt",
),
)
}

View File

@@ -0,0 +1,40 @@
/*
* 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.tasks.internal
import com.facebook.react.tests.createProject
import com.facebook.react.tests.createTestTask
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class BuildCodegenCLITaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun buildCodegenCli_inputProperties_areSetCorrectly() {
val project = createProject(tempFolder.root)
val bashPath = tempFolder.newFile("bash").absolutePath
val logFile = tempFolder.newFile("logfile.out")
val fileTree = project.fileTree(".")
val task =
createTestTask<BuildCodegenCLITask> { task ->
task.bashWindowsHome.set(bashPath)
task.logFile.set(logFile)
task.inputFiles.set(fileTree)
task.outputFiles.set(fileTree)
}
assertThat(task.bashWindowsHome.get()).isEqualTo(bashPath)
assertThat(task.logFile.get().asFile).isEqualTo(logFile)
assertThat(task.inputFiles.get()).isEqualTo(fileTree)
assertThat(task.outputFiles.get()).isEqualTo(fileTree)
}
}

View File

@@ -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.
*/
package com.facebook.react.tasks.internal
import com.facebook.react.tests.createTestTask
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class CustomExecTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun customExec_inputProperties_areSetCorrectly() {
val outFile = tempFolder.newFile("stdout")
val errFile = tempFolder.newFile("stderr")
val task =
createTestTask<CustomExecTask> { task ->
task.errorOutputFile.set(errFile)
task.standardOutputFile.set(outFile)
}
assertThat(task.errorOutputFile.get().asFile).isEqualTo(errFile)
assertThat(task.standardOutputFile.get().asFile).isEqualTo(outFile)
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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.tasks.internal
import com.facebook.react.tests.createProject
import com.facebook.react.tests.createTestTask
import java.io.*
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class PrepareBoostTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun prepareBoostTask_withMissingConfiguration_fails() {
val task = createTestTask<PrepareBoostTask>()
assertThatThrownBy { task.taskAction() }
.isInstanceOf(IllegalStateException::class.java)
.hasMessage(
"Cannot query the value of task ':PrepareBoostTask' property 'boostVersion' because it has no value available."
)
}
@Test
fun prepareBoostTask_copiesCMakefile() {
val boostpath = tempFolder.newFolder("boostpath")
val output = tempFolder.newFolder("output")
val project = createProject()
val boostThirdPartyJniPath = File(project.projectDir, "src/main/jni/third-party/boost/")
val task =
createTestTask<PrepareBoostTask>(project = project) {
it.boostPath.setFrom(boostpath)
it.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
it.boostVersion.set("1.0.0")
it.outputDir.set(output)
}
File(boostThirdPartyJniPath, "CMakeLists.txt").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(output.listFiles()).extracting("name").contains("CMakeLists.txt")
}
@Test
fun prepareBoostTask_copiesAsmFiles() {
val boostpath = tempFolder.newFolder("boostpath")
val boostThirdPartyJniPath = tempFolder.newFolder("boostpath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareBoostTask> { task ->
task.boostPath.setFrom(boostpath)
task.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
task.boostVersion.set("1.0.0")
task.outputDir.set(output)
}
File(boostpath, "asm/asm.S").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(File(output, "asm/asm.S")).exists()
}
@Test
fun prepareBoostTask_copiesBoostSourceFiles() {
val boostpath = tempFolder.newFolder("boostpath")
val boostThirdPartyJniPath = tempFolder.newFolder("boostpath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareBoostTask> { task ->
task.boostPath.setFrom(boostpath)
task.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
task.boostVersion.set("1.0.0")
task.outputDir.set(output)
}
File(boostpath, "boost_1.0.0/boost/config.hpp").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(File(output, "boost_1.0.0/boost/config.hpp")).exists()
}
@Test
fun prepareBoostTask_copiesVersionlessBoostSourceFiles() {
val boostpath = tempFolder.newFolder("boostpath")
val boostThirdPartyJniPath = tempFolder.newFolder("boostpath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareBoostTask> { task ->
task.boostPath.setFrom(boostpath)
task.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
task.boostVersion.set("1.0.0")
task.outputDir.set(output)
}
File(boostpath, "boost/boost/config.hpp").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(File(output, "boost_1.0.0/boost/config.hpp")).exists()
}
}

View File

@@ -0,0 +1,211 @@
/*
* 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.tasks.internal
import com.facebook.react.tests.createProject
import com.facebook.react.tests.createTestTask
import java.io.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class PrepareGflagsTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test(expected = IllegalStateException::class)
fun prepareGflagsTask_withMissingConfiguration_fails() {
val task = createTestTask<PrepareGflagsTask>()
task.taskAction()
}
@Test
fun prepareGflagsTask_copiesCMakefile() {
val gflagspath = tempFolder.newFolder("gflagspath")
val output = tempFolder.newFolder("output")
val project = createProject()
val gflagsThirdPartyPath = File(project.projectDir, "src/main/jni/third-party/gflags/")
val task =
createTestTask<PrepareGflagsTask>(project = project) {
it.gflagsPath.setFrom(gflagspath)
it.gflagsThirdPartyPath.set(gflagsThirdPartyPath)
it.gflagsVersion.set("1.0.0")
it.outputDir.set(output)
}
File(gflagsThirdPartyPath, "CMakeLists.txt").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(output.listFiles()!!.any { it.name == "CMakeLists.txt" }).isTrue()
}
@Test
fun prepareGflagsTask_copiesSourceCodeAndHeaders() {
val gflagspath = tempFolder.newFolder("gflagspath")
val gflagsThirdPartyPath = tempFolder.newFolder("gflagspath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareGflagsTask> {
it.gflagsPath.setFrom(gflagspath)
it.gflagsThirdPartyPath.set(gflagsThirdPartyPath)
it.gflagsVersion.set("1.0.0")
it.outputDir.set(output)
}
File(gflagspath, "gflags-1.0.0/src/gflags.cc").apply {
parentFile.mkdirs()
createNewFile()
}
File(gflagspath, "gflags-1.0.0/src/util.h").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(File(output, "gflags/gflags.cc").exists()).isTrue()
assertThat(File(output, "gflags/util.h").exists()).isTrue()
}
@Test
fun prepareGflagsTask_replacesTokenCorrectly() {
val gflagspath = tempFolder.newFolder("gflagspath")
val gflagsThirdPartyPath = tempFolder.newFolder("gflagspath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareGflagsTask> { taskConfig ->
taskConfig.gflagsPath.setFrom(gflagspath)
taskConfig.gflagsThirdPartyPath.set(gflagsThirdPartyPath)
taskConfig.gflagsVersion.set("1.0.0")
taskConfig.outputDir.set(output)
}
File(gflagspath, "gflags-1.0.0/src/gflags_declare.h.in").apply {
parentFile.mkdirs()
writeText(
"""
#define GFLAGS_NAMESPACE @GFLAGS_NAMESPACE@
#include <string>
#if @HAVE_STDINT_H@
# include <stdint.h>
#elif @HAVE_SYS_TYPES_H@
# include <sys/types.h>
#elif @HAVE_INTTYPES_H@
# include <inttypes.h>
#endif
namespace GFLAGS_NAMESPACE {
#if @GFLAGS_INTTYPES_FORMAT_C99@ // C99
typedef int32_t int32;
typedef uint32_t uint32;
typedef int64_t int64;
typedef uint64_t uint64;
#elif @GFLAGS_INTTYPES_FORMAT_BSD@ // BSD
typedef int32_t int32;
typedef u_int32_t uint32;
typedef int64_t int64;
typedef u_int64_t uint64;
#elif @GFLAGS_INTTYPES_FORMAT_VC7@ // Windows
typedef __int32 int32;
typedef unsigned __int32 uint32;
typedef __int64 int64;
typedef unsigned __int64 uint64;
#else
# error Do not know how to define a 32-bit integer quantity on your system
#endif
} // namespace GFLAGS_NAMESPACE
"""
)
}
File(gflagspath, "gflags-1.0.0/src/config.h.in").apply {
parentFile.mkdirs()
createNewFile()
writeText("#cmakedefine")
}
File(gflagspath, "gflags-1.0.0/src/gflags_ns.h.in").apply {
parentFile.mkdirs()
createNewFile()
writeText("@ns@ @NS@")
}
File(gflagspath, "gflags-1.0.0/src/gflags.h.in").apply {
parentFile.mkdirs()
createNewFile()
writeText("@GFLAGS_ATTRIBUTE_UNUSED@\n@INCLUDE_GFLAGS_NS_H@")
}
File(gflagspath, "gflags-1.0.0/src/gflags_completions.h.in").apply {
parentFile.mkdirs()
createNewFile()
writeText("@GFLAGS_NAMESPACE@")
}
task.taskAction()
val declareFile = File(output, "gflags/gflags_declare.h")
assertThat(declareFile.exists()).isTrue()
assertEquals(
declareFile.readText(),
"""
#define GFLAGS_NAMESPACE gflags
#include <string>
#if 1
# include <stdint.h>
#elif 1
# include <sys/types.h>
#elif 1
# include <inttypes.h>
#endif
namespace GFLAGS_NAMESPACE {
#if 1 // C99
typedef int32_t int32;
typedef uint32_t uint32;
typedef int64_t int64;
typedef uint64_t uint64;
#elif 1 // BSD
typedef int32_t int32;
typedef u_int32_t uint32;
typedef int64_t int64;
typedef u_int64_t uint64;
#elif 1 // Windows
typedef __int32 int32;
typedef unsigned __int32 uint32;
typedef __int64 int64;
typedef unsigned __int64 uint64;
#else
# error Do not know how to define a 32-bit integer quantity on your system
#endif
} // namespace GFLAGS_NAMESPACE
""",
)
val configFile = File(output, "gflags/config.h")
assertThat(configFile.exists()).isTrue()
assertEquals(configFile.readText(), "//cmakedefine")
val nsFile = File(output, "gflags/gflags_google.h")
assertThat(nsFile.exists()).isTrue()
assertEquals(nsFile.readText(), "google GOOGLE")
val gflagsFile = File(output, "gflags/gflags.h")
assertThat(gflagsFile.exists()).isTrue()
assertEquals(gflagsFile.readText(), "\n#include \"gflags/gflags_google.h\"")
val completionsFile = File(output, "gflags/gflags_completions.h")
assertThat(completionsFile.exists()).isTrue()
assertEquals(completionsFile.readText(), "gflags")
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.tasks.internal
import com.facebook.react.tests.createProject
import com.facebook.react.tests.createTestTask
import java.io.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class PrepareGlogTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test(expected = IllegalStateException::class)
fun prepareGlogTask_withMissingConfiguration_fails() {
val task = createTestTask<PrepareGlogTask>()
task.taskAction()
}
@Test
fun prepareGlogTask_copiesCMakefile() {
val glogpath = tempFolder.newFolder("glogpath")
val output = tempFolder.newFolder("output")
val project = createProject()
val glogThirdPartyJniPath = File(project.projectDir, "src/main/jni/third-party/glog/")
val task =
createTestTask<PrepareGlogTask>(project = project) {
it.glogPath.setFrom(glogpath)
it.glogThirdPartyJniPath.set(glogThirdPartyJniPath)
it.glogVersion.set("1.0.0")
it.outputDir.set(output)
}
File(glogThirdPartyJniPath, "CMakeLists.txt").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(output.listFiles()!!.any { it.name == "CMakeLists.txt" }).isTrue()
}
@Test
fun prepareGlogTask_copiesConfigHeaderFile() {
val glogpath = tempFolder.newFolder("glogpath")
val output = tempFolder.newFolder("output")
val project = createProject()
val glogThirdPartyJniPath = File(project.projectDir, "src/main/jni/third-party/glog/")
val task =
createTestTask<PrepareGlogTask>(project = project) {
it.glogPath.setFrom(glogpath)
it.glogThirdPartyJniPath.set(glogThirdPartyJniPath)
it.glogVersion.set("1.0.0")
it.outputDir.set(output)
}
File(glogThirdPartyJniPath, "config.h").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(output.listFiles()!!.any { it.name == "config.h" }).isTrue()
}
@Test
fun prepareGlogTask_copiesSourceCode() {
val glogpath = tempFolder.newFolder("glogpath")
val glogThirdPartyJniPath = tempFolder.newFolder("glogpath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareGlogTask> {
it.glogPath.setFrom(glogpath)
it.glogThirdPartyJniPath.set(glogThirdPartyJniPath)
it.glogVersion.set("1.0.0")
it.outputDir.set(output)
}
File(glogpath, "glog-1.0.0/src/glog.cpp").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(File(output, "glog-1.0.0/src/glog.cpp").exists()).isTrue()
}
@Test
fun prepareGlogTask_replacesTokenCorrectly() {
val glogpath = tempFolder.newFolder("glogpath")
val glogThirdPartyJniPath = tempFolder.newFolder("glogpath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareGlogTask> { task ->
task.glogPath.setFrom(glogpath)
task.glogThirdPartyJniPath.set(glogThirdPartyJniPath)
task.glogVersion.set("1.0.0")
task.outputDir.set(output)
}
File(glogpath, "glog-1.0.0/src/glog.h.in").apply {
parentFile.mkdirs()
writeText("ac_google_start_namespace")
}
task.taskAction()
val expectedFile = File(output, "glog.h")
assertThat(expectedFile.exists()).isTrue()
assertThat(expectedFile.readText()).isEqualTo("ac_google_start_namespace")
}
@Test
fun prepareGlogTask_exportsHeaderCorrectly() {
val glogpath = tempFolder.newFolder("glogpath")
val glogThirdPartyJniPath = tempFolder.newFolder("glogpath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareGlogTask> { task ->
task.glogPath.setFrom(glogpath)
task.glogThirdPartyJniPath.set(glogThirdPartyJniPath)
task.glogVersion.set("1.0.0")
task.outputDir.set(output)
}
File(glogpath, "glog-1.0.0/src/logging.h.in").apply {
parentFile.mkdirs()
writeText("ac_google_start_namespace")
}
task.taskAction()
assertThat(File(output, "exported/glog/logging.h").exists()).isTrue()
}
}

View File

@@ -0,0 +1,211 @@
/*
* 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.tasks.internal
import com.facebook.react.tasks.internal.utils.PrefabPreprocessingEntry
import com.facebook.react.tests.createProject
import com.facebook.react.tests.createTestTask
import java.io.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class PreparePrefabHeadersTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun preparePrefabHeadersTask_withMissingConfiguration_doesNothing() {
val task = createTestTask<PreparePrefabHeadersTask>()
task.taskAction()
}
@Test
fun preparePrefabHeadersTask_withSingleEntry_copiesHeaderFile() {
val outputDir = tempFolder.newFolder("output")
File(tempFolder.root, "input/hello.h").createNewPathAndFile()
val project = createProject(projectDir = tempFolder.root)
val task =
createTestTask<PreparePrefabHeadersTask>(project = project) {
it.outputDir.set(outputDir)
it.input.set(listOf(PrefabPreprocessingEntry("sample_library", "input/" to "")))
}
task.taskAction()
assertThat(File(outputDir, "sample_library/hello.h")).exists()
}
@Test
fun preparePrefabHeadersTask_withSingleEntry_respectsPrefix() {
val expectedPrefix = "react/render/something/"
val outputDir = tempFolder.newFolder("output")
File(tempFolder.root, "input/hello.h").createNewPathAndFile()
val project = createProject(projectDir = tempFolder.root)
val task =
createTestTask<PreparePrefabHeadersTask>(project = project) {
it.outputDir.set(outputDir)
it.input.set(
listOf(PrefabPreprocessingEntry("sample_library", "input/" to expectedPrefix))
)
}
task.taskAction()
assertThat(File(outputDir, "sample_library/${expectedPrefix}hello.h")).exists()
}
@Test
fun preparePrefabHeadersTask_ignoresUnnecessaryFiles() {
val expectedPrefix = "react/render/something/"
val outputDir = tempFolder.newFolder("output")
File(tempFolder.root, "input/hello.hpp").createNewPathAndFile()
File(tempFolder.root, "input/hello.cpp").createNewPathAndFile()
File(tempFolder.root, "input/CMakeLists.txt").createNewPathAndFile()
val project = createProject(projectDir = tempFolder.root)
val task =
createTestTask<PreparePrefabHeadersTask>(project = project) {
it.outputDir.set(outputDir)
it.input.set(
listOf(PrefabPreprocessingEntry("sample_library", "input/" to expectedPrefix))
)
}
task.taskAction()
assertThat(File(outputDir, "sample_library/hello.hpp")).doesNotExist()
assertThat(File(outputDir, "sample_library/hello.cpp")).doesNotExist()
assertThat(File(outputDir, "sample_library/CMakeLists.txt")).doesNotExist()
}
@Test
fun preparePrefabHeadersTask_withMultiplePaths_copiesHeaderFiles() {
val outputDir = tempFolder.newFolder("output")
File(tempFolder.root, "input/component1/hello1.h").createNewPathAndFile()
File(tempFolder.root, "input/component2/debug/hello2.h").createNewPathAndFile()
val project = createProject(projectDir = tempFolder.root)
val task =
createTestTask<PreparePrefabHeadersTask>(project = project) {
it.outputDir.set(outputDir)
it.input.set(
listOf(
PrefabPreprocessingEntry(
"sample_library",
listOf("input/component1/" to "", "input/component2/" to ""),
),
)
)
}
task.taskAction()
assertThat(File(outputDir, "sample_library/hello1.h")).exists()
assertThat(File(outputDir, "sample_library/debug/hello2.h")).exists()
}
@Test
fun preparePrefabHeadersTask_withMultipleEntries_copiesHeaderFiles() {
val outputDir = tempFolder.newFolder("output")
File(tempFolder.root, "input/lib1/hello1.h").createNewPathAndFile()
File(tempFolder.root, "input/lib2/hello2.h").createNewPathAndFile()
val project = createProject(projectDir = tempFolder.root)
val task =
createTestTask<PreparePrefabHeadersTask>(project = project) {
it.outputDir.set(outputDir)
it.input.set(
listOf(
PrefabPreprocessingEntry("libraryone", "input/lib1/" to ""),
PrefabPreprocessingEntry("librarytwo", "input/lib2/" to ""),
)
)
}
task.taskAction()
assertThat(File(outputDir, "libraryone/hello1.h")).exists()
assertThat(File(outputDir, "librarytwo/hello2.h")).exists()
}
@Test
fun preparePrefabHeadersTask_withReusedHeaders_copiesHeadersTwice() {
val outputDir = tempFolder.newFolder("output")
File(tempFolder.root, "input/lib1/hello1.h").createNewPathAndFile()
File(tempFolder.root, "input/lib2/hello2.h").createNewPathAndFile()
File(tempFolder.root, "input/shared/sharedheader.h").createNewPathAndFile()
val project = createProject(projectDir = tempFolder.root)
val task =
createTestTask<PreparePrefabHeadersTask>(project = project) {
it.outputDir.set(outputDir)
it.input.set(
listOf(
PrefabPreprocessingEntry(
"libraryone",
listOf("input/lib1/" to "", "input/shared/" to "shared/"),
),
PrefabPreprocessingEntry(
"librarytwo",
listOf("input/lib2/" to "", "input/shared/" to "shared/"),
),
)
)
}
task.taskAction()
assertThat(File(outputDir, "libraryone/hello1.h")).exists()
assertThat(File(outputDir, "libraryone/shared/sharedheader.h")).exists()
assertThat(File(outputDir, "librarytwo/hello2.h")).exists()
assertThat(File(outputDir, "librarytwo/shared/sharedheader.h")).exists()
}
@Test
fun preparePrefabHeadersTask_withBoostHeaders_filtersThemCorrectly() {
val outputDir = tempFolder.newFolder("output")
File(tempFolder.root, "boost/boost/config.hpp").createNewPathAndFile()
File(tempFolder.root, "boost/boost/operators.hpp").createNewPathAndFile()
File(tempFolder.root, "boost/boost/config/default/default.hpp").createNewPathAndFile()
File(tempFolder.root, "boost/boost/core/core.hpp").createNewPathAndFile()
File(tempFolder.root, "boost/boost/detail/workaround.hpp").createNewPathAndFile()
File(tempFolder.root, "boost/boost/preprocessor/preprocessor.hpp").createNewPathAndFile()
File(tempFolder.root, "boost/boost/preprocessor/detail/preprocessor_detail.hpp")
.createNewPathAndFile()
File(tempFolder.root, "boost/boost/anothermodule/wedontuse.hpp").createNewPathAndFile()
val project = createProject(projectDir = tempFolder.root)
val task =
createTestTask<PreparePrefabHeadersTask>(project = project) { task ->
task.outputDir.set(outputDir)
task.input.set(listOf(PrefabPreprocessingEntry("sample_library", "boost/" to "")))
}
task.taskAction()
assertThat(File(outputDir, "sample_library/boost/config.hpp")).exists()
assertThat(File(outputDir, "sample_library/boost/operators.hpp")).exists()
assertThat(File(outputDir, "sample_library/boost/config/default/default.hpp")).exists()
assertThat(File(outputDir, "sample_library/boost/core/core.hpp")).exists()
assertThat(File(outputDir, "sample_library/boost/detail/workaround.hpp")).exists()
assertThat(File(outputDir, "sample_library/boost/preprocessor/preprocessor.hpp")).exists()
assertThat(File(outputDir, "sample_library/boost/preprocessor/detail/preprocessor_detail.hpp"))
.exists()
assertThat(File(outputDir, "sample_library/boost/anothermodule/wedontuse.hpp")).doesNotExist()
}
private fun File.createNewPathAndFile() {
parentFile.mkdirs()
createNewFile()
}
}

View File

@@ -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.
*/
package com.facebook.react.tasks.internal.utils
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class PrefabPreprocessingEntryTest {
@Test
fun secondaryConstructor_createsAList() {
val sampleEntry =
PrefabPreprocessingEntry(
libraryName = "justALibrary",
pathToPrefixCouple = "aPath" to "andAPrefix",
)
assertThat(sampleEntry.pathToPrefixCouples.size).isEqualTo(1)
assertThat(sampleEntry.pathToPrefixCouples[0].first).isEqualTo("aPath")
assertThat(sampleEntry.pathToPrefixCouples[0].second).isEqualTo("andAPrefix")
}
}

View File

@@ -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.
*/
package com.facebook.react.tests
import java.io.*
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.testfixtures.ProjectBuilder
internal fun createProject(projectDir: File? = null): Project {
val project =
ProjectBuilder.builder()
.apply {
if (projectDir != null) {
withProjectDir(projectDir)
}
}
.build()
project.plugins.apply("com.android.library")
project.plugins.apply("com.facebook.react")
return project
}
internal inline fun <reified T : Task> createTestTask(
project: Project = createProject(),
taskName: String = T::class.java.simpleName,
crossinline block: (T) -> Unit = {},
): T = project.tasks.register(taskName, T::class.java) { block(it) }.get()
/** A util function to zip a list of files from [contents] inside the zipfile at [destination]. */
internal fun zipFiles(destination: File, contents: List<File>) {
ZipOutputStream(BufferedOutputStream(FileOutputStream(destination.absolutePath))).use { out ->
for (file in contents) {
FileInputStream(file).use { fi ->
BufferedInputStream(fi).use { origin ->
val entry = ZipEntry(file.name)
out.putNextEntry(entry)
origin.copyTo(out, 1024)
}
}
}
}
}
/** A util function to create a zip given a list of dummy files path. */
internal fun createZip(dest: File, paths: List<String>) {
val env = mapOf("create" to "true")
val uri = URI.create("jar:file:$dest")
FileSystems.newFileSystem(uri, env).use { zipfs ->
paths.forEach { path ->
val zipEntryPath = zipfs.getPath(path)
val zipEntryFolder = zipEntryPath.subpath(0, zipEntryPath.nameCount - 1)
Files.createDirectories(zipEntryFolder)
Files.createFile(zipEntryPath)
}
}
}

View File

@@ -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.
*/
package com.facebook.react.utils
import java.io.File
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class AgpConfiguratorUtilsTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun getPackageNameFromManifest_withEmptyFile_returnsNull() {
val mainFolder = tempFolder.newFolder("awesome-module/src/main/")
val manifest = File(mainFolder, "AndroidManifest.xml").apply { writeText("") }
val actual = getPackageNameFromManifest(manifest)
assertThat(actual).isNull()
}
@Test
fun getPackageNameFromManifest_withMissingPackage_returnsNull() {
val mainFolder = tempFolder.newFolder("awesome-module/src/main/")
val manifest =
File(mainFolder, "AndroidManifest.xml").apply {
writeText(
// language=xml
"""
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
"""
.trimIndent()
)
}
val actual = getPackageNameFromManifest(manifest)
assertThat(actual).isNull()
}
@Test
fun getPackageNameFromManifest_withPackage_returnsPackage() {
val mainFolder = tempFolder.newFolder("awesome-module/src/main/")
val manifest =
File(mainFolder, "AndroidManifest.xml").apply {
writeText(
// language=xml
"""
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.facebook.react" >
</manifest>
"""
.trimIndent()
)
}
val actual = getPackageNameFromManifest(manifest)
assertThat(actual).isNotNull()
assertThat(actual).isEqualTo("com.facebook.react")
}
}

View File

@@ -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.
*/
package com.facebook.react.utils
import com.facebook.react.tests.createProject
import com.facebook.react.utils.BackwardCompatUtils.configureBackwardCompatibilityReactMap
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class BackwardCompatUtilsTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun configureBackwardCompatibilityReactMap_addsEmptyReactMap() {
val project = createProject()
configureBackwardCompatibilityReactMap(project)
assertThat(project.extensions.extraProperties.has("react")).isTrue()
@Suppress("UNCHECKED_CAST")
assertThat(project.extensions.extraProperties.get("react") as Map<String, Any?>).isEmpty()
}
@Test
fun configureBackwardCompatibilityReactMap_withExistingMapSetByUser_wipesTheMap() {
val project = createProject()
project.extensions.extraProperties.set("react", mapOf("enableHermes" to true))
configureBackwardCompatibilityReactMap(project)
assertThat(project.extensions.extraProperties.has("react")).isTrue()
@Suppress("UNCHECKED_CAST")
assertThat(project.extensions.extraProperties.get("react") as Map<String, Any?>).isEmpty()
}
}

View File

@@ -0,0 +1,829 @@
/*
* 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.utils
import com.facebook.react.tests.createProject
import com.facebook.react.utils.DependencyUtils.configureDependencies
import com.facebook.react.utils.DependencyUtils.configureRepositories
import com.facebook.react.utils.DependencyUtils.exclusiveEnterpriseRepository
import com.facebook.react.utils.DependencyUtils.getDependencySubstitutions
import com.facebook.react.utils.DependencyUtils.mavenRepoFromURI
import com.facebook.react.utils.DependencyUtils.mavenRepoFromUrl
import com.facebook.react.utils.DependencyUtils.readVersionAndGroupStrings
import com.facebook.react.utils.DependencyUtils.shouldAddJitPack
import java.net.URI
import org.assertj.core.api.Assertions.assertThat
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
import org.gradle.testfixtures.ProjectBuilder
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class DependencyUtilsTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun configureRepositories_withProjectPropertySet_configuresMavenLocalCorrectly() {
val localMaven = tempFolder.newFolder("m2")
val localMavenURI = localMaven.toURI()
val project = createProject()
project.extensions.extraProperties.set("react.internal.mavenLocalRepo", localMaven.absolutePath)
configureRepositories(project)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == localMavenURI
}
)
.isNotNull()
}
@Test
fun configureRepositories_containsSnapshotRepo() {
val repositoryURI = URI.create("https://central.sonatype.com/repository/maven-snapshots/")
val project = createProject()
configureRepositories(project)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNotNull()
}
@Test
fun configureRepositories_containsMavenCentral() {
val repositoryURI = URI.create("https://repo.maven.apache.org/maven2/")
val project = createProject()
configureRepositories(project)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNotNull()
}
@Test
fun configureRepositories_containsGoogleRepo() {
val repositoryURI = URI.create("https://dl.google.com/dl/android/maven2/")
val project = createProject()
configureRepositories(project)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNotNull()
}
@Test
fun configureRepositories_containsJitPack() {
val repositoryURI = URI.create("https://www.jitpack.io")
val project = createProject()
configureRepositories(project)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNotNull()
}
@Test
fun configureRepositories_withExclusiveEnterpriseRepository_replacesAllRepositories() {
val repositoryURI = URI.create("https://maven.myfabolousorganization.it")
val project = createProject()
project.rootProject.extensions.extraProperties.set(
"exclusiveEnterpriseRepository",
repositoryURI.toString(),
)
configureRepositories(project)
assertThat(project.repositories).hasSize(1)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNotNull()
}
@Test
fun configureRepositories_withIncludeJitpackRepositoryFalse_doesNotContainJitPack() {
val repositoryURI = URI.create("https://www.jitpack.io")
var project = createProject()
project.extensions.extraProperties.set("includeJitpackRepository", "false")
configureRepositories(project)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNull()
// We test both with scoped and unscoped property
project = createProject()
project.extensions.extraProperties.set("react.includeJitpackRepository", "false")
configureRepositories(project)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNull()
}
@Test
fun configureRepositories_withincludeJitpackRepositoryTrue_containJitPack() {
val repositoryURI = URI.create("https://www.jitpack.io")
var project = createProject()
project.extensions.extraProperties.set("includeJitpackRepository", "true")
configureRepositories(project)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNotNull()
// We test both with scoped and unscoped property
project = createProject()
project.extensions.extraProperties.set("react.includeJitpackRepository", "true")
configureRepositories(project)
assertThat(
project.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNotNull()
}
@Test
fun configureRepositories_withProjectPropertySet_hasHigherPriorityThanMavenCentral() {
val localMaven = tempFolder.newFolder("m2")
val localMavenURI = localMaven.toURI()
val mavenCentralURI = URI.create("https://repo.maven.apache.org/maven2/")
val project = createProject()
project.extensions.extraProperties.set("react.internal.mavenLocalRepo", localMaven.absolutePath)
configureRepositories(project)
val indexOfLocalRepo =
project.repositories.indexOfFirst {
it is MavenArtifactRepository && it.url == localMavenURI
}
val indexOfMavenCentral =
project.repositories.indexOfFirst {
it is MavenArtifactRepository && it.url == mavenCentralURI
}
assertThat(indexOfLocalRepo < indexOfMavenCentral).isTrue()
}
@Test
fun configureRepositories_snapshotRepoHasHigherPriorityThanMavenCentral() {
val repositoryURI = URI.create("https://central.sonatype.com/repository/maven-snapshots/")
val mavenCentralURI = URI.create("https://repo.maven.apache.org/maven2/")
val project = createProject()
configureRepositories(project)
val indexOfSnapshotRepo =
project.repositories.indexOfFirst {
it is MavenArtifactRepository && it.url == repositoryURI
}
val indexOfMavenCentral =
project.repositories.indexOfFirst {
it is MavenArtifactRepository && it.url == mavenCentralURI
}
assertThat(indexOfSnapshotRepo < indexOfMavenCentral).isTrue()
}
@Test
fun configureRepositories_appliesToAllProjects() {
val repositoryURI = URI.create("https://repo.maven.apache.org/maven2/")
val rootProject = ProjectBuilder.builder().build()
val appProject = ProjectBuilder.builder().withName("app").withParent(rootProject).build()
val libProject = ProjectBuilder.builder().withName("lib").withParent(rootProject).build()
configureRepositories(appProject)
assertThat(
appProject.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNotNull()
assertThat(
libProject.repositories.firstOrNull {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isNotNull()
}
@Test
fun configureRepositories_withPreviousExclusionRulesOnMavenCentral_appliesCorrectly() {
val repositoryURI = URI.create("https://repo.maven.apache.org/maven2/")
val rootProject = ProjectBuilder.builder().build()
val appProject = ProjectBuilder.builder().withName("app").withParent(rootProject).build()
val libProject = ProjectBuilder.builder().withName("lib").withParent(rootProject).build()
// Let's emulate a library which set an `excludeGroup` on `com.facebook.react` for Central.
libProject.repositories.mavenCentral { repo ->
repo.content { content -> content.excludeGroup("com.facebook.react") }
}
configureRepositories(appProject)
// We need to make sure we have Maven Central defined twice, one by the library,
// and another is the override by RNGP.
assertThat(
libProject.repositories.count {
it is MavenArtifactRepository && it.url == repositoryURI
}
)
.isEqualTo(2)
}
@Test
fun configureDependencies_withEmptyVersion_doesNothing() {
val project = createProject()
configureDependencies(project, DependencyUtils.Coordinates("", "", ""))
assertThat(project.configurations.first().resolutionStrategy.forcedModules.isEmpty()).isTrue()
}
@Test
fun configureDependencies_withVersionString_appliesResolutionStrategy_withClassicHermes() {
val project = createProject()
configureDependencies(project, DependencyUtils.Coordinates("1.2.3", "4.5.6", "7.8.9"))
val forcedModules = project.configurations.first().resolutionStrategy.forcedModules
assertThat(forcedModules.any { it.toString() == "com.facebook.react:react-android:1.2.3" })
.isTrue()
assertThat(forcedModules.any { it.toString() == "com.facebook.hermes:hermes-android:4.5.6" })
.isTrue()
}
@Test
fun configureDependencies_withVersionString_appliesResolutionStrategy_withHermesV1() {
val project = createProject()
configureDependencies(
project,
DependencyUtils.Coordinates("1.2.3", "4.5.6", "7.8.9"),
hermesV1Enabled = true,
)
val forcedModules = project.configurations.first().resolutionStrategy.forcedModules
assertThat(forcedModules.any { it.toString() == "com.facebook.react:react-android:1.2.3" })
.isTrue()
assertThat(forcedModules.any { it.toString() == "com.facebook.hermes:hermes-android:7.8.9" })
.isTrue()
}
@Test
fun configureDependencies_withVersionString_appliesOnAllProjects_withClassicHermes() {
val rootProject = ProjectBuilder.builder().build()
val appProject = ProjectBuilder.builder().withName("app").withParent(rootProject).build()
val libProject = ProjectBuilder.builder().withName("lib").withParent(rootProject).build()
appProject.plugins.apply("com.android.application")
libProject.plugins.apply("com.android.library")
configureDependencies(appProject, DependencyUtils.Coordinates("1.2.3", "4.5.6", "7.8.9"))
val appForcedModules = appProject.configurations.first().resolutionStrategy.forcedModules
val libForcedModules = libProject.configurations.first().resolutionStrategy.forcedModules
assertThat(appForcedModules.any { it.toString() == "com.facebook.react:react-android:1.2.3" })
.isTrue()
assertThat(appForcedModules.any { it.toString() == "com.facebook.hermes:hermes-android:4.5.6" })
.isTrue()
assertThat(libForcedModules.any { it.toString() == "com.facebook.react:react-android:1.2.3" })
.isTrue()
assertThat(libForcedModules.any { it.toString() == "com.facebook.hermes:hermes-android:4.5.6" })
.isTrue()
}
@Test
fun configureDependencies_withVersionString_appliesOnAllProjects_withHermesV1() {
val rootProject = ProjectBuilder.builder().build()
val appProject = ProjectBuilder.builder().withName("app").withParent(rootProject).build()
val libProject = ProjectBuilder.builder().withName("lib").withParent(rootProject).build()
appProject.plugins.apply("com.android.application")
libProject.plugins.apply("com.android.library")
configureDependencies(
appProject,
DependencyUtils.Coordinates("1.2.3", "4.5.6", "7.8.9"),
hermesV1Enabled = true,
)
val appForcedModules = appProject.configurations.first().resolutionStrategy.forcedModules
val libForcedModules = libProject.configurations.first().resolutionStrategy.forcedModules
assertThat(appForcedModules.any { it.toString() == "com.facebook.react:react-android:1.2.3" })
.isTrue()
assertThat(appForcedModules.any { it.toString() == "com.facebook.hermes:hermes-android:7.8.9" })
.isTrue()
assertThat(libForcedModules.any { it.toString() == "com.facebook.react:react-android:1.2.3" })
.isTrue()
assertThat(libForcedModules.any { it.toString() == "com.facebook.hermes:hermes-android:7.8.9" })
.isTrue()
}
@Test
fun configureDependencies_withVersionStringAndGroupString_appliesOnAllProjects_withClassicHermes() {
val rootProject = ProjectBuilder.builder().build()
val appProject = ProjectBuilder.builder().withName("app").withParent(rootProject).build()
val libProject = ProjectBuilder.builder().withName("lib").withParent(rootProject).build()
appProject.plugins.apply("com.android.application")
libProject.plugins.apply("com.android.library")
configureDependencies(
appProject,
DependencyUtils.Coordinates(
"1.2.3",
"4.5.6",
"7.8.9",
"io.github.test",
"io.github.test.hermes",
),
)
val appForcedModules = appProject.configurations.first().resolutionStrategy.forcedModules
val libForcedModules = libProject.configurations.first().resolutionStrategy.forcedModules
assertThat(appForcedModules.any { it.toString() == "io.github.test:react-android:1.2.3" })
.isTrue()
assertThat(
appForcedModules.any { it.toString() == "io.github.test.hermes:hermes-android:4.5.6" }
)
.isTrue()
assertThat(libForcedModules.any { it.toString() == "io.github.test:react-android:1.2.3" })
.isTrue()
assertThat(
libForcedModules.any { it.toString() == "io.github.test.hermes:hermes-android:4.5.6" }
)
.isTrue()
}
@Test
fun configureDependencies_withVersionStringAndGroupString_appliesOnAllProjects_withHermesV1() {
val rootProject = ProjectBuilder.builder().build()
val appProject = ProjectBuilder.builder().withName("app").withParent(rootProject).build()
val libProject = ProjectBuilder.builder().withName("lib").withParent(rootProject).build()
appProject.plugins.apply("com.android.application")
libProject.plugins.apply("com.android.library")
configureDependencies(
appProject,
DependencyUtils.Coordinates(
"1.2.3",
"4.5.6",
"7.8.9",
"io.github.test",
"io.github.test.hermes",
),
hermesV1Enabled = true,
)
val appForcedModules = appProject.configurations.first().resolutionStrategy.forcedModules
val libForcedModules = libProject.configurations.first().resolutionStrategy.forcedModules
assertThat(appForcedModules.any { it.toString() == "io.github.test:react-android:1.2.3" })
.isTrue()
assertThat(
appForcedModules.any { it.toString() == "io.github.test.hermes:hermes-android:7.8.9" }
)
.isTrue()
assertThat(libForcedModules.any { it.toString() == "io.github.test:react-android:1.2.3" })
.isTrue()
assertThat(
libForcedModules.any { it.toString() == "io.github.test.hermes:hermes-android:7.8.9" }
)
.isTrue()
}
@Test
fun getDependencySubstitutions_withDefaultGroup_substitutesCorrectly_withClassicHermes() {
val dependencySubstitutions =
getDependencySubstitutions(DependencyUtils.Coordinates("0.42.0", "0.42.0", "0.43.0"))
assertThat("com.facebook.react:react-native").isEqualTo(dependencySubstitutions[0].first)
assertThat("com.facebook.react:react-android:0.42.0")
.isEqualTo(dependencySubstitutions[0].second)
assertThat(
"The react-native artifact was deprecated in favor of react-android due to https://github.com/facebook/react-native/issues/35210."
)
.isEqualTo(dependencySubstitutions[0].third)
assertThat("com.facebook.react:hermes-engine").isEqualTo(dependencySubstitutions[1].first)
assertThat("com.facebook.hermes:hermes-android:0.42.0")
.isEqualTo(dependencySubstitutions[1].second)
assertThat(
"The hermes-engine artifact was deprecated in favor of hermes-android due to https://github.com/facebook/react-native/issues/35210."
)
.isEqualTo(dependencySubstitutions[1].third)
}
@Test
fun getDependencySubstitutions_withDefaultGroup_substitutesCorrectly_withHermesV1() {
val dependencySubstitutions =
getDependencySubstitutions(
DependencyUtils.Coordinates("0.42.0", "0.42.0", "0.43.0"),
hermesV1Enabled = true,
)
assertThat("com.facebook.react:react-native").isEqualTo(dependencySubstitutions[0].first)
assertThat("com.facebook.react:react-android:0.42.0")
.isEqualTo(dependencySubstitutions[0].second)
assertThat(
"The react-native artifact was deprecated in favor of react-android due to https://github.com/facebook/react-native/issues/35210."
)
.isEqualTo(dependencySubstitutions[0].third)
assertThat("com.facebook.react:hermes-engine").isEqualTo(dependencySubstitutions[1].first)
assertThat("com.facebook.hermes:hermes-android:0.43.0")
.isEqualTo(dependencySubstitutions[1].second)
assertThat(
"The hermes-engine artifact was deprecated in favor of hermes-android due to https://github.com/facebook/react-native/issues/35210."
)
.isEqualTo(dependencySubstitutions[1].third)
}
@Test
fun getDependencySubstitutions_withCustomGroup_substitutesCorrectly_withClassicHermes() {
val dependencySubstitutions =
getDependencySubstitutions(
DependencyUtils.Coordinates(
"0.42.0",
"0.42.0",
"0.43.0",
"io.github.test",
"io.github.test.hermes",
)
)
assertThat("com.facebook.react:react-native").isEqualTo(dependencySubstitutions[0].first)
assertThat("io.github.test:react-android:0.42.0").isEqualTo(dependencySubstitutions[0].second)
assertThat(
"The react-native artifact was deprecated in favor of react-android due to https://github.com/facebook/react-native/issues/35210."
)
.isEqualTo(dependencySubstitutions[0].third)
assertThat("com.facebook.react:hermes-engine").isEqualTo(dependencySubstitutions[1].first)
assertThat("io.github.test.hermes:hermes-android:0.42.0")
.isEqualTo(dependencySubstitutions[1].second)
assertThat(
"The hermes-engine artifact was deprecated in favor of hermes-android due to https://github.com/facebook/react-native/issues/35210."
)
.isEqualTo(dependencySubstitutions[1].third)
assertThat("com.facebook.react:hermes-android").isEqualTo(dependencySubstitutions[2].first)
assertThat("io.github.test.hermes:hermes-android:0.42.0")
.isEqualTo(dependencySubstitutions[2].second)
assertThat("The hermes-android artifact was moved to com.facebook.hermes publishing group.")
.isEqualTo(dependencySubstitutions[2].third)
assertThat("com.facebook.react:react-android").isEqualTo(dependencySubstitutions[3].first)
assertThat("io.github.test:react-android:0.42.0").isEqualTo(dependencySubstitutions[3].second)
assertThat("The react-android dependency was modified to use the correct Maven group.")
.isEqualTo(dependencySubstitutions[3].third)
assertThat("com.facebook.react:hermes-android").isEqualTo(dependencySubstitutions[4].first)
assertThat("io.github.test.hermes:hermes-android:0.42.0")
.isEqualTo(dependencySubstitutions[4].second)
assertThat("The hermes-android dependency was modified to use the correct Maven group.")
.isEqualTo(dependencySubstitutions[4].third)
}
@Test
fun getDependencySubstitutions_withCustomGroup_substitutesCorrectly_withHermesV1() {
val dependencySubstitutions =
getDependencySubstitutions(
DependencyUtils.Coordinates(
"0.42.0",
"0.42.0",
"0.43.0",
"io.github.test",
"io.github.test.hermes",
),
hermesV1Enabled = true,
)
assertThat("com.facebook.react:react-native").isEqualTo(dependencySubstitutions[0].first)
assertThat("io.github.test:react-android:0.42.0").isEqualTo(dependencySubstitutions[0].second)
assertThat(
"The react-native artifact was deprecated in favor of react-android due to https://github.com/facebook/react-native/issues/35210."
)
.isEqualTo(dependencySubstitutions[0].third)
assertThat("com.facebook.react:hermes-engine").isEqualTo(dependencySubstitutions[1].first)
assertThat("io.github.test.hermes:hermes-android:0.43.0")
.isEqualTo(dependencySubstitutions[1].second)
assertThat(
"The hermes-engine artifact was deprecated in favor of hermes-android due to https://github.com/facebook/react-native/issues/35210."
)
.isEqualTo(dependencySubstitutions[1].third)
assertThat("com.facebook.react:hermes-android").isEqualTo(dependencySubstitutions[2].first)
assertThat("io.github.test.hermes:hermes-android:0.43.0")
.isEqualTo(dependencySubstitutions[2].second)
assertThat("The hermes-android artifact was moved to com.facebook.hermes publishing group.")
.isEqualTo(dependencySubstitutions[2].third)
assertThat("com.facebook.react:react-android").isEqualTo(dependencySubstitutions[3].first)
assertThat("io.github.test:react-android:0.42.0").isEqualTo(dependencySubstitutions[3].second)
assertThat("The react-android dependency was modified to use the correct Maven group.")
.isEqualTo(dependencySubstitutions[3].third)
assertThat("com.facebook.react:hermes-android").isEqualTo(dependencySubstitutions[4].first)
assertThat("io.github.test.hermes:hermes-android:0.43.0")
.isEqualTo(dependencySubstitutions[4].second)
assertThat("The hermes-android dependency was modified to use the correct Maven group.")
.isEqualTo(dependencySubstitutions[4].third)
}
@Test
fun readVersionString_withCorrectVersionString_returnsIt() {
val propertiesFile =
tempFolder.newFile("gradle.properties").apply {
writeText(
"""
VERSION_NAME=1000.0.0
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val hermesVersionFile =
tempFolder.newFile("version.properties").apply {
writeText(
"""
HERMES_VERSION_NAME=1000.0.0
HERMES_V1_VERSION_NAME=1000.0.0
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val strings = readVersionAndGroupStrings(propertiesFile, hermesVersionFile)
val versionString = strings.versionString
val hermesVersionString = strings.hermesVersionString
val hermesV1VersionString = strings.hermesV1VersionString
assertThat(versionString).isEqualTo("1000.0.0")
assertThat(hermesVersionString).isEqualTo("1000.0.0")
assertThat(hermesV1VersionString).isEqualTo("1000.0.0")
}
@Test
fun readVersionString_withNightlyVersionString_returnsSnapshotVersion() {
val propertiesFile =
tempFolder.newFile("gradle.properties").apply {
writeText(
"""
VERSION_NAME=0.0.0-20221101-2019-cfe811ab1
HERMES_VERSION_NAME=0.12.0-commitly-20221101-2019-cfe811ab1
HERMES_V1_VERSION_NAME=250829098.0.0-stable
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val hermesVersionFile =
tempFolder.newFile("version.properties").apply {
writeText(
"""
HERMES_VERSION_NAME=0.14.0
HERMES_V1_VERSION_NAME=250829098.0.0-stable
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val strings = readVersionAndGroupStrings(propertiesFile, hermesVersionFile)
val versionString = strings.versionString
val hermesVersionString = strings.hermesVersionString
val hermesV1VersionString = strings.hermesV1VersionString
assertThat(versionString).isEqualTo("0.0.0-20221101-2019-cfe811ab1-SNAPSHOT")
assertThat(hermesVersionString).isEqualTo("0.14.0")
assertThat(hermesV1VersionString).isEqualTo("250829098.0.0-stable")
}
@Test
fun readVersionString_withMissingVersionString_returnsEmpty() {
val propertiesFile =
tempFolder.newFile("gradle.properties").apply {
writeText(
"""
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val hermesVersionFile =
tempFolder.newFile("version.properties").apply {
writeText(
"""
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val strings = readVersionAndGroupStrings(propertiesFile, hermesVersionFile)
val versionString = strings.versionString
val hermesVersionString = strings.hermesVersionString
val hermesV1VersionString = strings.hermesV1VersionString
assertThat(versionString).isEqualTo("")
assertThat(hermesVersionString).isEqualTo("")
assertThat(hermesV1VersionString).isEqualTo("")
}
@Test
fun readVersionString_withEmptyVersionString_returnsEmpty() {
val propertiesFile =
tempFolder.newFile("gradle.properties").apply {
writeText(
"""
VERSION_NAME=
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val hermesVersionFile =
tempFolder.newFile("version.properties").apply {
writeText(
"""
HERMES_VERSION_NAME=
HERMES_V1_VERSION_NAME=
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val strings = readVersionAndGroupStrings(propertiesFile, hermesVersionFile)
val versionString = strings.versionString
val hermesVersionString = strings.hermesVersionString
val hermesV1VersionString = strings.hermesV1VersionString
assertThat(versionString).isEqualTo("")
assertThat(hermesVersionString).isEqualTo("")
assertThat(hermesV1VersionString).isEqualTo("")
}
@Test
fun readGroupString_withCorrectGroupString_returnsIt() {
val propertiesFile =
tempFolder.newFile("gradle.properties").apply {
writeText(
"""
react.internal.publishingGroup=io.github.test
react.internal.hermesPublishingGroup=io.github.test
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val hermesVersionFile =
tempFolder.newFile("version.properties").apply {
writeText(
"""
HERMES_VERSION_NAME=
HERMES_V1_VERSION_NAME=
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val strings = readVersionAndGroupStrings(propertiesFile, hermesVersionFile)
val reactGroupString = strings.reactGroupString
val hermesGroupString = strings.hermesGroupString
assertThat(reactGroupString).isEqualTo("io.github.test")
assertThat(hermesGroupString).isEqualTo("io.github.test")
}
@Test
fun readGroupString_withEmptyGroupString_returnsDefault() {
val propertiesFile =
tempFolder.newFile("gradle.properties").apply {
writeText(
"""
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val hermesVersionFile =
tempFolder.newFile("version.properties").apply {
writeText(
"""
HERMES_VERSION_NAME=
HERMES_V1_VERSION_NAME=
ANOTHER_PROPERTY=true
"""
.trimIndent()
)
}
val strings = readVersionAndGroupStrings(propertiesFile, hermesVersionFile)
val reactGroupString = strings.reactGroupString
val hermesGroupString = strings.hermesGroupString
assertThat(reactGroupString).isEqualTo("com.facebook.react")
assertThat(hermesGroupString).isEqualTo("com.facebook.hermes")
}
@Test
fun mavenRepoFromUrl_worksCorrectly() {
val process = createProject()
val mavenRepo = process.mavenRepoFromUrl("https://hello.world")
assertThat(mavenRepo.url).isEqualTo(URI.create("https://hello.world"))
}
@Test
fun mavenRepoFromURI_worksCorrectly() {
val process = createProject()
val repoFolder = tempFolder.newFolder("maven-repo")
val mavenRepo = process.mavenRepoFromURI(repoFolder.toURI())
assertThat(mavenRepo.url).isEqualTo(repoFolder.toURI())
}
@Test
fun shouldAddJitPack_withScopedProperty() {
val project = createProject(tempFolder.root)
project.extensions.extraProperties.set("react.includeJitpackRepository", "false")
assertThat(project.shouldAddJitPack()).isFalse()
}
@Test
fun shouldAddJitPack_withUnscopedProperty() {
val project = createProject(tempFolder.root)
project.extensions.extraProperties.set("includeJitpackRepository", "false")
assertThat(project.shouldAddJitPack()).isFalse()
}
@Test
fun shouldAddJitPack_defaultIsTrue() {
val project = createProject(tempFolder.root)
assertThat(project.shouldAddJitPack()).isTrue()
}
@Test
fun exclusiveEnterpriseRepository_withScopedProperty() {
val project = createProject(tempFolder.root)
project.extensions.extraProperties.set(
"react.exclusiveEnterpriseRepository",
"https://maven.myfabolousorganization.it",
)
assertThat(project.exclusiveEnterpriseRepository())
.isEqualTo("https://maven.myfabolousorganization.it")
}
@Test
fun exclusiveEnterpriseRepository_withUnscopedProperty() {
val project = createProject(tempFolder.root)
project.extensions.extraProperties.set(
"exclusiveEnterpriseRepository",
"https://maven.myfabolousorganization.it",
)
assertThat(project.exclusiveEnterpriseRepository())
.isEqualTo("https://maven.myfabolousorganization.it")
}
@Test
fun exclusiveEnterpriseRepository_defaultIsTrue() {
val project = createProject(tempFolder.root)
assertThat(project.exclusiveEnterpriseRepository()).isNull()
}
}

View File

@@ -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.
*/
package com.facebook.react.utils
import java.io.File
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class FileUtilsTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun moveTo_movesCorrectly() {
val fileToMove = tempFolder.newFile().apply { writeText("42") }
val destFolder = tempFolder.newFolder("destFolder")
val destFile = File(destFolder, "destFile")
fileToMove.moveTo(destFile)
assertThat(destFile).hasContent("42")
assertThat(fileToMove).doesNotExist()
}
@Test
fun recreateDir_worksCorrectly() {
val subFolder = tempFolder.newFolder()
File(subFolder, "1").apply { writeText("1") }
File(subFolder, "2").apply { writeText("2") }
File(subFolder, "subDir").apply { mkdirs() }
File(subFolder, "subDir/3").apply { writeText("3") }
subFolder.recreateDir()
assertThat(subFolder).exists()
assertThat(subFolder.listFiles()).hasSize(0)
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.utils
import com.facebook.react.utils.NdkConfiguratorUtils.getPackagingOptionsForVariant
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class NdkConfiguratorUtilsTest {
@Test
fun getPackagingOptionsForVariant_withHermesEnabledAndThirdPartyJSCDisabled() {
val (excludes, includes) =
getPackagingOptionsForVariant(
hermesEnabled = true,
useThirdPartyJSC = false,
)
assertThat(excludes).containsExactly("**/libjsc.so", "**/libjsctooling.so")
assertThat(includes).doesNotContain("**/libjsc.so", "**/libjsctooling.so")
assertThat(includes).containsExactly("**/libhermesvm.so", "**/libhermestooling.so")
assertThat(excludes).doesNotContain("**/libhermesvm.so", "**/libhermestooling.so")
}
@Test
fun getPackagingOptionsForVariant_withHermesEnabledAndThirdPartyJSC() {
val (excludes, includes) =
getPackagingOptionsForVariant(
hermesEnabled = true,
useThirdPartyJSC = true,
)
assertThat(excludes).containsExactly("**/libjsc.so", "**/libjsctooling.so")
assertThat(includes).doesNotContain("**/libjsc.so", "**/libjsctooling.so")
assertThat(includes).containsExactly("**/libhermesvm.so", "**/libhermestooling.so")
assertThat(excludes).doesNotContain("**/libhermesvm.so", "**/libhermestooling.so")
}
@Test
fun getPackagingOptionsForVariant_withHermesDisabledAndThirdPartyJSCDisabled() {
val (excludes, includes) =
getPackagingOptionsForVariant(
hermesEnabled = false,
useThirdPartyJSC = false,
)
assertThat(excludes).containsExactly("**/libhermesvm.so", "**/libhermestooling.so")
assertThat(includes).doesNotContain("**/libhermesvm.so", "**/libhermestooling.so")
assertThat(includes).containsExactly("**/libjsc.so", "**/libjsctooling.so")
assertThat(excludes).doesNotContain("**/libjsc.so", "**/libjsctooling.so")
}
@Test
fun getPackagingOptionsForVariant_withHermesDisabledAndThirdPartyJSC() {
val (excludes, includes) =
getPackagingOptionsForVariant(
hermesEnabled = false,
useThirdPartyJSC = true,
)
assertThat(includes).containsExactly("**/libjsc.so")
assertThat(excludes)
.containsExactly("**/libhermesvm.so", "**/libhermestooling.so", "**/libjsctooling.so")
}
}

View File

@@ -0,0 +1,332 @@
/*
* 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.utils
import com.facebook.react.ReactExtension
import com.facebook.react.TestReactExtension
import com.facebook.react.tests.OS
import com.facebook.react.tests.OsRule
import com.facebook.react.tests.WithOs
import java.io.File
import org.assertj.core.api.Assertions.assertThat
import org.gradle.testfixtures.ProjectBuilder
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class PathUtilsTest {
@get:Rule val tempFolder = TemporaryFolder()
@get:Rule val osRule = OsRule()
@Test
fun detectedEntryFile_withProvidedVariable() {
val extension = TestReactExtension(ProjectBuilder.builder().build())
val expected = tempFolder.newFile("fake.index.js")
extension.entryFile.set(expected)
val actual = detectedEntryFile(extension)
assertThat(actual).isEqualTo(expected)
}
@Test
fun detectedEntryFile_withAndroidEntryPoint() {
val extension = TestReactExtension(ProjectBuilder.builder().build())
extension.root.set(tempFolder.root)
tempFolder.newFile("index.android.js")
val actual = detectedEntryFile(extension)
assertThat(actual).isEqualTo(File(tempFolder.root, "index.android.js"))
}
@Test
fun detectedEntryFile_withDefaultEntryPoint() {
val extension = TestReactExtension(ProjectBuilder.builder().build())
extension.root.set(tempFolder.root)
val actual = detectedEntryFile(extension)
assertThat(actual).isEqualTo(File(tempFolder.root, "index.js"))
}
@Test
fun detectedEntryFile_withEnvironmentVariable() {
val extension = TestReactExtension(ProjectBuilder.builder().build())
val expected = tempFolder.newFile("./fromenv.index.js")
// As we can't override env variable for tests, we're going to emulate them here.
val envVariable = "./fromenv.index.js"
extension.root.set(tempFolder.root)
val actual = detectedEntryFile(extension, envVariable)
assertThat(actual).isEqualTo(expected)
}
@Test
fun detectedCliPath_withCliPathFromExtensionAndFileExists_returnsIt() {
val project = ProjectBuilder.builder().build()
val cliFile = tempFolder.newFile("cli.js").apply { createNewFile() }
val extension = TestReactExtension(project)
extension.cliFile.set(cliFile)
val actual = detectedCliFile(extension)
assertThat(actual).isEqualTo(cliFile)
}
@Test
fun detectedCliPath_withCliFromNodeModules() {
val project = ProjectBuilder.builder().build()
val extension = TestReactExtension(project)
File(tempFolder.root, "node_modules/react-native/cli.js").apply {
parentFile.mkdirs()
writeText("<!-- nothing to see here -->")
}
val locationToResolveFrom = File(tempFolder.root, "a-subdirectory").apply { mkdirs() }
extension.root.set(locationToResolveFrom)
val actual = detectedCliFile(extension)
assertThat(actual.readText()).isEqualTo("<!-- nothing to see here -->")
}
@Test(expected = IllegalStateException::class)
fun detectedCliPath_failsIfNotFound() {
val project = ProjectBuilder.builder().build()
val extension = TestReactExtension(project)
// Because react-native is now a package, it is always
// accessible from <root>/node_modules/react-native
// We need to provide location where cli.js file won't be resolved
extension.root.set(tempFolder.root)
detectedCliFile(extension)
}
@Test
fun projectPathToLibraryName_withSimplePath() {
assertThat(projectPathToLibraryName(":sample")).isEqualTo("SampleSpec")
}
@Test
fun projectPathToLibraryName_withComplexPath() {
assertThat(projectPathToLibraryName(":sample:android:app")).isEqualTo("SampleAndroidAppSpec")
}
@Test
fun projectPathToLibraryName_withKebabCase() {
assertThat(projectPathToLibraryName("sample-android-app")).isEqualTo("SampleAndroidAppSpec")
}
@Test
fun projectPathToLibraryName_withDotsAndUnderscores() {
assertThat(projectPathToLibraryName("sample_android.app")).isEqualTo("SampleAndroidAppSpec")
}
@Test
fun detectOSAwareHermesCommand_withProvidedCommand() {
assertThat(detectOSAwareHermesCommand(tempFolder.root, "./my-home/hermes"))
.isEqualTo("./my-home/hermes")
}
@Test
fun detectOSAwareHermesCommand_withHermescBuiltLocally() {
// As we can't mock env variables, we skip this test if an override of the Hermes
// path has been provided.
assumeTrue(System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR") == null)
tempFolder.newFolder("node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/")
val expected =
tempFolder.newFile(
"node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/hermesc"
)
assertThat(detectOSAwareHermesCommand(tempFolder.root, "")).isEqualTo(expected.toString())
}
@Test
@WithOs(OS.MAC)
fun detectOSAwareHermesCommand_withHermescFromNPM() {
tempFolder.newFolder("node_modules/hermes-compiler/hermesc/osx-bin/")
val expected = tempFolder.newFile("node_modules/hermes-compiler/hermesc/osx-bin/hermesc")
assertThat(detectOSAwareHermesCommand(tempFolder.root, "")).isEqualTo(expected.toString())
}
@Test(expected = IllegalStateException::class)
@WithOs(OS.MAC)
fun detectOSAwareHermesCommand_failsIfNotFound() {
detectOSAwareHermesCommand(tempFolder.root, "")
}
@Test
@WithOs(OS.MAC)
fun detectOSAwareHermesCommand_withProvidedCommand_takesPrecedence() {
tempFolder.newFolder("node_modules/react-native/sdks/hermes/build/bin/")
tempFolder.newFile("node_modules/react-native/sdks/hermes/build/bin/hermesc")
tempFolder.newFolder("node_modules/react-native/sdks/hermesc/osx-bin/")
tempFolder.newFile("node_modules/react-native/sdks/hermesc/osx-bin/hermesc")
assertThat(detectOSAwareHermesCommand(tempFolder.root, "./my-home/hermes"))
.isEqualTo("./my-home/hermes")
}
@Test
@WithOs(OS.MAC)
fun detectOSAwareHermesCommand_withoutProvidedCommand_builtHermescTakesPrecedence() {
// As we can't mock env variables, we skip this test if an override of the Hermes
// path has been provided.
assumeTrue(System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR") == null)
tempFolder.newFolder("node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/")
val expected =
tempFolder.newFile(
"node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/hermesc"
)
tempFolder.newFolder("node_modules/react-native/sdks/hermesc/osx-bin/")
tempFolder.newFile("node_modules/react-native/sdks/hermesc/osx-bin/hermesc")
assertThat(detectOSAwareHermesCommand(tempFolder.root, "")).isEqualTo(expected.toString())
}
@Test
fun getBuiltHermescFile_withoutOverride() {
assertThat(getBuiltHermescFile(tempFolder.root, ""))
.isEqualTo(
File(
tempFolder.root,
"node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/hermesc",
)
)
}
@Test
@WithOs(OS.WIN)
fun getBuiltHermescFile_onWindows_withoutOverride() {
assertThat(getBuiltHermescFile(tempFolder.root, ""))
.isEqualTo(
File(
tempFolder.root,
"node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/hermesc.exe",
)
)
}
@Test
fun getBuiltHermescFile_withOverride() {
assertThat(getBuiltHermescFile(tempFolder.root, "/home/ci/hermes"))
.isEqualTo(File("/home/ci/hermes/build/bin/hermesc"))
}
@Test
@WithOs(OS.WIN)
fun getHermesCBin_onWindows_returnsHermescExe() {
assertThat(getHermesCBin()).isEqualTo("hermesc.exe")
}
@Test
@WithOs(OS.LINUX)
fun getHermesCBin_onLinux_returnsHermesc() {
assertThat(getHermesCBin()).isEqualTo("hermesc")
}
@Test
@WithOs(OS.MAC)
fun getHermesCBin_onMac_returnsHermesc() {
assertThat(getHermesCBin()).isEqualTo("hermesc")
}
@Test
fun findPackageJsonFile_withFileInParentFolder_picksItUp() {
tempFolder.newFile("package.json")
val moduleFolder = tempFolder.newFolder("awesome-module")
val project = ProjectBuilder.builder().withProjectDir(moduleFolder).build()
project.plugins.apply("com.android.library")
project.plugins.apply("com.facebook.react")
val extension = project.extensions.getByType(ReactExtension::class.java)
assertThat(findPackageJsonFile(project, extension.root))
.isEqualTo(project.file("../package.json"))
}
@Test
fun findPackageJsonFile_withFileConfiguredInExtension_picksItUp() {
val moduleFolder = tempFolder.newFolder("awesome-module")
val localFile = File(moduleFolder, "package.json").apply { writeText("{}") }
val project = ProjectBuilder.builder().withProjectDir(moduleFolder).build()
project.plugins.apply("com.android.library")
project.plugins.apply("com.facebook.react")
val extension =
project.extensions.getByType(ReactExtension::class.java).apply { root.set(moduleFolder) }
assertThat(findPackageJsonFile(project, extension.root)).isEqualTo(localFile)
}
@Test
fun readPackageJsonFile_withMissingFile_returnsNull() {
val moduleFolder = tempFolder.newFolder("awesome-module")
val project = ProjectBuilder.builder().withProjectDir(moduleFolder).build()
project.plugins.apply("com.android.library")
project.plugins.apply("com.facebook.react")
val extension =
project.extensions.getByType(ReactExtension::class.java).apply { root.set(moduleFolder) }
val actual = readPackageJsonFile(project, extension.root)
assertThat(actual).isNull()
}
@Test
fun readPackageJsonFile_withFileConfiguredInExtension_andMissingCodegenConfig_returnsNullCodegenConfig() {
val moduleFolder = tempFolder.newFolder("awesome-module")
File(moduleFolder, "package.json").apply { writeText("{}") }
val project = ProjectBuilder.builder().withProjectDir(moduleFolder).build()
project.plugins.apply("com.android.library")
project.plugins.apply("com.facebook.react")
val extension =
project.extensions.getByType(ReactExtension::class.java).apply { root.set(moduleFolder) }
val actual = readPackageJsonFile(project, extension.root)
assertThat(actual).isNotNull()
assertThat(actual!!.codegenConfig).isNull()
}
@Test
fun readPackageJsonFile_withFileConfiguredInExtension_andHavingCodegenConfig_returnsValidCodegenConfig() {
val moduleFolder = tempFolder.newFolder("awesome-module")
File(moduleFolder, "package.json").apply {
writeText(
// language=json
"""
{
"name": "a-library",
"codegenConfig": {}
}
"""
.trimIndent()
)
}
val project = ProjectBuilder.builder().withProjectDir(moduleFolder).build()
project.plugins.apply("com.android.library")
project.plugins.apply("com.facebook.react")
val extension =
project.extensions.getByType(ReactExtension::class.java).apply { root.set(moduleFolder) }
val actual = readPackageJsonFile(project, extension.root)
assertThat(actual).isNotNull()
assertThat(actual!!.codegenConfig).isNotNull()
}
}

View File

@@ -0,0 +1,246 @@
/*
* 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.utils
import com.facebook.react.TestReactExtension
import com.facebook.react.model.ModelCodegenConfig
import com.facebook.react.model.ModelPackageJson
import com.facebook.react.tests.createProject
import com.facebook.react.utils.ProjectUtils.getReactNativeArchitectures
import com.facebook.react.utils.ProjectUtils.isEdgeToEdgeEnabled
import com.facebook.react.utils.ProjectUtils.isHermesEnabled
import com.facebook.react.utils.ProjectUtils.isHermesV1Enabled
import com.facebook.react.utils.ProjectUtils.isNewArchEnabled
import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson
import java.io.File
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class ProjectUtilsTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test
fun isNewArchEnabled_alwaysReturnsTrue() {
assertThat(createProject().isNewArchEnabled()).isTrue()
}
@Test
fun isHermesEnabled_returnsTrueByDefault() {
assertThat(createProject().isHermesEnabled).isTrue()
}
@Test
fun isHermesEnabled_withDisabledViaProperty_returnsFalse() {
val project = createProject()
project.extensions.extraProperties.set("hermesEnabled", "false")
assertThat(project.isHermesEnabled).isFalse()
}
@Test
fun isHermesEnabled_withEnabledViaProperty_returnsTrue() {
val project = createProject()
project.extensions.extraProperties.set("hermesEnabled", "true")
assertThat(project.isHermesEnabled).isTrue()
}
@Test
fun isHermesEnabled_withInvalidViaProperty_returnsTrue() {
val project = createProject()
project.extensions.extraProperties.set("hermesEnabled", "¯\\_(ツ)_/¯")
assertThat(project.isHermesEnabled).isTrue()
}
@Test
fun isHermesEnabled_withDisabledViaExt_returnsFalse() {
val project = createProject()
val extMap = mapOf("enableHermes" to false)
project.extensions.extraProperties.set("react", extMap)
assertThat(project.isHermesEnabled).isFalse()
}
@Test
fun isHermesEnabled_withEnabledViaExt_returnsTrue() {
val project = createProject()
val extMap = mapOf("enableHermes" to true)
project.extensions.extraProperties.set("react", extMap)
assertThat(project.isHermesEnabled).isTrue()
}
@Test
fun isHermesEnabled_withDisabledViaExtAsString_returnsFalse() {
val project = createProject()
val extMap = mapOf("enableHermes" to "false")
project.extensions.extraProperties.set("react", extMap)
assertThat(project.isHermesEnabled).isFalse()
}
@Test
fun isHermesEnabled_withInvalidViaExt_returnsTrue() {
val project = createProject()
val extMap = mapOf("enableHermes" to "¯\\_(ツ)_/¯")
project.extensions.extraProperties.set("react", extMap)
assertThat(project.isHermesEnabled).isTrue()
}
@Test
fun isEdgeToEdgeEnabled_returnsFalseByDefault() {
assertThat(createProject().isEdgeToEdgeEnabled).isFalse()
}
@Test
fun isEdgeToEdgeEnabled_withDisabledViaProperty_returnsFalse() {
val project = createProject()
project.extensions.extraProperties.set("edgeToEdgeEnabled", "false")
assertThat(project.isEdgeToEdgeEnabled).isFalse()
}
@Test
fun isEdgeToEdgeEnabled_withEnabledViaProperty_returnsTrue() {
val project = createProject()
project.extensions.extraProperties.set("edgeToEdgeEnabled", "true")
assertThat(project.isEdgeToEdgeEnabled).isTrue()
}
@Test
fun isEdgeToEdgeEnabled_withInvalidViaProperty_returnsFalse() {
val project = createProject()
project.extensions.extraProperties.set("edgeToEdgeEnabled", "¯\\_(ツ)_/¯")
assertThat(project.isEdgeToEdgeEnabled).isFalse()
}
@Test
fun isHermesV1Enabled_returnsFalseByDefault() {
assertThat(createProject().isHermesV1Enabled).isFalse()
}
@Test
fun isHermesV1Enabled_withDisabledViaProperty_returnsFalse() {
val project = createProject()
project.extensions.extraProperties.set("hermesV1Enabled", "false")
assertThat(project.isHermesV1Enabled).isFalse()
}
@Test
fun isHermesV1Enabled_withEnabledViaProperty_returnsTrue() {
val project = createProject()
project.extensions.extraProperties.set("hermesV1Enabled", "true")
assertThat(project.isHermesV1Enabled).isTrue()
}
@Test
fun isHermesV1Enabled_withInvalidViaProperty_returnsFalse() {
val project = createProject()
project.extensions.extraProperties.set("hermesV1Enabled", "¯\\_(ツ)_/¯")
assertThat(project.isHermesV1Enabled).isFalse()
}
@Test
fun needsCodegenFromPackageJson_withCodegenConfigInPackageJson_returnsTrue() {
val project = createProject()
val extension = TestReactExtension(project)
File(tempFolder.root, "package.json").apply {
writeText(
// language=json
"""
{
"name": "a-library",
"codegenConfig": {}
}
"""
.trimIndent()
)
}
extension.root.set(tempFolder.root)
assertThat(project.needsCodegenFromPackageJson(extension.root)).isTrue()
}
@Test
fun needsCodegenFromPackageJson_withMissingCodegenConfigInPackageJson_returnsFalse() {
val project = createProject()
val extension = TestReactExtension(project)
File(tempFolder.root, "package.json").apply {
writeText(
// language=json
"""
{
"name": "a-library"
}
"""
.trimIndent()
)
}
extension.root.set(tempFolder.root)
assertThat(project.needsCodegenFromPackageJson(extension.root)).isFalse()
}
@Test
fun needsCodegenFromPackageJson_withCodegenConfigInModel_returnsTrue() {
val project = createProject()
val model = ModelPackageJson("1000.0.0", ModelCodegenConfig(null, null, null, null, false))
assertThat(project.needsCodegenFromPackageJson(model)).isTrue()
}
@Test
fun needsCodegenFromPackageJson_withMissingCodegenConfigInModel_returnsFalse() {
val project = createProject()
val model = ModelPackageJson("1000.0.0", null)
assertThat(project.needsCodegenFromPackageJson(model)).isFalse()
}
@Test
fun needsCodegenFromPackageJson_withMissingPackageJson_returnsFalse() {
val project = createProject()
val extension = TestReactExtension(project)
assertThat(project.needsCodegenFromPackageJson(extension.root)).isFalse()
}
@Test
fun getReactNativeArchitectures_withMissingProperty_returnsEmptyList() {
val project = createProject()
assertThat(project.getReactNativeArchitectures().isEmpty()).isTrue()
}
@Test
fun getReactNativeArchitectures_withEmptyProperty_returnsEmptyList() {
val project = createProject()
project.extensions.extraProperties.set("reactNativeArchitectures", "")
assertThat(project.getReactNativeArchitectures().isEmpty()).isTrue()
}
@Test
fun getReactNativeArchitectures_withSingleArch_returnsSingleton() {
val project = createProject()
project.extensions.extraProperties.set("reactNativeArchitectures", "x86")
val archs = project.getReactNativeArchitectures()
assertThat(archs.size).isEqualTo(1)
assertThat(archs[0]).isEqualTo("x86")
}
@Test
fun getReactNativeArchitectures_withMultipleArch_returnsList() {
val project = createProject()
project.extensions.extraProperties.set(
"reactNativeArchitectures",
"armeabi-v7a,arm64-v8a,x86,x86_64",
)
val archs = project.getReactNativeArchitectures()
assertThat(archs.size).isEqualTo(4)
assertThat(archs[0]).isEqualTo("armeabi-v7a")
assertThat(archs[1]).isEqualTo("arm64-v8a")
assertThat(archs[2]).isEqualTo("x86")
assertThat(archs[3]).isEqualTo("x86_64")
}
}