@callstack/react-native-brownfield 6.0.1 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +12 -12
  3. package/android/src/main/java/com/callstack/reactnativebrownfield/RetryableOnce.kt +27 -0
  4. package/android/src/test/java/com/callstack/reactnativebrownfield/RetryableOnceTest.kt +37 -0
  5. package/gradle-plugin/brownfield/build.gradle.kts +4 -0
  6. package/gradle-plugin/brownfield/gradle/libs.versions.toml +2 -0
  7. package/gradle-plugin/brownfield/gradle.properties +1 -1
  8. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +24 -7
  9. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +4 -37
  10. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/JNILibsProcessor.kt +206 -104
  11. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ResourceTaskProcessor.kt +201 -4
  12. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantHelper.kt +7 -0
  13. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantTaskProvider.kt +4 -0
  14. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/GradleExclude.kt +31 -0
  15. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt +2 -0
  16. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjector.kt +28 -3
  17. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +14 -1
  18. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/TaskOutputs.kt +40 -0
  19. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +14 -6
  20. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/HardExcludePredicateTest.kt +28 -0
  21. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ConfigureProcessJniTaskTest.kt +60 -0
  22. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/CopyExplodedResTaskTest.kt +368 -0
  23. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/JniLibsPlanTest.kt +252 -0
  24. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MergeClassesLocalJarsTest.kt +73 -0
  25. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MetroResDirWiringTest.kt +39 -0
  26. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ProcessAndCopyJniLibsTaskTest.kt +136 -0
  27. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/GradleExcludesTest.kt +81 -0
  28. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjectorTest.kt +56 -0
  29. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +5 -0
  30. package/ios/ReactNativeBrownfield.swift +3 -1
  31. package/ios/ReactNativeBrownfieldModule.mm +35 -7
  32. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  33. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  34. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  35. package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -1
  36. package/lib/commonjs/index.js +1 -1
  37. package/lib/commonjs/index.js.map +1 -1
  38. package/lib/commonjs/scaffold/index.js +1 -1
  39. package/lib/commonjs/scaffold/index.js.map +1 -1
  40. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  41. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  42. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  43. package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -1
  44. package/lib/module/index.js +1 -1
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/scaffold/index.js +1 -1
  47. package/lib/module/scaffold/index.js.map +1 -1
  48. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  49. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
  51. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -1
  54. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  55. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  56. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
  57. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  58. package/lib/typescript/module/src/index.d.ts.map +1 -1
  59. package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -1
  60. package/package.json +2 -2
  61. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
  62. package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +42 -0
  63. package/src/expo-config-plugin/template/android/build.gradle.kts +1 -1
  64. package/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +7 -0
  65. package/src/index.ts +4 -2
  66. package/src/scaffold/index.ts +1 -0
@@ -66,6 +66,10 @@ class VariantTaskProvider(val project: Project) {
66
66
  it.dependsOn(VariantHelper.getJavaCompileTask(project, capitalizedVariantName))
67
67
  it.variantName.set(variantName)
68
68
  it.inputClassesJars.from(aarLibraries.map { aarLibrary -> aarLibrary.getClassesJarFile() })
69
+ // libs/*.jar of each library (#489)
70
+ it.inputClassesJars.from(
71
+ aarLibraries.map { aarLibrary -> project.fileTree(aarLibrary.getLibsDir()) { tree -> tree.include("*.jar") } },
72
+ )
69
73
  it.outputDir.set(DirectoryManager.getMergeClassDirectory(variantName))
70
74
  }
71
75
  }
