@callstack/react-native-brownfield 5.1.0 → 5.2.0

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 (59) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +22 -4
  3. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +3 -1
  4. package/gradle-plugin/brownfield/build.gradle.kts +74 -2
  5. package/gradle-plugin/brownfield/gradle/libs.versions.toml +7 -2
  6. package/gradle-plugin/brownfield/gradle/wrapper/gradle-wrapper.properties +1 -1
  7. package/gradle-plugin/brownfield/gradle.properties +1 -1
  8. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/artifacts/RncTransitiveDependencyDiscoverer.kt +96 -0
  9. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/ExpoPublishingHelper.kt +10 -214
  10. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/BrownfieldPublishingInfo.kt +7 -0
  11. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +123 -1
  12. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +6 -1
  13. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt +12 -1
  14. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/{expo/utils/BrownfieldPrimitives.kt → shared/DependencyInfo.kt} +3 -8
  15. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/DependencyPublishability.kt +22 -0
  16. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/GradleDependencyCollector.kt +44 -0
  17. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt +2 -0
  18. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjector.kt +181 -0
  19. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/TransitiveDependencyFiltering.kt +17 -0
  20. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/{expo/utils → shared}/VersionMediatingDependencySet.kt +1 -1
  21. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +11 -0
  22. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/artifacts/RncTransitiveDependencyDiscovererTest.kt +171 -0
  23. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/expo/ExpoPublishingHelperGradleFallbackTest.kt +47 -0
  24. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +155 -0
  25. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/LegacyConsumerConfigurationTest.kt +110 -0
  26. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/DependencyPublishabilityTest.kt +67 -0
  27. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjectorTest.kt +222 -0
  28. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/TransitiveDependencyFilteringTest.kt +37 -0
  29. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/VersionMediatingDependencySetTest.kt +42 -0
  30. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +11 -0
  31. package/ios/BrownfieldSurfaceRegistry.swift +93 -0
  32. package/ios/Views/ReactNativeView.swift +34 -5
  33. package/ios/Views/ReactNativeViewController.swift +120 -1
  34. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  35. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  36. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  37. package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -2
  38. package/lib/commonjs/expo-config-plugin/template/android/proguard-rules.pro +1 -1
  39. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  40. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  41. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  42. package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -2
  43. package/lib/module/expo-config-plugin/template/android/proguard-rules.pro +1 -1
  44. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  45. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +11 -1
  46. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +3 -2
  48. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  49. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  50. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +11 -1
  51. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  52. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +3 -2
  53. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  54. package/package.json +2 -2
  55. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
  56. package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +31 -2
  57. package/src/expo-config-plugin/template/android/build.gradle.kts +1 -2
  58. package/src/expo-config-plugin/template/android/proguard-rules.pro +1 -1
  59. package/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +3 -2
