expo-brownfield 56.0.4 → 56.0.6

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 CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.6 — 2026-05-11
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - Fix reading updates app.manifest and meta-data tags ([#45655](https://github.com/expo/expo/pull/45655) by [@gabrieldonadel](https://github.com/gabrieldonadel))
18
+
19
+ ## 56.0.5 — 2026-05-08
20
+
21
+ _This version does not introduce any user-facing changes._
22
+
13
23
  ## 56.0.4 — 2026-05-07
14
24
 
15
25
  ### 🐛 Bug fixes
@@ -4,7 +4,7 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'expo.modules.brownfield'
7
- version = '56.0.4'
7
+ version = '56.0.6'
8
8
 
9
9
  expoModule {
10
10
  canBePublished false
@@ -14,7 +14,7 @@ android {
14
14
  namespace "expo.modules.brownfield"
15
15
  defaultConfig {
16
16
  versionCode 1
17
- versionName '56.0.4'
17
+ versionName '56.0.6'
18
18
  }
19
19
  }
20
20
 
@@ -16,9 +16,9 @@ class ExpoBrownfieldSetupPlugin : Plugin<Project> {
16
16
  project.afterEvaluate { project ->
17
17
  setupSourceSets(project)
18
18
  setupCopyingAutolinking(project)
19
- setupBundleDependencyForRelease(project)
20
19
  setupCopyingNativeLibsForType(project, "Release")
21
20
  setupCopyingNativeLibsForType(project, "Debug")
21
+ setupHostAppArtifactForwardingForRelease(project)
22
22
  wireDevLauncherTasks(project)
23
23
  }
24
24
  }
@@ -58,7 +58,7 @@ class ExpoBrownfieldSetupPlugin : Plugin<Project> {
58
58
 
59
59
  libraryExtension.sourceSets.getByName("release").apply {
60
60
  jniLibs.srcDirs("libsRelease")
61
- assets.srcDirs("$appBuildDir/generated/assets/react/release")
61
+ // release assets src dir is wired in setupHostAppArtifactForwardingForRelease
62
62
  res.srcDirs("$appBuildDir/generated/res/react/release")
63
63
  }
64
64
 
@@ -120,18 +120,74 @@ class ExpoBrownfieldSetupPlugin : Plugin<Project> {
120
120
  }
121
121
 
122
122
  /**
123
- * Setup the dependency of the bundle tasks.
123
+ * Forward the host `:app` module's build-time outputs into the published brownfield AAR so the
124
+ * runtime React Native + expo libraries inside the AAR find the configuration they need.
124
125
  *
125
- * Needed to include bundle and assets in the release variant.
126
+ * Two pieces are forwarded:
126
127
  *
127
- * @param brownfieldProject The brownfield project to setup the dependency of the bundle tasks
128
- * for.
128
+ * 1. `:app:mergeReleaseAssets` output (everything that AGP would bundle into the host APK's
129
+ * `assets/`). This includes the RN JS bundle and hashed assets, expo-updates' `app.manifest`,
130
+ * expo-constants' `app.config`, and any other generated asset emitted by a host-side gradle
131
+ * plugin. Forwarding the merged output (rather than picking specific generator tasks) makes
132
+ * this future-proof: any new expo library that emits an asset on the `:app` side is picked
133
+ * up automatically. Transitive dep: `mergeReleaseAssets` requires `createBundleReleaseJsAndAssets`
134
+ * so the old `setupBundleDependencyForRelease` is no longer needed.
135
+ *
136
+ * 2. Every `<application>` `<meta-data>` entry from `:app/src/main/AndroidManifest.xml`,
137
+ * written into a generated release-variant manifest that AGP merges into the consumer.
138
+ * Covers expo-updates' `EXPO_UPDATE_URL`, expo-notifications' default icon/color, and
139
+ * anything else a config plugin injects.
140
+ *
141
+ * @param brownfieldProject The brownfield project.
129
142
  */
130
- internal fun setupBundleDependencyForRelease(brownfieldProject: Project) {
143
+ internal fun setupHostAppArtifactForwardingForRelease(brownfieldProject: Project) {
131
144
  val appProject = findAppProject(brownfieldProject)
145
+ val mergeAssetsTask = appProject.tasks.findByName("mergeReleaseAssets") ?: run {
146
+ brownfieldProject.logger.lifecycle(
147
+ "brownfield: \":${appProject.name}:mergeReleaseAssets\" task not found; " +
148
+ "skipping host-app asset forwarding."
149
+ )
150
+ return
151
+ }
152
+
153
+ val libraryExtension = getLibraryExtension(brownfieldProject)
154
+ val moduleBuildDir = brownfieldProject.layout.buildDirectory.get().asFile
155
+
156
+ val hostAssetsDir = File(moduleBuildDir, "generated/assets/hostApp/release")
157
+ val copyHostAssetsTask =
158
+ brownfieldProject.tasks.register("copyHostAppAssetsRelease", Copy::class.java) { task ->
159
+ task.dependsOn(mergeAssetsTask)
160
+ task.from(mergeAssetsTask.outputs.files)
161
+ task.into(hostAssetsDir)
162
+ }
163
+ libraryExtension.sourceSets.getByName("release").assets.srcDirs(hostAssetsDir)
164
+
165
+ val hostManifestFile =
166
+ File(moduleBuildDir, "generated/manifest/hostApp/release/AndroidManifest.xml")
167
+ val appManifest = File(appProject.projectDir, "src/main/AndroidManifest.xml")
168
+ val appStrings = File(appProject.projectDir, "src/main/res/values/strings.xml")
169
+
170
+ val generateHostManifestTask =
171
+ brownfieldProject.tasks.register("generateBrownfieldHostAppManifestRelease") { task ->
172
+ task.inputs.file(appManifest)
173
+ if (appStrings.exists()) {
174
+ task.inputs.file(appStrings)
175
+ }
176
+ task.outputs.file(hostManifestFile)
177
+ task.doLast {
178
+ hostManifestFile.parentFile.mkdirs()
179
+ hostManifestFile.writeText(buildForwardedApplicationManifest(appManifest, appStrings))
180
+ }
181
+ }
182
+ libraryExtension.sourceSets.getByName("release").manifest.srcFile(hostManifestFile)
183
+
132
184
  brownfieldProject.tasks.named("preReleaseBuild").configure { task ->
133
- task.dependsOn(":${appProject.name}:createBundleReleaseJsAndAssets")
185
+ task.dependsOn(copyHostAssetsTask)
186
+ task.dependsOn(generateHostManifestTask)
134
187
  }
188
+ brownfieldProject.tasks
189
+ .matching { it.name == "processReleaseManifest" || it.name == "processReleaseMainManifest" }
190
+ .configureEach { task -> task.dependsOn(generateHostManifestTask) }
135
191
  }
136
192
 
137
193
  /**
@@ -266,7 +322,7 @@ class ExpoBrownfieldSetupPlugin : Plugin<Project> {
266
322
  /**
267
323
  * Add explicit dependency between the `sourceDebugJar` and `generateServiceApolloSources`
268
324
  * tasks in `expo-dev-launcher` project.
269
- *
325
+ *
270
326
  * @param brownfieldProject The brownfield project
271
327
  */
272
328
  private fun wireDevLauncherTasks(brownfieldProject: Project) {
@@ -275,7 +331,7 @@ class ExpoBrownfieldSetupPlugin : Plugin<Project> {
275
331
 
276
332
  val sourceDebugTask = devLauncherProject.tasks.findByName("sourceDebugJar")
277
333
  val apolloSourcesTask = devLauncherProject.tasks.findByName("generateServiceApolloSources")
278
-
334
+
279
335
  if (sourceDebugTask == null || apolloSourcesTask == null) {
280
336
  brownfieldProject.logger.warn("WARNING: Application uses expo-dev-launcher but tasks: sourceDebugJar and generateServiceApolloSources")
281
337
  brownfieldProject.logger.warn("Skipping explicitly defining dependency between the tasks...")
@@ -284,7 +340,7 @@ class ExpoBrownfieldSetupPlugin : Plugin<Project> {
284
340
 
285
341
  sourceDebugTask.dependsOn(apolloSourcesTask)
286
342
  } catch (e: GradleException) {
287
- // no-op
343
+ // no-op
288
344
  }
289
345
  }
290
346
  }
@@ -1,6 +1,11 @@
1
+ @file:JvmName("BrownfieldSetupUtilsKt")
2
+
1
3
  package expo.modules.plugin
2
4
 
5
+ import java.io.File
6
+ import javax.xml.parsers.DocumentBuilderFactory
3
7
  import org.gradle.api.Project
8
+ import org.w3c.dom.Element
4
9
 
5
10
  /**
6
11
  * Find the app project in the root project.
@@ -14,3 +19,103 @@ internal fun findAppProject(project: Project): Project {
14
19
  it.plugins.hasPlugin("com.android.application")
15
20
  } ?: throw IllegalStateException("App project not found in the root project")
16
21
  }
22
+
23
+ /**
24
+ * Build a release-variant AndroidManifest.xml that forwards every `<application>`
25
+ * `<meta-data>` entry from the expo app's manifest into the brownfield library's release
26
+ * manifest, so AGP merges them into the consumer's manifest at AAR consumption time.
27
+ *
28
+ * This covers any expo library whose config plugin injects runtime configuration into
29
+ * the expo app's AndroidManifest at `expo prebuild` time (expo-updates, expo-notifications,
30
+ * etc.). Without forwarding, the brownfield library's runtime modules read empty meta-data
31
+ * and silently disable themselves.
32
+ *
33
+ * Value forms supported:
34
+ * - Literal `android:value="..."` (preserved as-is).
35
+ * - String resource refs `android:value="@string/foo"` are resolved against the expo
36
+ * app's `res/values/strings.xml` and inlined. The brownfield AAR cannot share those
37
+ * strings with the consumer because resources are not currently forwarded.
38
+ * - `android:resource="@drawable/foo"` (or any other resource ref) is preserved as a
39
+ * `resource` attribute. Note: for the consumer to actually resolve it, the referenced
40
+ * resource needs to ship in the brownfield AAR (drawable, color, etc.).
41
+ */
42
+ fun buildForwardedApplicationManifest(appManifest: File, appStrings: File): String {
43
+ val empty = """<?xml version="1.0" encoding="utf-8"?>
44
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
45
+ """
46
+ if (!appManifest.exists()) return empty
47
+
48
+ val factory = DocumentBuilderFactory.newInstance().apply { isNamespaceAware = false }
49
+ val doc = factory.newDocumentBuilder().parse(appManifest)
50
+ val applicationNodes = doc.getElementsByTagName("application")
51
+ if (applicationNodes.length == 0) return empty
52
+ val applicationEl = applicationNodes.item(0) as? Element ?: return empty
53
+
54
+ val strings = parseStringResources(appStrings)
55
+ val children = applicationEl.childNodes
56
+
57
+ data class MetaEntry(val name: String, val attr: String, val value: String)
58
+ val entries = mutableListOf<MetaEntry>()
59
+ for (i in 0 until children.length) {
60
+ val el = children.item(i) as? Element ?: continue
61
+ if (el.tagName != "meta-data") continue
62
+ val name = el.getAttribute("android:name")
63
+ if (name.isNullOrEmpty()) continue
64
+ val literalValue = el.getAttribute("android:value")
65
+ val resourceRef = el.getAttribute("android:resource")
66
+ when {
67
+ !literalValue.isNullOrEmpty() ->
68
+ entries.add(MetaEntry(name, "android:value", resolveResourceReference(literalValue, strings)))
69
+ !resourceRef.isNullOrEmpty() ->
70
+ entries.add(MetaEntry(name, "android:resource", resourceRef))
71
+ }
72
+ }
73
+ if (entries.isEmpty()) return empty
74
+
75
+ return buildString {
76
+ append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
77
+ append("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n")
78
+ append(" <application>\n")
79
+ entries.forEach { entry ->
80
+ append(" <meta-data android:name=\"")
81
+ append(escapeXmlAttribute(entry.name))
82
+ append("\" ")
83
+ append(entry.attr)
84
+ append("=\"")
85
+ append(escapeXmlAttribute(entry.value))
86
+ append("\" />\n")
87
+ }
88
+ append(" </application>\n")
89
+ append("</manifest>\n")
90
+ }
91
+ }
92
+
93
+ private fun parseStringResources(file: File): Map<String, String> {
94
+ if (!file.exists()) return emptyMap()
95
+ val factory = DocumentBuilderFactory.newInstance().apply { isNamespaceAware = false }
96
+ val doc = factory.newDocumentBuilder().parse(file)
97
+ val list = doc.getElementsByTagName("string")
98
+ val result = mutableMapOf<String, String>()
99
+ for (i in 0 until list.length) {
100
+ val el = list.item(i) as? Element ?: continue
101
+ val name = el.getAttribute("name") ?: continue
102
+ if (name.isEmpty()) continue
103
+ result[name] = el.textContent
104
+ }
105
+ return result
106
+ }
107
+
108
+ private fun resolveResourceReference(raw: String, strings: Map<String, String>): String {
109
+ if (raw.startsWith("@string/")) {
110
+ val key = raw.removePrefix("@string/")
111
+ return strings[key] ?: raw
112
+ }
113
+ return raw
114
+ }
115
+
116
+ private fun escapeXmlAttribute(value: String): String =
117
+ value.replace("&", "&amp;")
118
+ .replace("<", "&lt;")
119
+ .replace(">", "&gt;")
120
+ .replace("\"", "&quot;")
121
+ .replace("'", "&apos;")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-brownfield",
3
- "version": "56.0.4",
3
+ "version": "56.0.6",
4
4
  "description": "Toolkit and APIs for adding brownfield setup to Expo projects",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -40,7 +40,7 @@
40
40
  "chalk": "^4.1.2",
41
41
  "commander": "^14.0.3",
42
42
  "diff": "^5.2.0",
43
- "expo-build-properties": "~56.0.4",
43
+ "expo-build-properties": "~56.0.6",
44
44
  "expo-manifests": "~56.0.1",
45
45
  "ora": "^5.4.1",
46
46
  "prompts": "^2.4.2"
@@ -53,15 +53,15 @@
53
53
  "@types/prompts": "^2.0.6",
54
54
  "@types/react": "~19.2.0",
55
55
  "glob": "^13.0.6",
56
- "expo": "56.0.0-preview.6",
56
+ "create-expo": "3.7.3",
57
57
  "expo-module-scripts": "56.0.2",
58
- "create-expo": "3.7.3"
58
+ "expo": "56.0.0-preview.8"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "expo": "*",
62
62
  "react": "*"
63
63
  },
64
- "gitHead": "d7b4e5edff4bf2e619d2c2f16d158798c6d592ef",
64
+ "gitHead": "42013232893cb2aa71ab218e9b422d4a8476b3f0",
65
65
  "scripts": {
66
66
  "build": "expo-module build",
67
67
  "clean": "expo-module clean",