@@ -0,0 +1,31 @@
1
+ package com.callstack.react.brownfield.shared
2
+
3
+ import org.gradle.api.Project
4
+
5
+ /**
6
+ * The variant-independent configurations that feed what the host app gets. A variant's own
7
+ * (`debugRuntimeClasspath`) would leak into every variant, since the published list is shared, and a
8
+ * tool's (lint, detekt) may exclude things the app still needs.
9
+ */
10
+ private val PUBLISHED_CONFIG_NAMES = TRANSITIVE_DEPENDENCY_CONFIG_NAMES + "embed"
11
+
12
+ /**
13
+ * The configuration-level excludes that keep a dependency out of the brownfield module's build.
14
+ * The plugin reads what embedded libraries declare, not what Gradle resolves, so without these an
15
+ * excluded dependency would still be published and the host app would resolve it. `:app`'s rules
16
+ * are not read: they describe the standalone RN app, which may drop things the host needs.
17
+ */
18
+ internal fun gradleExcludes(project: Project): List<GradleExclude> =
19
+ project.configurations
20
+ .filter { it.name in PUBLISHED_CONFIG_NAMES }
21
+ .flatMap { it.excludeRules }
22
+ .map { GradleExclude(it.group, it.module) }
23
+ .distinct()
24
+
25
+ /** One exclude rule. Gradle types both parts as non-null, but a rule without one leaves it null. */
26
+ internal data class GradleExclude(val group: String?, val module: String?) {
27
+ fun matches(
28
+ groupId: String,
29
+ artifactId: String,
30
+ ) = (group == null || group == groupId) && (module == null || module == artifactId)
31
+ }
@@ -54,6 +54,8 @@ abstract class MergeClassesTask : DefaultTask() {
54
54
  copyTask.from(mergedOutputDir)
55
55
  copyTask.into(javacDir)
56
56
  copyTask.exclude("META-INF/")
57
+ // Root module descriptor of a library's local jar, not a class of the AAR.
58
+ copyTask.exclude("module-info.class")
57
59
  }
58
60
 
59
61
  project.copy { copyTask ->
@@ -19,6 +19,7 @@ import org.gradle.api.publish.tasks.GenerateModuleMetadata
19
19
  internal fun mutatePomDependenciesNode(
20
20
  dependenciesNode: Node,
21
21
  dependencies: VersionMediatingDependencySet,
22
+ excludes: List<GradleExclude> = emptyList(),
22
23
  shouldExclude: (groupId: String, artifactId: String) -> Boolean,
23
24
  ) {
24
25
  dependenciesNode.children()
@@ -49,6 +50,21 @@ internal fun mutatePomDependenciesNode(
49
50
  childTags.forEach { (tagName, tagValue) ->
50
51
  newDepNode.appendNode(tagName, tagValue)
51
52
  }
53
+ appendExclusions(newDepNode, excludes)
54
+ }
55
+ }
56
+ }
57
+
58
+ private fun appendExclusions(
59
+ dependencyNode: Node,
60
+ excludes: List<GradleExclude>,
61
+ ) {
62
+ if (excludes.isEmpty()) return
63
+ val exclusionsNode = dependencyNode.appendNode("exclusions")
64
+ excludes.forEach { exclude ->
65
+ exclusionsNode.appendNode("exclusion").apply {
66
+ appendNode("groupId", exclude.group ?: "*")
67
+ appendNode("artifactId", exclude.module ?: "*")
52
68
  }
53
69
  }
54
70
  }
@@ -69,6 +85,7 @@ internal fun resolveOrCreateDependenciesNode(root: Node): Node =
69
85
  internal fun mutateModuleJsonVariants(
70
86
  json: Map<*, *>,
71
87
  dependencies: VersionMediatingDependencySet,
88
+ excludes: List<GradleExclude> = emptyList(),
72
89
  shouldExclude: (groupId: String, artifactId: String) -> Boolean,
73
90
  ) {
74
91
  @Suppress("UNCHECKED_CAST")
@@ -98,6 +115,9 @@ internal fun mutateModuleJsonVariants(
98
115
  dependencyToAdd.version?.let { version ->
99
116
  put("version", mapOf("requires" to version))
100
117
  }
118
+ if (excludes.isNotEmpty()) {
119
+ put("excludes", excludes.map { mapOf("group" to (it.group ?: "*"), "module" to (it.module ?: "*")) })
120
+ }
101
121
  },
102
122
  )
