@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @callstack/react-native-brownfield
2
2
 
3
+ ## 5.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#467](https://github.com/callstack/react-native-brownfield/pull/467) [`31c3939`](https://github.com/callstack/react-native-brownfield/commit/31c39399aad0efb098876d35a4fbbe80835d5dba) Thanks [@hurali97](https://github.com/hurali97)! - agp v9 compliance
8
+
9
+ - [#467](https://github.com/callstack/react-native-brownfield/pull/467) [`31c3939`](https://github.com/callstack/react-native-brownfield/commit/31c39399aad0efb098876d35a4fbbe80835d5dba) Thanks [@hurali97](https://github.com/hurali97)! - add expo sdk 58
10
+
11
+ ### Patch Changes
12
+
13
+ - [#477](https://github.com/callstack/react-native-brownfield/pull/477) [`9e57d43`](https://github.com/callstack/react-native-brownfield/commit/9e57d4384f93459526a41084ee82355dd43f4ed9) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - fix: publish dependencies of community libraries (e.g. react-native-screens, lottie-react-native) embedded in Expo brownfield AARs; the published POM now lists them, previously host apps could crash with NoClassDefFoundError
14
+ - Updated dependencies [[`31c3939`](https://github.com/callstack/react-native-brownfield/commit/31c39399aad0efb098876d35a4fbbe80835d5dba)]:
15
+ - @callstack/brownfield-cli@5.2.0
16
+
17
+ ## 5.1.1
18
+
19
+ ### Patch Changes
20
+
21
+ - [#458](https://github.com/callstack/react-native-brownfield/pull/458) [`bf976fd`](https://github.com/callstack/react-native-brownfield/commit/bf976fdebc1abb228c03213c9a1b41d9967d0909) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - Bump the Brownfield Gradle Plugin version injected by the Expo config plugin to `2.0.0-alpha09`.
22
+
23
+ - [#462](https://github.com/callstack/react-native-brownfield/pull/462) [`2f7d5da`](https://github.com/callstack/react-native-brownfield/commit/2f7d5daf661808fdfdd1caae89236dbdfec3f62e) Thanks [@likevy](https://github.com/likevy)! - fixes [#457](https://github.com/callstack/react-native-brownfield/issues/457), android back callbacks survived RN fragment view destruction, retaining delegates and intercepting back presses after re-entry
24
+
25
+ - [#470](https://github.com/callstack/react-native-brownfield/pull/470) [`09162c2`](https://github.com/callstack/react-native-brownfield/commit/09162c27bd8455b67bc0e2890a4ffe8dae5b9eae) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - Fix `popToNative()` on iOS closing every React Native screen instead of only the topmost one.
26
+
27
+ `popToNative()` is delivered as an app-wide notification, so every live React Native surface acted on it — including surfaces buried under other screens, and each surface acted twice when hosted by SwiftUI. In a stack such as RN → native → RN, a single call tore down the whole navigation stack, taking native screens with it.
28
+
29
+ A React Native surface now acts only when it is the topmost one on screen, and only once per host.
30
+
31
+ Two consequences worth knowing about:
32
+ - When no React Native surface is on screen — for example one hidden behind a full-screen native modal — `popToNative()` is now a no-op instead of popping the hidden screen. It logs when this happens.
33
+ - `popToNative()` no longer dismisses a modally presented native screen that merely _contains_ a React Native surface. Only a surface that owns a navigation stack entry pops.
34
+
35
+ - Updated dependencies []:
36
+ - @callstack/brownfield-cli@5.1.1
37
+
3
38
  ## 5.1.0
4
39
 
5
40
  ### Minor Changes
@@ -202,6 +202,19 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
202
202
  moduleName: String,
203
203
  reactDelegate: ReactDelegateWrapper? = null,
204
204
  launchOptions: Bundle? = null,
205
+ ): FrameLayout = createView(activity, moduleName, reactDelegate, launchOptions, activity)
206
+
207
+ /**
208
+ * Use the owner of the view when embedding React Native inside a fragment or
209
+ * another container whose lifetime is shorter than the activity.
210
+ * The four-argument overload retains its original JVM signature.
211
+ */
212
+ fun createView(
213
+ activity: FragmentActivity?,
214
+ moduleName: String,
215
+ reactDelegate: ReactDelegateWrapper?,
216
+ launchOptions: Bundle?,
217
+ lifecycleOwner: LifecycleOwner?,
205
218
  ): FrameLayout {
206
219
  val reactHost = shared.reactHost
207
220
  val resolvedDelegate =
@@ -215,12 +228,17 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
215
228
  }
216
229
 
217
230
  // Register back press callback
218
- activity?.onBackPressedDispatcher?.addCallback(backPressedCallback)
231
+ if (lifecycleOwner != null) {
232
+ activity?.onBackPressedDispatcher?.addCallback(lifecycleOwner, backPressedCallback)
233
+ }
219
234
  // invoked on the last RN screen exit
220
235
  resolvedDelegate.setHardwareBackHandler {
221
236
  backPressedCallback.isEnabled = false
222
- activity?.onBackPressedDispatcher?.onBackPressed()
223
- backPressedCallback.isEnabled = true
237
+ try {
238
+ activity?.onBackPressedDispatcher?.onBackPressed()
239
+ } finally {
240
+ backPressedCallback.isEnabled = true
241
+ }
224
242
  }
225
243
 
226
244
  /**
@@ -230,7 +248,7 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
230
248
  * In such a case, we set the lifeCycle observer.
231
249
  */
232
250
  if (reactDelegate == null) {
233
- activity?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate))
251
+ lifecycleOwner?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate))
234
252
  }
235
253
 
236
254
  resolvedDelegate.loadApp()
@@ -54,7 +54,9 @@ class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
54
54
  return ReactNativeBrownfield.shared.createView(
55
55
  activity,
56
56
  moduleName,
57
- this.reactDelegate as ReactDelegateWrapper
57
+ this.reactDelegate as ReactDelegateWrapper,
58
+ launchOptions = null,
59
+ lifecycleOwner = viewLifecycleOwner
58
60
  )
59
61
  }
60
62
 
@@ -103,11 +103,83 @@ repositories {
103
103
  google()
104
104
  }
105
105
 
106
+ // AGP for GradleRunner.withPluginClasspath(), which only sees runtime dependencies, so TestKit
107
+ // projects can still apply com.android.library
108
+ val testKitPluginClasspath: Configuration by configurations.creating
109
+
106
110
  dependencies {
107
- implementation(libs.agp)
108
- implementation(libs.common)
111
+ // AGP is provided by the consuming build's buildscript classpath. Keeping it
112
+ // compileOnly stops the published module from pinning AGP (and therefore the
113
+ // minimum Gradle version) for every app that applies this plugin.
114
+ compileOnly(libs.agp)
115
+ compileOnly(libs.common)
109
116
  implementation(libs.asm.commons)
110
117
  implementation(libs.versioncompare)
118
+ testImplementation(libs.agp)
119
+ testImplementation(libs.common)
120
+ testImplementation(libs.junit.jupiter)
121
+ testImplementation(gradleTestKit())
122
+ testImplementation(kotlin("test"))
123
+ testKitPluginClasspath(libs.agp)
124
+ }
125
+
126
+ tasks.pluginUnderTestMetadata {
127
+ pluginClasspath.from(testKitPluginClasspath)
128
+ }
129
+
130
+ /**
131
+ * AGP versions the plugin is tested against, oldest first.
132
+ *
133
+ * The first entry is the floor, and it is the same number as `agp` in `gradle/libs.versions.toml`
134
+ * (what the plugin compiles against) and `RNBrownfieldPlugin.MIN_AGP` (what the apply-time gate
135
+ * enforces). The compiler covers "we accidentally used an API newer than the floor"; this matrix
136
+ * covers the other direction — "we used something a newer AGP has since removed or changed".
137
+ */
138
+ val agpMatrix = listOf(libs.versions.agp.get(), "9.2.1")
139
+
140
+ /**
141
+ * An AGP below the matrix floor. Not supported, and not built against — it exists only so a test can
142
+ * prove `RNBrownfieldPlugin.MIN_AGP` actually rejects it instead of letting the consumer fail later
143
+ * with an unattributable `NoSuchMethodError`.
144
+ */
145
+ val belowFloorAgp = "8.9.0"
146
+
147
+ // A TestKit-ready AGP classpath per version above, keyed by version.
148
+ val agpClasspaths =
149
+ (agpMatrix + belowFloorAgp).associateWith { agpVersion ->
150
+ val suffix = agpVersion.replace('.', '_')
151
+ val scope = configurations.dependencyScope("testkitAgp$suffix")
152
+ dependencies.add(scope.name, "com.android.tools.build:gradle:$agpVersion")
153
+ configurations.resolvable("testkitAgp${suffix}Classpath") {
154
+ extendsFrom(scope.get())
155
+ }
156
+ }
157
+
158
+ // `withPluginClasspath()` builds the TestKit fixture classpath from the plugin's runtime
159
+ // classpath, which no longer carries AGP now that it is compileOnly. The fixtures apply
160
+ // `com.android.library`, so hand the floor AGP back to them here — test-only, so nothing
161
+ // reaches the published module.
162
+ tasks.named<PluginUnderTestMetadata>("pluginUnderTestMetadata") {
163
+ pluginClasspath.from(agpClasspaths.getValue(agpMatrix.first()))
164
+ }
165
+
166
+ tasks.test {
167
+ useJUnitPlatform()
168
+
169
+ // Tests that want a specific AGP build their own TestKit classpath from these, via
170
+ // `withPluginClasspath(files)` instead of the no-arg `withPluginClasspath()`.
171
+ val mainSourceSet = sourceSets.main.get()
172
+ systemProperty("agp.matrix.versions", agpMatrix.joinToString(","))
173
+ systemProperty("agp.belowFloor.version", belowFloorAgp)
174
+ agpClasspaths.forEach { (agpVersion, agpClasspath) ->
175
+ val pluginClasspath = files(mainSourceSet.output, mainSourceSet.runtimeClasspath, agpClasspath)
176
+ inputs.files(pluginClasspath).withPropertyName("agpMatrix-$agpVersion").withNormalizer(ClasspathNormalizer::class)
177
+ jvmArgumentProviders.add(
178
+ CommandLineArgumentProvider {
179
+ listOf("-Dagp.classpath.$agpVersion=${pluginClasspath.joinToString(File.pathSeparator)}")
180
+ },
181
+ )
182
+ }
111
183
  }
112
184
 
113
185
  tasks.named("detekt").configure {
@@ -2,10 +2,14 @@
2
2
  kotlinJvm = "2.2.21"
3
3
  ktlint = "12.1.1"
4
4
  detekt = "1.23.7"
5
- agp = "8.5.2"
6
- common = "31.2.2" # do not bump it for now, as it throws an error for incompatible AGP used
5
+ # The AGP the plugin compiles against: deliberately the OLDEST supported version, so the
6
+ # compiler rejects any accidental use of a newer AGP API. Newer AGP is covered by the
7
+ # TestKit matrix in build.gradle.kts. Keep in sync with MIN_AGP in RNBrownfieldPlugin.kt.
8
+ agp = "8.12.0"
9
+ common = "31.12.0"
7
10
  asm-commons = "9.7"
8
11
  versioncompare = "1.5.0"
12
+ junit = "5.11.4"
9
13
 
10
14
  [plugins]
11
15
  kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlinJvm" }
@@ -17,3 +21,4 @@ agp = { module = "com.android.tools.build:gradle", name = "agp", version.ref = "
17
21
  common = { module = "com.android.tools:common", version.ref = "common" }
18
22
  asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm-commons" }
19
23
  versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "versioncompare" }
24
+ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
@@ -1,6 +1,6 @@
1
1
  distributionBase=GRADLE_USER_HOME
2
2
  distributionPath=wrapper/dists
3
- distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
3
+ distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
4
4
  networkTimeout=10000
5
5
  validateDistributionUrl=true
6
6
  zipStoreBase=GRADLE_USER_HOME
@@ -1,6 +1,6 @@
1
1
  PROJECT_ID=com.callstack.react.brownfield
2
2
  ARTIFACT_ID=brownfield-gradle-plugin
3
- VERSION=2.0.0-alpha08
3
+ VERSION=2.0.0-alpha10
4
4
  GROUP=com.callstack.react
5
5
  IMPLEMENTATION_CLASS=com.callstack.react.brownfield.plugin.RNBrownfieldPlugin
6
6
 
@@ -0,0 +1,96 @@
1
+ package com.callstack.react.brownfield.artifacts
2
+
3
+ import com.callstack.react.brownfield.shared.Logging
4
+ import com.callstack.react.brownfield.shared.TRANSITIVE_DEPENDENCY_CONFIG_NAMES
5
+ import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo
6
+ import com.callstack.react.brownfield.shared.VersionMediatingDependencySet
7
+ import com.callstack.react.brownfield.shared.collectPublishableGradleDependencies
8
+ import com.callstack.react.brownfield.shared.isPublishableCoordinate
9
+ import org.gradle.api.Project
10
+ import org.gradle.api.artifacts.Dependency
11
+
12
+ /**
13
+ * Result of [RncTransitiveDependencyDiscoverer.discover].
14
+ *
15
+ * @param dependencies The mediated set of third-party dependencies to inject.
16
+ * @param supersededCoordinates Coordinates the consumer declares itself that an embedded module
17
+ * also needs. The caller must drop their pre-existing POM/module.json entries: [dependencies]
18
+ * already carries the mediated (never-lower) version, so keeping both duplicates or shadows it.
19
+ */
20
+ data class RncTransitiveDependencyDiscoveryResult(
21
+ val dependencies: VersionMediatingDependencySet,
22
+ val supersededCoordinates: Set<Pair<String, String>>,
23
+ )
24
+
25
+ /**
26
+ * Discovers third-party dependencies of the native modules embedded into the fat AAR, for
27
+ * publication into its POM/module metadata. The RNC-CLI counterpart to
28
+ * `ExpoPublishingHelper.appendExpoTransitiveDependenciesFromGradle`.
29
+ */
30
+ class RncTransitiveDependencyDiscoverer(private val project: Project) {
31
+ fun discover(artifacts: List<UnresolvedArtifactInfo>): RncTransitiveDependencyDiscoveryResult {
32
+ val discovered = VersionMediatingDependencySet()
33
+ val supersededCoordinates = mutableSetOf<Pair<String, String>>()
34
+
35
+ artifacts
36
+ .filter { it.isExpoPublishDependency != true }
37
+ .forEach { artifact -> discoverFromArtifact(artifact, discovered, supersededCoordinates) }
38
+
39
+ return RncTransitiveDependencyDiscoveryResult(discovered, supersededCoordinates)
40
+ }
41
+
42
+ private fun discoverFromArtifact(
43
+ artifact: UnresolvedArtifactInfo,
44
+ discovered: VersionMediatingDependencySet,
45
+ supersededCoordinates: MutableSet<Pair<String, String>>,
46
+ ) {
47
+ val moduleProject = project.rootProject.findProject(":${artifact.moduleName}")
48
+ if (moduleProject == null) {
49
+ Logging.log(
50
+ "WARNING: Could not discover transitive dependencies for embedded module " +
51
+ "'${artifact.moduleName}' - no Gradle project found at " +
52
+ "':${artifact.moduleName}' in the root project",
53
+ )
54
+ return
55
+ }
56
+
57
+ collectPublishableGradleDependencies(moduleProject).forEach { moduleDependency ->
58
+ val consumerDeclaration = consumerDeclaredDependency(moduleDependency.groupId, moduleDependency.artifactId)
59
+
60
+ if (consumerDeclaration != null) {
61
+ // Supersede on the declaration existing, not on it having a version: a versionless
62
+ // one (BOM/platform) still leaves a stale entry that must be replaced, not doubled.
63
+ supersededCoordinates.add(moduleDependency.groupId to moduleDependency.artifactId)
64
+
65
+ // Module-side versions are already validated upstream; this is the consumer-side
66
+ // equivalent, keeping a dynamic version from winning mediation and being published.
67
+ val consumerVersioned = moduleDependency.copy(version = consumerDeclaration.version)
68
+ if (isPublishableCoordinate(consumerVersioned)) {
69
+ discovered.add(consumerVersioned)
70
+ }
71
+ }
72
+
73
+ discovered.add(moduleDependency)
74
+ }
75
+ }
76
+
77
+ /**
78
+ * The consumer's own declaration of this coordinate, or null if it doesn't declare it. Returns
79
+ * the [Dependency], not its version, so "declared without a version" stays distinguishable from
80
+ * "not declared" — collapsing the two leaks duplicate POM entries.
81
+ *
82
+ * Note this narrows a dynamic declaration: consumer `appcompat:1.+` against module `1.0.0`
83
+ * publishes `1.0.0`, possibly lower than `1.+` would have resolved to. Intended — a published
84
+ * `1.+` is broken for downstream consumers.
85
+ */
86
+ private fun consumerDeclaredDependency(
87
+ groupId: String,
88
+ artifactId: String,
89
+ ): Dependency? {
90
+ return TRANSITIVE_DEPENDENCY_CONFIG_NAMES.firstNotNullOfOrNull { configName ->
91
+ project.configurations.findByName(configName)?.dependencies?.firstOrNull {
92
+ it.group == groupId && it.name == artifactId
93
+ }
94
+ }
95
+ }
96
+ }
@@ -1,20 +1,15 @@
1
1
  package com.callstack.react.brownfield.expo
2
2
 
3
3
  import com.android.utils.forEach
4
- import com.callstack.react.brownfield.expo.utils.DependencyInfo
5
4
  import com.callstack.react.brownfield.expo.utils.ExpoGradleProjectProjection
6
5
  import com.callstack.react.brownfield.expo.utils.LocalMavenUtils
7
- import com.callstack.react.brownfield.expo.utils.VersionMediatingDependencySet
8
6
  import com.callstack.react.brownfield.expo.utils.asExpoGradleProjectProjection
9
7
  import com.callstack.react.brownfield.shared.Constants
8
+ import com.callstack.react.brownfield.shared.DependencyInfo
10
9
  import com.callstack.react.brownfield.shared.Logging
11
- import groovy.json.JsonOutput
12
- import groovy.json.JsonSlurper
13
- import groovy.util.NodeList
10
+ import com.callstack.react.brownfield.shared.VersionMediatingDependencySet
11
+ import com.callstack.react.brownfield.shared.collectPublishableGradleDependencies
14
12
  import org.gradle.api.Project
15
- import org.gradle.api.publish.PublishingExtension
16
- import org.gradle.api.publish.maven.MavenPublication
17
- import org.gradle.api.publish.tasks.GenerateModuleMetadata
18
13
  import org.w3c.dom.Node
19
14
  import java.io.File
20
15
  import javax.xml.parsers.DocumentBuilderFactory
@@ -29,6 +24,11 @@ fun Node.getChildNodeByName(nodeName: String): Node? {
29
24
  }
30
25
 
31
26
  open class ExpoPublishingHelper(val brownfieldAppProject: Project) {
27
+ /**
28
+ * Discovers *which* Expo projects are publishable, not their transitive dependencies: this runs
29
+ * during `apply()`, when those projects may not have configured their own dependencies yet.
30
+ * [discoverAllExpoTransitiveDependencies] handles that later.
31
+ */
32
32
  fun configure(): List<ExpoGradleProjectProjection> {
33
33
  val discoverableExpoProjects = getDiscoverableExpoProjects()
34
34
 
@@ -39,29 +39,10 @@ open class ExpoPublishingHelper(val brownfieldAppProject: Project) {
39
39
  ) { it.name },
40
40
  )
41
41
 
42
- val expoTransitiveDependencies =
43
- discoverAllExpoTransitiveDependencies(
44
- expoProjects = discoverableExpoProjects,
45
- )
46
-
47
- Logging.log(
48
- "Collected a total of ${expoTransitiveDependencies.size} unique Expo transitive " +
49
- "dependencies for brownfield app project publishing",
50
- )
51
- expoTransitiveDependencies.forEach {
52
- Logging.log(
53
- "(*) dependency ${it.groupId}:${it.artifactId}:${it.version} (scope: ${it.scope}, " +
54
- "${if (it.optional) "optional" else "required"})",
55
- )
56
- }
57
-
58
- reconfigurePOM(expoTransitiveDependencies)
59
- reconfigureGradleModuleJSON(expoTransitiveDependencies)
60
-
61
42
  return discoverableExpoProjects
62
43
  }
63
44
 
64
- protected fun shouldExcludeDependency(
45
+ internal fun shouldExcludeDependency(
65
46
  groupId: String,
66
47
  artifactId: String,
67
48
  ): Boolean {
@@ -78,174 +59,6 @@ open class ExpoPublishingHelper(val brownfieldAppProject: Project) {
78
59
  return (isRootProjectArtifact || isExpoArtifact)
79
60
  }
80
61
 
81
- /**
82
- * Modifies the generated Gradle Module Metadata file to inject Expo transitive dependencies.
83
- * @param discoveredExpoTransitiveDependencies Set of DependencyInfo
84
- * representing Expo transitive dependencies to add.
85
- */
86
- @Suppress("LongMethod")
87
- protected fun reconfigureGradleModuleJSON(discoveredExpoTransitiveDependencies: VersionMediatingDependencySet) {
88
- val removeDependenciesFromModuleFileTask =
89
- brownfieldAppProject.tasks.register("removeDependenciesFromModuleFile")
90
- removeDependenciesFromModuleFileTask.configure { task ->
91
- task.doLast {
92
- val moduleBuildDir = brownfieldAppProject.layout.buildDirectory.get()
93
-
94
- File("$moduleBuildDir/publications/mavenAar/module.json").run {
95
- val json = inputStream().use { JsonSlurper().parse(it) as Map<*, *> }
96
-
97
- discoveredExpoTransitiveDependencies.forEach { dependencyToAdd ->
98
- @Suppress("UNCHECKED_CAST")
99
- (json["variants"] as? List<MutableMap<String, Any>>)?.forEach { variant ->
100
- Logging.log(
101
- "Injecting dependency to Gradle module JSON for variant " +
102
- "'${variant["name"]}': ${dependencyToAdd.groupId}:" +
103
- "${dependencyToAdd.artifactId}:${dependencyToAdd.version}",
104
- )
105
-
106
- (variant["dependencies"] as? MutableList<MutableMap<String, Any>>)?.add(
107
- mutableMapOf<String, Any>(
108
- "group" to dependencyToAdd.groupId,
109
- "module" to dependencyToAdd.artifactId,
110
- ).apply {
111
- dependencyToAdd.version?.let { version ->
112
- put(
113
- "version",
114
- mapOf(
115
- "requires" to version,
116
- ),
117
- )
118
- }
119
- },
120
- )
121
- }
122
- }
123
-
124
- @Suppress("UNCHECKED_CAST")
125
- (json["variants"] as? List<MutableMap<String, Any>>)?.forEach { variant ->
126
- (variant["dependencies"] as? MutableList<Map<String, Any>>)?.removeAll {
127
- val group = it["group"] as String
128
- val module = it["module"] as String
129
-
130
- val shouldBeExcluded =
131
- shouldExcludeDependency(
132
- groupId = group,
133
- artifactId = module,
134
- )
135
-
136
- if (shouldBeExcluded) {
137
- Logging.log(
138
- "Removing excluded dependency from Gradle module JSON: $group:$module",
139
- )
140
- }
141
-
142
- shouldBeExcluded
143
- }
144
-
145
- writer().use {
146
- it.write(
147
- JsonOutput.prettyPrint(
148
- JsonOutput.toJson(
149
- json,
150
- ),
151
- ),
152
- )
153
- }
154
- }
155
- }
156
- }
157
- }
158
-
159
- brownfieldAppProject.tasks.withType(GenerateModuleMetadata::class.java)
160
- .configureEach {
161
- it.finalizedBy(removeDependenciesFromModuleFileTask.get())
162
- }
163
- }
164
-
165
- /**
166
- * Modifies the generated Maven POM file to inject Expo transitive dependencies.
167
- * @param discoveredExpoTransitiveDependencies Set of DependencyInfo
168
- * representing Expo transitive dependencies to add.
169
- */
170
- @Suppress("LongMethod")
171
- protected fun reconfigurePOM(discoveredExpoTransitiveDependencies: VersionMediatingDependencySet) {
172
- brownfieldAppProject.pluginManager.withPlugin("maven-publish") {
173
- brownfieldAppProject.extensions.configure(PublishingExtension::class.java) { publishing ->
174
- publishing.publications.withType(MavenPublication::class.java)
175
- .configureEach { pub ->
176
- Logging.log(
177
- "Configuring POM for publication '${pub.name}' to include Expo transitive dependencies",
178
- )
179
-
180
- pub.pom.withXml {
181
- val root = it.asNode()
182
-
183
- // below: obtains a view of the <dependencies> node(s)
184
- // inside the POM XML; in practice, there should be only one such node
185
- val dependenciesNodeList =
186
- root.get("dependencies") as NodeList
187
- val dependenciesNode =
188
- dependenciesNodeList.first() as groovy.util.Node
189
-
190
- // below: inject the discovered Expo transitive dependencies
191
- // into the POM's <dependencies> node
192
- discoveredExpoTransitiveDependencies.forEach { dependencyToAdd ->
193
- Logging.log(
194
- "Injecting dependency to POM: ${dependencyToAdd.groupId}:" +
195
- "${dependencyToAdd.artifactId}:${dependencyToAdd.version}",
196
- )
197
-
198
- val childTags =
199
- mutableMapOf(
200
- "groupId" to dependencyToAdd.groupId,
201
- "artifactId" to dependencyToAdd.artifactId,
202
- "scope" to dependencyToAdd.scope,
203
- "optional" to dependencyToAdd.optional.toString(),
204
- )
205
-
206
- if (dependencyToAdd.version?.isNotBlank() == true) {
207
- childTags["version"] = dependencyToAdd.version
208
- }
209
-
210
- dependenciesNode.appendNode("dependency").let { newDepNode ->
211
- childTags.forEach { (tagName, tagValue) ->
212
- newDepNode.appendNode(tagName, tagValue)
213
- }
214
- }
215
- }
216
-
217
- // below: filter out dependencies that should be excluded
218
- dependenciesNode.children()
219
- .filterIsInstance<groovy.util.Node>()
220
- .filter { dependency ->
221
- val groupId =
222
- (dependency["groupId"] as NodeList).text()
223
- val artifactId =
224
- (dependency["artifactId"] as NodeList).text()
225
-
226
- val shouldBeExcluded =
227
- shouldExcludeDependency(
228
- groupId = groupId,
229
- artifactId = artifactId,
230
- )
231
-
232
- if (shouldBeExcluded) {
233
- Logging.log(
234
- "Removing excluded dependency from POM: $groupId:$artifactId",
235
- )
236
- }
237
-
238
- shouldBeExcluded
239
- }
240
- .forEach { dependency ->
241
- dependenciesNode.remove(dependency)
242
- }
243
- }
244
- }
245
- }
246
- }
247
- }
248
-
249
62
  fun discoverAllExpoTransitiveDependencies(expoProjects: Iterable<ExpoGradleProjectProjection>): VersionMediatingDependencySet {
250
63
  var discoveredExpoTransitiveDependencies = VersionMediatingDependencySet()
251
64
  expoProjects.forEach { expoProj ->
@@ -368,24 +181,7 @@ open class ExpoPublishingHelper(val brownfieldAppProject: Project) {
368
181
  pkgProject: Project,
369
182
  dependencies: VersionMediatingDependencySet,
370
183
  ) {
371
- /**
372
- * Not accounting for variant specific configurations as Expo packages are not
373
- * using it. Should we face any issues/needs to account for it, we can do it here.
374
- */
375
- listOf("implementation", "api", "runtime").forEach {
376
- val configuration = pkgProject.configurations.findByName(it)
377
- configuration?.dependencies?.forEach { dep ->
378
- if (dep.group != null) {
379
- dependencies.add(
380
- DependencyInfo.fromGradleDep(
381
- groupId = dep.group!!,
382
- artifactId = dep.name,
383
- version = dep.version,
384
- ),
385
- )
386
- }
387
- }
388
- }
184
+ dependencies.addAll(collectPublishableGradleDependencies(pkgProject))
389
185
  }
390
186
 
391
187
  /**
@@ -0,0 +1,7 @@
1
+ package com.callstack.react.brownfield.expo.utils
2
+
3
+ data class BrownfieldPublishingInfo(
4
+ val groupId: String,
5
+ val artifactId: String,
6
+ val version: String,
7
+ )