@@ -0,0 +1,171 @@
1
+ package com.callstack.react.brownfield.artifacts
2
+
3
+ import com.callstack.react.brownfield.shared.DependencyInfo
4
+ import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo
5
+ import org.gradle.testfixtures.ProjectBuilder
6
+ import org.junit.jupiter.api.Test
7
+ import kotlin.test.assertEquals
8
+ import kotlin.test.assertFalse
9
+ import kotlin.test.assertTrue
10
+
11
+ class RncTransitiveDependencyDiscovererTest {
12
+ private fun defaultArtifacts(root: org.gradle.api.Project) =
13
+ listOf(
14
+ UnresolvedArtifactInfo(
15
+ moduleGroup = root.name,
16
+ moduleName = "react-native-fake-module",
17
+ moduleVersion = "unspecified",
18
+ file = null,
19
+ isExpoPublishDependency = false,
20
+ ),
21
+ )
22
+
23
+ @Test
24
+ fun `a dynamic consumer-declared version never becomes a mediation candidate`() {
25
+ // Module-side versions are validated by collectPublishableGradleDependencies; the
26
+ // consumer side must be validated too, or `1.+` can win mediation and be published.
27
+ val root = ProjectBuilder.builder().build()
28
+ val consumer = ProjectBuilder.builder().withParent(root).withName("BrownfieldLib").build()
29
+ val embeddedModule = ProjectBuilder.builder().withParent(root).withName("react-native-fake-module").build()
30
+
31
+ embeddedModule.configurations.create("implementation")
32
+ embeddedModule.dependencies.add("implementation", "androidx.appcompat:appcompat:1.0.0")
33
+
34
+ consumer.configurations.create("implementation")
35
+ consumer.dependencies.add("implementation", "androidx.appcompat:appcompat:1.+")
36
+
37
+ val result = RncTransitiveDependencyDiscoverer(consumer).discover(defaultArtifacts(root))
38
+
39
+ val appcompat = result.dependencies.first { it.groupId == "androidx.appcompat" }
40
+ assertEquals("1.0.0", appcompat.version, "the dynamic consumer version must not win mediation")
41
+ assertFalse(result.dependencies.any { it.version == "1.+" }, "'1.+' must never reach the published set")
42
+ }
43
+
44
+ @Test
45
+ fun `a versionless consumer declaration still supersedes, so the coordinate is not duplicated`() {
46
+ // Declared via BOM/platform, so `.version` is null. A version-only lookup returns null
47
+ // here, never marks the coordinate superseded, and leaves two entries for it in the POM.
48
+ val root = ProjectBuilder.builder().build()
49
+ val consumer = ProjectBuilder.builder().withParent(root).withName("BrownfieldLib").build()
50
+ val embeddedModule = ProjectBuilder.builder().withParent(root).withName("react-native-fake-module").build()
51
+
52
+ embeddedModule.configurations.create("implementation")
53
+ embeddedModule.dependencies.add("implementation", "androidx.appcompat:appcompat:1.7.1")
54
+
55
+ consumer.configurations.create("implementation")
56
+ consumer.dependencies.add("implementation", "androidx.appcompat:appcompat")
57
+
58
+ val result = RncTransitiveDependencyDiscoverer(consumer).discover(defaultArtifacts(root))
59
+
60
+ assertTrue(
61
+ result.supersededCoordinates.contains("androidx.appcompat" to "appcompat"),
62
+ "a declared-but-versionless coordinate must still be superseded",
63
+ )
64
+ assertEquals("1.7.1", result.dependencies.first { it.groupId == "androidx.appcompat" }.version)
65
+ }
66
+
67
+ @Test
68
+ fun `discovers a module's direct external dependencies, skipping project deps and non-publishable coordinates`() {
69
+ val root = ProjectBuilder.builder().build()
70
+ val consumer = ProjectBuilder.builder().withParent(root).withName("BrownfieldLib").build()
71
+ val embeddedModule = ProjectBuilder.builder().withParent(root).withName("react-native-fake-module").build()
72
+ val siblingProject = ProjectBuilder.builder().withParent(root).withName("some-other-module").build()
73
+
74
+ embeddedModule.configurations.create("implementation")
75
+ embeddedModule.configurations.create("api")
76
+ embeddedModule.configurations.create("runtimeOnly")
77
+
78
+ embeddedModule.dependencies.add("implementation", "androidx.appcompat:appcompat:1.7.1")
79
+ embeddedModule.dependencies.add("implementation", "com.facebook.react:react-native:+")
80
+ embeddedModule.dependencies.add("runtimeOnly", "androidx.annotation:annotation:1.9.1")
81
+ embeddedModule.dependencies.add(
82
+ "implementation",
83
+ embeddedModule.dependencies.project(mapOf("path" to siblingProject.path)),
84
+ )
85
+
86
+ consumer.configurations.create("api")
87
+
88
+ val result = RncTransitiveDependencyDiscoverer(consumer).discover(defaultArtifacts(root))
89
+ val discovered = result.dependencies
90
+
91
+ assertEquals(2, discovered.size)
92
+ assertTrue(
93
+ discovered.contains(
94
+ DependencyInfo("androidx.appcompat", "appcompat", "1.7.1", "compile", false),
95
+ ),
96
+ )
97
+ assertTrue(
98
+ discovered.contains(
99
+ DependencyInfo("androidx.annotation", "annotation", "1.9.1", "runtime", false),
100
+ ),
101
+ )
102
+ assertFalse(discovered.contains(DependencyInfo("com.facebook.react", "react-native", "+", "compile", false)))
103
+ assertFalse(discovered.contains(DependencyInfo(root.name, "some-other-module", "unspecified", "compile", false)))
104
+ assertTrue(result.supersededCoordinates.isEmpty())
105
+ }
106
+
107
+ @Test
108
+ fun `publishes a runtimeOnly dependency under Maven runtime scope, not compile`() {
109
+ val root = ProjectBuilder.builder().build()
110
+ val consumer = ProjectBuilder.builder().withParent(root).withName("BrownfieldLib").build()
111
+ val embeddedModule = ProjectBuilder.builder().withParent(root).withName("react-native-fake-module").build()
112
+
113
+ embeddedModule.configurations.create("implementation")
114
+ embeddedModule.configurations.create("api")
115
+ embeddedModule.configurations.create("runtimeOnly")
116
+ embeddedModule.dependencies.add("runtimeOnly", "androidx.annotation:annotation:1.9.1")
117
+
118
+ val discovered = RncTransitiveDependencyDiscoverer(consumer).discover(defaultArtifacts(root)).dependencies
119
+
120
+ val annotationDep = discovered.first { it.groupId == "androidx.annotation" }
121
+ assertEquals("runtime", annotationDep.scope)
122
+ }
123
+
124
+ @Test
125
+ fun `a module's higher version requirement wins over, and supersedes, the consumer's own lower declaration`() {
126
+ val root = ProjectBuilder.builder().build()
127
+ val consumer = ProjectBuilder.builder().withParent(root).withName("BrownfieldLib").build()
128
+ val embeddedModule = ProjectBuilder.builder().withParent(root).withName("react-native-fake-module").build()
129
+
130
+ embeddedModule.configurations.create("implementation")
131
+ embeddedModule.configurations.create("api")
132
+ embeddedModule.configurations.create("runtimeOnly")
133
+ embeddedModule.dependencies.add("implementation", "androidx.appcompat:appcompat:1.7.1")
134
+
135
+ consumer.configurations.create("api")
136
+ consumer.dependencies.add("api", "androidx.appcompat:appcompat:1.6.0")
137
+
138
+ val result = RncTransitiveDependencyDiscoverer(consumer).discover(defaultArtifacts(root))
139
+
140
+ val appcompatDep = result.dependencies.first { it.groupId == "androidx.appcompat" }
141
+ assertEquals("1.7.1", appcompatDep.version, "the module's higher requirement must win mediation")
142
+ assertTrue(
143
+ result.supersededCoordinates.contains("androidx.appcompat" to "appcompat"),
144
+ "the consumer's stale lower-version entry must be marked for replacement",
145
+ )
146
+ }
147
+
148
+ @Test
149
+ fun `does not regress or supersede when the consumer's own version already satisfies the module`() {
150
+ val root = ProjectBuilder.builder().build()
151
+ val consumer = ProjectBuilder.builder().withParent(root).withName("BrownfieldLib").build()
152
+ val embeddedModule = ProjectBuilder.builder().withParent(root).withName("react-native-fake-module").build()
153
+
154
+ embeddedModule.configurations.create("implementation")
155
+ embeddedModule.configurations.create("api")
156
+ embeddedModule.configurations.create("runtimeOnly")
157
+ embeddedModule.dependencies.add("implementation", "androidx.appcompat:appcompat:1.6.0")
158
+
159
+ consumer.configurations.create("api")
160
+ consumer.dependencies.add("api", "androidx.appcompat:appcompat:1.7.1")
161
+
162
+ val result = RncTransitiveDependencyDiscoverer(consumer).discover(defaultArtifacts(root))
163
+
164
+ val appcompatDep = result.dependencies.first { it.groupId == "androidx.appcompat" }
165
+ assertEquals("1.7.1", appcompatDep.version, "the consumer's already-higher version must be preserved, not downgraded")
166
+ assertTrue(
167
+ result.supersededCoordinates.contains("androidx.appcompat" to "appcompat"),
168
+ "still marked superseded so the injector re-declares it at the mediated (unchanged) version instead of duplicating it",
169
+ )
170
+ }
171
+ }
@@ -0,0 +1,47 @@
1
+ package com.callstack.react.brownfield.expo
2
+
3
+ import com.callstack.react.brownfield.shared.VersionMediatingDependencySet
4
+ import org.gradle.api.Project
5
+ import org.gradle.testfixtures.ProjectBuilder
6
+ import org.junit.jupiter.api.Test
7
+ import kotlin.test.assertEquals
8
+ import kotlin.test.assertTrue
9
+
10
+ class ExpoPublishingHelperGradleFallbackTest {
11
+ @Test
12
+ fun `Gradle-fallback discovery picks up runtimeOnly dependencies`() {
13
+ val root = ProjectBuilder.builder().build()
14
+ val expoPkgProject = ProjectBuilder.builder().withParent(root).withName("expo-fake-module").build()
15
+
16
+ expoPkgProject.configurations.create("implementation")
17
+ expoPkgProject.configurations.create("api")
18
+ expoPkgProject.configurations.create("runtimeOnly")
19
+ expoPkgProject.dependencies.add("runtimeOnly", "androidx.annotation:annotation:1.9.1")
20
+
21
+ val helper =
22
+ object : ExpoPublishingHelper(brownfieldAppProject = root) {
23
+ fun exposedAppend(
24
+ pkgProject: Project,
25
+ deps: VersionMediatingDependencySet,
26
+ ) {
27
+ appendExpoTransitiveDependenciesFromGradle(pkgProject, deps)
28
+ }
29
+ }
30
+
31
+ val discovered = VersionMediatingDependencySet()
32
+ helper.exposedAppend(expoPkgProject, discovered)
33
+
34
+ // Coordinate-only check: VersionMediatingDependencySet keys on groupId/artifactId, so
35
+ // passing a scope here would be inert and would read as contradicting the scope
36
+ // assertion below.
37
+ assertTrue(
38
+ discovered.any { it.groupId == "androidx.annotation" && it.artifactId == "annotation" },
39
+ "expected the runtimeOnly dependency to be discovered via the Gradle fallback path",
40
+ )
41
+ assertEquals(
42
+ "runtime",
43
+ discovered.first { it.groupId == "androidx.annotation" }.scope,
44
+ "runtimeOnly dependencies should be published under Maven's runtime scope, not compile",
45
+ )
46
+ }
47
+ }
@@ -0,0 +1,155 @@
1
+ package com.callstack.react.brownfield.plugin
2
+
3
+ import org.gradle.testkit.runner.GradleRunner
4
+ import org.junit.jupiter.api.Assumptions.assumeTrue
5
+ import org.junit.jupiter.api.Test
6
+ import org.junit.jupiter.api.io.TempDir
7
+ import org.junit.jupiter.params.ParameterizedTest
8
+ import org.junit.jupiter.params.provider.MethodSource
9
+ import java.io.File
10
+ import kotlin.test.assertTrue
11
+
12
+ /**
13
+ * Configures a minimal android library against every AGP version in the matrix declared in
14
+ * `build.gradle.kts`.
15
+ *
16
+ * The plugin compiles against the OLDEST supported AGP, which is what stops it from reaching for an
17
+ * API newer AGP added. That leaves the opposite direction uncovered: an API the floor has but a
18
+ * newer AGP removed, renamed or narrowed compiles cleanly and only breaks at the consumer. This
19
+ * test is the coverage for that direction, so the newest entry in the matrix is the one earning its
20
+ * keep here.
21
+ *
22
+ * Skipped without an Android SDK, since the plugin requires `com.android.library`.
23
+ */
24
+ class AgpCompatibilityMatrixTest {
25
+ @ParameterizedTest(name = "configures cleanly on AGP {0}")
26
+ @MethodSource("agpVersions")
27
+ fun `the plugin configures an android library on every supported AGP`(
28
+ agpVersion: String,
29
+ @TempDir projectDir: File,
30
+ ) {
31
+ val androidSdk = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT")
32
+ assumeTrue(
33
+ androidSdk != null && File(androidSdk).isDirectory,
34
+ "no Android SDK available (ANDROID_HOME/ANDROID_SDK_ROOT); skipping AGP-dependent test",
35
+ )
36
+
37
+ writeFixture(projectDir, androidSdk!!)
38
+
39
+ // `--dry-run` still runs configuration in full — variant callbacks, source set wiring, task
40
+ // graph construction — which is where every AGP API the plugin touches is exercised. It just
41
+ // 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()
48
+ }
49
+
50
+ @Test
51
+ fun `the apply-time gate rejects AGP older than the floor`(
52
+ @TempDir projectDir: File,
53
+ ) {
54
+ val androidSdk = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT")
55
+ assumeTrue(
56
+ androidSdk != null && File(androidSdk).isDirectory,
57
+ "no Android SDK available (ANDROID_HOME/ANDROID_SDK_ROOT); skipping AGP-dependent test",
58
+ )
59
+
60
+ writeFixture(projectDir, androidSdk!!)
61
+
62
+ val belowFloor = requireNotNull(System.getProperty("agp.belowFloor.version"))
63
+ val result =
64
+ GradleRunner.create()
65
+ .withProjectDir(projectDir)
66
+ .withPluginClasspath(pluginClasspathFor(belowFloor))
67
+ .withArguments("tasks")
68
+ .buildAndFail()
69
+
70
+ assertTrue(
71
+ result.output.contains("requires Android Gradle Plugin"),
72
+ "expected the plugin's own AGP gate to reject AGP $belowFloor, but the build failed " +
73
+ "for some other reason:\n${result.output}",
74
+ )
75
+ }
76
+
77
+ private fun pluginClasspathFor(agpVersion: String): List<File> {
78
+ val raw =
79
+ requireNotNull(System.getProperty("agp.classpath.$agpVersion")) {
80
+ "no TestKit classpath was injected for AGP $agpVersion; check the agpMatrix wiring in build.gradle.kts"
81
+ }
82
+ return raw.split(File.pathSeparator).filter { it.isNotBlank() }.map(::File)
83
+ }
84
+
85
+ private fun writeFixture(
86
+ projectDir: File,
87
+ androidSdk: String,
88
+ ) {
89
+ File(projectDir, "settings.gradle.kts").writeText(
90
+ """
91
+ dependencyResolutionManagement {
92
+ repositories {
93
+ google()
94
+ mavenCentral()
95
+ }
96
+ }
97
+ rootProject.name = "agp-matrix-consumer"
98
+ include(":app")
99
+ include(":brownfieldlib")
100
+ """.trimIndent(),
101
+ )
102
+ File(projectDir, "local.properties").writeText("sdk.dir=$androidSdk\n")
103
+ // Stands in for the RN app project. It has to be a real `com.android.application` because
104
+ // the brownfield plugin wires into AGP's own app-side tasks; the extra registrations cover
105
+ // the tasks the React Native Gradle plugin would otherwise contribute. Empty registrations
106
+ // are enough: this test is about AGP API compatibility, not about what those tasks produce.
107
+ File(projectDir, "app").mkdirs()
108
+ File(projectDir, "app/build.gradle.kts").writeText(
109
+ """
110
+ plugins {
111
+ id("com.android.application")
112
+ }
113
+
114
+ android {
115
+ namespace = "com.example.agpmatrix.app"
116
+ compileSdk = 34
117
+ }
118
+
119
+ listOf(
120
+ "generateAutolinkingPackageList",
121
+ "createBundleDebugJsAndAssets",
122
+ "createBundleReleaseJsAndAssets",
123
+ ).forEach { name -> tasks.register(name) }
124
+ """.trimIndent(),
125
+ )
126
+
127
+ File(projectDir, "brownfieldlib").mkdirs()
128
+ File(projectDir, "brownfieldlib/build.gradle.kts").writeText(
129
+ """
130
+ plugins {
131
+ id("com.android.library")
132
+ id("com.callstack.react.brownfield")
133
+ `maven-publish`
134
+ }
135
+
136
+ android {
137
+ namespace = "com.example.agpmatrix"
138
+ compileSdk = 34
139
+ }
140
+
141
+ // Contributed by the React Native Gradle plugin in a real consumer; stubbed here for the
142
+ // same reason as the app-side tasks above.
143
+ tasks.register("generateCodegenSchemaFromJavaScript")
144
+ """.trimIndent(),
145
+ )
146
+ }
147
+
148
+ private companion object {
149
+ @JvmStatic
150
+ fun agpVersions(): List<String> =
151
+ requireNotNull(System.getProperty("agp.matrix.versions")) {
152
+ "agp.matrix.versions was not injected; check the agpMatrix wiring in build.gradle.kts"
153
+ }.split(",").filter { it.isNotBlank() }
154
+ }
155
+ }
@@ -0,0 +1,110 @@
1
+ package com.callstack.react.brownfield.plugin
2
+
3
+ import com.callstack.react.brownfield.shared.Constants
4
+ import org.gradle.testkit.runner.GradleRunner
5
+ import org.junit.jupiter.api.Assumptions.assumeTrue
6
+ import org.junit.jupiter.api.Test
7
+ import org.junit.jupiter.api.io.TempDir
8
+ import java.io.File
9
+ import kotlin.test.assertFalse
10
+
11
+ /**
12
+ * Guards the pre-migration consumer shape no in-repo app has any more: one that still hand-registers
13
+ * `removeDependenciesFromModuleFile` as the setup docs instruct. If the plugin ever claims that name
14
+ * again, configuration fails outright for every consumer who hasn't migrated.
15
+ *
16
+ * Skipped without an Android SDK, since the plugin requires `com.android.library`.
17
+ */
18
+ class LegacyConsumerConfigurationTest {
19
+ @Test
20
+ fun `a project still registering the documented legacy task configures successfully`(
21
+ @TempDir projectDir: File,
22
+ ) {
23
+ val androidSdk = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT")
24
+ assumeTrue(
25
+ androidSdk != null && File(androidSdk).isDirectory,
26
+ "no Android SDK available (ANDROID_HOME/ANDROID_SDK_ROOT); skipping AGP-dependent test",
27
+ )
28
+
29
+ writeLegacyConsumerFixture(projectDir, androidSdk!!)
30
+
31
+ val result =
32
+ GradleRunner.create()
33
+ .withProjectDir(projectDir)
34
+ .withPluginClasspath()
35
+ .withArguments("tasks", "--stacktrace")
36
+ .forwardOutput()
37
+ .build()
38
+
39
+ // build() already failed above on any unrelated fixture breakage; this narrows a failure
40
+ // here to the duplicate-task-name case specifically.
41
+ assertFalse(
42
+ result.output.contains("as a task with that name already exists"),
43
+ "build output reports a duplicate task name; the plugin most likely claimed " +
44
+ "'${Constants.LEGACY_CONSUMER_MODULE_METADATA_TASK_NAME}', which the setup docs " +
45
+ "reserve for consumers",
46
+ )
47
+ }
48
+
49
+ private fun writeLegacyConsumerFixture(
50
+ projectDir: File,
51
+ androidSdk: String,
52
+ ) {
53
+ File(projectDir, "settings.gradle.kts").writeText(
54
+ """
55
+ dependencyResolutionManagement {
56
+ repositories {
57
+ google()
58
+ mavenCentral()
59
+ }
60
+ }
61
+ rootProject.name = "legacy-consumer"
62
+ include(":app")
63
+ include(":brownfieldlib")
64
+ """.trimIndent(),
65
+ )
66
+ File(projectDir, "local.properties").writeText("sdk.dir=$androidSdk\n")
67
+ // ArtifactsResolver looks for an `app` project; an empty stub is enough, this test is about
68
+ // the task container, not discovery.
69
+ File(projectDir, "app").mkdirs()
70
+ File(projectDir, "app/build.gradle.kts").writeText("")
71
+
72
+ // The `tasks.register("removeDependenciesFromModuleFile")` block below is verbatim from
73
+ // the published setup docs. Do not "modernize" it — its whole purpose is to reproduce
74
+ // what a not-yet-migrated consumer actually has in their build script.
75
+ File(projectDir, "brownfieldlib").mkdirs()
76
+ File(projectDir, "brownfieldlib/build.gradle.kts").writeText(
77
+ """
78
+ import groovy.json.JsonOutput
79
+ import groovy.json.JsonSlurper
80
+
81
+ plugins {
82
+ id("com.android.library")
83
+ id("com.callstack.react.brownfield")
84
+ `maven-publish`
85
+ }
86
+
87
+ android {
88
+ namespace = "com.example.legacyconsumer"
89
+ compileSdk = 34
90
+ }
91
+
92
+ val moduleBuildDir: Directory = layout.buildDirectory.get()
93
+
94
+ tasks.register("removeDependenciesFromModuleFile") {
95
+ doLast {
96
+ file("${'$'}moduleBuildDir/publications/mavenAar/module.json").run {
97
+ val json = inputStream().use { JsonSlurper().parse(it) as Map<String, Any> }
98
+ (json["variants"] as? List<MutableMap<String, Any>>)?.forEach { variant ->
99
+ (variant["dependencies"] as? MutableList<Map<String, Any>>)?.removeAll {
100
+ it["group"] == rootProject.name
101
+ }
102
+ }
103
+ writer().use { it.write(JsonOutput.prettyPrint(JsonOutput.toJson(json))) }
104
+ }
105
+ }
106
+ }
107
+ """.trimIndent(),
108
+ )
109
+ }
110
+ }
@@ -0,0 +1,67 @@
1
+ package com.callstack.react.brownfield.shared
2
+
3
+ import org.junit.jupiter.api.Test
4
+ import kotlin.test.assertFalse
5
+ import kotlin.test.assertTrue
6
+
7
+ class DependencyPublishabilityTest {
8
+ @Test
9
+ fun `rejects a fully dynamic version`() {
10
+ val dep = DependencyInfo("com.facebook.react", "react-native", "+", "compile", false)
11
+ assertFalse(isPublishableCoordinate(dep))
12
+ }
13
+
14
+ @Test
15
+ fun `rejects a partially dynamic version`() {
16
+ val dep = DependencyInfo("androidx.core", "core-ktx", "1.+", "compile", false)
17
+ assertFalse(isPublishableCoordinate(dep))
18
+ }
19
+
20
+ @Test
21
+ fun `rejects a null version`() {
22
+ val dep = DependencyInfo("com.facebook.react", "react-android", null, "compile", false)
23
+ assertFalse(isPublishableCoordinate(dep))
24
+ }
25
+
26
+ @Test
27
+ fun `rejects a blank version`() {
28
+ val dep = DependencyInfo("com.facebook.react", "react-android", " ", "compile", false)
29
+ assertFalse(isPublishableCoordinate(dep))
30
+ }
31
+
32
+ @Test
33
+ fun `accepts a normal pinned version`() {
34
+ val dep = DependencyInfo("androidx.appcompat", "appcompat", "1.7.1", "compile", false)
35
+ assertTrue(isPublishableCoordinate(dep))
36
+ }
37
+
38
+ @Test
39
+ fun `rejects latest_release`() {
40
+ val dep = DependencyInfo("androidx.core", "core-ktx", "latest.release", "compile", false)
41
+ assertFalse(isPublishableCoordinate(dep))
42
+ }
43
+
44
+ @Test
45
+ fun `rejects latest_integration`() {
46
+ val dep = DependencyInfo("androidx.core", "core-ktx", "latest.integration", "compile", false)
47
+ assertFalse(isPublishableCoordinate(dep))
48
+ }
49
+
50
+ @Test
51
+ fun `rejects a closed Maven version range`() {
52
+ val dep = DependencyInfo("androidx.core", "core-ktx", "[1.0,2.0]", "compile", false)
53
+ assertFalse(isPublishableCoordinate(dep))
54
+ }
55
+
56
+ @Test
57
+ fun `rejects a half-open Maven version range`() {
58
+ val dep = DependencyInfo("androidx.core", "core-ktx", "[1.0,2.0)", "compile", false)
59
+ assertFalse(isPublishableCoordinate(dep))
60
+ }
61
+
62
+ @Test
63
+ fun `rejects an unbounded Maven version range`() {
64
+ val dep = DependencyInfo("androidx.core", "core-ktx", "[1.0,)", "compile", false)
65
+ assertFalse(isPublishableCoordinate(dep))
66
+ }
67
+ }