@callstack/react-native-brownfield 3.6.1 → 3.8.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 (96) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/ReactBrownfield.podspec +1 -0
  3. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +38 -23
  4. package/ios/ExpoHostRuntime.swift +23 -5
  5. package/ios/ReactNativeBrownfield.swift +17 -0
  6. package/ios/ReactNativeHostRuntime.swift +10 -0
  7. package/ios/ReactNativeViewController.swift +58 -6
  8. package/lib/commonjs/expo-config-plugin/android/utils/androidManifest.js +2 -0
  9. package/lib/commonjs/expo-config-plugin/android/utils/androidManifest.js.map +1 -0
  10. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  11. package/lib/commonjs/expo-config-plugin/android/utils/expo-updates.js +2 -0
  12. package/lib/commonjs/expo-config-plugin/android/utils/expo-updates.js.map +1 -0
  13. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  14. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  15. package/lib/commonjs/expo-config-plugin/android/withBrownfieldAndroid.js +1 -1
  16. package/lib/commonjs/expo-config-plugin/android/withBrownfieldAndroid.js.map +1 -1
  17. package/lib/commonjs/expo-config-plugin/expoUtils.js +1 -1
  18. package/lib/commonjs/expo-config-plugin/expoUtils.js.map +1 -1
  19. package/lib/commonjs/expo-config-plugin/ios/utils/expo-updates.js +2 -0
  20. package/lib/commonjs/expo-config-plugin/ios/utils/expo-updates.js.map +1 -0
  21. package/lib/commonjs/expo-config-plugin/ios/withBrownfieldIos.js +1 -1
  22. package/lib/commonjs/expo-config-plugin/ios/withBrownfieldIos.js.map +1 -1
  23. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +1 -1
  24. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  25. package/lib/commonjs/expo-config-plugin/template/android/AndroidManifest.xml +1 -1
  26. package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.post55.kt +1 -7
  27. package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.pre55.kt +6 -8
  28. package/lib/commonjs/expo-config-plugin/template/android/strings.xml +4 -0
  29. package/lib/commonjs/expo-config-plugin/template/ios/patchExpoPre55.sh +2 -0
  30. package/lib/module/expo-config-plugin/android/utils/androidManifest.js +2 -0
  31. package/lib/module/expo-config-plugin/android/utils/androidManifest.js.map +1 -0
  32. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  33. package/lib/module/expo-config-plugin/android/utils/expo-updates.js +2 -0
  34. package/lib/module/expo-config-plugin/android/utils/expo-updates.js.map +1 -0
  35. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  36. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  37. package/lib/module/expo-config-plugin/android/withBrownfieldAndroid.js +1 -1
  38. package/lib/module/expo-config-plugin/android/withBrownfieldAndroid.js.map +1 -1
  39. package/lib/module/expo-config-plugin/expoUtils.js +1 -1
  40. package/lib/module/expo-config-plugin/expoUtils.js.map +1 -1
  41. package/lib/module/expo-config-plugin/ios/utils/expo-updates.js +2 -0
  42. package/lib/module/expo-config-plugin/ios/utils/expo-updates.js.map +1 -0
  43. package/lib/module/expo-config-plugin/ios/withBrownfieldIos.js +1 -1
  44. package/lib/module/expo-config-plugin/ios/withBrownfieldIos.js.map +1 -1
  45. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +1 -1
  46. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  47. package/lib/module/expo-config-plugin/template/android/AndroidManifest.xml +1 -1
  48. package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.post55.kt +1 -7
  49. package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.pre55.kt +6 -8
  50. package/lib/module/expo-config-plugin/template/android/strings.xml +4 -0
  51. package/lib/module/expo-config-plugin/template/ios/patchExpoPre55.sh +2 -0
  52. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/androidManifest.d.ts +13 -0
  53. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/androidManifest.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  55. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/expo-updates.d.ts +12 -0
  56. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/expo-updates.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +20 -1
  58. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/src/expo-config-plugin/android/withBrownfieldAndroid.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/src/expo-config-plugin/expoUtils.d.ts +1 -0
  61. package/lib/typescript/commonjs/src/expo-config-plugin/expoUtils.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/src/expo-config-plugin/ios/utils/expo-updates.d.ts +30 -0
  63. package/lib/typescript/commonjs/src/expo-config-plugin/ios/utils/expo-updates.d.ts.map +1 -0
  64. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withBrownfieldIos.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +29 -0
  66. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  67. package/lib/typescript/module/src/expo-config-plugin/android/utils/androidManifest.d.ts +13 -0
  68. package/lib/typescript/module/src/expo-config-plugin/android/utils/androidManifest.d.ts.map +1 -0
  69. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  70. package/lib/typescript/module/src/expo-config-plugin/android/utils/expo-updates.d.ts +12 -0
  71. package/lib/typescript/module/src/expo-config-plugin/android/utils/expo-updates.d.ts.map +1 -0
  72. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +20 -1
  73. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  74. package/lib/typescript/module/src/expo-config-plugin/android/withBrownfieldAndroid.d.ts.map +1 -1
  75. package/lib/typescript/module/src/expo-config-plugin/expoUtils.d.ts +1 -0
  76. package/lib/typescript/module/src/expo-config-plugin/expoUtils.d.ts.map +1 -1
  77. package/lib/typescript/module/src/expo-config-plugin/ios/utils/expo-updates.d.ts +30 -0
  78. package/lib/typescript/module/src/expo-config-plugin/ios/utils/expo-updates.d.ts.map +1 -0
  79. package/lib/typescript/module/src/expo-config-plugin/ios/withBrownfieldIos.d.ts.map +1 -1
  80. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +29 -0
  81. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  82. package/package.json +4 -4
  83. package/src/expo-config-plugin/android/utils/androidManifest.ts +131 -0
  84. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
  85. package/src/expo-config-plugin/android/utils/expo-updates.ts +106 -0
  86. package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +122 -8
  87. package/src/expo-config-plugin/android/withBrownfieldAndroid.ts +18 -1
  88. package/src/expo-config-plugin/expoUtils.ts +14 -0
  89. package/src/expo-config-plugin/ios/utils/expo-updates.ts +168 -0
  90. package/src/expo-config-plugin/ios/withBrownfieldIos.ts +13 -1
  91. package/src/expo-config-plugin/ios/xcodeHelpers.ts +217 -4
  92. package/src/expo-config-plugin/template/android/AndroidManifest.xml +1 -1
  93. package/src/expo-config-plugin/template/android/ReactNativeHostManager.post55.kt +1 -7
  94. package/src/expo-config-plugin/template/android/ReactNativeHostManager.pre55.kt +6 -8
  95. package/src/expo-config-plugin/template/android/strings.xml +4 -0
  96. package/src/expo-config-plugin/template/ios/patchExpoPre55.sh +2 -0
