Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/32214

This Diff updates part of the Gradle plugin to use the Gradle Lazy Api (mostly `Property` classes).
This will defer the time when those value are accessed from configuration to execution phase.

So far I've converted the `Extension` class that should be our primary entry point to the public API.
I haven't converted the tasks as well, therefore we have several `.get()` calls around now.
I'll take care of them once I go over the Tasks file as well.

Moreover, I added some documentation to the Extention properties, as those will show up in the `build.gradle`
autocompletion for our users.

On the API point of view, this is going to be a breaking change for users that are testing the Gradle plugin AND
are on Gradle Kotlin DSL (which I believe are really limited so I don't think is a problem at the moment). Users
relying on `react.gradle` are unaffected.

Changelog:
[Internal] [Changed] - Use Gradle Lazy API

Reviewed By: ShikaSD

Differential Revision: D30902517

fbshipit-source-id: 5af4625e901b82f4b1c65bd631aa4bb9b505b2d0
This commit is contained in:
Nicola Corti 2021-09-15 05:28:10 -07:00 коммит произвёл Facebook GitHub Bot
Родитель c8873fc43a
Коммит 578cba2338
6 изменённых файлов: 207 добавлений и 74 удалений

Просмотреть файл

@ -9,29 +9,147 @@ package com.facebook.react
import com.android.build.gradle.api.BaseVariant
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.MapProperty
import org.gradle.api.provider.Property
open class ReactAppExtension(private val project: Project) {
var applyAppPlugin: Boolean = false
var composeSourceMapsPath: String = "node_modules/react-native/scripts/compose-source-maps.js"
var bundleAssetName: String = "index.android.bundle"
var entryFile: File? = null
var bundleCommand: String = "bundle"
var reactRoot: File = File(project.projectDir, "../../")
var inputExcludes: List<String> = listOf("android/**", "ios/**")
var bundleConfig: String? = null
var enableVmCleanup: Boolean = true
var hermesCommand: String = "node_modules/hermes-engine/%OS-BIN%/hermesc"
var cliPath: String? = null
var nodeExecutableAndArgs: List<String> = listOf("node")
var enableHermes: Boolean = false
var enableHermesForVariant: (BaseVariant) -> Boolean = { enableHermes }
var devDisabledInVariants: List<String> = emptyList()
abstract class ReactAppExtension @Inject constructor(project: Project) {
private val objects = project.objects
/**
* Whether the React App plugin should apply its logic or not. Set it to false if you're still
* relying on `react.gradle` to configure your build. Default: false
*/
val applyAppPlugin: Property<Boolean> = objects.property(Boolean::class.java).convention(false)
/**
* The path to the react root folder. This is the path to the root folder where the `node_modules`
* folder is present. All the CLI commands will be invoked from this folder as working directory.
*
* Default: $projectDir/../../
*/
val reactRoot: DirectoryProperty =
objects.directoryProperty().convention(project.layout.projectDirectory.dir("../../"))
/**
* 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 path to the React Native CLI. If not specified, the plugin will try to resolve it looking
* for `react-native` CLI inside `node_modules` in [reactRoot].
*/
val cliPath: Property<String> = objects.property(String::class.java)
/**
* 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 [reactRoot].
*/
val bundleCommand: Property<String> = objects.property(String::class.java).convention("bundle")
/**
* Custom configuration for the [bundleCommand]. If provided it will be passed over with a
* `--config` flag to the bundle command.
*/
val bundleConfig: Property<String> = objects.property(String::class.java)
/**
* 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")
/**
* Variant Name to File destination map that allows to specify where is the resource dir for a
* specific variant. If a value is supplied, the plugin will copy the bundled resource for that
* variant from `generated/res/react/<variant>` into the custom specified location. Default: {}
*/
val resourcesDir: MapProperty<String, File> =
objects.mapProperty(String::class.java, File::class.java).convention(emptyMap())
/**
* Variant Name to File destination map that allows to specify where is the asset dir for a
* specific variant. If a value is supplied, the plugin will copy the bundled JS for that variant
* from `generated/assets/react/<variant>` into the custom specified location. Default: {}
*/
val jsBundleDir: MapProperty<String, File> =
objects.mapProperty(String::class.java, File::class.java).convention(emptyMap())
/** ANT-style excludes for the bundle command. Default: ["android / **", "ios / **"] */
val inputExcludes: ListProperty<String> =
objects.listProperty(String::class.java).convention(listOf("android/**", "ios/**"))
/**
* Toggles the VM Cleanup step. If enabled, before the bundle task we will clean up all the
* unnecessary files. If disabled, the developers will have to manually cleanup the files.
* Default: true
*/
val enableVmCleanup: 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 disable dev mode for certain variants. That's useful if you have a production variant
* (say `canary`) where you don't want dev mode to be enabled. Default: []
*/
val devDisabledInVariants: ListProperty<String> =
objects.listProperty(String::class.java).convention(emptyList())
/**
* Variant Name to Boolean map that allows to toggle the bundle command for a specific variant.
* Default: {}
*/
// todo maybe lambda as for hermes?
var bundleIn: Map<String, Boolean> = emptyMap()
var extraPackagerArgs: List<String> = emptyList()
var hermesFlagsDebug: List<String> = emptyList()
var hermesFlagsRelease: List<String> = listOf("-O", "-output-source-map")
var resourcesDir: Map<String, File> = emptyMap()
var jsBundleDir: Map<String, File> = emptyMap()
val bundleIn: MapProperty<String, Boolean> =
objects.mapProperty(String::class.java, Boolean::class.java).convention(emptyMap())
/** Hermes Config */
/** The command to use to invoke hermes. Default is `hermesc` for the correct OS. */
val hermesCommand: Property<String> =
objects.property(String::class.java).convention("node_modules/hermes-engine/%OS-BIN%/hermesc")
/** Toggle Hermes for the whole build. Default: false */
val enableHermes: Property<Boolean> = objects.property(Boolean::class.java).convention(false)
/**
* Functional interface to selectively enabled Hermes only on specific [BaseVariant] Default: will
* return [enableHermes] for all the variants.
*/
var enableHermesForVariant: (BaseVariant) -> Boolean = { enableHermes.get() }
/** Flags to pass to Hermes for Debug variants. Default: [] */
val hermesFlagsDebug: ListProperty<String> =
objects.listProperty(String::class.java).convention(emptyList())
/** Flags to pass to Hermes for Release variants. Default: ["-O", "-output-source-map"] */
val hermesFlagsRelease: ListProperty<String> =
objects.listProperty(String::class.java).convention(listOf("-O", "-output-source-map"))
/**
* The path to the Compose Source Map script. Default:
* "node_modules/react-native/scripts/compose-source-maps.js"
*/
val composeSourceMapsPath: Property<String> =
objects
.property(String::class.java)
.convention("node_modules/react-native/scripts/compose-source-maps.js")
}

Просмотреть файл

@ -23,7 +23,7 @@ class ReactAppPlugin : Plugin<Project> {
private fun applyAppPlugin(project: Project) {
val config = project.extensions.create("reactApp", ReactAppExtension::class.java, project)
if (config.applyAppPlugin) {
if (config.applyAppPlugin.getOrElse(false)) {
project.afterEvaluate {
val androidConfiguration = project.extensions.getByType(BaseExtension::class.java)
project.configureDevPorts(androidConfiguration)

Просмотреть файл

@ -32,17 +32,16 @@ internal fun Project.configureReactTasks(variant: BaseVariant, config: ReactAppE
val jsBundleDir = File(buildDir, "generated/assets/react/$targetPath")
val resourcesDir = File(buildDir, "generated/res/react/$targetPath")
val jsBundleFile = File(jsBundleDir, config.bundleAssetName)
val bundleAssetName = config.bundleAssetName.get()
val jsBundleFile = File(jsBundleDir, bundleAssetName)
val jsSourceMapsDir = File(buildDir, "generated/sourcemaps/react/$targetPath")
val jsIntermediateSourceMapsDir = File(buildDir, "intermediates/sourcemaps/react/$targetPath")
val jsPackagerSourceMapFile =
File(jsIntermediateSourceMapsDir, "${config.bundleAssetName}.packager.map")
val jsCompilerSourceMapFile =
File(jsIntermediateSourceMapsDir, "${config.bundleAssetName}.compiler.map")
val jsOutputSourceMapFile = File(jsSourceMapsDir, "${config.bundleAssetName}.map")
val jsPackagerSourceMapFile = File(jsIntermediateSourceMapsDir, "${bundleAssetName}.packager.map")
val jsCompilerSourceMapFile = File(jsIntermediateSourceMapsDir, "${bundleAssetName}.compiler.map")
val jsOutputSourceMapFile = File(jsSourceMapsDir, "${bundleAssetName}.map")
// Additional node and packager commandline arguments
val nodeExecutableAndArgs = config.nodeExecutableAndArgs
val nodeExecutableAndArgs = config.nodeExecutableAndArgs.get()
val cliPath = detectedCliPath(project.projectDir, config)
val execCommand = nodeExecutableAndArgs + cliPath
@ -54,19 +53,21 @@ internal fun Project.configureReactTasks(variant: BaseVariant, config: ReactAppE
it.group = REACT_GROUP
it.description = "create JS bundle and assets for $targetName."
it.reactRoot = config.reactRoot
it.reactRoot = config.reactRoot.get().asFile
it.sources =
fileTree(config.reactRoot) { fileTree -> fileTree.setExcludes(config.inputExcludes) }
fileTree(config.reactRoot) { fileTree ->
fileTree.setExcludes(config.inputExcludes.get())
}
it.execCommand = execCommand
it.bundleCommand = config.bundleCommand
it.devEnabled = !(variant.name in config.devDisabledInVariants || isRelease)
it.bundleCommand = config.bundleCommand.get()
it.devEnabled = !(variant.name in config.devDisabledInVariants.get() || isRelease)
it.entryFile = detectedEntryFile(config)
val extraArgs = mutableListOf<String>()
if (config.bundleConfig != null) {
if (config.bundleConfig.isPresent) {
extraArgs.add("--config")
extraArgs.add(config.bundleConfig.orEmpty())
extraArgs.add(config.bundleConfig.get())
}
// Hermes doesn't require JS minification.
@ -75,7 +76,7 @@ internal fun Project.configureReactTasks(variant: BaseVariant, config: ReactAppE
extraArgs.add("false")
}
extraArgs.addAll(config.extraPackagerArgs)
extraArgs.addAll(config.extraPackagerArgs.get())
it.extraArgs = extraArgs
@ -94,11 +95,12 @@ internal fun Project.configureReactTasks(variant: BaseVariant, config: ReactAppE
it.group = REACT_GROUP
it.description = "bundle hermes resources for $targetName"
it.reactRoot = config.reactRoot
it.reactRoot = config.reactRoot.get().asFile
it.hermesCommand = detectedHermesCommand(config)
it.hermesFlags = if (isRelease) config.hermesFlagsRelease else config.hermesFlagsDebug
it.hermesFlags =
if (isRelease) config.hermesFlagsRelease.get() else config.hermesFlagsDebug.get()
it.jsBundleFile = jsBundleFile
it.composeSourceMapsCommand = nodeExecutableAndArgs + config.composeSourceMapsPath
it.composeSourceMapsCommand = nodeExecutableAndArgs + config.composeSourceMapsPath.get()
it.jsPackagerSourceMapFile = jsPackagerSourceMapFile
it.jsCompilerSourceMapFile = jsCompilerSourceMapFile
it.jsOutputSourceMapFile = jsOutputSourceMapFile
@ -136,15 +138,15 @@ internal fun Project.configureReactTasks(variant: BaseVariant, config: ReactAppE
val mergeAssetsTask = variant.mergeAssetsProvider
val preBundleTask = tasks.named("build${targetName}PreBundle")
val resourcesDirConfigValue = config.resourcesDir[variant.name]
if (resourcesDirConfigValue != null) {
val resourcesDirConfigValue = config.resourcesDir.getting(variant.name)
if (resourcesDirConfigValue.isPresent) {
val currentCopyResTask =
tasks.register("copy${targetName}BundledResources", Copy::class.java) {
it.group = "react"
it.description = "copy bundled resources into custom location for $targetName."
it.from(resourcesDir)
it.into(file(resourcesDirConfigValue))
it.into(file(resourcesDirConfigValue.get()))
it.dependsOn(bundleTask)
@ -156,7 +158,7 @@ internal fun Project.configureReactTasks(variant: BaseVariant, config: ReactAppE
}
packageTask.configure {
if (config.enableVmCleanup) {
if (config.enableVmCleanup.get()) {
it.doFirst { cleanupVMFiles(enableHermes, isRelease, targetPath) }
}
}
@ -168,9 +170,9 @@ internal fun Project.configureReactTasks(variant: BaseVariant, config: ReactAppE
it.from(jsBundleDir)
val jsBundleDirConfigValue = config.jsBundleDir[targetName]
if (jsBundleDirConfigValue != null) {
it.into(jsBundleDirConfigValue)
val jsBundleDirConfigValue = config.jsBundleDir.getting(targetName)
if (jsBundleDirConfigValue.isPresent) {
it.into(jsBundleDirConfigValue.get())
} else {
it.into(mergeAssetsTask.map { mergeFoldersTask -> mergeFoldersTask.outputDir.get() })
}
@ -225,12 +227,12 @@ private fun Project.cleanupVMFiles(enableHermes: Boolean, isRelease: Boolean, ta
}
private fun BaseVariant.checkBundleEnabled(config: ReactAppExtension): Boolean {
if (name in config.bundleIn) {
return config.bundleIn.getValue(name)
if (config.bundleIn.getting(name).isPresent) {
return config.bundleIn.getting(name).get()
}
if (buildType.name in config.bundleIn) {
return config.bundleIn.getValue(buildType.name)
if (config.bundleIn.getting(buildType.name).isPresent) {
return config.bundleIn.getting(buildType.name).get()
}
return isRelease

Просмотреть файл

@ -21,7 +21,8 @@ import org.apache.tools.ant.taskdefs.condition.Os
* @param config The [ReactAppExtension] configured for this project
*/
internal fun detectedEntryFile(config: ReactAppExtension): File =
detectEntryFile(entryFile = config.entryFile, reactRoot = config.reactRoot)
detectEntryFile(
entryFile = config.entryFile.orNull?.asFile, reactRoot = config.reactRoot.get().asFile)
/**
* Computes the CLI location for React Native. The Algo follows this order:
@ -36,8 +37,8 @@ internal fun detectedCliPath(
): String =
detectCliPath(
projectDir = projectDir,
reactRoot = config.reactRoot,
preconfiguredCliPath = config.cliPath)
reactRoot = config.reactRoot.get().asFile,
preconfiguredCliPath = config.cliPath.orNull)
/**
* Computes the `hermesc` command location. The Algo follows this order:
@ -47,7 +48,7 @@ internal fun detectedCliPath(
* 3. Fails otherwise
*/
internal fun detectedHermesCommand(config: ReactAppExtension): String =
detectOSAwareHermesCommand(config.hermesCommand)
detectOSAwareHermesCommand(config.hermesCommand.get())
private fun detectEntryFile(entryFile: File?, reactRoot: File): File =
when {

Просмотреть файл

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its 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 TestReactAppExtension(project: Project) : ReactAppExtension(project)

Просмотреть файл

@ -7,7 +7,7 @@
package com.facebook.react.tests
import com.facebook.react.ReactAppExtension
import com.facebook.react.TestReactAppExtension
import com.facebook.react.utils.detectedCliPath
import com.facebook.react.utils.detectedEntryFile
import com.facebook.react.utils.detectedHermesCommand
@ -24,9 +24,9 @@ class PathUtilsTest {
@Test
fun detectedEntryFile_withProvidedVariable() {
val extension = ReactAppExtension(ProjectBuilder.builder().build())
val extension = TestReactAppExtension(ProjectBuilder.builder().build())
val expected = tempFolder.newFile("fake.index.js")
extension.entryFile = expected
extension.entryFile.set(expected)
val actual = detectedEntryFile(extension)
@ -35,8 +35,8 @@ class PathUtilsTest {
@Test
fun detectedEntryFile_withAndroidEntryPoint() {
val extension = ReactAppExtension(ProjectBuilder.builder().build())
extension.reactRoot = tempFolder.root
val extension = TestReactAppExtension(ProjectBuilder.builder().build())
extension.reactRoot.set(tempFolder.root)
tempFolder.newFile("index.android.js")
val actual = detectedEntryFile(extension)
@ -46,8 +46,8 @@ class PathUtilsTest {
@Test
fun detectedEntryFile_withDefaultEntryPoint() {
val extension = ReactAppExtension(ProjectBuilder.builder().build())
extension.reactRoot = tempFolder.root
val extension = TestReactAppExtension(ProjectBuilder.builder().build())
extension.reactRoot.set(tempFolder.root)
val actual = detectedEntryFile(extension)
@ -57,22 +57,22 @@ class PathUtilsTest {
@Test
fun detectedCliPath_withCliPathFromExtension() {
val project = ProjectBuilder.builder().build()
val extension = ReactAppExtension(project)
val expected = File(project.projectDir, "fake-cli.sh").apply { writeText("#!/bin/bash") }
extension.cliPath = "./fake-cli.sh"
val extension = TestReactAppExtension(project)
val expected = File(project.projectDir, "fake-cli.sh")
extension.cliPath.set("fake-cli.sh")
val actual = detectedCliPath(project.projectDir, extension)
assertEquals(expected.canonicalPath, File(actual).canonicalPath)
assertEquals(expected.toString(), actual)
}
@Test
fun detectedCliPath_withCliPathFromExtensionInParentFolder() {
val rootProject = ProjectBuilder.builder().build()
val project = ProjectBuilder.builder().withParent(rootProject).build()
val extension = ReactAppExtension(project)
val extension = TestReactAppExtension(project)
val expected = File(rootProject.projectDir, "cli-in-root.sh").apply { writeText("#!/bin/bash") }
extension.cliPath = "../cli-in-root.sh"
extension.cliPath.set("../cli-in-root.sh")
val actual = detectedCliPath(project.projectDir, extension)
@ -82,8 +82,8 @@ class PathUtilsTest {
@Test
fun detectedCliPath_withCliFromNodeModules() {
val project = ProjectBuilder.builder().build()
val extension = ReactAppExtension(project)
extension.reactRoot = tempFolder.root
val extension = TestReactAppExtension(project)
extension.reactRoot.set(tempFolder.root)
val expected =
File(tempFolder.root, "node_modules/react-native/cli.js").apply {
parentFile.mkdirs()
@ -98,16 +98,16 @@ class PathUtilsTest {
@Test(expected = IllegalStateException::class)
fun detectedCliPath_failsIfNotFound() {
val project = ProjectBuilder.builder().build()
val extension = ReactAppExtension(project)
val extension = TestReactAppExtension(project)
detectedCliPath(project.projectDir, extension)
}
@Test
fun detectedHermesCommand_withPathFromExtension() {
val extension = ReactAppExtension(ProjectBuilder.builder().build())
val extension = TestReactAppExtension(ProjectBuilder.builder().build())
val expected = tempFolder.newFile("hermesc")
extension.hermesCommand = expected.toString()
extension.hermesCommand.set(expected.toString())
val actual = detectedHermesCommand(extension)
@ -116,7 +116,7 @@ class PathUtilsTest {
@Test
fun detectedHermesCommand_withOSSpecificBin() {
val extension = ReactAppExtension(ProjectBuilder.builder().build())
val extension = TestReactAppExtension(ProjectBuilder.builder().build())
val actual = detectedHermesCommand(extension)