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 com.android.build.gradle.api.BaseVariant
import java.io.File import java.io.File
import javax.inject.Inject
import org.gradle.api.Project 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) { abstract class ReactAppExtension @Inject constructor(project: Project) {
var applyAppPlugin: Boolean = false
var composeSourceMapsPath: String = "node_modules/react-native/scripts/compose-source-maps.js" private val objects = project.objects
var bundleAssetName: String = "index.android.bundle"
var entryFile: File? = null /**
var bundleCommand: String = "bundle" * Whether the React App plugin should apply its logic or not. Set it to false if you're still
var reactRoot: File = File(project.projectDir, "../../") * relying on `react.gradle` to configure your build. Default: false
var inputExcludes: List<String> = listOf("android/**", "ios/**") */
var bundleConfig: String? = null val applyAppPlugin: Property<Boolean> = objects.property(Boolean::class.java).convention(false)
var enableVmCleanup: Boolean = true
var hermesCommand: String = "node_modules/hermes-engine/%OS-BIN%/hermesc" /**
var cliPath: String? = null * The path to the react root folder. This is the path to the root folder where the `node_modules`
var nodeExecutableAndArgs: List<String> = listOf("node") * folder is present. All the CLI commands will be invoked from this folder as working directory.
var enableHermes: Boolean = false *
var enableHermesForVariant: (BaseVariant) -> Boolean = { enableHermes } * Default: $projectDir/../../
var devDisabledInVariants: List<String> = emptyList() */
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? // todo maybe lambda as for hermes?
var bundleIn: Map<String, Boolean> = emptyMap() val bundleIn: MapProperty<String, Boolean> =
var extraPackagerArgs: List<String> = emptyList() objects.mapProperty(String::class.java, Boolean::class.java).convention(emptyMap())
var hermesFlagsDebug: List<String> = emptyList()
var hermesFlagsRelease: List<String> = listOf("-O", "-output-source-map") /** Hermes Config */
var resourcesDir: Map<String, File> = emptyMap()
var jsBundleDir: Map<String, File> = emptyMap() /** 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) { private fun applyAppPlugin(project: Project) {
val config = project.extensions.create("reactApp", ReactAppExtension::class.java, project) val config = project.extensions.create("reactApp", ReactAppExtension::class.java, project)
if (config.applyAppPlugin) { if (config.applyAppPlugin.getOrElse(false)) {
project.afterEvaluate { project.afterEvaluate {
val androidConfiguration = project.extensions.getByType(BaseExtension::class.java) val androidConfiguration = project.extensions.getByType(BaseExtension::class.java)
project.configureDevPorts(androidConfiguration) project.configureDevPorts(androidConfiguration)

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

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

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

@ -21,7 +21,8 @@ import org.apache.tools.ant.taskdefs.condition.Os
* @param config The [ReactAppExtension] configured for this project * @param config The [ReactAppExtension] configured for this project
*/ */
internal fun detectedEntryFile(config: ReactAppExtension): File = 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: * Computes the CLI location for React Native. The Algo follows this order:
@ -36,8 +37,8 @@ internal fun detectedCliPath(
): String = ): String =
detectCliPath( detectCliPath(
projectDir = projectDir, projectDir = projectDir,
reactRoot = config.reactRoot, reactRoot = config.reactRoot.get().asFile,
preconfiguredCliPath = config.cliPath) preconfiguredCliPath = config.cliPath.orNull)
/** /**
* Computes the `hermesc` command location. The Algo follows this order: * Computes the `hermesc` command location. The Algo follows this order:
@ -47,7 +48,7 @@ internal fun detectedCliPath(
* 3. Fails otherwise * 3. Fails otherwise
*/ */
internal fun detectedHermesCommand(config: ReactAppExtension): String = internal fun detectedHermesCommand(config: ReactAppExtension): String =
detectOSAwareHermesCommand(config.hermesCommand) detectOSAwareHermesCommand(config.hermesCommand.get())
private fun detectEntryFile(entryFile: File?, reactRoot: File): File = private fun detectEntryFile(entryFile: File?, reactRoot: File): File =
when { 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 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.detectedCliPath
import com.facebook.react.utils.detectedEntryFile import com.facebook.react.utils.detectedEntryFile
import com.facebook.react.utils.detectedHermesCommand import com.facebook.react.utils.detectedHermesCommand
@ -24,9 +24,9 @@ class PathUtilsTest {
@Test @Test
fun detectedEntryFile_withProvidedVariable() { fun detectedEntryFile_withProvidedVariable() {
val extension = ReactAppExtension(ProjectBuilder.builder().build()) val extension = TestReactAppExtension(ProjectBuilder.builder().build())
val expected = tempFolder.newFile("fake.index.js") val expected = tempFolder.newFile("fake.index.js")
extension.entryFile = expected extension.entryFile.set(expected)
val actual = detectedEntryFile(extension) val actual = detectedEntryFile(extension)
@ -35,8 +35,8 @@ class PathUtilsTest {
@Test @Test
fun detectedEntryFile_withAndroidEntryPoint() { fun detectedEntryFile_withAndroidEntryPoint() {
val extension = ReactAppExtension(ProjectBuilder.builder().build()) val extension = TestReactAppExtension(ProjectBuilder.builder().build())
extension.reactRoot = tempFolder.root extension.reactRoot.set(tempFolder.root)
tempFolder.newFile("index.android.js") tempFolder.newFile("index.android.js")
val actual = detectedEntryFile(extension) val actual = detectedEntryFile(extension)
@ -46,8 +46,8 @@ class PathUtilsTest {
@Test @Test
fun detectedEntryFile_withDefaultEntryPoint() { fun detectedEntryFile_withDefaultEntryPoint() {
val extension = ReactAppExtension(ProjectBuilder.builder().build()) val extension = TestReactAppExtension(ProjectBuilder.builder().build())
extension.reactRoot = tempFolder.root extension.reactRoot.set(tempFolder.root)
val actual = detectedEntryFile(extension) val actual = detectedEntryFile(extension)
@ -57,22 +57,22 @@ class PathUtilsTest {
@Test @Test
fun detectedCliPath_withCliPathFromExtension() { fun detectedCliPath_withCliPathFromExtension() {
val project = ProjectBuilder.builder().build() val project = ProjectBuilder.builder().build()
val extension = ReactAppExtension(project) val extension = TestReactAppExtension(project)
val expected = File(project.projectDir, "fake-cli.sh").apply { writeText("#!/bin/bash") } val expected = File(project.projectDir, "fake-cli.sh")
extension.cliPath = "./fake-cli.sh" extension.cliPath.set("fake-cli.sh")
val actual = detectedCliPath(project.projectDir, extension) val actual = detectedCliPath(project.projectDir, extension)
assertEquals(expected.canonicalPath, File(actual).canonicalPath) assertEquals(expected.toString(), actual)
} }
@Test @Test
fun detectedCliPath_withCliPathFromExtensionInParentFolder() { fun detectedCliPath_withCliPathFromExtensionInParentFolder() {
val rootProject = ProjectBuilder.builder().build() val rootProject = ProjectBuilder.builder().build()
val project = ProjectBuilder.builder().withParent(rootProject).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") } 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) val actual = detectedCliPath(project.projectDir, extension)
@ -82,8 +82,8 @@ class PathUtilsTest {
@Test @Test
fun detectedCliPath_withCliFromNodeModules() { fun detectedCliPath_withCliFromNodeModules() {
val project = ProjectBuilder.builder().build() val project = ProjectBuilder.builder().build()
val extension = ReactAppExtension(project) val extension = TestReactAppExtension(project)
extension.reactRoot = tempFolder.root extension.reactRoot.set(tempFolder.root)
val expected = val expected =
File(tempFolder.root, "node_modules/react-native/cli.js").apply { File(tempFolder.root, "node_modules/react-native/cli.js").apply {
parentFile.mkdirs() parentFile.mkdirs()
@ -98,16 +98,16 @@ class PathUtilsTest {
@Test(expected = IllegalStateException::class) @Test(expected = IllegalStateException::class)
fun detectedCliPath_failsIfNotFound() { fun detectedCliPath_failsIfNotFound() {
val project = ProjectBuilder.builder().build() val project = ProjectBuilder.builder().build()
val extension = ReactAppExtension(project) val extension = TestReactAppExtension(project)
detectedCliPath(project.projectDir, extension) detectedCliPath(project.projectDir, extension)
} }
@Test @Test
fun detectedHermesCommand_withPathFromExtension() { fun detectedHermesCommand_withPathFromExtension() {
val extension = ReactAppExtension(ProjectBuilder.builder().build()) val extension = TestReactAppExtension(ProjectBuilder.builder().build())
val expected = tempFolder.newFile("hermesc") val expected = tempFolder.newFile("hermesc")
extension.hermesCommand = expected.toString() extension.hermesCommand.set(expected.toString())
val actual = detectedHermesCommand(extension) val actual = detectedHermesCommand(extension)
@ -116,7 +116,7 @@ class PathUtilsTest {
@Test @Test
fun detectedHermesCommand_withOSSpecificBin() { fun detectedHermesCommand_withOSSpecificBin() {
val extension = ReactAppExtension(ProjectBuilder.builder().build()) val extension = TestReactAppExtension(ProjectBuilder.builder().build())
val actual = detectedHermesCommand(extension) val actual = detectedHermesCommand(extension)