From 75bd9f011b7e4183e8175066ebe21848e4d09351 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Fri, 7 Aug 2026 16:55:57 +0500 Subject: [PATCH 1/4] feat: separate library plugin from RNGP --- packages/gradle-plugin/build.gradle.kts | 8 + packages/gradle-plugin/package.json | 3 + .../build.gradle.kts | 42 ++++ .../react/ReactCodegenConfigurator.kt | 158 ++++++++++++++ .../react/ReactLibraryConfigurator.kt | 34 +++ .../utils/LibraryAgpConfiguratorUtils.kt | 58 +++++ .../build.gradle.kts | 1 + .../kotlin/com/facebook/react/ReactPlugin.kt | 202 ++---------------- .../react/utils/AgpConfiguratorUtils.kt | 42 ---- .../com/facebook/react/utils/PathUtils.kt | 43 ---- .../com/facebook/react/ReactPluginTest.kt | 14 ++ .../build.gradle.kts | 70 ++++++ .../com/facebook/react/ReactLibraryPlugin.kt | 42 ++++ .../facebook/react/ReactLibraryPluginTest.kt | 106 +++++++++ packages/gradle-plugin/settings.gradle.kts | 2 + .../gradle-plugin/shared/build.gradle.kts | 2 + .../com/facebook/react/ReactExtension.kt | 0 .../com/facebook/react/ReactPluginIds.kt | 13 ++ .../com/facebook/react/ReactPluginUtils.kt | 58 +++++ .../react/internal/PrivateReactExtension.kt | 0 .../tasks/GenerateCodegenArtifactsTask.kt | 0 .../react/tasks/GenerateCodegenSchemaTask.kt | 0 .../facebook/react/utils/CodegenPathUtils.kt | 54 +++++ .../react/utils/ProjectCodegenUtils.kt | 23 ++ .../com/facebook/react/ReactExtensionTest.kt | 0 .../tasks/GenerateCodegenArtifactsTaskTest.kt | 0 .../tasks/GenerateCodegenSchemaTaskTest.kt | 0 .../com/facebook/react/tests/TaskTestUtils.kt | 28 +++ .../android/build.gradle.kts | 2 +- yarn.lock | 8 +- 30 files changed, 740 insertions(+), 273 deletions(-) create mode 100644 packages/gradle-plugin/react-native-gradle-plugin-shared/build.gradle.kts create mode 100644 packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/ReactCodegenConfigurator.kt create mode 100644 packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/ReactLibraryConfigurator.kt create mode 100644 packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/utils/LibraryAgpConfiguratorUtils.kt create mode 100644 packages/gradle-plugin/react-native-library-plugin/build.gradle.kts create mode 100644 packages/gradle-plugin/react-native-library-plugin/src/main/kotlin/com/facebook/react/ReactLibraryPlugin.kt create mode 100644 packages/gradle-plugin/react-native-library-plugin/src/test/kotlin/com/facebook/react/ReactLibraryPluginTest.kt rename packages/gradle-plugin/{react-native-gradle-plugin => shared}/src/main/kotlin/com/facebook/react/ReactExtension.kt (100%) create mode 100644 packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactPluginIds.kt create mode 100644 packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactPluginUtils.kt rename packages/gradle-plugin/{react-native-gradle-plugin => shared}/src/main/kotlin/com/facebook/react/internal/PrivateReactExtension.kt (100%) rename packages/gradle-plugin/{react-native-gradle-plugin => shared}/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt (100%) rename packages/gradle-plugin/{react-native-gradle-plugin => shared}/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTask.kt (100%) create mode 100644 packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/CodegenPathUtils.kt create mode 100644 packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/ProjectCodegenUtils.kt rename packages/gradle-plugin/{react-native-gradle-plugin => shared}/src/test/kotlin/com/facebook/react/ReactExtensionTest.kt (100%) rename packages/gradle-plugin/{react-native-gradle-plugin => shared}/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTaskTest.kt (100%) rename packages/gradle-plugin/{react-native-gradle-plugin => shared}/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTaskTest.kt (100%) create mode 100644 packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/tests/TaskTestUtils.kt diff --git a/packages/gradle-plugin/build.gradle.kts b/packages/gradle-plugin/build.gradle.kts index 80021f061fb7..116845054dbe 100644 --- a/packages/gradle-plugin/build.gradle.kts +++ b/packages/gradle-plugin/build.gradle.kts @@ -12,6 +12,8 @@ plugins { tasks.register("build") { dependsOn( + ":react-native-gradle-plugin-shared:build", + ":react-native-library-plugin:build", ":react-native-gradle-plugin:build", ":settings-plugin:build", ":shared-testutil:build", @@ -21,6 +23,8 @@ tasks.register("build") { tasks.register("clean") { dependsOn( + ":react-native-gradle-plugin-shared:clean", + ":react-native-library-plugin:clean", ":react-native-gradle-plugin:clean", ":settings-plugin:clean", ":shared-testutil:clean", @@ -30,6 +34,8 @@ tasks.register("clean") { tasks.named("ktfmtCheck") { dependsOn( + ":react-native-gradle-plugin-shared:ktfmtCheck", + ":react-native-library-plugin:ktfmtCheck", ":react-native-gradle-plugin:ktfmtCheck", ":settings-plugin:ktfmtCheck", ":shared-testutil:ktfmtCheck", @@ -39,6 +45,8 @@ tasks.named("ktfmtCheck") { tasks.named("ktfmtFormat") { dependsOn( + ":react-native-gradle-plugin-shared:ktfmtFormat", + ":react-native-library-plugin:ktfmtFormat", ":react-native-gradle-plugin:ktfmtFormat", ":settings-plugin:ktfmtFormat", ":shared-testutil:ktfmtFormat", diff --git a/packages/gradle-plugin/package.json b/packages/gradle-plugin/package.json index 236f12d39865..b6d660709cc5 100644 --- a/packages/gradle-plugin/package.json +++ b/packages/gradle-plugin/package.json @@ -30,6 +30,9 @@ "gradlew", "gradlew.bat", "README.md", + "react-native-gradle-plugin-shared", + "react-native-library-plugin", + "!react-native-library-plugin/src/test", "react-native-gradle-plugin", "!react-native-gradle-plugin/src/test", "settings-plugin", diff --git a/packages/gradle-plugin/react-native-gradle-plugin-shared/build.gradle.kts b/packages/gradle-plugin/react-native-gradle-plugin-shared/build.gradle.kts new file mode 100644 index 000000000000..a5668402e782 --- /dev/null +++ b/packages/gradle-plugin/react-native-gradle-plugin-shared/build.gradle.kts @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +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) +} + +repositories { + google() + mavenCentral() +} + +group = "com.facebook.react" + +dependencies { + implementation(project(":shared")) + implementation(gradleApi()) + implementation(libs.android.gradle.plugin) +} + +java { targetCompatibility = JavaVersion.VERSION_11 } + +kotlin { jvmToolchain(17) } + +tasks.withType().configureEach { + compilerOptions { + apiVersion.set(KotlinVersion.KOTLIN_2_0) + jvmTarget.set(JvmTarget.JVM_11) + allWarningsAsErrors.set( + project.properties["enableWarningsAsErrors"]?.toString()?.toBoolean() ?: false + ) + } +} diff --git a/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/ReactCodegenConfigurator.kt b/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/ReactCodegenConfigurator.kt new file mode 100644 index 000000000000..c35ba61f3c42 --- /dev/null +++ b/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/ReactCodegenConfigurator.kt @@ -0,0 +1,158 @@ +/* + * 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.GenerateCodegenArtifactsTask +import com.facebook.react.tasks.GenerateCodegenSchemaTask +import com.facebook.react.utils.JsonUtils +import com.facebook.react.utils.findPackageJsonFile +import java.io.File +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.api.tasks.TaskProvider + +@Suppress("UnstableApiUsage") +object ReactCodegenConfigurator { + fun configureCodegen( + project: Project, + localExtension: ReactExtension, + rootExtension: PrivateReactExtension, + isLibrary: Boolean, + needsCodegenFromPackageJson: (Project, PrivateReactExtension) -> Boolean, + ) { + val generatedSrcDir: Provider = + project.layout.buildDirectory.dir("generated/source/codegen") + + if (isLibrary) { + localExtension.jsRootDir.convention(project.layout.projectDirectory.dir("../")) + } else { + localExtension.jsRootDir.convention(localExtension.root) + } + + val generateCodegenArtifactsTask = + registerCodegenTasks( + project = project, + rootExtension = rootExtension, + generatedSrcDir = generatedSrcDir, + packageJsonFile = { findPackageJsonFile(project, rootExtension.root) }, + schemaTaskName = "generateCodegenSchemaFromJavaScript", + artifactsTaskName = "generateCodegenArtifactsFromSchema", + configureJsRoot = { task, packageJson -> + val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) } + val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir + + if (packageJson != null && jsSrcsDirInPackageJson != null) { + task.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson)) + } else { + task.jsRootDir.set(localExtension.jsRootDir) + } + }, + configureCodegenArtifacts = { task, _ -> + task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName) + task.libraryName.set(localExtension.libraryName) + }, + onlyIf = { packageJson -> + val needsCodegenFromPackageJson = + needsCodegenFromPackageJson(project, rootExtension) + val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) } + val includesGeneratedCode = + parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false + (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode + }, + ) + + if (isLibrary) { + project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).finalizeDsl { ext + -> + ext.sourceSets + .getByName("main") + .java + .directories + .add(generatedSrcDir.get().dir("java").asFile.path) + } + } else { + project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl { + ext -> + ext.sourceSets + .getByName("main") + .java + .directories + .add(generatedSrcDir.get().dir("java").asFile.path) + } + } + + project.tasks.named("preBuild", Task::class.java).dependsOn(generateCodegenArtifactsTask) + } + + fun registerCodegenTasks( + project: Project, + rootExtension: PrivateReactExtension, + generatedSrcDir: Provider, + packageJsonFile: () -> File?, + schemaTaskName: String, + artifactsTaskName: String, + configureJsRoot: (GenerateCodegenSchemaTask, File?) -> Unit, + configureCodegenArtifacts: (GenerateCodegenArtifactsTask, File?) -> Unit, + onlyIf: (File?) -> Boolean = { true }, + ): TaskProvider { + val generateCodegenSchemaTask = + project.tasks.register( + schemaTaskName, + GenerateCodegenSchemaTask::class.java, + ) { task -> + val packageJson = packageJsonFile() + + task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) + task.codegenDir.set(rootExtension.codegenDir) + task.generatedSrcDir.set(generatedSrcDir) + task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath) + + configureJsRoot(task, packageJson) + + task.jsInputFiles.set( + project.fileTree(task.jsRootDir) { tree -> + tree.include("**/*.js") + tree.include("**/*.jsx") + tree.include("**/*.ts") + tree.include("**/*.tsx") + + tree.exclude("node_modules/**/*") + tree.exclude("**/*.d.ts") + tree.exclude("**/build/**/*") + } + ) + val shouldRunTask = onlyIf(packageJson) + task.onlyIf { shouldRunTask } + } + + return project.tasks.register( + artifactsTaskName, + GenerateCodegenArtifactsTask::class.java, + ) { task -> + val packageJson = packageJsonFile() + + task.dependsOn(generateCodegenSchemaTask) + task.reactNativeDir.set(rootExtension.reactNativeDir) + task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) + task.generatedSrcDir.set(generatedSrcDir) + task.packageJsonFile.set(packageJson) + task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath) + + configureCodegenArtifacts(task, packageJson) + + val shouldRunTask = onlyIf(packageJson) + task.onlyIf { shouldRunTask } + } + } +} diff --git a/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/ReactLibraryConfigurator.kt b/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/ReactLibraryConfigurator.kt new file mode 100644 index 000000000000..716075fcd0ad --- /dev/null +++ b/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/ReactLibraryConfigurator.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react + +import com.facebook.react.internal.PrivateReactExtension +import com.facebook.react.utils.LibraryAgpConfiguratorUtils.configureBuildConfigFieldsForLibraries +import com.facebook.react.utils.LibraryAgpConfiguratorUtils.configureNamespaceForLibraries +import com.facebook.react.utils.ProjectCodegenUtils.needsCodegenFromPackageJson +import org.gradle.api.Project + +object ReactLibraryConfigurator { + fun configure( + project: Project, + extension: ReactExtension, + rootExtension: PrivateReactExtension, + ) { + configureBuildConfigFieldsForLibraries(project) + configureNamespaceForLibraries(project) + ReactCodegenConfigurator.configureCodegen( + project = project, + localExtension = extension, + rootExtension = rootExtension, + isLibrary = true, + needsCodegenFromPackageJson = { currentProject, privateExtension -> + currentProject.needsCodegenFromPackageJson(privateExtension.root) + }, + ) + } +} diff --git a/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/utils/LibraryAgpConfiguratorUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/utils/LibraryAgpConfiguratorUtils.kt new file mode 100644 index 000000000000..13e956f78f9f --- /dev/null +++ b/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/utils/LibraryAgpConfiguratorUtils.kt @@ -0,0 +1,58 @@ +/* + * 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.LibraryAndroidComponentsExtension +import java.io.File +import javax.xml.parsers.DocumentBuilder +import javax.xml.parsers.DocumentBuilderFactory +import org.gradle.api.Project +import org.w3c.dom.Element + +@Suppress("UnstableApiUsage") +object LibraryAgpConfiguratorUtils { + fun configureBuildConfigFieldsForLibraries(project: Project) { + project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).finalizeDsl { ext + -> + ext.buildFeatures.buildConfig = true + } + } + + fun configureNamespaceForLibraries(project: Project) { + project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).finalizeDsl { ext + -> + if (ext.namespace == null) { + val manifestFile = + project.layout.projectDirectory.file("src/main/AndroidManifest.xml").asFile + manifestFile + .takeIf { it.exists() } + ?.let { file -> + getPackageNameFromManifest(file)?.let { packageName -> + ext.namespace = packageName + } + } + } + } + } +} + +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 + } +} diff --git a/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts b/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts index c4f92db2e1a1..0c014114e36c 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts +++ b/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts @@ -37,6 +37,7 @@ gradlePlugin { group = "com.facebook.react" dependencies { + implementation(project(":react-native-gradle-plugin-shared")) implementation(project(":shared")) implementation(gradleApi()) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index f7bd68eeb2e5..f270e3a93aef 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -8,20 +8,16 @@ 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.model.ModelAutolinkingDependenciesJson 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 @@ -31,7 +27,6 @@ import com.facebook.react.utils.JsonUtils import com.facebook.react.utils.NdkConfiguratorUtils.configureReactNativeNdk import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson import com.facebook.react.utils.PropertyUtils -import com.facebook.react.utils.findPackageJsonFile import java.io.File import kotlin.system.exitProcess import org.gradle.api.Plugin @@ -45,17 +40,13 @@ import org.gradle.internal.jvm.Jvm class ReactPlugin : Plugin { 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, - ) + ReactPluginUtils.failIfBothReactPluginsApplied( + project, + currentPluginId = ReactPluginIds.REACT_APP_PLUGIN, + conflictingPluginId = ReactPluginIds.REACT_LIBRARY_PLUGIN, + ) + val extension = ReactPluginUtils.createReactExtension(project) + val rootExtension = ReactPluginUtils.createPrivateReactExtension(project) // Warn users if they still have the hermesV1Enabled property set. if ( @@ -82,10 +73,7 @@ class ReactPlugin : Plugin { 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) + ReactPluginUtils.configurePrivateReactExtensionFromApp(rootExtension, extension) project.afterEvaluate { val reactNativeDir = extension.reactNativeDir.get().asFile @@ -110,16 +98,22 @@ class ReactPlugin : Plugin { } } configureAutolinking(project, extension, rootExtension) - configureCodegen(project, extension, rootExtension, isLibrary = false) + ReactCodegenConfigurator.configureCodegen( + project = project, + localExtension = extension, + rootExtension = rootExtension, + isLibrary = false, + needsCodegenFromPackageJson = { currentProject, privateExtension -> + currentProject.needsCodegenFromPackageJson(privateExtension.root) + }, + ) configureResources(project, extension) configureBuildTypesForApp(project) } // Library Only Configuration project.pluginManager.withPlugin("com.android.library") { - configureBuildConfigFieldsForLibraries(project) - configureNamespaceForLibraries(project) - configureCodegen(project, extension, rootExtension, isLibrary = true) + ReactLibraryConfigurator.configure(project, extension, rootExtension) } } @@ -154,164 +148,6 @@ class ReactPlugin : Plugin { } } - /** 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 = - 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 tasks to produce schema from JS files and generate artifacts from schema. - val generateCodegenArtifactsTask = - registerCodegenTasks( - project = project, - rootExtension = rootExtension, - generatedSrcDir = generatedSrcDir, - packageJsonFile = { findPackageJsonFile(project, rootExtension.root) }, - schemaTaskName = "generateCodegenSchemaFromJavaScript", - artifactsTaskName = "generateCodegenArtifactsFromSchema", - configureJsRoot = { task, packageJson -> - // 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 parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) } - val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir - - if (packageJson != null && jsSrcsDirInPackageJson != null) { - task.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson)) - } else { - task.jsRootDir.set(localExtension.jsRootDir) - } - }, - configureCodegenArtifacts = { task, _ -> - task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName) - task.libraryName.set(localExtension.libraryName) - }, - onlyIf = { packageJson -> - // Please note that needsCodegenFromPackageJson is triggering a read of the - // package.json at configuration time as we need to feed the onlyIf condition of this - // task. Therefore, needsCodegenFromPackageJson needs to be invoked inside this - // lambda. - val needsCodegenFromPackageJson = - project.needsCodegenFromPackageJson(rootExtension.root) - val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) } - val includesGeneratedCode = - parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false - (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode - }, - ) - - // We update the android configuration to include the generated sources. - // This is 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 - .directories - .add(generatedSrcDir.get().dir("java").asFile.path) - } - } else { - project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl { - ext -> - ext.sourceSets - .getByName("main") - .java - .directories - .add(generatedSrcDir.get().dir("java").asFile.path) - } - } - - // `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) - } - - private fun registerCodegenTasks( - project: Project, - rootExtension: PrivateReactExtension, - generatedSrcDir: Provider, - packageJsonFile: () -> File?, - schemaTaskName: String, - artifactsTaskName: String, - configureJsRoot: (GenerateCodegenSchemaTask, File?) -> Unit, - configureCodegenArtifacts: (GenerateCodegenArtifactsTask, File?) -> Unit, - onlyIf: (File?) -> Boolean = { true }, - ): TaskProvider { - // We create the task to produce schema from JS files. - val generateCodegenSchemaTask = - project.tasks.register( - schemaTaskName, - GenerateCodegenSchemaTask::class.java, - ) { task -> - val packageJson = packageJsonFile() - - task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) - task.codegenDir.set(rootExtension.codegenDir) - task.generatedSrcDir.set(generatedSrcDir) - task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath) - - configureJsRoot(task, packageJson) - - task.jsInputFiles.set( - project.fileTree(task.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 avoid picking them up for execution - // avoidance. - tree.exclude("**/build/**/*") - } - ) - val shouldRunTask = onlyIf(packageJson) - task.onlyIf { shouldRunTask } - } - - // We create the task to generate Java code from schema. - return project.tasks.register( - artifactsTaskName, - GenerateCodegenArtifactsTask::class.java, - ) { task -> - val packageJson = packageJsonFile() - - task.dependsOn(generateCodegenSchemaTask) - task.reactNativeDir.set(rootExtension.reactNativeDir) - task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) - task.generatedSrcDir.set(generatedSrcDir) - task.packageJsonFile.set(packageJson) - task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath) - - configureCodegenArtifacts(task, packageJson) - - // The caller decides whether codegen should run. For app/library projects this depends on - // package.json and includesGeneratedCode. Pure C++ dependencies are filtered before task - // registration, so their generated tasks can always run. - val shouldRunTask = onlyIf(packageJson) - task.onlyIf { shouldRunTask } - } - } - /** This function sets up Autolinking for App users */ private fun configureAutolinking( project: Project, @@ -417,7 +253,7 @@ class ReactPlugin : Plugin { val generatedSrcDir = generatedPureCxxSourceDir.map { it.dir(libraryName) } val taskNameSuffix = taskNameSuffixForDependency(dependency) - registerCodegenTasks( + ReactCodegenConfigurator.registerCodegenTasks( project = project, rootExtension = rootExtension, generatedSrcDir = generatedSrcDir, diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt index 8d2b31f13390..efd934dd13bc 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/AgpConfiguratorUtils.kt @@ -8,19 +8,14 @@ package com.facebook.react.utils import com.android.build.api.variant.ApplicationAndroidComponentsExtension -import com.android.build.api.variant.LibraryAndroidComponentsExtension 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 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 { @@ -75,12 +70,6 @@ internal object AgpConfiguratorUtils { project.pluginManager.withPlugin("com.android.library", action) } - fun configureBuildConfigFieldsForLibraries(project: Project) { - project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).finalizeDsl { ext -> - ext.buildFeatures.buildConfig = true - } - } - fun configureDevServerLocation(project: Project) { val devServerIp = project.properties["reactNativeDevServerIp"]?.toString() ?: getHostIpAddress() val devServerPort = @@ -105,41 +94,10 @@ internal object AgpConfiguratorUtils { project.pluginManager.withPlugin("com.android.library", action) } - fun configureNamespaceForLibraries(project: Project) { - project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).finalizeDsl { ext -> - if (ext.namespace == null) { - val manifestFile = - project.layout.projectDirectory.file("src/main/AndroidManifest.xml").asFile - 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() diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt index e1acfc8f62d7..a1909d32a9ca 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt @@ -10,11 +10,9 @@ package com.facebook.react.utils import com.facebook.react.ReactExtension -import com.facebook.react.model.ModelPackageJson 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: @@ -196,47 +194,6 @@ internal fun getHermesOSBin(): String { ) } -internal fun projectPathToLibraryName(projectPath: String): String = - projectPath - .split(':', '-', '_', '.') - .joinToString("") { token -> token.replaceFirstChar { it.titlecase() } } - .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/" diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt index f19b140f4425..4f4746c4b364 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt @@ -11,6 +11,7 @@ import com.facebook.react.model.ModelAutolinkingDependenciesJson import java.io.File import org.assertj.core.api.Assertions.assertThat import org.intellij.lang.annotations.Language +import org.gradle.testfixtures.ProjectBuilder import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -19,6 +20,19 @@ class ReactPluginTest { @get:Rule val tempFolder = TemporaryFolder() + @Test + fun apply_withAndroidLibrary_configuresLegacyLibraryCodegenTasks() { + val project = ProjectBuilder.builder().withProjectDir(tempFolder.newFolder("library")).build() + + project.plugins.apply("com.android.library") + project.plugins.apply("com.facebook.react") + + assertThat(project.extensions.findByType(ReactExtension::class.java)).isNotNull() + assertThat(project.tasks.findByName("generateCodegenSchemaFromJavaScript")).isNotNull() + assertThat(project.tasks.findByName("generateCodegenArtifactsFromSchema")).isNotNull() + assertThat(project.tasks.findByName("createBundleDebugJsAndAssets")).isNull() + } + @Test fun getPureCxxCodegenDependencies_filtersDependenciesCorrectly() { val includesGeneratedCode = createPackage("includes-generated-code", true) diff --git a/packages/gradle-plugin/react-native-library-plugin/build.gradle.kts b/packages/gradle-plugin/react-native-library-plugin/build.gradle.kts new file mode 100644 index 000000000000..9e704ebef3bb --- /dev/null +++ b/packages/gradle-plugin/react-native-library-plugin/build.gradle.kts @@ -0,0 +1,70 @@ +/* + * 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("reactlibrary") { + id = "com.facebook.react.library" + implementationClass = "com.facebook.react.ReactLibraryPlugin" + } + } +} + +group = "com.facebook.react" + +dependencies { + implementation(project(":react-native-gradle-plugin-shared")) + implementation(project(":shared")) + + implementation(gradleApi()) + implementation(libs.kotlin.gradle.plugin) + implementation(libs.android.gradle.plugin) + + testImplementation(libs.junit) + testImplementation(libs.assertj) + testImplementation(project(":react-native-gradle-plugin")) + testImplementation(project(":shared-testutil")) +} + +java { targetCompatibility = JavaVersion.VERSION_11 } + +kotlin { jvmToolchain(17) } + +tasks.withType().configureEach { + compilerOptions { + apiVersion.set(KotlinVersion.KOTLIN_2_0) + jvmTarget.set(JvmTarget.JVM_11) + allWarningsAsErrors.set( + project.properties["enableWarningsAsErrors"]?.toString()?.toBoolean() ?: false + ) + } +} + +tasks.withType().configureEach { + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showExceptions = true + showCauses = true + showStackTraces = true + } +} diff --git a/packages/gradle-plugin/react-native-library-plugin/src/main/kotlin/com/facebook/react/ReactLibraryPlugin.kt b/packages/gradle-plugin/react-native-library-plugin/src/main/kotlin/com/facebook/react/ReactLibraryPlugin.kt new file mode 100644 index 000000000000..c536b5ca8f4a --- /dev/null +++ b/packages/gradle-plugin/react-native-library-plugin/src/main/kotlin/com/facebook/react/ReactLibraryPlugin.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class ReactLibraryPlugin : Plugin { + override fun apply(project: Project) { + ReactPluginUtils.failIfBothReactPluginsApplied( + project, + currentPluginId = ReactPluginIds.REACT_LIBRARY_PLUGIN, + conflictingPluginId = ReactPluginIds.REACT_APP_PLUGIN, + ) + failIfAndroidApplicationPluginApplied(project) + + val extension = ReactPluginUtils.createReactExtension(project) + val rootExtension = ReactPluginUtils.createPrivateReactExtension(project) + + project.pluginManager.withPlugin("com.android.library") { + ReactLibraryConfigurator.configure(project, extension, rootExtension) + } + } + + private fun failIfAndroidApplicationPluginApplied(project: Project) { + fun fail(): Nothing = + error( + "${ReactPluginIds.REACT_LIBRARY_PLUGIN} can only be used with Android library " + + "projects. Use ${ReactPluginIds.REACT_APP_PLUGIN} for Android applications." + ) + + if (project.pluginManager.hasPlugin("com.android.application")) { + fail() + } + project.pluginManager.withPlugin("com.android.application") { fail() } + } +} diff --git a/packages/gradle-plugin/react-native-library-plugin/src/test/kotlin/com/facebook/react/ReactLibraryPluginTest.kt b/packages/gradle-plugin/react-native-library-plugin/src/test/kotlin/com/facebook/react/ReactLibraryPluginTest.kt new file mode 100644 index 000000000000..3059486bc919 --- /dev/null +++ b/packages/gradle-plugin/react-native-library-plugin/src/test/kotlin/com/facebook/react/ReactLibraryPluginTest.kt @@ -0,0 +1,106 @@ +/* + * 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.internal.PrivateReactExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class ReactLibraryPluginTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test + fun apply_createsReactExtensions() { + val project = ProjectBuilder.builder().build() + + project.plugins.apply("com.facebook.react.library") + + assertThat(project.extensions.findByType(ReactExtension::class.java)).isNotNull() + assertThat(project.rootProject.extensions.findByType(PrivateReactExtension::class.java)) + .isNotNull() + } + + @Test + fun apply_afterAndroidLibrary_configuresLibraryCodegenTasks() { + val project = ProjectBuilder.builder().withProjectDir(tempFolder.newFolder("library")).build() + + project.plugins.apply("com.android.library") + project.plugins.apply("com.facebook.react.library") + + assertThat(project.tasks.findByName("generateCodegenSchemaFromJavaScript")).isNotNull() + assertThat(project.tasks.findByName("generateCodegenArtifactsFromSchema")).isNotNull() + } + + @Test + fun apply_beforeAndroidLibrary_configuresLibraryCodegenTasks() { + val project = ProjectBuilder.builder().withProjectDir(tempFolder.newFolder("library")).build() + + project.plugins.apply("com.facebook.react.library") + project.plugins.apply("com.android.library") + + assertThat(project.tasks.findByName("generateCodegenSchemaFromJavaScript")).isNotNull() + assertThat(project.tasks.findByName("generateCodegenArtifactsFromSchema")).isNotNull() + } + + @Test + fun apply_afterReactPlugin_fails() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("com.facebook.react") + + assertThatThrownBy { project.plugins.apply("com.facebook.react.library") } + .hasRootCauseMessage( + "A React Native Android project must not apply both com.facebook.react and " + + "com.facebook.react.library. Use com.facebook.react.library for Android " + + "libraries and com.facebook.react for Android applications." + ) + } + + @Test + fun apply_beforeReactPlugin_failsWhenReactPluginIsApplied() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("com.facebook.react.library") + + assertThatThrownBy { project.plugins.apply("com.facebook.react") } + .hasRootCauseMessage( + "A React Native Android project must not apply both com.facebook.react and " + + "com.facebook.react.library. Use com.facebook.react.library for Android " + + "libraries and com.facebook.react for Android applications." + ) + } + + @Test + fun apply_afterAndroidApplication_fails() { + val project = + ProjectBuilder.builder().withProjectDir(tempFolder.newFolder("application")).build() + project.plugins.apply("com.android.application") + + assertThatThrownBy { project.plugins.apply("com.facebook.react.library") } + .hasRootCauseMessage( + "com.facebook.react.library can only be used with Android library projects. " + + "Use com.facebook.react for Android applications." + ) + } + + @Test + fun apply_beforeAndroidApplication_failsWhenAndroidApplicationIsApplied() { + val project = + ProjectBuilder.builder().withProjectDir(tempFolder.newFolder("application")).build() + project.plugins.apply("com.facebook.react.library") + + assertThatThrownBy { project.plugins.apply("com.android.application") } + .hasRootCauseMessage( + "com.facebook.react.library can only be used with Android library projects. " + + "Use com.facebook.react for Android applications." + ) + } +} diff --git a/packages/gradle-plugin/settings.gradle.kts b/packages/gradle-plugin/settings.gradle.kts index 14f1e9c2063e..ce1eda647ad3 100644 --- a/packages/gradle-plugin/settings.gradle.kts +++ b/packages/gradle-plugin/settings.gradle.kts @@ -16,6 +16,8 @@ pluginManagement { plugins { id("org.gradle.toolchains.foojay-resolver-convention").version("1.0.0") } include( + ":react-native-gradle-plugin-shared", + ":react-native-library-plugin", ":react-native-gradle-plugin", ":settings-plugin", ":shared", diff --git a/packages/gradle-plugin/shared/build.gradle.kts b/packages/gradle-plugin/shared/build.gradle.kts index 9395aab53483..6a5090f9f692 100644 --- a/packages/gradle-plugin/shared/build.gradle.kts +++ b/packages/gradle-plugin/shared/build.gradle.kts @@ -20,6 +20,7 @@ repositories { mavenCentral() } group = "com.facebook.react" dependencies { + implementation(gradleApi()) implementation(libs.gson) implementation(libs.guava) testImplementation(libs.junit) @@ -43,6 +44,7 @@ tasks.withType().configureEach { } tasks.withType().configureEach { + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") testLogging { exceptionFormat = TestExceptionFormat.FULL showExceptions = true diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactExtension.kt similarity index 100% rename from packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt rename to packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactExtension.kt diff --git a/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactPluginIds.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactPluginIds.kt new file mode 100644 index 000000000000..fd357a807ee9 --- /dev/null +++ b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactPluginIds.kt @@ -0,0 +1,13 @@ +/* + * 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 + +object ReactPluginIds { + const val REACT_APP_PLUGIN = "com.facebook.react" + const val REACT_LIBRARY_PLUGIN = "com.facebook.react.library" +} diff --git a/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactPluginUtils.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactPluginUtils.kt new file mode 100644 index 000000000000..157efc9d4167 --- /dev/null +++ b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/ReactPluginUtils.kt @@ -0,0 +1,58 @@ +/* + * 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.internal.PrivateReactExtension +import org.gradle.api.Project + +object ReactPluginUtils { + fun createReactExtension(project: Project): ReactExtension = + project.extensions.findByType(ReactExtension::class.java) + ?: project.extensions.create("react", ReactExtension::class.java, project) + + fun createPrivateReactExtension(project: Project): PrivateReactExtension = + project.rootProject.extensions.findByType(PrivateReactExtension::class.java) + ?: project.rootProject.extensions.create( + "privateReact", + PrivateReactExtension::class.java, + project, + ) + + fun configurePrivateReactExtensionFromApp( + privateExtension: PrivateReactExtension, + reactExtension: ReactExtension, + ) { + privateExtension.root.set(reactExtension.root) + privateExtension.reactNativeDir.set(reactExtension.reactNativeDir) + privateExtension.codegenDir.set(reactExtension.codegenDir) + privateExtension.nodeExecutableAndArgs.set(reactExtension.nodeExecutableAndArgs) + } + + fun failIfBothReactPluginsApplied( + project: Project, + currentPluginId: String, + conflictingPluginId: String, + ) { + fun fail(): Nothing = + error( + "A React Native Android project must not apply both " + + "${ReactPluginIds.REACT_APP_PLUGIN} and ${ReactPluginIds.REACT_LIBRARY_PLUGIN}. " + + "Use ${ReactPluginIds.REACT_LIBRARY_PLUGIN} for Android libraries and " + + "${ReactPluginIds.REACT_APP_PLUGIN} for Android applications." + ) + + if (project.pluginManager.hasPlugin(conflictingPluginId)) { + fail() + } + project.pluginManager.withPlugin(conflictingPluginId) { + if (project.pluginManager.hasPlugin(currentPluginId)) { + fail() + } + } + } +} diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/internal/PrivateReactExtension.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/internal/PrivateReactExtension.kt similarity index 100% rename from packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/internal/PrivateReactExtension.kt rename to packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/internal/PrivateReactExtension.kt diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt similarity index 100% rename from packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt rename to packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTask.kt diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTask.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTask.kt similarity index 100% rename from packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTask.kt rename to packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTask.kt diff --git a/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/CodegenPathUtils.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/CodegenPathUtils.kt new file mode 100644 index 000000000000..8f2f2861f67b --- /dev/null +++ b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/CodegenPathUtils.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.utils + +import com.facebook.react.model.ModelPackageJson +import java.io.File +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty + +fun projectPathToLibraryName(projectPath: String): String = + projectPath + .split(':', '-', '_', '.') + .joinToString("") { token -> token.replaceFirstChar { it.titlecase() } } + .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). + */ +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 otherwise. + * + * Please note that this function accesses the [DirectoryProperty] parameter and calls .get() on it, + * so calling this during apply() of the ReactPlugin is not recommended. It should be invoked inside + * lazy lambdas or at execution time. + */ +fun readPackageJsonFile( + project: Project, + rootProperty: DirectoryProperty, +): ModelPackageJson? { + val packageJson = findPackageJsonFile(project, rootProperty) + return packageJson?.let { JsonUtils.fromPackageJson(it) } +} diff --git a/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/ProjectCodegenUtils.kt b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/ProjectCodegenUtils.kt new file mode 100644 index 000000000000..d31878f6b92d --- /dev/null +++ b/packages/gradle-plugin/shared/src/main/kotlin/com/facebook/react/utils/ProjectCodegenUtils.kt @@ -0,0 +1,23 @@ +/* + * 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.model.ModelPackageJson +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty + +object ProjectCodegenUtils { + fun Project.needsCodegenFromPackageJson(rootProperty: DirectoryProperty): Boolean { + val parsedPackageJson = readPackageJsonFile(this, rootProperty) + return needsCodegenFromPackageJson(parsedPackageJson) + } + + fun Project.needsCodegenFromPackageJson(model: ModelPackageJson?): Boolean { + return model?.codegenConfig != null + } +} diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactExtensionTest.kt b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/ReactExtensionTest.kt similarity index 100% rename from packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactExtensionTest.kt rename to packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/ReactExtensionTest.kt diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTaskTest.kt b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTaskTest.kt similarity index 100% rename from packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTaskTest.kt rename to packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenArtifactsTaskTest.kt diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTaskTest.kt b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTaskTest.kt similarity index 100% rename from packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTaskTest.kt rename to packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/tasks/GenerateCodegenSchemaTaskTest.kt diff --git a/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/tests/TaskTestUtils.kt b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/tests/TaskTestUtils.kt new file mode 100644 index 000000000000..c87092364de3 --- /dev/null +++ b/packages/gradle-plugin/shared/src/test/kotlin/com/facebook/react/tests/TaskTestUtils.kt @@ -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.tests + +import java.io.File +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.testfixtures.ProjectBuilder + +internal fun createProject(projectDir: File? = null): Project = + ProjectBuilder.builder() + .apply { + if (projectDir != null) { + withProjectDir(projectDir) + } + } + .build() + +internal inline fun 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() diff --git a/packages/react-native-popup-menu-android/android/build.gradle.kts b/packages/react-native-popup-menu-android/android/build.gradle.kts index 52b353d89fc1..b7bb6ec6459c 100644 --- a/packages/react-native-popup-menu-android/android/build.gradle.kts +++ b/packages/react-native-popup-menu-android/android/build.gradle.kts @@ -6,7 +6,7 @@ */ plugins { - id("com.facebook.react") + id("com.facebook.react.library") id("com.android.library") } diff --git a/yarn.lock b/yarn.lock index 1a0833da46ce..deb57c95218f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5192,10 +5192,10 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -hermes-compiler@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/hermes-compiler/-/hermes-compiler-0.0.0.tgz#8d9f6a0b2740ce34d71258fec684e7b6bfd97efa" - integrity sha512-boVFutx6ME/Km2mB6vvsQcdnazEYYI/jV1pomx1wcFUG/EVqTkr5CU0CW9bKipOA/8Hyu3NYwW3THg2Q1kNCfA== +hermes-compiler@250829098.0.14: + version "250829098.0.14" + resolved "https://registry.yarnpkg.com/hermes-compiler/-/hermes-compiler-250829098.0.14.tgz#4fbe8c18d277d5b766c4650c7bc4b3dddd6fd9c1" + integrity sha512-5meXwsZxgiqFaJjNzwjzI9IyUkuGGBisu+z9BvQWmGVpjH6nz11hgqkyxe4dl8UAdyIV4lTbz91+Dlnjz0VxqA== hermes-eslint@0.36.1: version "0.36.1" From c1d929a821f1eb795f25289c466f562162b58193 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Tue, 11 Aug 2026 13:41:57 +0500 Subject: [PATCH 2/4] fix: yarn lock --- packages/react-native/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 47d4675facb7..c50f084ac42e 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -175,7 +175,7 @@ "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", - "hermes-compiler": "0.0.0", + "hermes-compiler": "260318099.0.1", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.84.3", diff --git a/yarn.lock b/yarn.lock index deb57c95218f..bce75df254e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5192,10 +5192,10 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -hermes-compiler@250829098.0.14: - version "250829098.0.14" - resolved "https://registry.yarnpkg.com/hermes-compiler/-/hermes-compiler-250829098.0.14.tgz#4fbe8c18d277d5b766c4650c7bc4b3dddd6fd9c1" - integrity sha512-5meXwsZxgiqFaJjNzwjzI9IyUkuGGBisu+z9BvQWmGVpjH6nz11hgqkyxe4dl8UAdyIV4lTbz91+Dlnjz0VxqA== +hermes-compiler@260318099.0.1: + version "260318099.0.1" + resolved "https://registry.yarnpkg.com/hermes-compiler/-/hermes-compiler-260318099.0.1.tgz#bc073e380bfcb9286d1bcbf69e83933a515b7e2d" + integrity sha512-jDXx48/z7ULUr2+sf+A3+3RQRiwcvH4Y+mhlxMdUhNQGOsnQY8eeDgy+8yhKrDHbLvRfW2JwEnDFfmyf96F5SQ== hermes-eslint@0.36.1: version "0.36.1" From 12b87606949b5419025d06fe6e709e0a8fa74dd3 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Wed, 12 Aug 2026 16:22:54 +0500 Subject: [PATCH 3/4] feat: add AGP9 kotlin compat Co-authored-by: Lukmccall --- .../facebook/react/utils/KotlinPluginUtils.kt | 42 +++++++++++++++++++ .../kotlin/com/facebook/react/ReactPlugin.kt | 2 + .../com/facebook/react/ReactPluginTest.kt | 1 + .../com/facebook/react/ReactLibraryPlugin.kt | 2 + .../facebook/react/ReactLibraryPluginTest.kt | 10 +++++ 5 files changed, 57 insertions(+) create mode 100644 packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/utils/KotlinPluginUtils.kt diff --git a/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/utils/KotlinPluginUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/utils/KotlinPluginUtils.kt new file mode 100644 index 000000000000..3fd6248b99b0 --- /dev/null +++ b/packages/gradle-plugin/react-native-gradle-plugin-shared/src/main/kotlin/com/facebook/react/utils/KotlinPluginUtils.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.utils + +import com.android.build.api.variant.AndroidComponentsExtension +import org.gradle.api.Project + +private const val AGP_BUILT_IN_KOTLIN_MAJOR = 9 + +object KotlinPluginUtils { + fun applyKotlinAndroidPluginIfNeeded(project: Project) { + if (!project.hasBuiltInKotlinSupport()) { + // AGP 9 comes with built-in Kotlin support, so applying `kotlin-android` alongside it causes + // both plugins to register the same `kotlin` extension. + project.applyPluginIfNeeded("kotlin-android") + } + } + + /** + * Returns whether the project uses AGP's built-in Kotlin support. It is enabled by default on AGP + * 9 and later, and can be disabled with `android.builtInKotlin=false`. + */ + private fun Project.hasBuiltInKotlinSupport(): Boolean { + val androidComponents = extensions.findByType(AndroidComponentsExtension::class.java) + ?: return false + if (androidComponents.pluginVersion.major < AGP_BUILT_IN_KOTLIN_MAJOR) { + return false + } + return findProperty("android.builtInKotlin")?.toString()?.toBoolean() ?: true + } + + private fun Project.applyPluginIfNeeded(pluginId: String) { + if (!pluginManager.hasPlugin(pluginId)) { + pluginManager.apply(pluginId) + } + } +} diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index f270e3a93aef..eec2ec02568b 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -24,6 +24,7 @@ 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.KotlinPluginUtils.applyKotlinAndroidPluginIfNeeded import com.facebook.react.utils.NdkConfiguratorUtils.configureReactNativeNdk import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson import com.facebook.react.utils.PropertyUtils @@ -113,6 +114,7 @@ class ReactPlugin : Plugin { // Library Only Configuration project.pluginManager.withPlugin("com.android.library") { + applyKotlinAndroidPluginIfNeeded(project) ReactLibraryConfigurator.configure(project, extension, rootExtension) } } diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt index 4f4746c4b364..909f68d1ad0c 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/ReactPluginTest.kt @@ -31,6 +31,7 @@ class ReactPluginTest { assertThat(project.tasks.findByName("generateCodegenSchemaFromJavaScript")).isNotNull() assertThat(project.tasks.findByName("generateCodegenArtifactsFromSchema")).isNotNull() assertThat(project.tasks.findByName("createBundleDebugJsAndAssets")).isNull() + assertThat(project.pluginManager.hasPlugin("kotlin-android")).isFalse() } @Test diff --git a/packages/gradle-plugin/react-native-library-plugin/src/main/kotlin/com/facebook/react/ReactLibraryPlugin.kt b/packages/gradle-plugin/react-native-library-plugin/src/main/kotlin/com/facebook/react/ReactLibraryPlugin.kt index c536b5ca8f4a..c303d87d2f61 100644 --- a/packages/gradle-plugin/react-native-library-plugin/src/main/kotlin/com/facebook/react/ReactLibraryPlugin.kt +++ b/packages/gradle-plugin/react-native-library-plugin/src/main/kotlin/com/facebook/react/ReactLibraryPlugin.kt @@ -7,6 +7,7 @@ package com.facebook.react +import com.facebook.react.utils.KotlinPluginUtils.applyKotlinAndroidPluginIfNeeded import org.gradle.api.Plugin import org.gradle.api.Project @@ -23,6 +24,7 @@ class ReactLibraryPlugin : Plugin { val rootExtension = ReactPluginUtils.createPrivateReactExtension(project) project.pluginManager.withPlugin("com.android.library") { + applyKotlinAndroidPluginIfNeeded(project) ReactLibraryConfigurator.configure(project, extension, rootExtension) } } diff --git a/packages/gradle-plugin/react-native-library-plugin/src/test/kotlin/com/facebook/react/ReactLibraryPluginTest.kt b/packages/gradle-plugin/react-native-library-plugin/src/test/kotlin/com/facebook/react/ReactLibraryPluginTest.kt index 3059486bc919..a182bcf515ac 100644 --- a/packages/gradle-plugin/react-native-library-plugin/src/test/kotlin/com/facebook/react/ReactLibraryPluginTest.kt +++ b/packages/gradle-plugin/react-native-library-plugin/src/test/kotlin/com/facebook/react/ReactLibraryPluginTest.kt @@ -52,6 +52,16 @@ class ReactLibraryPluginTest { assertThat(project.tasks.findByName("generateCodegenArtifactsFromSchema")).isNotNull() } + @Test + fun apply_withBuiltInKotlinSupport_doesNotApplyKotlinAndroidPlugin() { + val project = ProjectBuilder.builder().withProjectDir(tempFolder.newFolder("library")).build() + + project.plugins.apply("com.android.library") + project.plugins.apply("com.facebook.react.library") + + assertThat(project.pluginManager.hasPlugin("kotlin-android")).isFalse() + } + @Test fun apply_afterReactPlugin_fails() { val project = ProjectBuilder.builder().build() From 9159f843c6bb139cd8eb828c44e62950354ca37a Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Wed, 12 Aug 2026 16:39:08 +0500 Subject: [PATCH 4/4] feat: add deprecation notice --- .../src/main/kotlin/com/facebook/react/ReactPlugin.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index eec2ec02568b..cb47689e65eb 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -114,6 +114,11 @@ class ReactPlugin : Plugin { // Library Only Configuration project.pluginManager.withPlugin("com.android.library") { + project.logger.warn( + "WARNING: Using ${ReactPluginIds.REACT_APP_PLUGIN} with Android library project " + + "'${project.name}' is deprecated and will be removed in a future release. " + + "Please use the ${ReactPluginIds.REACT_LIBRARY_PLUGIN} plugin instead." + ) applyKotlinAndroidPluginIfNeeded(project) ReactLibraryConfigurator.configure(project, extension, rootExtension) }