diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt index f1b4bbf6419b3d..62f775086417cd 100644 --- a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactExtension.kt @@ -169,7 +169,7 @@ abstract class ReactExtension @Inject constructor(project: Project) { .convention(listOf("npx", "@react-native-community/cli", "config")) /** - * Location of the lock files used to consider wether autolinking [autolinkConfigCommand] should + * Location of the lock files used to consider whether autolinking [autolinkConfigCommand] should * re-execute or not. If file collection is unchanged, the autolinking command will not be * re-executed. * 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 2c5bea4099c53f..4dffba5547c90f 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 @@ -12,6 +12,7 @@ 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.tasks.RunAutolinkingConfigTask import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForApp import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForLibraries import com.facebook.react.utils.AgpConfiguratorUtils.configureDevPorts @@ -78,6 +79,7 @@ class ReactPlugin : Plugin { project.configureReactTasks(variant = variant, config = extension) } } + configureAutolinking(project, extension) configureCodegen(project, extension, rootExtension, isLibrary = false) } @@ -209,4 +211,21 @@ class ReactPlugin : Plugin { // This will invoke the codegen before compiling the entire project. project.tasks.named("preBuild", Task::class.java).dependsOn(generateCodegenArtifactsTask) } + + /** This function sets up Autolinking for App users */ + private fun configureAutolinking( + project: Project, + extension: ReactExtension, + ) { + val generatedAutolinkingDir: Provider = + project.layout.buildDirectory.dir("generated/autolinking") + val configOutputFile = generatedAutolinkingDir.get().file("config-output.json") + + project.tasks.register("runAutolinkingConfig", RunAutolinkingConfigTask::class.java) { task -> + task.autolinkConfigCommand.set(extension.autolinkConfigCommand) + task.autolinkConfigFile.set(extension.autolinkConfigFile) + task.autolinkOutputFile.set(configOutputFile) + task.autolinkLockFiles.set(extension.autolinkLockFiles) + } + } } diff --git a/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTask.kt b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTask.kt new file mode 100644 index 00000000000000..f22a657946da9e --- /dev/null +++ b/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTask.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.tasks + +import com.facebook.react.utils.windowsAwareCommandLine +import java.io.FileOutputStream +import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* + +/** + * A task that will run @react-native-community/cli config if necessary to generate the autolinking + * configuration file. + */ +abstract class RunAutolinkingConfigTask : Exec() { + + init { + group = "react" + } + + @get:Input abstract val autolinkConfigCommand: ListProperty + + /* + * We don't want to re-run config if the lockfiles haven't changed. + * So we have the lockfiles as @InputFiles for this task. + */ + @get:InputFiles abstract val autolinkLockFiles: Property + + @get:InputFile @get:Optional abstract val autolinkConfigFile: RegularFileProperty + + @get:OutputFile abstract val autolinkOutputFile: RegularFileProperty + + override fun exec() { + wipeOutputDir() + setupCommandLine() + super.exec() + } + + internal fun setupCommandLine() { + if (!autolinkConfigFile.isPresent || !autolinkConfigFile.get().asFile.exists()) { + setupConfigCommandLine() + } else { + setupConfigCopyCommandLine() + } + } + + internal fun wipeOutputDir() { + autolinkOutputFile.asFile.get().apply { + deleteRecursively() + parentFile.mkdirs() + } + } + + internal fun setupConfigCommandLine() { + workingDir(project.projectDir) + standardOutput = FileOutputStream(autolinkOutputFile.get().asFile) + commandLine( + windowsAwareCommandLine( + *autolinkConfigCommand.get().toTypedArray(), + )) + } + + internal fun setupConfigCopyCommandLine() { + workingDir(project.projectDir) + commandLine( + windowsAwareCommandLine( + "cp", + autolinkConfigFile.get().asFile.absolutePath, + autolinkOutputFile.get().asFile.absolutePath)) + } +} diff --git a/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTaskTest.kt b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTaskTest.kt new file mode 100644 index 00000000000000..32470eca12c09f --- /dev/null +++ b/packages/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/RunAutolinkingConfigTaskTest.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.tasks + +import com.facebook.react.tests.createProject +import com.facebook.react.tests.createTestTask +import java.io.File +import java.io.FileOutputStream +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class RunAutolinkingConfigTaskTest { + + @get:Rule val tempFolder = TemporaryFolder() + + @Test + fun runAutolinkingConfigTask_groupIsSetCorrectly() { + val task = createTestTask {} + assertEquals("react", task.group) + } + + @Test + fun runAutolinkingConfigTask_staticInputs_areSetCorrectly() { + val project = createProject() + + val task = + createTestTask { + it.autolinkConfigCommand.set(listOf("rm", "-rf", "/")) + it.autolinkLockFiles.set(project.files("packager.lock", "another-packager.lock")) + it.autolinkConfigFile.set(tempFolder.newFile("dependencies.json")) + it.autolinkOutputFile.set(tempFolder.newFile("output.json")) + } + + assertEquals(3, task.inputs.files.files.size) + task.autolinkLockFiles.get().files.forEach { + assertTrue( + it.name == "depedencies.json" || + it.name == "packager.lock" || + it.name == "another-packager.lock") + } + + assertTrue(task.inputs.properties.containsKey("autolinkConfigCommand")) + assertEquals(1, task.outputs.files.files.size) + assertEquals(File(tempFolder.root, "output.json"), task.outputs.files.singleFile) + assertEquals(listOf("rm", "-rf", "/"), task.autolinkConfigCommand.get()) + + assertEquals(2, task.autolinkLockFiles.get().files.size) + task.autolinkLockFiles.get().files.forEach { + assertTrue(it.name == "packager.lock" || it.name == "another-packager.lock") + } + + assertEquals(File(tempFolder.root, "dependencies.json"), task.autolinkConfigFile.get().asFile) + assertEquals(File(tempFolder.root, "output.json"), task.autolinkOutputFile.get().asFile) + } + + @Test + fun wipeOutputDir_worksCorrectly() { + val outputDir = + tempFolder.newFolder("output").apply { + File(this, "output.json").createNewFile() + File(this, "NothingToSeeHere.java").createNewFile() + } + + val task = createTestTask { it.autolinkOutputFile.set(outputDir) } + task.wipeOutputDir() + + assertFalse(outputDir.exists()) + } + + @Test + fun setupConfigCommandLine_worksCorrectly() { + val project = createProject() + + val task = + createTestTask(project) { + it.autolinkConfigCommand.set(listOf("rm", "-rf", "/")) + it.autolinkOutputFile.set(tempFolder.newFile("output.json")) + } + task.setupConfigCommandLine() + + assertEquals(project.projectDir, task.workingDir) + assertTrue(task.standardOutput is FileOutputStream) + assertEquals(listOf("rm", "-rf", "/"), task.commandLine) + } + + @Test + fun setupConfigCopyCommandLine_worksCorrectly() { + val project = createProject() + + val task = + createTestTask(project) { + it.autolinkConfigFile.set(tempFolder.newFile("dependencies.json")) + it.autolinkOutputFile.set(tempFolder.newFile("output.json")) + } + task.setupConfigCopyCommandLine() + + assertEquals(project.projectDir, task.workingDir) + assertTrue(task.standardOutput !is FileOutputStream) + assertEquals("cp", task.commandLine[0]) + assertEquals(File(tempFolder.root, "dependencies.json").absolutePath, task.commandLine[1]) + assertEquals(File(tempFolder.root, "output.json").absolutePath, task.commandLine[2]) + } + + @Test + fun setupCommandLine_withoutAutolinkConfigFileConfigured_invokesCommand() { + val project = createProject() + + val task = + createTestTask(project) { + it.autolinkConfigCommand.set(listOf("rm", "-rf", "/")) + it.autolinkOutputFile.set(tempFolder.newFile("output.json")) + } + task.setupCommandLine() + + assertEquals(listOf("rm", "-rf", "/"), task.commandLine) + } + + @Test + fun setupCommandLine_withoutMissingConfigFile_invokesCommand() { + val project = createProject() + + val task = + createTestTask(project) { + it.autolinkConfigCommand.set(listOf("rm", "-rf", "/")) + it.autolinkConfigFile.set(File(tempFolder.root, "dependencies.json")) + it.autolinkOutputFile.set(tempFolder.newFile("output.json")) + } + task.setupCommandLine() + + assertEquals(listOf("rm", "-rf", "/"), task.commandLine) + } + + @Test + fun setupCommandLine_withoutExistingConfigFile_invokesCp() { + val project = createProject() + val configFile = tempFolder.newFile("dependencies.json").apply { writeText("¯\\_(ツ)_/¯") } + + val task = + createTestTask(project) { + it.autolinkConfigCommand.set(listOf("rm", "-rf", "/")) + it.autolinkConfigFile.set(configFile) + it.autolinkOutputFile.set(tempFolder.newFile("output.json")) + } + task.setupCommandLine() + + assertEquals( + listOf("cp", configFile.absolutePath, File(tempFolder.root, "output.json").absolutePath), + task.commandLine) + } +} diff --git a/packages/rn-tester/android/app/build.gradle.kts b/packages/rn-tester/android/app/build.gradle.kts index 30b5ad58d5c7a8..65323bf84bfd24 100644 --- a/packages/rn-tester/android/app/build.gradle.kts +++ b/packages/rn-tester/android/app/build.gradle.kts @@ -61,6 +61,10 @@ react { // The hermes compiler command to run. By default it is 'hermesc' hermesCommand = "$reactNativeDirPath/ReactAndroid/hermes-engine/build/hermes/bin/hermesc" enableHermesOnlyInVariants = listOf("hermesDebug", "hermesRelease") + + /* Autolinking */ + // The location of the monorepo lockfiles to `config` is cached correctly. + autolinkLockFiles = files("$rootDir/yarn.lock") } /** Run Proguard to shrink the Java bytecode in release builds. */