@@ -0,0 +1,131 @@
1
+ export type AndroidManifestMetaDataEntry = {
2
+ name: string;
3
+ value: string;
4
+ };
5
+
6
+ export type AndroidStringResourceEntry = {
7
+ name: string;
8
+ value: string;
9
+ };
10
+
11
+ const APPLICATION_BLOCK_REGEX = /<application\b[\s\S]*?<\/application>/;
12
+ const META_DATA_TAG_REGEX =
13
+ /<meta-data\b[\s\S]*?(?:\/>|>[\s\S]*?<\/meta-data>)/g;
14
+ const STRING_TAG_REGEX = /<string\b[\s\S]*?>[\s\S]*?<\/string>/g;
15
+
16
+ export function extractApplicationMetaData(
17
+ manifestContent: string
18
+ ): AndroidManifestMetaDataEntry[] {
19
+ const applicationBlock = manifestContent.match(APPLICATION_BLOCK_REGEX)?.[0];
20
+
21
+ if (!applicationBlock) {
22
+ return [];
23
+ }
24
+
25
+ return (applicationBlock.match(META_DATA_TAG_REGEX) ?? [])
26
+ .map(parseMetaDataTag)
27
+ .filter(
28
+ (metaDataEntry): metaDataEntry is AndroidManifestMetaDataEntry =>
29
+ metaDataEntry !== null
30
+ );
31
+ }
32
+
33
+ export function extractStringResourcesFromXml(
34
+ stringsContent: string,
35
+ resourceNames: string[]
36
+ ): AndroidStringResourceEntry[] {
37
+ if (resourceNames.length === 0) {
38
+ return [];
39
+ }
40
+
41
+ return resourceNames
42
+ .map((name) => extractStringResource(stringsContent, name))
43
+ .filter(
44
+ (stringResource): stringResource is AndroidStringResourceEntry =>
45
+ stringResource !== null
46
+ );
47
+ }
48
+
49
+ export function renderLibraryManifestApplication(
50
+ metaDataEntries: AndroidManifestMetaDataEntry[]
51
+ ): string {
52
+ if (metaDataEntries.length === 0) {
53
+ return '';
54
+ }
55
+
56
+ const renderedMetaDataEntries = metaDataEntries
57
+ .map(
58
+ ({ name, value }) =>
59
+ ` <meta-data android:name="${name}" android:value="${value}" />`
60
+ )
61
+ .join('\n');
62
+
63
+ return ` <application>\n${renderedMetaDataEntries}\n </application>`;
64
+ }
65
+
66
+ export function renderLibraryStringResources(
67
+ stringResources: AndroidStringResourceEntry[]
68
+ ): string {
69
+ if (stringResources.length === 0) {
70
+ return '';
71
+ }
72
+
73
+ return stringResources
74
+ .map(({ name, value }) => ` <string name="${name}">${value}</string>`)
75
+ .join('\n');
76
+ }
77
+
78
+ function parseMetaDataTag(
79
+ metaDataTag: string
80
+ ): AndroidManifestMetaDataEntry | null {
81
+ const name = getAndroidAttribute(metaDataTag, 'name');
82
+ const value = getAndroidAttribute(metaDataTag, 'value');
83
+
84
+ return parseMetaDataAttributes(name, value);
85
+ }
86
+
87
+ function parseMetaDataAttributes(
88
+ name: string | undefined,
89
+ value: string | undefined
90
+ ): AndroidManifestMetaDataEntry | null {
91
+ if (name === undefined || value === undefined) {
92
+ return null;
93
+ }
94
+ return { name, value };
95
+ }
96
+
97
+ function extractStringResource(
98
+ stringsContent: string,
99
+ resourceName: string
100
+ ): AndroidStringResourceEntry | null {
101
+ const stringTag = (stringsContent.match(STRING_TAG_REGEX) ?? []).find(
102
+ (tag) => getAttribute(tag, 'name') === resourceName
103
+ );
104
+
105
+ if (!stringTag) {
106
+ return null;
107
+ }
108
+
109
+ const value = stringTag.match(/>([\s\S]*?)<\/string>/)?.[1];
110
+
111
+ if (value === undefined) {
112
+ return null;
113
+ }
114
+
115
+ return { name: resourceName, value };
116
+ }
117
+
118
+ function getAndroidAttribute(
119
+ metaDataTag: string,
120
+ attributeName: string
121
+ ): string | undefined {
122
+ return getAttribute(metaDataTag, `android:${attributeName}`);
123
+ }
124
+
125
+ function getAttribute(tag: string, attributeName: string): string | undefined {
126
+ const attributeRegex = new RegExp(
127
+ `\\b${attributeName}\\s*=\\s*(['"])([\\s\\S]*?)\\1`
128
+ );
129
+
130
+ return tag.match(attributeRegex)?.[2];
131
+ }
@@ -1,2 +1,2 @@
1
- export const BROWNFIELD_PLUGIN_VERSION = '1.0.2';
1
+ export const BROWNFIELD_PLUGIN_VERSION = '1.1.0';
2
2
  export const brownfieldGradlePluginDependency = `classpath("com.callstack.react:brownfield-gradle-plugin:${BROWNFIELD_PLUGIN_VERSION}")`;
