@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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
package com.callstack.react.brownfield.plugin
|
|
2
2
|
|
|
3
|
+
import com.android.build.api.AndroidPluginVersion
|
|
3
4
|
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
|
4
5
|
import com.android.build.api.variant.LibraryVariant
|
|
5
6
|
import com.callstack.react.brownfield.artifacts.ArtifactsResolver
|
|
7
|
+
import com.callstack.react.brownfield.artifacts.RncTransitiveDependencyDiscoverer
|
|
6
8
|
import com.callstack.react.brownfield.expo.ExpoPublishingHelper
|
|
7
9
|
import com.callstack.react.brownfield.expo.utils.ExpoGradleProjectProjection
|
|
8
10
|
import com.callstack.react.brownfield.processors.AssetTaskProcessor
|
|
@@ -15,9 +17,13 @@ import com.callstack.react.brownfield.processors.VariantHelper
|
|
|
15
17
|
import com.callstack.react.brownfield.processors.VariantPackagesProperty
|
|
16
18
|
import com.callstack.react.brownfield.processors.VariantTaskProvider
|
|
17
19
|
import com.callstack.react.brownfield.shared.BaseProject
|
|
20
|
+
import com.callstack.react.brownfield.shared.Constants
|
|
18
21
|
import com.callstack.react.brownfield.shared.Constants.PROJECT_ID
|
|
19
22
|
import com.callstack.react.brownfield.shared.Logging
|
|
23
|
+
import com.callstack.react.brownfield.shared.PublishingMetadataInjector
|
|
20
24
|
import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo
|
|
25
|
+
import com.callstack.react.brownfield.shared.VersionMediatingDependencySet
|
|
26
|
+
import com.callstack.react.brownfield.shared.dropHardExcludedDependencies
|
|
21
27
|
import com.callstack.react.brownfield.utils.AndroidArchiveLibrary
|
|
22
28
|
import com.callstack.react.brownfield.utils.DirectoryManager
|
|
23
29
|
import com.callstack.react.brownfield.utils.Extension
|
|
@@ -36,6 +42,7 @@ class RNBrownfieldPlugin : Plugin<Project> {
|
|
|
36
42
|
|
|
37
43
|
override fun apply(project: Project) {
|
|
38
44
|
verifyAndroidPluginApplied(project)
|
|
45
|
+
verifyAgpVersion(project)
|
|
39
46
|
|
|
40
47
|
this.project = project
|
|
41
48
|
initializers()
|
|
@@ -54,8 +61,9 @@ class RNBrownfieldPlugin : Plugin<Project> {
|
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
var expoProjects = listOf<ExpoGradleProjectProjection>()
|
|
64
|
+
var expoPublishingHelper: ExpoPublishingHelper? = null
|
|
57
65
|
if (this.isExpoProject) {
|
|
58
|
-
|
|
66
|
+
expoPublishingHelper = ExpoPublishingHelper(brownfieldAppProject = project)
|
|
59
67
|
expoProjects = expoPublishingHelper.configure()
|
|
60
68
|
}
|
|
61
69
|
|
|
@@ -65,6 +73,18 @@ class RNBrownfieldPlugin : Plugin<Project> {
|
|
|
65
73
|
val artifactsResolver = ArtifactsResolver(project, isExpoProject)
|
|
66
74
|
val artifacts = artifactsResolver.processDefaultDependencies(expoProjects)
|
|
67
75
|
|
|
76
|
+
// Registered eagerly: Gradle disallows registering tasks from afterEvaluate. Only the
|
|
77
|
+
// dependency set supplied below has to wait.
|
|
78
|
+
val injector = PublishingMetadataInjector(project)
|
|
79
|
+
|
|
80
|
+
// whenReady, not afterEvaluate: afterEvaluate guarantees only that *this* project is
|
|
81
|
+
// configured, not the embedded module projects we read dependencies from — theirs were
|
|
82
|
+
// empirically still empty at that point. whenReady fires once the whole task graph is
|
|
83
|
+
// resolved (so every embedded module is configured) but before any task runs.
|
|
84
|
+
project.gradle.taskGraph.whenReady {
|
|
85
|
+
configureTransitiveDependencyInjection(project, injector, expoPublishingHelper, expoProjects, artifacts)
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
val variantTaskProvider = VariantTaskProvider(project)
|
|
69
89
|
|
|
70
90
|
val androidComponents = project.extensions.getByType(LibraryAndroidComponentsExtension::class.java)
|
|
@@ -74,9 +94,89 @@ class RNBrownfieldPlugin : Plugin<Project> {
|
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
companion object {
|
|
97
|
+
/**
|
|
98
|
+
* Oldest AGP the plugin supports. This is one number that must hold in three places: the
|
|
99
|
+
* `agp` version the plugin compiles against in `gradle/libs.versions.toml`, the floor of the
|
|
100
|
+
* TestKit AGP matrix in `build.gradle.kts`, and this gate. Raise all three together.
|
|
101
|
+
*/
|
|
102
|
+
val MIN_AGP = AndroidPluginVersion(8, 12)
|
|
103
|
+
|
|
77
104
|
const val EXPO_PROJECT_LOCATOR = ":expo"
|
|
78
105
|
}
|
|
79
106
|
|
|
107
|
+
@Suppress("LongMethod")
|
|
108
|
+
private fun configureTransitiveDependencyInjection(
|
|
109
|
+
project: Project,
|
|
110
|
+
injector: PublishingMetadataInjector,
|
|
111
|
+
expoPublishingHelper: ExpoPublishingHelper?,
|
|
112
|
+
expoProjects: List<ExpoGradleProjectProjection>,
|
|
113
|
+
artifacts: List<UnresolvedArtifactInfo>,
|
|
114
|
+
) {
|
|
115
|
+
val transitiveDeps = VersionMediatingDependencySet()
|
|
116
|
+
var rncSupersededCoordinates: Set<Pair<String, String>> = emptySet()
|
|
117
|
+
|
|
118
|
+
if (isExpoProject && expoPublishingHelper != null) {
|
|
119
|
+
val expoTransitiveDeps = expoPublishingHelper.discoverAllExpoTransitiveDependencies(expoProjects)
|
|
120
|
+
Logging.log("Merged ${expoTransitiveDeps.size} transitive dependencies discovered from Expo")
|
|
121
|
+
expoTransitiveDeps.forEach {
|
|
122
|
+
Logging.log(
|
|
123
|
+
"(*) dependency ${it.groupId}:${it.artifactId}:${it.version} (scope: ${it.scope}, " +
|
|
124
|
+
"${if (it.optional) "optional" else "required"})",
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
transitiveDeps.addAll(expoTransitiveDeps)
|
|
128
|
+
}
|
|
129
|
+
// Also on Expo: the path above only covers Expo modules, not community libraries.
|
|
130
|
+
if (extension.experimentalIncludeTransitiveDependencies) {
|
|
131
|
+
val rncDiscovery = RncTransitiveDependencyDiscoverer(project).discover(artifacts)
|
|
132
|
+
Logging.log(
|
|
133
|
+
"Merged ${rncDiscovery.dependencies.size} transitive dependencies discovered by the RNC discoverer",
|
|
134
|
+
)
|
|
135
|
+
transitiveDeps.addAll(rncDiscovery.dependencies)
|
|
136
|
+
rncSupersededCoordinates = rncDiscovery.supersededCoordinates
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isExpoProject || extension.experimentalIncludeTransitiveDependencies) {
|
|
140
|
+
// Coordinates that must never be published: they're embedded in the AAR, not
|
|
141
|
+
// resolvable from Maven. Distinct from "superseded" coordinates, which ARE re-injected
|
|
142
|
+
// (replacing a stale entry) and so must not be dropped here.
|
|
143
|
+
val hardExcludePredicate: (String, String) -> Boolean = { groupId, artifactId ->
|
|
144
|
+
(expoPublishingHelper?.shouldExcludeDependency(groupId, artifactId) ?: (groupId == project.rootProject.name)) ||
|
|
145
|
+
artifacts.any { it.moduleGroup == groupId && it.moduleName == artifactId } ||
|
|
146
|
+
expoProjects.any { it.publication?.groupId == groupId && it.publication?.artifactId == artifactId }
|
|
147
|
+
}
|
|
148
|
+
dropHardExcludedDependencies(transitiveDeps, hardExcludePredicate)
|
|
149
|
+
|
|
150
|
+
Logging.log(
|
|
151
|
+
"Total of ${transitiveDeps.size} unique transitive dependencies merged for POM/module.json injection",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
val removalPredicate: (String, String) -> Boolean = { groupId, artifactId ->
|
|
155
|
+
hardExcludePredicate(groupId, artifactId) || rncSupersededCoordinates.contains(groupId to artifactId)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
injector.configure(transitiveDeps, removalPredicate)
|
|
159
|
+
Logging.log("PublishingMetadataInjector ran: injected merged transitive dependencies into POM and Gradle Module Metadata")
|
|
160
|
+
|
|
161
|
+
// Informational only — throwing would recreate the upgrade break that the namespaced
|
|
162
|
+
// task name exists to prevent. `names` avoids realizing the task from whenReady.
|
|
163
|
+
if (project.tasks.names.contains(Constants.LEGACY_CONSUMER_MODULE_METADATA_TASK_NAME)) {
|
|
164
|
+
Logging.log(
|
|
165
|
+
"NOTE: this project still registers a hand-written " +
|
|
166
|
+
"'${Constants.LEGACY_CONSUMER_MODULE_METADATA_TASK_NAME}' task. The plugin now performs " +
|
|
167
|
+
"that removal itself (plus transitive-dependency injection) via " +
|
|
168
|
+
"'${Constants.MODULE_METADATA_POST_PROCESS_TASK_NAME}'. The hand-written task is " +
|
|
169
|
+
"redundant and can be deleted; leaving it in place is harmless.",
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
Logging.log(
|
|
174
|
+
"PublishingMetadataInjector skipped: project is not an Expo project and " +
|
|
175
|
+
"experimentalIncludeTransitiveDependencies is disabled",
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
80
180
|
private fun initializers() {
|
|
81
181
|
RClassTransformer.project = project
|
|
82
182
|
Logging.project = project
|
|
@@ -100,6 +200,28 @@ class RNBrownfieldPlugin : Plugin<Project> {
|
|
|
100
200
|
}
|
|
101
201
|
}
|
|
102
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Fails fast on AGP versions older than the plugin supports.
|
|
205
|
+
*
|
|
206
|
+
* The plugin compiles against [MIN_AGP] (see `agp` in `gradle/libs.versions.toml`), so anything
|
|
207
|
+
* at or above it is guaranteed to have every AGP symbol the plugin references. Below it, the
|
|
208
|
+
* consumer would instead hit a `NoClassDefFoundError`/`NoSuchMethodError` somewhere deep in
|
|
209
|
+
* configuration, with nothing pointing back at this plugin.
|
|
210
|
+
*/
|
|
211
|
+
private fun verifyAgpVersion(project: Project) {
|
|
212
|
+
val current =
|
|
213
|
+
project.extensions
|
|
214
|
+
.getByType(LibraryAndroidComponentsExtension::class.java)
|
|
215
|
+
.pluginVersion
|
|
216
|
+
if (current < MIN_AGP) {
|
|
217
|
+
throw ProjectConfigurationException(
|
|
218
|
+
"$PROJECT_ID requires Android Gradle Plugin $MIN_AGP or newer, " +
|
|
219
|
+
"but this project resolved AGP $current",
|
|
220
|
+
Throwable("Upgrade the Android Gradle Plugin to $MIN_AGP or newer"),
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
103
225
|
private fun getAarLibraries(
|
|
104
226
|
artifacts: List<UnresolvedArtifactInfo>,
|
|
105
227
|
variantName: String,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.callstack.react.brownfield.plugin
|
|
2
2
|
|
|
3
|
+
import com.android.build.api.dsl.AndroidSourceSet
|
|
3
4
|
import com.android.build.api.dsl.LibraryExtension
|
|
4
5
|
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
|
5
6
|
import com.callstack.react.brownfield.exceptions.NameSpaceNotFound
|
|
@@ -41,7 +42,11 @@ object RNSourceSets {
|
|
|
41
42
|
val componentsExtension = project.extensions.getByType(LibraryAndroidComponentsExtension::class.java)
|
|
42
43
|
|
|
43
44
|
// Move the non-variant-specific configuration out of the loop
|
|
44
|
-
|
|
45
|
+
// The parameter is typed as the `AndroidSourceSet` supertype on purpose. AGP 9 narrows
|
|
46
|
+
// this container to `AndroidLibrarySourceSet`, and letting Kotlin infer that bakes the
|
|
47
|
+
// AGP 9-only type into the lambda's `execute` signature, which blows up with
|
|
48
|
+
// NoClassDefFoundError on AGP 8 consumers. `AndroidSourceSet` is identical in both.
|
|
49
|
+
androidExtension.sourceSets.named("main") { sourceSet: AndroidSourceSet ->
|
|
45
50
|
// This path is not variant-specific, so it's added once here.
|
|
46
51
|
sourceSet.java.directories.add("${getModuleBuildDir()}/generated/autolinking/src/main/java")
|
|
47
52
|
}
|
package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/Constants.kt
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package com.callstack.react.brownfield.shared
|
|
2
2
|
|
|
3
|
-
import com.callstack.react.brownfield.expo.utils.DependencyInfo
|
|
4
3
|
import com.callstack.react.brownfield.utils.StringMatcher
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -40,6 +39,18 @@ object Constants {
|
|
|
40
39
|
|
|
41
40
|
const val INTERMEDIATES_TEMP_DIR = PLUGIN_NAME
|
|
42
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Plugin-owned task that post-processes generated Gradle Module Metadata.
|
|
44
|
+
*
|
|
45
|
+
* Never rename to the bare [LEGACY_CONSUMER_MODULE_METADATA_TASK_NAME]: the plugin registers
|
|
46
|
+
* during `apply()`, before the consumer's script body, so claiming that name breaks every
|
|
47
|
+
* consumer still following the setup docs with "a task with that name already exists".
|
|
48
|
+
*/
|
|
49
|
+
const val MODULE_METADATA_POST_PROCESS_TASK_NAME = "brownfieldRemoveDependenciesFromModuleFile"
|
|
50
|
+
|
|
51
|
+
/** The name the setup docs tell consumers to register themselves; must stay free for them. */
|
|
52
|
+
const val LEGACY_CONSUMER_MODULE_METADATA_TASK_NAME = "removeDependenciesFromModuleFile"
|
|
53
|
+
|
|
43
54
|
val BROWNFIELD_EXPO_TRANSITIVE_DEPS_WHITELISTED_MODULES_FOR_DISCOVERY =
|
|
44
55
|
setOf("expo-modules-core", "expo-constants", "expo", "expo-updates")
|
|
45
56
|
val BROWNFIELD_EXPO_TRANSITIVE_DEPS_ARTIFACTS_BLACKLIST =
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
package com.callstack.react.brownfield.
|
|
2
|
-
|
|
3
|
-
data class BrownfieldPublishingInfo(
|
|
4
|
-
val groupId: String,
|
|
5
|
-
val artifactId: String,
|
|
6
|
-
val version: String,
|
|
7
|
-
)
|
|
1
|
+
package com.callstack.react.brownfield.shared
|
|
8
2
|
|
|
9
3
|
data class DependencyInfo(
|
|
10
4
|
val groupId: String,
|
|
@@ -18,12 +12,13 @@ data class DependencyInfo(
|
|
|
18
12
|
groupId: String,
|
|
19
13
|
artifactId: String,
|
|
20
14
|
version: String?,
|
|
15
|
+
scope: String = "compile",
|
|
21
16
|
): DependencyInfo {
|
|
22
17
|
return DependencyInfo(
|
|
23
18
|
groupId = groupId,
|
|
24
19
|
artifactId = artifactId,
|
|
25
20
|
version = version,
|
|
26
|
-
scope =
|
|
21
|
+
scope = scope,
|
|
27
22
|
optional = false,
|
|
28
23
|
)
|
|
29
24
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.shared
|
|
2
|
+
|
|
3
|
+
/** Matches Maven/Gradle version-range syntax, e.g. `[1.0,2.0)`, `(,2.0]`, `[1.0,)`. */
|
|
4
|
+
private val VERSION_RANGE_REGEX = Regex("^[\\[(].*,.*[\\])]$")
|
|
5
|
+
|
|
6
|
+
/** Reserved Gradle/Maven dynamic-version keywords that resolve to whatever is newest at build time. */
|
|
7
|
+
private val DYNAMIC_VERSION_KEYWORDS = setOf("latest.release", "latest.integration", "latest", "release")
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether [dependency] is safe to publish as-is. Rejects dynamic versions (`+`, `latest.*`, ranges)
|
|
11
|
+
* and blank ones: the former resolve non-reproducibly for consumers, the latter emit a
|
|
12
|
+
* `<dependency>` with no version at all.
|
|
13
|
+
*/
|
|
14
|
+
fun isPublishableCoordinate(dependency: DependencyInfo): Boolean {
|
|
15
|
+
val version = dependency.version
|
|
16
|
+
if (version.isNullOrBlank()) return false
|
|
17
|
+
val trimmed = version.trim()
|
|
18
|
+
if (trimmed.contains("+")) return false
|
|
19
|
+
if (trimmed.lowercase() in DYNAMIC_VERSION_KEYWORDS) return false
|
|
20
|
+
if (VERSION_RANGE_REGEX.matches(trimmed)) return false
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.shared
|
|
2
|
+
|
|
3
|
+
import org.gradle.api.Project
|
|
4
|
+
import org.gradle.api.internal.artifacts.dependencies.DefaultProjectDependency
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configurations inspected when discovering a project's direct external dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Not flavor-aware — `releaseImplementation` and friends are missed. Fine for Expo packages, which
|
|
10
|
+
* don't use them; extend if an RNC-CLI native module turns out to.
|
|
11
|
+
*/
|
|
12
|
+
val TRANSITIVE_DEPENDENCY_CONFIG_NAMES = listOf("implementation", "api", "runtimeOnly")
|
|
13
|
+
|
|
14
|
+
/** Maven scope per configuration; `runtimeOnly` maps to `runtime` to keep it off the compile classpath. */
|
|
15
|
+
private val CONFIG_NAME_TO_SCOPE =
|
|
16
|
+
mapOf(
|
|
17
|
+
"implementation" to "compile",
|
|
18
|
+
"api" to "compile",
|
|
19
|
+
"runtimeOnly" to "runtime",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* [project]'s direct external (non-project) dependencies on [TRANSITIVE_DEPENDENCY_CONFIG_NAMES],
|
|
24
|
+
* minus anything [isPublishableCoordinate] rejects. Shared by the Expo fallback and RNC paths so
|
|
25
|
+
* both get the same guarantees.
|
|
26
|
+
*/
|
|
27
|
+
fun collectPublishableGradleDependencies(project: Project): List<DependencyInfo> {
|
|
28
|
+
val result = mutableListOf<DependencyInfo>()
|
|
29
|
+
|
|
30
|
+
TRANSITIVE_DEPENDENCY_CONFIG_NAMES.forEach { configName ->
|
|
31
|
+
val configuration = project.configurations.findByName(configName) ?: return@forEach
|
|
32
|
+
val scope = CONFIG_NAME_TO_SCOPE.getValue(configName)
|
|
33
|
+
|
|
34
|
+
configuration.dependencies.forEach { dependency ->
|
|
35
|
+
if (dependency is DefaultProjectDependency) return@forEach
|
|
36
|
+
val group = dependency.group ?: return@forEach
|
|
37
|
+
|
|
38
|
+
val info = DependencyInfo.fromGradleDep(group, dependency.name, dependency.version, scope)
|
|
39
|
+
if (isPublishableCoordinate(info)) result.add(info)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
@@ -11,11 +11,13 @@ import org.gradle.api.tasks.Classpath
|
|
|
11
11
|
import org.gradle.api.tasks.Input
|
|
12
12
|
import org.gradle.api.tasks.OutputDirectory
|
|
13
13
|
import org.gradle.api.tasks.TaskAction
|
|
14
|
+
import org.gradle.api.tasks.UntrackedTask
|
|
14
15
|
import java.io.File
|
|
15
16
|
import java.nio.file.Files
|
|
16
17
|
import java.nio.file.Path
|
|
17
18
|
import java.nio.file.Paths
|
|
18
19
|
|
|
20
|
+
@UntrackedTask(because = "Copies classes into Java and Kotlin compilation directories owned by other tasks")
|
|
19
21
|
abstract class MergeClassesTask : DefaultTask() {
|
|
20
22
|
@get:Classpath
|
|
21
23
|
abstract val inputClassesJars: ConfigurableFileCollection
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.shared
|
|
2
|
+
|
|
3
|
+
import groovy.json.JsonOutput
|
|
4
|
+
import groovy.json.JsonSlurper
|
|
5
|
+
import groovy.util.Node
|
|
6
|
+
import groovy.util.NodeList
|
|
7
|
+
import org.gradle.api.Project
|
|
8
|
+
import org.gradle.api.publish.PublishingExtension
|
|
9
|
+
import org.gradle.api.publish.maven.MavenPublication
|
|
10
|
+
import org.gradle.api.publish.tasks.GenerateModuleMetadata
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Removes every `<dependency>` matching [shouldExclude] from [dependenciesNode], then appends one
|
|
14
|
+
* per entry in [dependencies].
|
|
15
|
+
*
|
|
16
|
+
* Removal must run before appending: [shouldExclude] matches on coordinate alone, so afterwards it
|
|
17
|
+
* couldn't tell a stale superseded entry from the one just appended for it, and would drop both.
|
|
18
|
+
*/
|
|
19
|
+
internal fun mutatePomDependenciesNode(
|
|
20
|
+
dependenciesNode: Node,
|
|
21
|
+
dependencies: VersionMediatingDependencySet,
|
|
22
|
+
shouldExclude: (groupId: String, artifactId: String) -> Boolean,
|
|
23
|
+
) {
|
|
24
|
+
dependenciesNode.children()
|
|
25
|
+
.filterIsInstance<Node>()
|
|
26
|
+
.filter { dependency ->
|
|
27
|
+
val groupId = (dependency["groupId"] as NodeList).text()
|
|
28
|
+
val artifactId = (dependency["artifactId"] as NodeList).text()
|
|
29
|
+
shouldExclude(groupId, artifactId)
|
|
30
|
+
}
|
|
31
|
+
.forEach { dependency ->
|
|
32
|
+
dependenciesNode.remove(dependency)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dependencies.forEach { dependencyToAdd ->
|
|
36
|
+
val childTags =
|
|
37
|
+
mutableMapOf(
|
|
38
|
+
"groupId" to dependencyToAdd.groupId,
|
|
39
|
+
"artifactId" to dependencyToAdd.artifactId,
|
|
40
|
+
"scope" to dependencyToAdd.scope,
|
|
41
|
+
"optional" to dependencyToAdd.optional.toString(),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if (dependencyToAdd.version?.isNotBlank() == true) {
|
|
45
|
+
childTags["version"] = dependencyToAdd.version
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
dependenciesNode.appendNode("dependency").let { newDepNode ->
|
|
49
|
+
childTags.forEach { (tagName, tagValue) ->
|
|
50
|
+
newDepNode.appendNode(tagName, tagValue)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The POM's `<dependencies>` element, created if absent — a publication with no dependencies has
|
|
58
|
+
* none, and `first()` throws on it. Extracted so tests exercise this code rather than
|
|
59
|
+
* reimplementing the fallback and passing regardless of what production does.
|
|
60
|
+
*/
|
|
61
|
+
internal fun resolveOrCreateDependenciesNode(root: Node): Node =
|
|
62
|
+
(root.get("dependencies") as NodeList).firstOrNull() as? Node
|
|
63
|
+
?: root.appendNode("dependencies")
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The `module.json` equivalent of [mutatePomDependenciesNode], applied to every variant's
|
|
67
|
+
* `dependencies` list. Same removal-before-append requirement, for the same reason.
|
|
68
|
+
*/
|
|
69
|
+
internal fun mutateModuleJsonVariants(
|
|
70
|
+
json: Map<*, *>,
|
|
71
|
+
dependencies: VersionMediatingDependencySet,
|
|
72
|
+
shouldExclude: (groupId: String, artifactId: String) -> Boolean,
|
|
73
|
+
) {
|
|
74
|
+
@Suppress("UNCHECKED_CAST")
|
|
75
|
+
(json["variants"] as? List<MutableMap<String, Any>>)?.forEach { variant ->
|
|
76
|
+
(variant["dependencies"] as? MutableList<Map<String, Any>>)?.removeAll {
|
|
77
|
+
val group = it["group"] as String
|
|
78
|
+
val module = it["module"] as String
|
|
79
|
+
shouldExclude(group, module)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
dependencies.forEach { dependencyToAdd ->
|
|
84
|
+
@Suppress("UNCHECKED_CAST")
|
|
85
|
+
(json["variants"] as? List<MutableMap<String, Any>>)?.forEach { variant ->
|
|
86
|
+
// Known gap: every dependency lands in every variant, so runtime-scoped ones are
|
|
87
|
+
// compile-visible to Gradle consumers. A fix must key on provenance, NOT on
|
|
88
|
+
// `scope`, which means different things per path: on the Expo path "runtime" is what
|
|
89
|
+
// Gradle emits for `implementation` (73 of 84 deps across the upstream Expo POMs), on
|
|
90
|
+
// the RNC path it means `runtimeOnly`. Filtering on it would strip react-android and
|
|
91
|
+
// appcompat from Expo consumers. Needs a `fromRuntimeOnlyConfiguration` flag on
|
|
92
|
+
// DependencyInfo, and its own PR.
|
|
93
|
+
(variant["dependencies"] as? MutableList<MutableMap<String, Any>>)?.add(
|
|
94
|
+
mutableMapOf<String, Any>(
|
|
95
|
+
"group" to dependencyToAdd.groupId,
|
|
96
|
+
"module" to dependencyToAdd.artifactId,
|
|
97
|
+
).apply {
|
|
98
|
+
dependencyToAdd.version?.let { version ->
|
|
99
|
+
put("version", mapOf("requires" to version))
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Injects transitive dependencies into the generated Maven POM and Gradle Module Metadata for
|
|
109
|
+
* every `MavenPublication` on [project], removing any existing entry the exclusion predicate
|
|
110
|
+
* matches. Used identically by the Expo and RNC-CLI paths.
|
|
111
|
+
*
|
|
112
|
+
* Hooks are registered eagerly on construction, since Gradle disallows registering tasks from
|
|
113
|
+
* `afterEvaluate`. The dependency set isn't known until then, so [configure] supplies it later;
|
|
114
|
+
* until it's called, injection is disabled and the hooks are no-ops.
|
|
115
|
+
*/
|
|
116
|
+
class PublishingMetadataInjector(private val project: Project) {
|
|
117
|
+
private var dependencies = VersionMediatingDependencySet()
|
|
118
|
+
private var shouldExclude: (groupId: String, artifactId: String) -> Boolean = { _, _ -> false }
|
|
119
|
+
private var enabled = false
|
|
120
|
+
|
|
121
|
+
init {
|
|
122
|
+
registerModuleJsonInjectors()
|
|
123
|
+
registerPomInjector()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Supplies the dependencies to inject and enables injection on the hooks registered in [init]. */
|
|
127
|
+
fun configure(
|
|
128
|
+
dependencies: VersionMediatingDependencySet,
|
|
129
|
+
shouldExclude: (groupId: String, artifactId: String) -> Boolean,
|
|
130
|
+
) {
|
|
131
|
+
this.dependencies = dependencies
|
|
132
|
+
this.shouldExclude = shouldExclude
|
|
133
|
+
this.enabled = true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private fun registerModuleJsonInjectors() {
|
|
137
|
+
// One task, not one per publication registered from a GenerateModuleMetadata
|
|
138
|
+
// configureEach — that crashes with "DefaultTaskContainer#register(String) ... cannot be
|
|
139
|
+
// executed in the current context". Walking each task's outputFile at execution time also
|
|
140
|
+
// avoids hardcoding a publication name. Name is namespaced: see Constants.
|
|
141
|
+
val removeDependenciesFromModuleFileTask =
|
|
142
|
+
project.tasks.register(Constants.MODULE_METADATA_POST_PROCESS_TASK_NAME)
|
|
143
|
+
removeDependenciesFromModuleFileTask.configure { task ->
|
|
144
|
+
task.onlyIf { enabled }
|
|
145
|
+
task.doLast {
|
|
146
|
+
project.tasks.withType(GenerateModuleMetadata::class.java).forEach { metadataTask ->
|
|
147
|
+
val moduleFile = metadataTask.outputFile.get().asFile
|
|
148
|
+
if (!moduleFile.exists()) return@forEach
|
|
149
|
+
|
|
150
|
+
val json = moduleFile.inputStream().use { JsonSlurper().parse(it) as Map<*, *> }
|
|
151
|
+
mutateModuleJsonVariants(json, dependencies, shouldExclude)
|
|
152
|
+
moduleFile.writer().use {
|
|
153
|
+
it.write(JsonOutput.prettyPrint(JsonOutput.toJson(json)))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Finalizing with an already-registered task is safe here, unlike registering one.
|
|
160
|
+
project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { metadataTask ->
|
|
161
|
+
metadataTask.finalizedBy(removeDependenciesFromModuleFileTask)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private fun registerPomInjector() {
|
|
166
|
+
project.pluginManager.withPlugin("maven-publish") {
|
|
167
|
+
project.extensions.configure(PublishingExtension::class.java) { publishing ->
|
|
168
|
+
publishing.publications.withType(MavenPublication::class.java)
|
|
169
|
+
.configureEach { pub ->
|
|
170
|
+
pub.pom.withXml {
|
|
171
|
+
if (!enabled) return@withXml
|
|
172
|
+
|
|
173
|
+
val root = it.asNode()
|
|
174
|
+
val dependenciesNode = resolveOrCreateDependenciesNode(root)
|
|
175
|
+
mutatePomDependenciesNode(dependenciesNode, dependencies, shouldExclude)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.shared
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drops coordinates matching [hardExcludePredicate] — ones that are embedded in the AAR and so
|
|
5
|
+
* would never resolve from Maven. A discovery path can surface these without knowing it: the RNC
|
|
6
|
+
* discoverer has no Expo awareness, so an embedded module depending on an Expo module coordinate
|
|
7
|
+
* would otherwise be published as an ordinary dependency.
|
|
8
|
+
*
|
|
9
|
+
* [hardExcludePredicate] must never include superseded coordinates — those are meant to be
|
|
10
|
+
* re-injected with the mediated version, and dropping them here removes them entirely.
|
|
11
|
+
*/
|
|
12
|
+
internal fun dropHardExcludedDependencies(
|
|
13
|
+
transitiveDeps: VersionMediatingDependencySet,
|
|
14
|
+
hardExcludePredicate: (groupId: String, artifactId: String) -> Boolean,
|
|
15
|
+
): VersionMediatingDependencySet {
|
|
16
|
+
return transitiveDeps.filter { !hardExcludePredicate(it.groupId, it.artifactId) }
|
|
17
|
+
}
|
package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt
CHANGED
|
@@ -53,4 +53,15 @@ open class Extension {
|
|
|
53
53
|
* listOf("libdatadog-ndk.so")
|
|
54
54
|
*/
|
|
55
55
|
var ignoreEmbeddedLibs = listOf<String>()
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether to publish embedded native modules' third-party dependencies into the generated POM
|
|
59
|
+
* and Gradle Module Metadata, so consuming apps resolve them instead of declaring them by hand.
|
|
60
|
+
*
|
|
61
|
+
* Experimental; behavior may still change. On Expo projects, Expo modules' dependencies are
|
|
62
|
+
* published regardless; this covers the other native modules. Set to `false` to hand-declare them instead.
|
|
63
|
+
*
|
|
64
|
+
* Default is `true`.
|
|
65
|
+
*/
|
|
66
|
+
var experimentalIncludeTransitiveDependencies = true
|
|
56
67
|
}
|