From 0487108461010154cf959bb0bf6ba9e82fdcc4d1 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 10 Feb 2023 10:04:55 -0800 Subject: [PATCH] RNGP - Monorepo: Make sure libraries are honoring codegenDir provided by app (#36128) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36128 This commit fixes a problem which is making harder to use the New Architecture in monorepos. Specifically if a user specifies a `codegenDir` in their app, libraries should honor it. This is not the case today. The fix is to register an extension on the root project which will "pass" values from app to libraries. I've also cleaned up some of the logic in `readPackageJsonFile` function restricting the access to those functions only to `.root` which is the only field they're accessing. Fixes #35495 Changelog: [Android] [Fixed] - Better Monorepo support for New Architecture Reviewed By: cipolleschi Differential Revision: D43186767 fbshipit-source-id: 5c5ca39397306120b6b6622cb728633bd331e021 --- ReactAndroid/build.gradle | 5 -- .../kotlin/com/facebook/react/ReactPlugin.kt | 63 ++++++++++++------- .../react/internal/PrivateReactExtension.kt | 35 +++++++++++ .../com/facebook/react/utils/PathUtils.kt | 18 +++--- .../com/facebook/react/utils/ProjectUtils.kt | 6 +- .../com/facebook/react/utils/PathUtilsTest.kt | 10 +-- .../facebook/react/utils/ProjectUtilsTest.kt | 6 +- 7 files changed, 98 insertions(+), 45 deletions(-) create mode 100644 packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/internal/PrivateReactExtension.kt diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index 81f0e0e1cb..9ef5b784dc 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -667,12 +667,7 @@ react { // TODO: The library name is chosen for parity with Fabric components & iOS // This should be changed to a more generic name, e.g. `ReactCoreSpec`. libraryName = "rncore" - root = file("..") jsRootDir = file("../Libraries") - reactNativeDir = file("$projectDir/..") - // We search for the codegen in either one of the `node_modules` folder or in the - // root packages folder (that's for when we build from source without calling `yarn install`). - codegenDir = file(findNodeModulePath(projectDir, "@react-native/codegen") ?: "../packages/react-native-codegen/") } apply plugin: "org.jetbrains.kotlin.android" diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index 8aa1c5bbdb..4c8a6ff999 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -9,6 +9,7 @@ package com.facebook.react import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.internal.tasks.factory.dependsOn +import com.facebook.react.internal.PrivateReactExtension import com.facebook.react.tasks.BuildCodegenCLITask import com.facebook.react.tasks.GenerateCodegenArtifactsTask import com.facebook.react.tasks.GenerateCodegenSchemaTask @@ -34,8 +35,22 @@ class ReactPlugin : Plugin { 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) + // 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") @@ -54,12 +69,12 @@ class ReactPlugin : Plugin { project.configureReactTasks(variant = variant, config = extension) } } - configureCodegen(project, extension, isLibrary = false) + configureCodegen(project, extension, rootExtension, isLibrary = false) } // Library Only Configuration project.pluginManager.withPlugin("com.android.library") { - configureCodegen(project, extension, isLibrary = true) + configureCodegen(project, extension, rootExtension, isLibrary = true) } } @@ -82,12 +97,14 @@ class ReactPlugin : Plugin { } } - /** - * A plugin to enable react-native-codegen in Gradle environment. See the Gradle API docs for more - * information: https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html - */ + /** This function sets up `react-native-codegen` in our Gradle plugin. */ @Suppress("UnstableApiUsage") - private fun configureCodegen(project: Project, extension: ReactExtension, isLibrary: Boolean) { + private fun configureCodegen( + project: Project, + localExtension: ReactExtension, + rootExtension: PrivateReactExtension, + isLibrary: Boolean + ) { // First, we set up the output dir for the codegen. val generatedSrcDir = File(project.buildDir, "generated/source/codegen") @@ -95,21 +112,21 @@ class ReactPlugin : Plugin { // 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) { - extension.jsRootDir.convention(project.layout.projectDirectory.dir("../")) + localExtension.jsRootDir.convention(project.layout.projectDirectory.dir("../")) } else { - extension.jsRootDir.convention(extension.root) + localExtension.jsRootDir.convention(localExtension.root) } val buildCodegenTask = project.tasks.register("buildCodegenCLI", BuildCodegenCLITask::class.java) { - it.codegenDir.set(extension.codegenDir) + it.codegenDir.set(rootExtension.codegenDir) val bashWindowsHome = project.findProperty("REACT_WINDOWS_BASH") as String? it.bashWindowsHome.set(bashWindowsHome) // 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(extension) + val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root) it.onlyIf { isLibrary || needsCodegenFromPackageJson } } @@ -118,23 +135,24 @@ class ReactPlugin : Plugin { project.tasks.register( "generateCodegenSchemaFromJavaScript", GenerateCodegenSchemaTask::class.java) { it -> it.dependsOn(buildCodegenTask) - it.nodeExecutableAndArgs.set(extension.nodeExecutableAndArgs) - it.codegenDir.set(extension.codegenDir) + it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) + it.codegenDir.set(rootExtension.codegenDir) it.generatedSrcDir.set(generatedSrcDir) // 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, extension) + val packageJson = findPackageJsonFile(project, rootExtension.root) val parsedPackageJson = packageJson?.let { JsonUtils.fromCodegenJson(it) } val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir if (jsSrcsDirInPackageJson != null) { it.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson)) } else { - it.jsRootDir.set(extension.jsRootDir) + it.jsRootDir.set(localExtension.jsRootDir) } - val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(extension) + val needsCodegenFromPackageJson = + project.needsCodegenFromPackageJson(rootExtension.root) it.onlyIf { isLibrary || needsCodegenFromPackageJson } } @@ -143,17 +161,18 @@ class ReactPlugin : Plugin { project.tasks.register( "generateCodegenArtifactsFromSchema", GenerateCodegenArtifactsTask::class.java) { it.dependsOn(generateCodegenSchemaTask) - it.reactNativeDir.set(extension.reactNativeDir) - it.nodeExecutableAndArgs.set(extension.nodeExecutableAndArgs) + it.reactNativeDir.set(rootExtension.reactNativeDir) + it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs) it.generatedSrcDir.set(generatedSrcDir) - it.packageJsonFile.set(findPackageJsonFile(project, extension)) - it.codegenJavaPackageName.set(extension.codegenJavaPackageName) - it.libraryName.set(extension.libraryName) + it.packageJsonFile.set(findPackageJsonFile(project, rootExtension.root)) + it.codegenJavaPackageName.set(localExtension.codegenJavaPackageName) + it.libraryName.set(localExtension.libraryName) // 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(extension) + val needsCodegenFromPackageJson = + project.needsCodegenFromPackageJson(rootExtension.root) it.onlyIf { isLibrary || needsCodegenFromPackageJson } } diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/internal/PrivateReactExtension.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/internal/PrivateReactExtension.kt new file mode 100644 index 0000000000..e0de3b1ed2 --- /dev/null +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/internal/PrivateReactExtension.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +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 + +/** + * 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() + + val reactNativeDir: DirectoryProperty = objects.directoryProperty() + + val nodeExecutableAndArgs: ListProperty = objects.listProperty(String::class.java) + + val codegenDir: DirectoryProperty = objects.directoryProperty() +} diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt index 0710f04b99..2d94bf72a0 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt @@ -14,6 +14,7 @@ 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: @@ -189,13 +190,13 @@ internal fun projectPathToLibraryName(projectPath: String): String = * 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, extension: ReactExtension): File? { +internal fun findPackageJsonFile(project: Project, rootProperty: DirectoryProperty): File? { val inParent = project.file("../package.json") if (inParent.exists()) { return inParent } - val fromExtension = extension.root.file("package.json").orNull?.asFile + val fromExtension = rootProperty.file("package.json").orNull?.asFile if (fromExtension?.exists() == true) { return fromExtension } @@ -207,12 +208,15 @@ internal fun findPackageJsonFile(project: Project, extension: ReactExtension): F * 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 [ReactExtension] field properties 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. + * 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, extension: ReactExtension): ModelPackageJson? { - val packageJson = findPackageJsonFile(project, extension) +internal fun readPackageJsonFile( + project: Project, + rootProperty: DirectoryProperty +): ModelPackageJson? { + val packageJson = findPackageJsonFile(project, rootProperty) return packageJson?.let { JsonUtils.fromCodegenJson(it) } } diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/ProjectUtils.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/ProjectUtils.kt index ba490af271..e941da78b5 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/ProjectUtils.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/ProjectUtils.kt @@ -7,9 +7,9 @@ package com.facebook.react.utils -import com.facebook.react.ReactExtension import com.facebook.react.model.ModelPackageJson import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty internal object ProjectUtils { internal val Project.isNewArchEnabled: Boolean @@ -35,8 +35,8 @@ internal object ProjectUtils { HERMES_FALLBACK } - internal fun Project.needsCodegenFromPackageJson(extension: ReactExtension): Boolean { - val parsedPackageJson = readPackageJsonFile(this, extension) + internal fun Project.needsCodegenFromPackageJson(rootProperty: DirectoryProperty): Boolean { + val parsedPackageJson = readPackageJsonFile(this, rootProperty) return needsCodegenFromPackageJson(parsedPackageJson) } diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/PathUtilsTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/PathUtilsTest.kt index 7871122498..1239f372f4 100644 --- a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/PathUtilsTest.kt +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/PathUtilsTest.kt @@ -231,7 +231,7 @@ class PathUtilsTest { project.plugins.apply("com.facebook.react") val extension = project.extensions.getByType(ReactExtension::class.java) - assertEquals(project.file("../package.json"), findPackageJsonFile(project, extension)) + assertEquals(project.file("../package.json"), findPackageJsonFile(project, extension.root)) } @Test @@ -245,7 +245,7 @@ class PathUtilsTest { val extension = project.extensions.getByType(ReactExtension::class.java).apply { root.set(moduleFolder) } - assertEquals(localFile, findPackageJsonFile(project, extension)) + assertEquals(localFile, findPackageJsonFile(project, extension.root)) } @Test @@ -257,7 +257,7 @@ class PathUtilsTest { val extension = project.extensions.getByType(ReactExtension::class.java).apply { root.set(moduleFolder) } - val actual = readPackageJsonFile(project, extension) + val actual = readPackageJsonFile(project, extension.root) assertNull(actual) } @@ -272,7 +272,7 @@ class PathUtilsTest { val extension = project.extensions.getByType(ReactExtension::class.java).apply { root.set(moduleFolder) } - val actual = readPackageJsonFile(project, extension) + val actual = readPackageJsonFile(project, extension.root) assertNotNull(actual) assertNull(actual!!.codegenConfig) @@ -298,7 +298,7 @@ class PathUtilsTest { val extension = project.extensions.getByType(ReactExtension::class.java).apply { root.set(moduleFolder) } - val actual = readPackageJsonFile(project, extension) + val actual = readPackageJsonFile(project, extension.root) assertNotNull(actual) assertNotNull(actual!!.codegenConfig) diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/ProjectUtilsTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/ProjectUtilsTest.kt index 164088ef13..70ea15401c 100644 --- a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/ProjectUtilsTest.kt +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/ProjectUtilsTest.kt @@ -125,7 +125,7 @@ class ProjectUtilsTest { .trimIndent()) } extension.root.set(tempFolder.root) - assertTrue(project.needsCodegenFromPackageJson(extension)) + assertTrue(project.needsCodegenFromPackageJson(extension.root)) } @Test @@ -143,7 +143,7 @@ class ProjectUtilsTest { .trimIndent()) } extension.root.set(tempFolder.root) - assertFalse(project.needsCodegenFromPackageJson(extension)) + assertFalse(project.needsCodegenFromPackageJson(extension.root)) } @Test @@ -167,7 +167,7 @@ class ProjectUtilsTest { val project = createProject() val extension = TestReactExtension(project) - assertFalse(project.needsCodegenFromPackageJson(extension)) + assertFalse(project.needsCodegenFromPackageJson(extension.root)) } @Test