@callstack/react-native-brownfield 5.0.2 → 5.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.
- package/CHANGELOG.md +35 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +47 -4
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +23 -1
- package/gradle-plugin/brownfield/build.gradle.kts +7 -0
- package/gradle-plugin/brownfield/gradle/libs.versions.toml +2 -0
- 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 +92 -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/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/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/BrownfieldReactHostPreloader.h +30 -0
- package/ios/BrownfieldReactHostPreloader.m +113 -0
- package/ios/BrownfieldSurfaceRegistry.swift +93 -0
- package/ios/Expo/ExpoHostRuntime.swift +70 -8
- package/ios/JSBundleLoadObserver.swift +64 -17
- package/ios/ReactHostPreloading.swift +177 -0
- package/ios/ReactNativeBrownfield.swift +36 -1
- package/ios/Vanilla/ReactNativeHostRuntime.swift +28 -6
- 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/template/android/ReactNativeHostManager.post55.kt +6 -1
- package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.post55.kt +6 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/package.json +2 -2
- package/src/expo-config-plugin/android/utils/constants.ts +1 -1
- package/src/expo-config-plugin/template/android/ReactNativeHostManager.post55.kt +6 -1
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.shared
|
|
2
|
+
|
|
3
|
+
import groovy.util.NodeList
|
|
4
|
+
import groovy.xml.XmlParser
|
|
5
|
+
import org.gradle.testfixtures.ProjectBuilder
|
|
6
|
+
import org.junit.jupiter.api.Test
|
|
7
|
+
import kotlin.test.assertEquals
|
|
8
|
+
import kotlin.test.assertNotNull
|
|
9
|
+
import kotlin.test.assertNull
|
|
10
|
+
import kotlin.test.assertTrue
|
|
11
|
+
|
|
12
|
+
class PublishingMetadataInjectorTaskRegistrationTest {
|
|
13
|
+
@Test
|
|
14
|
+
fun `does not claim the task name the setup docs tell consumers to register`() {
|
|
15
|
+
val project = ProjectBuilder.builder().build()
|
|
16
|
+
|
|
17
|
+
PublishingMetadataInjector(project)
|
|
18
|
+
|
|
19
|
+
assertNull(
|
|
20
|
+
project.tasks.findByName(Constants.LEGACY_CONSUMER_MODULE_METADATA_TASK_NAME),
|
|
21
|
+
"the plugin must leave this name free: the published setup docs tell every bare-RN " +
|
|
22
|
+
"consumer to register it themselves, and the plugin applies before their script body",
|
|
23
|
+
)
|
|
24
|
+
assertNotNull(
|
|
25
|
+
project.tasks.findByName(Constants.MODULE_METADATA_POST_PROCESS_TASK_NAME),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `a not-yet-migrated consumer can still register the legacy task after the plugin has run`() {
|
|
31
|
+
// The upgrade path at the task-container level: plugin registers during apply(), then the
|
|
32
|
+
// consumer's script body registers the task the docs told them to. Must not throw.
|
|
33
|
+
val project = ProjectBuilder.builder().build()
|
|
34
|
+
|
|
35
|
+
PublishingMetadataInjector(project)
|
|
36
|
+
project.tasks.register(Constants.LEGACY_CONSUMER_MODULE_METADATA_TASK_NAME)
|
|
37
|
+
|
|
38
|
+
assertNotNull(project.tasks.findByName(Constants.LEGACY_CONSUMER_MODULE_METADATA_TASK_NAME))
|
|
39
|
+
assertNotNull(project.tasks.findByName(Constants.MODULE_METADATA_POST_PROCESS_TASK_NAME))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class MutatePomDependenciesNodeTest {
|
|
44
|
+
private fun dependenciesNodeFrom(xml: String): groovy.util.Node {
|
|
45
|
+
val root = XmlParser().parseText(xml)
|
|
46
|
+
return (root.get("dependencies") as NodeList).first() as groovy.util.Node
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private fun versionsFor(
|
|
50
|
+
dependenciesNode: groovy.util.Node,
|
|
51
|
+
groupId: String,
|
|
52
|
+
artifactId: String,
|
|
53
|
+
): List<String> {
|
|
54
|
+
return dependenciesNode.children()
|
|
55
|
+
.filterIsInstance<groovy.util.Node>()
|
|
56
|
+
.filter {
|
|
57
|
+
(it["groupId"] as NodeList).text() == groupId && (it["artifactId"] as NodeList).text() == artifactId
|
|
58
|
+
}
|
|
59
|
+
.map { (it["version"] as NodeList).text() }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun `appends new dependencies with no pre-existing entries`() {
|
|
64
|
+
val dependenciesNode = dependenciesNodeFrom("<project><dependencies/></project>")
|
|
65
|
+
val toInject = VersionMediatingDependencySet()
|
|
66
|
+
toInject.add(DependencyInfo("androidx.core", "core-ktx", "1.17.0", "compile", false))
|
|
67
|
+
|
|
68
|
+
mutatePomDependenciesNode(dependenciesNode, toInject) { _, _ -> false }
|
|
69
|
+
|
|
70
|
+
assertEquals(listOf("1.17.0"), versionsFor(dependenciesNode, "androidx.core", "core-ktx"))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
fun `injects into a POM that has no dependencies element at all`() {
|
|
75
|
+
// Must call resolveOrCreateDependenciesNode — inlining the fallback here would make this
|
|
76
|
+
// pass even with the production path reverted to first().
|
|
77
|
+
val root = XmlParser().parseText("<project/>")
|
|
78
|
+
val toInject = VersionMediatingDependencySet()
|
|
79
|
+
toInject.add(DependencyInfo("androidx.core", "core-ktx", "1.17.0", "compile", false))
|
|
80
|
+
|
|
81
|
+
val dependenciesNode = resolveOrCreateDependenciesNode(root)
|
|
82
|
+
mutatePomDependenciesNode(dependenciesNode, toInject) { _, _ -> false }
|
|
83
|
+
|
|
84
|
+
assertEquals(listOf("1.17.0"), versionsFor(dependenciesNode, "androidx.core", "core-ktx"))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Test
|
|
88
|
+
fun `reuses the existing dependencies element instead of appending a second one`() {
|
|
89
|
+
val root =
|
|
90
|
+
XmlParser().parseText(
|
|
91
|
+
"""
|
|
92
|
+
<project>
|
|
93
|
+
<dependencies>
|
|
94
|
+
<dependency>
|
|
95
|
+
<groupId>androidx.core</groupId>
|
|
96
|
+
<artifactId>core-ktx</artifactId>
|
|
97
|
+
<version>1.17.0</version>
|
|
98
|
+
</dependency>
|
|
99
|
+
</dependencies>
|
|
100
|
+
</project>
|
|
101
|
+
""".trimIndent(),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
val dependenciesNode = resolveOrCreateDependenciesNode(root)
|
|
105
|
+
|
|
106
|
+
assertEquals(
|
|
107
|
+
1,
|
|
108
|
+
(root.get("dependencies") as NodeList).size,
|
|
109
|
+
"a POM that already has <dependencies> must not gain a second one",
|
|
110
|
+
)
|
|
111
|
+
assertEquals(listOf("1.17.0"), versionsFor(dependenciesNode, "androidx.core", "core-ktx"))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Test
|
|
115
|
+
fun `removes an unrelated excluded entry that is not being re-injected`() {
|
|
116
|
+
val dependenciesNode =
|
|
117
|
+
dependenciesNodeFrom(
|
|
118
|
+
"""
|
|
119
|
+
<project>
|
|
120
|
+
<dependencies>
|
|
121
|
+
<dependency>
|
|
122
|
+
<groupId>com.rnapp</groupId>
|
|
123
|
+
<artifactId>brownfieldlib</artifactId>
|
|
124
|
+
<version>unspecified</version>
|
|
125
|
+
</dependency>
|
|
126
|
+
</dependencies>
|
|
127
|
+
</project>
|
|
128
|
+
""".trimIndent(),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
mutatePomDependenciesNode(dependenciesNode, VersionMediatingDependencySet()) { groupId, _ ->
|
|
132
|
+
groupId == "com.rnapp"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
assertTrue(versionsFor(dependenciesNode, "com.rnapp", "brownfieldlib").isEmpty())
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Test
|
|
139
|
+
fun `a superseded stale entry is replaced by the mediated version, not left duplicated or missing`() {
|
|
140
|
+
// Consumer hand-declares 1.6.0, embedded module needs 1.7.1, so the coordinate is
|
|
141
|
+
// superseded and shouldExclude matches it. The mediated 1.7.1 must survive exactly once.
|
|
142
|
+
val dependenciesNode =
|
|
143
|
+
dependenciesNodeFrom(
|
|
144
|
+
"""
|
|
145
|
+
<project>
|
|
146
|
+
<dependencies>
|
|
147
|
+
<dependency>
|
|
148
|
+
<groupId>androidx.appcompat</groupId>
|
|
149
|
+
<artifactId>appcompat</artifactId>
|
|
150
|
+
<version>1.6.0</version>
|
|
151
|
+
<scope>runtime</scope>
|
|
152
|
+
</dependency>
|
|
153
|
+
</dependencies>
|
|
154
|
+
</project>
|
|
155
|
+
""".trimIndent(),
|
|
156
|
+
)
|
|
157
|
+
val mediated = VersionMediatingDependencySet()
|
|
158
|
+
mediated.add(DependencyInfo("androidx.appcompat", "appcompat", "1.7.1", "compile", false))
|
|
159
|
+
|
|
160
|
+
mutatePomDependenciesNode(dependenciesNode, mediated) { groupId, artifactId ->
|
|
161
|
+
groupId == "androidx.appcompat" && artifactId == "appcompat"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
assertEquals(listOf("1.7.1"), versionsFor(dependenciesNode, "androidx.appcompat", "appcompat"))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class MutateModuleJsonVariantsTest {
|
|
169
|
+
private fun jsonWithVariant(dependencies: MutableList<MutableMap<String, Any>>): MutableMap<String, Any> {
|
|
170
|
+
return mutableMapOf(
|
|
171
|
+
"variants" to
|
|
172
|
+
mutableListOf(
|
|
173
|
+
mutableMapOf<String, Any>(
|
|
174
|
+
"name" to "runtimeElements",
|
|
175
|
+
"dependencies" to dependencies,
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Suppress("UNCHECKED_CAST")
|
|
182
|
+
private fun dependenciesOf(json: Map<*, *>): List<Map<String, Any>> {
|
|
183
|
+
val variants = json["variants"] as List<Map<String, Any>>
|
|
184
|
+
return variants.first()["dependencies"] as List<Map<String, Any>>
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@Test
|
|
188
|
+
fun `appends new dependencies with no pre-existing entries`() {
|
|
189
|
+
val json = jsonWithVariant(mutableListOf())
|
|
190
|
+
val toInject = VersionMediatingDependencySet()
|
|
191
|
+
toInject.add(DependencyInfo("androidx.core", "core-ktx", "1.17.0", "compile", false))
|
|
192
|
+
|
|
193
|
+
mutateModuleJsonVariants(json, toInject) { _, _ -> false }
|
|
194
|
+
|
|
195
|
+
val deps = dependenciesOf(json)
|
|
196
|
+
assertEquals(1, deps.size)
|
|
197
|
+
assertEquals("androidx.core", deps.first()["group"])
|
|
198
|
+
assertEquals("core-ktx", deps.first()["module"])
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Test
|
|
202
|
+
fun `a superseded stale entry is replaced by the mediated version, not left duplicated or missing`() {
|
|
203
|
+
val json =
|
|
204
|
+
jsonWithVariant(
|
|
205
|
+
mutableListOf(
|
|
206
|
+
mutableMapOf("group" to "androidx.appcompat", "module" to "appcompat"),
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
val mediated = VersionMediatingDependencySet()
|
|
210
|
+
mediated.add(DependencyInfo("androidx.appcompat", "appcompat", "1.7.1", "compile", false))
|
|
211
|
+
|
|
212
|
+
mutateModuleJsonVariants(json, mediated) { group, module ->
|
|
213
|
+
group == "androidx.appcompat" && module == "appcompat"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
val deps = dependenciesOf(json)
|
|
217
|
+
assertEquals(1, deps.size, "expected exactly one entry, not zero (deleted) or two (duplicated)")
|
|
218
|
+
@Suppress("UNCHECKED_CAST")
|
|
219
|
+
val version = (deps.first()["version"] as? Map<String, Any>)?.get("requires")
|
|
220
|
+
assertEquals("1.7.1", version)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.shared
|
|
2
|
+
|
|
3
|
+
import org.junit.jupiter.api.Test
|
|
4
|
+
import kotlin.test.assertEquals
|
|
5
|
+
import kotlin.test.assertFalse
|
|
6
|
+
import kotlin.test.assertTrue
|
|
7
|
+
|
|
8
|
+
class TransitiveDependencyFilteringTest {
|
|
9
|
+
@Test
|
|
10
|
+
fun `drops a hard-excluded coordinate the RNC discoverer found, even though it has no Expo awareness`() {
|
|
11
|
+
// The RNC discoverer has no Expo blacklist, so an embedded module depending on an Expo
|
|
12
|
+
// coordinate would otherwise be published as a Maven dependency that resolves nowhere.
|
|
13
|
+
val transitiveDeps = VersionMediatingDependencySet()
|
|
14
|
+
transitiveDeps.add(DependencyInfo("androidx.core", "core-ktx", "1.17.0", "compile", false))
|
|
15
|
+
transitiveDeps.add(DependencyInfo("host.exp.exponent", "expo.modules.device", "56.0.4", "compile", false))
|
|
16
|
+
|
|
17
|
+
val hardExcludePredicate: (String, String) -> Boolean = { groupId, _ -> groupId == "host.exp.exponent" }
|
|
18
|
+
dropHardExcludedDependencies(transitiveDeps, hardExcludePredicate)
|
|
19
|
+
|
|
20
|
+
assertTrue(transitiveDeps.contains(DependencyInfo("androidx.core", "core-ktx", "1.17.0", "compile", false)))
|
|
21
|
+
assertFalse(transitiveDeps.any { it.groupId == "host.exp.exponent" })
|
|
22
|
+
assertEquals(1, transitiveDeps.size)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test
|
|
26
|
+
fun `does not drop a superseded coordinate that is meant to still be injected`() {
|
|
27
|
+
// hardExcludePredicate must never include the supersede check, or a mediated
|
|
28
|
+
// replacement for a consumer's stale hand-declared dependency would be dropped too.
|
|
29
|
+
val transitiveDeps = VersionMediatingDependencySet()
|
|
30
|
+
transitiveDeps.add(DependencyInfo("androidx.appcompat", "appcompat", "1.7.1", "compile", false))
|
|
31
|
+
|
|
32
|
+
dropHardExcludedDependencies(transitiveDeps) { _, _ -> false }
|
|
33
|
+
|
|
34
|
+
assertTrue(transitiveDeps.contains(DependencyInfo("androidx.appcompat", "appcompat", "1.7.1", "compile", false)))
|
|
35
|
+
assertEquals(1, transitiveDeps.size)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.shared
|
|
2
|
+
|
|
3
|
+
import org.junit.jupiter.api.Test
|
|
4
|
+
import kotlin.test.assertEquals
|
|
5
|
+
import kotlin.test.assertTrue
|
|
6
|
+
|
|
7
|
+
class VersionMediatingDependencySetTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun `keeps the higher version when the same coordinate is added twice`() {
|
|
10
|
+
val set = VersionMediatingDependencySet()
|
|
11
|
+
|
|
12
|
+
set.add(DependencyInfo("androidx.appcompat", "appcompat", "1.6.0", "compile", false))
|
|
13
|
+
set.add(DependencyInfo("androidx.appcompat", "appcompat", "1.7.1", "compile", false))
|
|
14
|
+
|
|
15
|
+
assertEquals(1, set.size)
|
|
16
|
+
assertTrue(set.contains(DependencyInfo("androidx.appcompat", "appcompat", "1.7.1", "compile", false)))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
fun `does not downgrade when a lower version is added second`() {
|
|
21
|
+
val set = VersionMediatingDependencySet()
|
|
22
|
+
|
|
23
|
+
set.add(DependencyInfo("androidx.appcompat", "appcompat", "1.7.1", "compile", false))
|
|
24
|
+
set.add(DependencyInfo("androidx.appcompat", "appcompat", "1.6.0", "compile", false))
|
|
25
|
+
|
|
26
|
+
val kept = set.first { it.groupId == "androidx.appcompat" }
|
|
27
|
+
assertEquals("1.7.1", kept.version)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test
|
|
31
|
+
fun `removeAll removes matching entries and returns them`() {
|
|
32
|
+
val set = VersionMediatingDependencySet()
|
|
33
|
+
set.add(DependencyInfo("host.exp.exponent", "expo", "1.0.0", "compile", false))
|
|
34
|
+
set.add(DependencyInfo("androidx.core", "core-ktx", "1.17.0", "compile", false))
|
|
35
|
+
|
|
36
|
+
val removed = set.removeAll { it.groupId == "host.exp.exponent" }
|
|
37
|
+
|
|
38
|
+
assertEquals(1, removed.size)
|
|
39
|
+
assertEquals(1, set.size)
|
|
40
|
+
assertTrue(set.contains(DependencyInfo("androidx.core", "core-ktx", "1.17.0", "compile", false)))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.utils
|
|
2
|
+
|
|
3
|
+
import org.junit.jupiter.api.Test
|
|
4
|
+
import kotlin.test.assertTrue
|
|
5
|
+
|
|
6
|
+
class ExtensionTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun `experimentalIncludeTransitiveDependencies defaults to true`() {
|
|
9
|
+
assertTrue(Extension().experimentalIncludeTransitiveDependencies)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates and starts the React Host, but does not create a view. React Native loads and evaluates
|
|
7
|
+
* the JavaScript bundle immediately, and not when it mounts the first React Native view.
|
|
8
|
+
* `viewWithModuleName:` makes the same call internally, thus a view that you create later uses the
|
|
9
|
+
* host that is ready.
|
|
10
|
+
*
|
|
11
|
+
* This class is in Objective-C, because Swift cannot make this call:
|
|
12
|
+
* `RCTReactNativeFactory.devMenuConfiguration` is nullable, but the related parameter is not
|
|
13
|
+
* nullable.
|
|
14
|
+
*/
|
|
15
|
+
@interface BrownfieldReactHostPreloader : NSObject
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Call this method on the main thread. If a host is already available, this method does nothing.
|
|
19
|
+
*
|
|
20
|
+
* @param reactNativeFactory An `RCTReactNativeFactory`, or a subclass, for example
|
|
21
|
+
* `ExpoReactNativeFactory`. The type is `id`, because this header must stay free of the React
|
|
22
|
+
* Native imports.
|
|
23
|
+
* @param launchOptions The launch options for the React Host.
|
|
24
|
+
*/
|
|
25
|
+
+ (void)preloadWithReactNativeFactory:(id)reactNativeFactory
|
|
26
|
+
launchOptions:(nullable NSDictionary *)launchOptions;
|
|
27
|
+
|
|
28
|
+
@end
|
|
29
|
+
|
|
30
|
+
NS_ASSUME_NONNULL_END
|