@@ -0,0 +1,106 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ import { Logger } from '../../logging';
5
+ import {
6
+ type AndroidManifestMetaDataEntry,
7
+ type AndroidStringResourceEntry,
8
+ extractApplicationMetaData,
9
+ extractStringResourcesFromXml,
10
+ } from './androidManifest';
11
+
12
+ const APP_MANIFEST_PATH_SEGMENTS = ['src', 'main', 'AndroidManifest.xml'];
13
+ const APP_STRINGS_PATH_SEGMENTS = [
14
+ 'src',
15
+ 'main',
16
+ 'res',
17
+ 'values',
18
+ 'strings.xml',
19
+ ];
20
+ const STRING_REFERENCE_REGEX = /^@string\/([A-Za-z0-9_.]+)$/;
21
+ const EXPO_UPDATES_META_DATA_PREFIX = 'expo.modules.updates.';
22
+
23
+ /**
24
+ * This copies values from the app's finalized Android files while running
25
+ * inside `withDangerousMod` / `withFinalizedMod`. In that phase, the stable
26
+ * cross-plugin input is the on-disk XML after Expo and other config plugins have
27
+ * finished mutating it, so extraction intentionally uses raw file
28
+ * contents as the source of truth.
29
+ */
30
+ export function readExpoUpdatesApplicationMetaData(
31
+ androidDir: string,
32
+ appModuleName: string
33
+ ): AndroidManifestMetaDataEntry[] {
34
+ const manifestPath = path.join(
35
+ androidDir,
36
+ appModuleName,
37
+ ...APP_MANIFEST_PATH_SEGMENTS
38
+ );
39
+
40
+ if (!fs.existsSync(manifestPath)) {
41
+ Logger.logDebug(
42
+ `App manifest not found, skipping metadata copy: ${manifestPath}`
43
+ );
44
+ return [];
45
+ }
46
+
47
+ return extractExpoUpdatesApplicationMetaData(
48
+ fs.readFileSync(manifestPath, 'utf8')
49
+ );
50
+ }
51
+
52
+ export function extractExpoUpdatesApplicationMetaData(
53
+ manifestContent: string
54
+ ): AndroidManifestMetaDataEntry[] {
55
+ return extractApplicationMetaData(manifestContent).filter(
56
+ isExpoUpdatesMetaDataEntry
57
+ );
58
+ }
59
+
60
+ export function readExpoUpdatesStringResources(
61
+ androidDir: string,
62
+ appModuleName: string,
63
+ metaDataEntries: AndroidManifestMetaDataEntry[]
64
+ ): AndroidStringResourceEntry[] {
65
+ const stringResourceNames = getReferencedStringResourceNames(metaDataEntries);
66
+
67
+ if (stringResourceNames.length === 0) {
68
+ return [];
69
+ }
70
+
71
+ const stringsPath = path.join(
72
+ androidDir,
73
+ appModuleName,
74
+ ...APP_STRINGS_PATH_SEGMENTS
75
+ );
76
+
77
+ if (!fs.existsSync(stringsPath)) {
78
+ Logger.logDebug(
79
+ `App strings not found, skipping string resource copy: ${stringsPath}`
80
+ );
81
+ return [];
82
+ }
83
+
84
+ return extractStringResourcesFromXml(
85
+ fs.readFileSync(stringsPath, 'utf8'),
86
+ stringResourceNames
87
+ );
88
+ }
89
+
90
+ function isExpoUpdatesMetaDataEntry(
91
+ metaDataEntry: AndroidManifestMetaDataEntry
92
+ ): boolean {
93
+ return metaDataEntry.name.startsWith(EXPO_UPDATES_META_DATA_PREFIX);
94
+ }
95
+
96
+ function getReferencedStringResourceNames(
97
+ metaDataEntries: AndroidManifestMetaDataEntry[]
98
+ ): string[] {
99
+ return [
100
+ ...new Set(
101
+ metaDataEntries
102
+ .map(({ value }) => value.match(STRING_REFERENCE_REGEX)?.[1])
103
+ .filter((name): name is string => name !== undefined)
104
+ ),
105
+ ];
106
+ }
@@ -9,7 +9,17 @@ import type {
9
9
  } from '../types';
