@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.
- package/CHANGELOG.md +35 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +22 -4
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +3 -1
- package/gradle-plugin/brownfield/build.gradle.kts +74 -2
- package/gradle-plugin/brownfield/gradle/libs.versions.toml +7 -2
- package/gradle-plugin/brownfield/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/gradle-plugin/brownfield/gradle.properties +1 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/artifacts/RncTransitiveDependencyDiscoverer.kt +96 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/ExpoPublishingHelper.kt +10 -214
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/expo/utils/BrownfieldPublishingInfo.kt +7 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +123 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +6 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt +12 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/{expo/utils/BrownfieldPrimitives.kt → shared/DependencyInfo.kt} +3 -8
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/DependencyPublishability.kt +22 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/GradleDependencyCollector.kt +44 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt +2 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjector.kt +181 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/TransitiveDependencyFiltering.kt +17 -0
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/{expo/utils → shared}/VersionMediatingDependencySet.kt +1 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +11 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/artifacts/RncTransitiveDependencyDiscovererTest.kt +171 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/expo/ExpoPublishingHelperGradleFallbackTest.kt +47 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +155 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/LegacyConsumerConfigurationTest.kt +110 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/DependencyPublishabilityTest.kt +67 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjectorTest.kt +222 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/TransitiveDependencyFilteringTest.kt +37 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/VersionMediatingDependencySetTest.kt +42 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +11 -0
- package/ios/BrownfieldSurfaceRegistry.swift +93 -0
- package/ios/Views/ReactNativeView.swift +34 -5
- package/ios/Views/ReactNativeViewController.swift +120 -1
- package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
- package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -2
- package/lib/commonjs/expo-config-plugin/template/android/proguard-rules.pro +1 -1
- package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
- package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
- package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -2
- package/lib/module/expo-config-plugin/template/android/proguard-rules.pro +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +11 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +3 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +11 -1
- package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +3 -2
- package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/expo-config-plugin/android/utils/constants.ts +1 -1
- package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +31 -2
- package/src/expo-config-plugin/template/android/build.gradle.kts +1 -2
- package/src/expo-config-plugin/template/android/proguard-rules.pro +1 -1
- 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
|
+
}
|