103
123
  }
@@ -116,6 +136,7 @@ internal fun mutateModuleJsonVariants(
116
136
  class PublishingMetadataInjector(private val project: Project) {
117
137
  private var dependencies = VersionMediatingDependencySet()
118
138
  private var shouldExclude: (groupId: String, artifactId: String) -> Boolean = { _, _ -> false }
139
+ private var excludes: List<GradleExclude> = emptyList()
119
140
  private var enabled = false
120
141
 
121
142
  init {
@@ -124,12 +145,16 @@ class PublishingMetadataInjector(private val project: Project) {
124
145
  }
125
146
 
126
147
  /** Supplies the dependencies to inject and enables injection on the hooks registered in [init]. */
127
- fun configure(
148
+ internal fun configure(
128
149
  dependencies: VersionMediatingDependencySet,
129
150
  shouldExclude: (groupId: String, artifactId: String) -> Boolean,
151
+ excludes: List<GradleExclude>,
130
152
  ) {
131
153
  this.dependencies = dependencies
132
154
  this.shouldExclude = shouldExclude
155
+ // Gradle writes the module's excludes onto the dependencies it declares; these are injected
156
+ // later, so they need them too, or an excluded library could still arrive through one.
157
+ this.excludes = excludes
133
158
  this.enabled = true
134
159
  }
135
160
 
@@ -148,7 +173,7 @@ class PublishingMetadataInjector(private val project: Project) {
148
173
  if (!moduleFile.exists()) return@forEach
149
174
 
150
175
  val json = moduleFile.inputStream().use { JsonSlurper().parse(it) as Map<*, *> }
151
- mutateModuleJsonVariants(json, dependencies, shouldExclude)
176
+ mutateModuleJsonVariants(json, dependencies, excludes, shouldExclude)
152
177
  moduleFile.writer().use {
153
178
  it.write(JsonOutput.prettyPrint(JsonOutput.toJson(json)))
154
179
  }
@@ -172,7 +197,7 @@ class PublishingMetadataInjector(private val project: Project) {
172
197
 
173
198
  val root = it.asNode()
174
199
  val dependenciesNode = resolveOrCreateDependenciesNode(root)
175
- mutatePomDependenciesNode(dependenciesNode, dependencies, shouldExclude)
200
+ mutatePomDependenciesNode(dependenciesNode, dependencies, excludes, shouldExclude)
176
201
  }
177
202
  }
178
203
  }
@@ -14,7 +14,9 @@ open class Extension {
14
14
  var appProjectName = "app"
15
15
 
16
16
  /**
17
- * Whether to use stripped .so files.
17
+ * Whether to take the .so files from the app module's stripped or unstripped native output. It
18
+ * does not change which .so files are embedded. AGP still strips the AAR's native libs when it
19
+ * packages the library.
18
20
  *
19
21
  * Default is `true`.
20
22
  */
@@ -54,6 +56,17 @@ open class Extension {
54
56
  */
55
57
  var ignoreEmbeddedLibs = listOf<String>()
56
58
 
59
+ /**
60
+ * Exact .so file names, taken from the app module's native output, to embed in the AAR on top
61
+ * of the app's own libs and the embedded libraries' libs. Use it for a .so the host app cannot
62
+ * get any other way: one the app module builds or ships itself, or one from a dependency the
63
+ * published AAR does not declare.
64
+ *
65
+ * Provide in this format:
66
+ * listOf("libmmkv.so")
67
+ */
68
+ var dynamicLibs = listOf<String>()
69
+
57
70
  /**
58
71
  * Whether to publish embedded native modules' third-party dependencies into the generated POM
59
72
  * and Gradle Module Metadata, so consuming apps resolve them instead of declaring them by hand.
@@ -0,0 +1,40 @@
1
+ package com.callstack.react.brownfield.utils
2
+
3
+ import org.gradle.api.GradleException
4
+ import org.gradle.api.Project
5
+ import org.gradle.api.file.DirectoryProperty
6
+ import java.lang.reflect.Method
7
+
8
+ /**
9
+ * Finds `taskName`'s `getterName()` that returns a `DirectoryProperty`. Resolving the task by name
10
+ * (rather than importing its class) keeps this plugin from depending on RNGP's/expo-updates'
11
+ * internal task types. If the task, or the property, isn't there, this fails the build with a
12
+ * clear message instead of silently producing an empty or stale directory.
13
+ */
14
+ internal fun directoryGetter(
15
+ appProject: Project,
16
+ taskName: String,
17
+ getterName: String,
18
+ ): Method {
19
+ val task =
20
+ appProject.tasks.findByName(taskName)
21
+ ?: throw GradleException(
22
+ "brownfield: expected task '$taskName' was not found in project " +
23
+ "'${appProject.path}'. Check that the plugin producing it is applied there.",
24
+ )
25
+
26
+ val getter =
27
+ runCatching { task.javaClass.getMethod(getterName) }.getOrElse {
28
+ throw GradleException(
29
+ "brownfield: task '$taskName' (${task.javaClass.name}) does not expose " +
30
+ "'$getterName()'. The plugin that registers it most likely changed its " +
31
+ "API; the brownfield Gradle plugin needs updating to match.",
32
+ it,
33
+ )
34
+ }
35
+
36
+ check(DirectoryProperty::class.java.isAssignableFrom(getter.returnType)) {
37
+ "brownfield: '$taskName.$getterName()' no longer returns a DirectoryProperty."
38
+ }
39
+ return getter
40
+ }
@@ -7,6 +7,7 @@ import org.junit.jupiter.api.io.TempDir
7
7
  import org.junit.jupiter.params.ParameterizedTest
8
8
  import org.junit.jupiter.params.provider.MethodSource
9
9
  import java.io.File
10
+ import kotlin.test.assertFalse
10
11
  import kotlin.test.assertTrue
11
12
 
12
13
  /**
@@ -39,12 +40,19 @@ class AgpCompatibilityMatrixTest {
39
40
  // `--dry-run` still runs configuration in full — variant callbacks, source set wiring, task
40
41
  // graph construction — which is where every AGP API the plugin touches is exercised. It just
41
42
  // skips the task actions, which would need a real RN app to produce anything meaningful.
42
- GradleRunner.create()
43
- .withProjectDir(projectDir)
44
- .withPluginClasspath(pluginClasspathFor(agpVersion))
45
- .withArguments("assembleDebug", "--dry-run", "--stacktrace")
46
- .forwardOutput()
47
- .build()
43
+ val result =
44
+ GradleRunner.create()
45
+ .withProjectDir(projectDir)
46
+ .withPluginClasspath(pluginClasspathFor(agpVersion))
47
+ .withArguments("assembleDebug", "--dry-run", "--stacktrace")
48
+ .forwardOutput()
49
+ .build()
50
+
51
+ // AGP 9 turns resValues off by default and warns on every read of variant.resValues.
52
+ assertFalse(
53
+ result.output.contains("Accessing value resValues"),
54
+ "the plugin reads variant.resValues although the feature is disabled",
55
+ )
48
56
  }
49
57
 
50
58
  @Test
@@ -0,0 +1,28 @@
1
+ package com.callstack.react.brownfield.plugin
2
+
3
+ import com.callstack.react.brownfield.shared.gradleExcludes
4
+ import org.gradle.testfixtures.ProjectBuilder
5
+ import org.junit.jupiter.api.Test
6
+ import org.junit.jupiter.api.io.TempDir
7
+ import java.io.File
8
+ import kotlin.test.assertFalse
9
+ import kotlin.test.assertTrue
10
+
11
+ class HardExcludePredicateTest {
12
+ @TempDir
13
+ lateinit var tempDir: File
14
+
15
+ @Test
16
+ fun `never publishes what the brownfield module excludes, or its own subprojects`() {
17
+ val root = ProjectBuilder.builder().withName("MyApp").withProjectDir(tempDir).build()
18
+ val lib = ProjectBuilder.builder().withName("brownfieldlib").withParent(root).build()
19
+ lib.configurations.create("implementation")
20
+ lib.configurations.all { it.exclude(mapOf("group" to "com.caverock", "module" to "androidsvg")) }
21
+
22
+ val excluded = hardExcludePredicate(lib, null, emptyList(), emptyList(), gradleExcludes(lib))
23
+
24
+ assertTrue(excluded("com.caverock", "androidsvg"))
25
+ assertTrue(excluded("MyApp", "react-native-screens"))
26
+ assertFalse(excluded("androidx.core", "core-ktx"))
27
+ }
28
+ }
@@ -0,0 +1,60 @@
1
+ package com.callstack.react.brownfield.processors
2
+
3
+ import com.callstack.react.brownfield.utils.Extension
4
+ import org.gradle.testfixtures.ProjectBuilder
5
+ import org.junit.jupiter.api.Test
6
+ import org.junit.jupiter.api.io.TempDir
7
+ import java.io.File
8
+ import kotlin.test.assertEquals
9
+ import kotlin.test.assertTrue
10
+
11
+ class ConfigureProcessJniTaskTest {
12
+ @TempDir
13
+ lateinit var tempDir: File
14
+
15
+ private fun configure(useStripped: Boolean): Pair<ProcessAndCopyJniLibsTask, File> {
16
+ val root = ProjectBuilder.builder().withProjectDir(tempDir).build()
17
+ val app = ProjectBuilder.builder().withName("app").withParent(root).build()
18
+ val lib = ProjectBuilder.builder().withName("BrownfieldLib").withParent(root).build()
19
+ val ext =
20
+ Extension().apply {
21
+ useStrippedSoFiles = useStripped
22
+ ignoreEmbeddedLibs = listOf("libdatadog")
23
+ dynamicLibs = listOf("libmmkv.so")
24
+ }
25
+ val explode = lib.tasks.register("explodeDevReleaseAar")
26
+ val task = lib.tasks.register("processJni", ProcessAndCopyJniLibsTask::class.java).get()
27
+
28
+ configureProcessJniTask(task, lib, app, ext, "devRelease", listOf(File(tempDir, "jni")), explode)
29
+ return task to app.layout.buildDirectory.get().asFile
30
+ }
31
+
32
+ @Test
33
+ fun `wires the stripped source, settings and report path`() {
34
+ val (task, appBuild) = configure(useStripped = true)
35
+
36
+ assertEquals(
37
+ File(appBuild, "intermediates/stripped_native_libs/devRelease/stripDevReleaseDebugSymbols/out/lib"),
38
+ task.appNativeLibsDir.get().asFile,
39
+ )
40
+ assertTrue(":app:stripDevReleaseDebugSymbols" in task.dependsOn, task.dependsOn.toString())
41
+ assertTrue(":BrownfieldLib:generateCodegenSchemaFromJavaScript" in task.dependsOn, task.dependsOn.toString())
42
+ assertEquals(listOf("libdatadog"), task.extraIgnoredLibs.get())
43
+ assertEquals(listOf("libmmkv.so"), task.dynamicLibs.get())
44
+ assertTrue(
45
+ task.skippedReportFile.get().asFile.invariantSeparatorsPath
46
+ .endsWith("BrownfieldLib/build/reports/brownfield/devRelease/skipped-so.txt"),
47
+ )
48
+ }
49
+
50
+ @Test
51
+ fun `wires the unstripped source`() {
52
+ val (task, appBuild) = configure(useStripped = false)
53
+
54
+ assertEquals(
55
+ File(appBuild, "intermediates/merged_native_libs/devRelease/mergeDevReleaseNativeLibs/out/lib"),
56
+ task.appNativeLibsDir.get().asFile,
57
+ )
58
+ assertTrue(":app:mergeDevReleaseNativeLibs" in task.dependsOn, task.dependsOn.toString())
59
+ }
60
+ }