10
10
  import { Logger } from '../logging';
11
11
  import { renderTemplate } from '../template/engine';
12
- import { getExpoInfo } from '../expoUtils';
12
+ import { getExpoInfo, hasExpoUpdatesInstalled } from '../expoUtils';
13
+ import {
14
+ type AndroidManifestMetaDataEntry,
15
+ type AndroidStringResourceEntry,
16
+ renderLibraryManifestApplication,
17
+ renderLibraryStringResources,
18
+ } from './utils/androidManifest';
19
+ import {
20
+ readExpoUpdatesApplicationMetaData,
21
+ readExpoUpdatesStringResources,
22
+ } from './utils/expo-updates';
13
23
  import { getHermesArtifact } from './utils/hermes';
14
24
 
15
25
  /**
@@ -20,7 +30,13 @@ export function createAndroidModule({
20
30
  config,
21
31
  rnVersion,
22
32
  isExpoPre55,
33
+ projectRoot,
23
34
  }: {
35
+ /**
36
+ * Expo app root (used to detect optional dependencies such as expo-updates)
37
+ */
38
+ projectRoot?: string;
39
+
24
40
  /**
25
41
  * Whether the Expo project is pre-55
26
42
  */
@@ -43,6 +59,7 @@ export function createAndroidModule({
43
59
  }): void {
44
60
  const { android } = config;
45
61
  const moduleDir = path.join(androidDir, android.moduleName);
62
+ const hasExpoUpdates = hasExpoUpdatesInstalled(projectRoot);
46
63
 
47
64
  Logger.logDebug(`Creating Android module in: ${androidDir}`);
48
65
 
@@ -70,10 +87,6 @@ export function createAndroidModule({
70
87
  relativePath: 'gradle.properties',
71
88
  content: renderTemplate('android', 'gradle.properties', {}),
72
89
  },
73
- {
74
- relativePath: 'src/main/AndroidManifest.xml',
75
- content: renderTemplate('android', 'AndroidManifest.xml', {}),
76
- },
77
90
  {
78
91
  relativePath: `src/main/java/${config.android.packageName.replace(/\./g, '/')}/ReactNativeHostManager.kt`,
79
92
  content: renderTemplate(
@@ -81,9 +94,19 @@ export function createAndroidModule({
81
94
  isExpoPre55
82
95
  ? 'ReactNativeHostManager.pre55.kt'
83
96
  : 'ReactNativeHostManager.post55.kt',
84
- {
85
- '{{PACKAGE_NAME}}': android.packageName,
86
- }
97
+ isExpoPre55
98
+ ? {
99
+ '{{PACKAGE_NAME}}': android.packageName,
100
+ '{{EXPO_UPDATES_IMPORTS}}': hasExpoUpdates
101
+ ? 'import expo.modules.updates.UpdatesController'
102
+ : '',
103
+ '{{EXPO_UPDATES_REACT_HOST_BLOCK}}': hasExpoUpdates
104
+ ? '\n UpdatesController.setReactHost(reactHost)\n'
105
+ : '\n',
106
+ }
107
+ : {
108
+ '{{PACKAGE_NAME}}': android.packageName,
109
+ }
87
110
  ),
88
111
  },
89
112
  {
@@ -111,11 +134,101 @@ export function createAndroidModule({
111
134
  Logger.logDebug(`Created file: ${filePath}`);
112
135
  }
113
136
 
137
+ syncAndroidModuleExpoUpdatesFromAppFiles({
138
+ androidDir,
139
+ config,
140
+ });
141
+
114
142
  Logger.logDebug(
115
143
  `Android module "${android.moduleName}" created at ${moduleDir}`
116
144
  );
117
145
  }
118
146
 
147
+ export function syncAndroidModuleManifest({
148
+ androidDir,
149
+ config,
150
+ expoUpdatesMetaData,
151
+ }: {
152
+ androidDir: string;
153
+ config: ResolvedBrownfieldPluginConfigWithAndroid;
154
+ expoUpdatesMetaData: AndroidManifestMetaDataEntry[];
155
+ }): void {
156
+ writeAndroidModuleFile(
157
+ path.join(androidDir, config.android.moduleName),
158
+ 'src/main/AndroidManifest.xml',
159
+ renderTemplate('android', 'AndroidManifest.xml', {
160
+ '{{APPLICATION_BLOCK}}':
161
+ renderLibraryManifestApplication(expoUpdatesMetaData),
162
+ })
163
+ );
164
+ }
165
+
166
+ export function syncAndroidModuleStringResources({
167
+ androidDir,
168
+ config,
169
+ expoUpdatesStringResources,
170
+ }: {
171
+ androidDir: string;
172
+ config: ResolvedBrownfieldPluginConfigWithAndroid;
173
+ expoUpdatesStringResources: AndroidStringResourceEntry[];
174
+ }): void {
175
+ writeAndroidModuleFile(
176
+ path.join(androidDir, config.android.moduleName),
177
+ 'src/main/res/values/strings.xml',
178
+ renderTemplate('android', 'strings.xml', {
179
+ '{{STRING_RESOURCES}}': renderLibraryStringResources(
180
+ expoUpdatesStringResources
181
+ ),
182
+ })
183
+ );
184
+ }
185
+
186
+ function writeAndroidModuleFile(
187
+ moduleDir: string,
188
+ relativePath: string,
189
+ content: string
190
+ ): void {
191
+ const filePath = path.join(moduleDir, relativePath);
192
+ const fileDir = path.dirname(filePath);
193
+
194
+ if (!fs.existsSync(fileDir)) {
195
+ fs.mkdirSync(fileDir, { recursive: true });
196
+ }
197
+
198
+ fs.writeFileSync(filePath, content, 'utf8');
199
+ Logger.logDebug(`Created file: ${filePath}`);
200
+ }
201
+
202
+ export function syncAndroidModuleExpoUpdatesFromAppFiles({
203
+ androidDir,
204
+ config,
205
+ }: {
206
+ androidDir: string;
207
+ config: ResolvedBrownfieldPluginConfigWithAndroid;
208
+ }): void {
209
+ const appModuleName = 'app';
210
+ const expoUpdatesMetaData = readExpoUpdatesApplicationMetaData(
211
+ androidDir,
212
+ appModuleName
213
+ );
214
+ const expoUpdatesStringResources = readExpoUpdatesStringResources(
215
+ androidDir,
216
+ appModuleName,
217
+ expoUpdatesMetaData
218
+ );
219
+
220
+ syncAndroidModuleManifest({
221
+ androidDir,
222
+ config,
223
+ expoUpdatesMetaData,
224
+ });
225
+ syncAndroidModuleStringResources({
226
+ androidDir,
227
+ config,
228
+ expoUpdatesStringResources,
229
+ });
230
+ }
231
+
119
232
  /**
120
233
  * Dangerous mod that creates the Android module directory and files
121
234
  */
@@ -154,6 +267,7 @@ export const withAndroidModuleFiles: ConfigPlugin<
154
267
  config: props,
155
268
  rnVersion,
156
269
  isExpoPre55,
270
+ projectRoot: dangerousConfig.modRequest.projectRoot,
157
271
  });
158
272
 
159
273
  return dangerousConfig;
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  withProjectBuildGradle,
3
3
  withSettingsGradle,
4
+ withFinalizedMod,
4
5
  type ConfigPlugin,
5
6
  } from '@expo/config-plugins';
6
7
 
@@ -8,7 +9,10 @@ import {
8
9
  modifyRootBuildGradle,
9
10
  modifySettingsGradle,
10
11
  } from './utils/gradleHelpers';
11
- import { withAndroidModuleFiles } from './withAndroidModuleFiles';
12
+ import {
13
+ syncAndroidModuleExpoUpdatesFromAppFiles,
14
+ withAndroidModuleFiles,
15
+ } from './withAndroidModuleFiles';
12
16
  import type { ResolvedBrownfieldPluginConfigWithAndroid } from '../types';
13
17
 
14
18
  /**
@@ -47,5 +51,18 @@ export const withBrownfieldAndroid: ConfigPlugin<
47
51
  // Step 3: create the Android module files using dangerous mod
48
52
  config = withAndroidModuleFiles(config, props);
49
53
 
54
+ // Step 4: sync module metadata after Expo writes all Android files
55
+ config = withFinalizedMod(config, [
56
+ 'android',
57
+ async (finalizedConfig) => {
58
+ syncAndroidModuleExpoUpdatesFromAppFiles({
59
+ androidDir: finalizedConfig.modRequest.platformProjectRoot,
60
+ config: props,
61
+ });
62
+
63
+ return finalizedConfig;
64
+ },
65
+ ]);
66
+
50
67
  return config;
51
68
  };
@@ -10,3 +10,17 @@ export function getExpoInfo(config: ExpoConfig) {
10
10
  isExpoPre55,
11
11
  };
12
12
  }
13
+
14
+ export function hasExpoUpdatesInstalled(
15
+ projectRoot: string | undefined
16
+ ): boolean {
17
+ if (!projectRoot) return false;
18
+ try {
19
+ require.resolve('expo-updates/package.json', {
20
+ paths: [projectRoot],
21
+ });
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
@@ -0,0 +1,168 @@
1
+ import type { XcodeProject } from '@expo/config-plugins';
2
+
3
+ import { Logger } from '../../logging';
4
+ import { SourceModificationError } from '../../errors/SourceModificationError';
5
+ import { ensureTargetHasFileReferenceInResourcesBuildPhase } from '../xcodeHelpers';
6
+
7
+ type PbxFileReference = {
8
+ [key: string]: unknown;
9
+ name?: string;
10
+ path?: string;
11
+ };
12
+
13
+ type PbxGroupLike = {
14
+ [key: string]: unknown;
15
+ name?: string;
16
+ path?: string;
17
+ children?: Array<{ value?: string; comment?: string } | string>;
18
+ };
19
+
20
+ const EXPO_PLIST_FILE_NAME = 'Expo.plist';
21
+ const EXPO_PLIST_PRIMARY_RELATIVE_PATH = 'Supporting/Expo.plist';
22
+ const RESOURCES_BUILD_PHASE_COMMENT = 'Resources';
23
+ const EXPO_PLIST_RESOURCE_COMMENT = 'Expo.plist in Resources';
24
+
25
+ function normalizePbxString(value: unknown): string {
26
+ return String(value ?? '').replace(/^"(.*)"$/, '$1');
27
+ }
28
+
29
+ function normalizePbxPathLike(value: unknown): string {
30
+ return normalizePbxString(value).replace(/^\.\//, '');
31
+ }
32
+
33
+ function isSupportingGroup(group: PbxGroupLike): boolean {
34
+ const groupName = normalizePbxString(group.name);
35
+ const groupPath = normalizePbxString(group.path);
36
+
37
+ return (
38
+ groupName === 'Supporting' ||
39
+ groupPath === 'Supporting' ||
40
+ groupPath.endsWith('/Supporting')
41
+ );
42
+ }
43
+
44
+ function groupContainsFileReference(
45
+ group: PbxGroupLike,
46
+ fileRefUuid: string
47
+ ): boolean {
48
+ const children = Array.isArray(group.children) ? group.children : [];
49
+
50
+ return children.some((child) => {
51
+ if (typeof child === 'string') {
52
+ return child === fileRefUuid;
53
+ }
54
+
55
+ return child?.value === fileRefUuid;
56
+ });
57
+ }
58
+
59
+ function isPrimaryExpoPlistMatch(
60
+ fileRefUuid: string,
61
+ fileRef: PbxFileReference,
62
+ groups?: Record<string, PbxGroupLike | string>
63
+ ): boolean {
64
+ const normalizedPath = normalizePbxPathLike(fileRef.path);
65
+ const fileName = normalizePbxString(fileRef.name);
66
+
67
+ if (
68
+ normalizedPath === EXPO_PLIST_PRIMARY_RELATIVE_PATH ||
69
+ normalizedPath.endsWith(`/${EXPO_PLIST_PRIMARY_RELATIVE_PATH}`)
70
+ ) {
71
+ return true;
72
+ }
73
+
74
+ if (
75
+ normalizedPath !== EXPO_PLIST_FILE_NAME &&
76
+ fileName !== EXPO_PLIST_FILE_NAME
77
+ ) {
78
+ return false;
79
+ }
80
+
81
+ return Object.entries(groups ?? {}).some(([groupUuid, group]) => {
82
+ if (groupUuid.endsWith('_comment') || typeof group === 'string') {
83
+ return false;
84
+ }
85
+
86
+ return (
87
+ isSupportingGroup(group) && groupContainsFileReference(group, fileRefUuid)
88
+ );
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Selects an existing PBXFileReference for the app-level Expo plist.
94
+ *
95
+ * Matches either:
96
+ * - a file reference whose own path is `Supporting/Expo.plist`
97
+ * - or an `Expo.plist` file reference that lives under a `Supporting` PBXGroup
98
+ */
99
+ export function selectExpoPlistFileReference(
100
+ fileReferences: Record<string, PbxFileReference | string>,
101
+ groups?: Record<string, PbxGroupLike | string>
102
+ ): string | null {
103
+ for (const [fileRefUuid, fileRef] of Object.entries(fileReferences)) {
104
+ if (fileRefUuid.endsWith('_comment') || typeof fileRef === 'string') {
105
+ continue;
106
+ }
107
+
108
+ if (isPrimaryExpoPlistMatch(fileRefUuid, fileRef, groups)) {
109
+ return fileRefUuid;
110
+ }
111
+ }
112
+
113
+ return null;
114
+ }
115
+
116
+ function getExpoPlistFileRefOrThrow(project: XcodeProject): string {
117
+ const fileReferences = project.pbxFileReferenceSection() as Record<
118
+ string,
119
+ PbxFileReference | string
120
+ >;
121
+ const groups = (project as any).hash?.project?.objects?.PBXGroup as
122
+ | Record<string, PbxGroupLike | string>
123
+ | undefined;
124
+ const existingExpoPlistFileRefUuid = selectExpoPlistFileReference(
125
+ fileReferences,
126
+ groups
127
+ );
128
+
129
+ if (existingExpoPlistFileRefUuid) {
130
+ return existingExpoPlistFileRefUuid;
131
+ }
132
+
133
+ throw new SourceModificationError(
134
+ `Could not find the "${EXPO_PLIST_PRIMARY_RELATIVE_PATH}" PBXFileReference needed for Expo.plist resource wiring`
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Ensures the framework target contains the app-level `Supporting/Expo.plist`
140
+ * in a `PBXResourcesBuildPhase`. This is idempotent and safe to call repeatedly.
141
+ */
142
+ export function ensureFrameworkHasExpoPlistResource(
143
+ project: XcodeProject,
144
+ frameworkTargetUUID: string
145
+ ): void {
146
+ const expoPlistFileRefUuid = getExpoPlistFileRefOrThrow(project);
147
+ const didAddExpoPlistResource =
148
+ ensureTargetHasFileReferenceInResourcesBuildPhase(
149
+ project,
150
+ frameworkTargetUUID,
151
+ expoPlistFileRefUuid,
152
+ {
153
+ resourcesBuildPhaseComment: RESOURCES_BUILD_PHASE_COMMENT,
154
+ buildFileComment: EXPO_PLIST_RESOURCE_COMMENT,
155
+ }
156
+ );
157
+
158
+ if (!didAddExpoPlistResource) {
159
+ Logger.logDebug(
160
+ 'Framework resources already include Supporting/Expo.plist'
161
+ );
162
+ return;
163
+ }
164
+
165
+ Logger.logDebug(
166
+ 'Added Supporting/Expo.plist to framework PBXResourcesBuildPhase'
167
+ );
168
+ }
@@ -11,10 +11,11 @@ import {
11
11
  copyBundleReactNativePhase,
12
12
  } from './xcodeHelpers';
13
13
  import { modifyPodfile } from './podfileHelpers';
14
+ import { ensureFrameworkHasExpoPlistResource } from './utils/expo-updates';
14
15
  import { withIosFrameworkFiles } from './withIosFrameworkFiles';
15
16
  import type { ResolvedBrownfieldPluginConfigWithIos } from '../types';
16
17
  import { Logger } from '../logging';
17
- import { getExpoInfo } from '../expoUtils';
18
+ import { getExpoInfo, hasExpoUpdatesInstalled } from '../expoUtils';
18
19
 
19
20
  /**
20
21
  * iOS Config Plugin for integration with @callstack/react-native-brownfield.
@@ -34,6 +35,7 @@ export const withBrownfieldIos: ConfigPlugin<
34
35
  // Step 1: modify the Xcode project to add framework target &
35
36
  config = withXcodeProject(config, (xcodeConfig) => {
36
37
  const { modResults: project, modRequest } = xcodeConfig;
38
+ const hasExpoUpdates = hasExpoUpdatesInstalled(modRequest.projectRoot);
37
39
 
38
40
  const { frameworkTargetUUID, targetAlreadyExists } = addFrameworkTarget(
39
41
  project,
@@ -41,6 +43,16 @@ export const withBrownfieldIos: ConfigPlugin<
41
43
  props.ios
42
44
  );
43
45
 
46
+ // Ensure Expo.plist is present in the framework resources phase when
47
+ // expo-updates is installed, including for pre-existing framework targets.
48
+ if (hasExpoUpdates) {
49
+ ensureFrameworkHasExpoPlistResource(project, frameworkTargetUUID);
50
+ } else {
51
+ Logger.logDebug(
52
+ 'Skipping Expo.plist framework resource wiring because expo-updates is not installed'
53
+ );
54
+ }
55
+
44
56
  if (targetAlreadyExists) {
45
57
  Logger.logDebug(
46
58
  `Skipping further Xcode modifications as framework target was already present`