@callstack/react-native-brownfield 6.0.0 → 6.1.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 (105) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/gradle-plugin/brownfield/build.gradle.kts +4 -0
  3. package/gradle-plugin/brownfield/gradle/libs.versions.toml +2 -0
  4. package/gradle-plugin/brownfield/gradle.properties +1 -1
  5. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +24 -7
  6. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +4 -37
  7. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/JNILibsProcessor.kt +206 -104
  8. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/ResourceTaskProcessor.kt +201 -4
  9. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantHelper.kt +7 -0
  10. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/processors/VariantTaskProvider.kt +4 -0
  11. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/GradleExclude.kt +31 -0
  12. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt +2 -0
  13. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjector.kt +28 -3
  14. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt +41 -4
  15. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +14 -1
  16. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/TaskOutputs.kt +40 -0
  17. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +14 -6
  18. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/HardExcludePredicateTest.kt +28 -0
  19. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ConfigureProcessJniTaskTest.kt +60 -0
  20. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/CopyExplodedResTaskTest.kt +368 -0
  21. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/JniLibsPlanTest.kt +252 -0
  22. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MergeClassesLocalJarsTest.kt +73 -0
  23. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/MetroResDirWiringTest.kt +39 -0
  24. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/processors/ProcessAndCopyJniLibsTaskTest.kt +136 -0
  25. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/GradleExcludesTest.kt +81 -0
  26. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/shared/PublishingMetadataInjectorTest.kt +56 -0
  27. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibraryNamespaceFallbackTest.kt +137 -0
  28. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/ExtensionTest.kt +5 -0
  29. package/ios/Expo/ExpoHostRuntime.swift +3 -1
  30. package/ios/Expo/ExpoUpdatesStartup.swift +2 -1
  31. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  32. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  33. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  34. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js +2 -2
  35. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  36. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  37. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  38. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  39. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  40. package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  41. package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -1
  42. package/lib/commonjs/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  43. package/lib/commonjs/index.js +1 -1
  44. package/lib/commonjs/index.js.map +1 -1
  45. package/lib/commonjs/scaffold/index.js +1 -1
  46. package/lib/commonjs/scaffold/index.js.map +1 -1
  47. package/lib/commonjs/scaffold/projectFiles.js +1 -1
  48. package/lib/commonjs/scaffold/projectFiles.js.map +1 -1
  49. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  50. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  51. package/lib/module/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  52. package/lib/module/expo-config-plugin/ios/podfileHelpers.js +2 -2
  53. package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  54. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  55. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  56. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  57. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  58. package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  59. package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -1
  60. package/lib/module/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  61. package/lib/module/index.js +1 -1
  62. package/lib/module/index.js.map +1 -1
  63. package/lib/module/scaffold/index.js +1 -1
  64. package/lib/module/scaffold/index.js.map +1 -1
  65. package/lib/module/scaffold/projectFiles.js +1 -1
  66. package/lib/module/scaffold/projectFiles.js.map +1 -1
  67. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  68. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
  71. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
  73. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
  75. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts +2 -2
  79. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts.map +1 -1
  80. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  81. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  82. package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  83. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
  84. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  85. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
  86. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  87. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +6 -0
  88. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  89. package/lib/typescript/module/src/index.d.ts.map +1 -1
  90. package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -1
  91. package/lib/typescript/module/src/scaffold/projectFiles.d.ts +2 -2
  92. package/lib/typescript/module/src/scaffold/projectFiles.d.ts.map +1 -1
  93. package/package.json +2 -2
  94. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
  95. package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +42 -0
  96. package/src/expo-config-plugin/ios/podfileHelpers.ts +47 -13
  97. package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +11 -1
  98. package/src/expo-config-plugin/ios/xcodeHelpers.ts +67 -9
  99. package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  100. package/src/expo-config-plugin/template/android/build.gradle.kts +1 -1
  101. package/src/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  102. package/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +7 -0
  103. package/src/index.ts +4 -2
  104. package/src/scaffold/index.ts +61 -4
  105. package/src/scaffold/projectFiles.ts +5 -3
@@ -40,6 +40,43 @@ function renderMissingDimensionStrategyBlock(
40
40
  return ` missingDimensionStrategy(${args})`;
41
41
  }
42
42
 
43
+ const COORDINATE_PART = /^[A-Za-z0-9._-]+$/;
44
+
45
+ /**
46
+ * Renders `excludeDependencies` as a `configurations.all` block. The Brownfield Gradle plugin
47
+ * leaves anything excluded this way out of the published POM and module.json. Entries become
48
+ * Kotlin string literals, so anything but a plain coordinate is rejected.
49
+ */
50
+ function renderExcludeDependenciesBlock(excludeDependencies: string[]): string {
51
+ // It comes from user JSON, so the type isn't guaranteed.
52
+ if (!Array.isArray(excludeDependencies)) {
53
+ throw new Error(
54
+ 'android.excludeDependencies must be a list of "group:module" or "group" strings.'
55
+ );
56
+ }
57
+ if (excludeDependencies.length === 0) {
58
+ return '';
59
+ }
60
+
61
+ const lines = excludeDependencies.map((entry) => {
62
+ const parts = entry.split(':');
63
+ if (
64
+ parts.length > 2 ||
65
+ !parts.every((part) => COORDINATE_PART.test(part))
66
+ ) {
67
+ throw new Error(
68
+ `android.excludeDependencies: '${entry}' is not in the form "group:module" or "group".`
69
+ );
70
+ }
71
+ const [group, module] = parts;
72
+ return module
73
+ ? ` exclude(group = "${group}", module = "${module}")`
74
+ : ` exclude(group = "${group}")`;
75
+ });
76
+
77
+ return `configurations.all {\n${lines.join('\n')}\n}\n\n`;
78
+ }
79
+
43
80
  export function resolveCompileSdkVersionExpression(
44
81
  config: ResolvedBrownfieldPluginConfigWithAndroid
45
82
  ): string {
@@ -154,6 +191,10 @@ export function createAndroidModule({
154
191
  android.missingDimensionStrategies
155
192
  );
156
193
 
194
+ const excludeDependenciesBlock = renderExcludeDependenciesBlock(
195
+ android.excludeDependencies
196
+ );
197
+
157
198
  // generate module files
158
199
  const files: RenderedTemplateFile[] = [
159
200
  {
@@ -171,6 +212,7 @@ export function createAndroidModule({
171
212
  '{{IS_MINIFY_ENABLED}}': minifyEnabled.toString(),
172
213
  '{{MISSING_DIMENSION_STRATEGIES}}': missingDimensionStrategiesList,
173
214
  '{{MISSING_DIMENSION_STRATEGY_BLOCK}}': missingDimensionStrategyBlock,
215
+ '{{EXCLUDE_DEPENDENCIES_BLOCK}}': excludeDependenciesBlock,
174
216
  }),
175
217
  },
176
218
  {
@@ -20,7 +20,7 @@ function ensureExpoDefinesForSDK55AndAbove(
20
20
  const defaultDeploymentTarget = expoMajor >= 56 ? '16.4' : '15.1';
21
21
  const hook = `
22
22
  ${BROWNFIELD_EXPO_GTE_55_SWIFT_DEFINES_MARKER_START}
23
- brownfield_ios_deployment_target = podfile_properties['ios.deploymentTarget'] || '${defaultDeploymentTarget}'
23
+ brownfield_ios_deployment_target = (defined?(podfile_properties) && podfile_properties['ios.deploymentTarget']) || '${defaultDeploymentTarget}'
24
24
  installer.pods_project.targets.each do |target|
25
25
  if target.name == 'ReactBrownfield'
26
26
  puts "[Brownfield] Adding definition of EXPO_SDK_GTE_55 to target: #{target.name}"
@@ -65,11 +65,23 @@ export function modifyPodfile(
65
65
  expoMajor?: number
66
66
  ): string {
67
67
  // check if the framework target is already included
68
- if (podfile.includes(`target '${frameworkName}'`)) {
69
- Logger.logDebug(
70
- `Framework target "${frameworkName}" already in Podfile, skipping modification`
68
+ if (
69
+ podfile.includes(`target '${frameworkName}'`) ||
70
+ podfile.includes(`target "${frameworkName}"`)
71
+ ) {
72
+ if (typeof expoMajor !== 'number') {
73
+ Logger.logDebug(
74
+ `Framework target "${frameworkName}" already in Podfile, skipping modification`
75
+ );
76
+ return podfile;
77
+ }
78
+
79
+ // A React Native CLI app can add Expo after it was scaffolded, so a re-run
80
+ // switches the existing target to the Expo host.
81
+ return applyExpoHooks(
82
+ ensureExpoHostInFrameworkTarget(podfile, frameworkName),
83
+ expoMajor
71
84
  );
72
- return podfile;
73
85
  }
74
86
 
75
87
  Logger.logDebug(`Modifying Podfile for framework: ${frameworkName}`);
@@ -113,12 +125,39 @@ export function modifyPodfile(
113
125
  Logger.logDebug(`Added framework target "${frameworkName}" to Podfile`);
114
126
 
115
127
  // Vanilla projects (expoMajor === undefined) get no Expo-only hooks.
116
- // Early-returning here lets TypeScript narrow `expoMajor` to `number`
117
- // for the rest of the function (no casts needed below).
118
128
  if (typeof expoMajor !== 'number') {
119
129
  return modifiedPodfile;
120
130
  }
121
131
 
132
+ return applyExpoHooks(modifiedPodfile, expoMajor);
133
+ }
134
+
135
+ function ensureExpoHostInFrameworkTarget(
136
+ podfile: string,
137
+ frameworkName: string
138
+ ): string {
139
+ if (/^[ \t]*ENV\['REACT_NATIVE_BROWNFIELD_USE_EXPO_HOST'\]/m.test(podfile)) {
140
+ return podfile;
141
+ }
142
+
143
+ const escapedName = frameworkName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
144
+ const targetLine = new RegExp(
145
+ `^([ \\t]*)(target\\s+['"]${escapedName}['"]\\s+do\\b[^\\r\\n]*(\\r?\\n))`,
146
+ 'm'
147
+ );
148
+ if (!targetLine.test(podfile)) {
149
+ throw new SourceModificationError(
150
+ `Couldn't find the "target '${frameworkName}' do" line in the Podfile to enable the Expo host. Add ENV['REACT_NATIVE_BROWNFIELD_USE_EXPO_HOST'] = '1' inside that target block manually.`
151
+ );
152
+ }
153
+
154
+ return podfile.replace(
155
+ targetLine,
156
+ `$1$2$1 ENV['REACT_NATIVE_BROWNFIELD_USE_EXPO_HOST'] = '1'$3`
157
+ );
158
+ }
159
+
160
+ function applyExpoHooks(podfile: string, expoMajor: number): string {
122
161
  if (expoMajor < MIN_SUPPORTED_EXPO_SDK_MAJOR_VERSION) {
123
162
  const versionLabel = expoMajor < 0 ? 'unknown' : String(expoMajor);
124
163
  throw new SourceModificationError(
@@ -126,10 +165,5 @@ export function modifyPodfile(
126
165
  );
127
166
  }
128
167
 
129
- modifiedPodfile = ensureExpoDefinesForSDK55AndAbove(
130
- modifiedPodfile,
131
- expoMajor
132
- );
133
-
134
- return modifiedPodfile;
168
+ return ensureExpoDefinesForSDK55AndAbove(podfile, expoMajor);
135
169
  }
@@ -26,9 +26,19 @@ export function getFrameworkSourceFiles(
26
26
  * opposite polarity: `false` here === 'vanilla' there.
27
27
  */
28
28
  useExpoHost?: boolean;
29
+ /**
30
+ * Vanilla template only: a React Native CLI app that uses Expo modules
31
+ * also needs `ensureExpoModulesProvider()` (the Expo template has it).
32
+ */
33
+ withExpoModules?: boolean;
29
34
  }
30
35
  ): RenderedTemplateFile[] {
31
36
  const useExpoHost = options?.useExpoHost ?? true;
37
+ const vanillaInterface =
38
+ renderTemplate('ios', 'FrameworkInterface.vanilla.swift', {}) +
39
+ (options?.withExpoModules
40
+ ? renderTemplate('ios', 'ExpoModulesProviderExtension.swift', {})
41
+ : '');
32
42
 
33
43
  return [
34
44
  {
@@ -37,7 +47,7 @@ export function getFrameworkSourceFiles(
37
47
  ? renderTemplate('ios', 'FrameworkInterface.swift', {
38
48
  '{{BUNDLE_IDENTIFIER}}': ios.bundleIdentifier,
39
49
  })
40
- : renderTemplate('ios', 'FrameworkInterface.vanilla.swift', {}),
50
+ : vanillaInterface,
41
51
  },
42
52
  {
43
53
  relativePath: 'Info.plist',
@@ -216,19 +216,29 @@ export function addSourceFilesBuildPhase(
216
216
  (filePath) => !filePath.endsWith('.plist')
217
217
  );
218
218
 
219
- // Idempotency: skip when the target already has a sources phase with this
220
- // comment, so re-running scaffolding does not duplicate the build phase.
221
- const nativeTargets = getRawProjectObjects(project).PBXNativeTarget as
219
+ // Idempotency: skip when the target already has a sources phase, so
220
+ // re-running scaffolding does not duplicate the build phase. CocoaPods
221
+ // rewrites the phase's comment to "Sources" on `pod install`, so match by
222
+ // the phase type as well as by our comment.
223
+ const rawObjects = getRawProjectObjects(project);
224
+ const nativeTargets = rawObjects.PBXNativeTarget as
222
225
  | Record<string, PbxNativeTarget>
223
226
  | undefined;
227
+ const sourcesPhases = (rawObjects.PBXSourcesBuildPhase ?? {}) as Record<
228
+ string,
229
+ unknown
230
+ >;
224
231
  const targetBuildPhases =
225
232
  nativeTargets?.[frameworkTargetUUID]?.buildPhases ?? [];
226
- const alreadyHasSourcePhase = targetBuildPhases.some((phase) =>
227
- hasBuildPhaseComment(
228
- { comment: typeof phase === 'string' ? undefined : phase.comment },
229
- options.frameworkName
230
- )
231
- );
233
+ const alreadyHasSourcePhase = targetBuildPhases.some((phase) => {
234
+ const phaseUuid = getReferencedUuid(phase);
235
+ return (
236
+ hasBuildPhaseComment(
237
+ { comment: typeof phase === 'string' ? undefined : phase.comment },
238
+ options.frameworkName
239
+ ) || Boolean(phaseUuid && sourcesPhases[phaseUuid])
240
+ );
241
+ });
232
242
  if (alreadyHasSourcePhase) {
233
243
  Logger.logDebug(
234
244
  `Sources build phase "${options.frameworkName}" already present on target ${frameworkTargetUUID}, skipping`
@@ -812,3 +822,51 @@ export function resolveFrameworkDeploymentTarget(
812
822
 
813
823
  return appTargetDeploymentTarget ?? '15.0';
814
824
  }
825
+
826
+ function isVersionLower(version: string, minimum: string): boolean {
827
+ const a = version.split('.').map(Number);
828
+ const b = minimum.split('.').map(Number);
829
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
830
+ const diff = (a[i] ?? 0) - (b[i] ?? 0);
831
+ if (diff !== 0) {
832
+ return diff < 0;
833
+ }
834
+ }
835
+ return false;
836
+ }
837
+
838
+ /**
839
+ * Raises the framework target's iOS deployment target to `minimum` in every
840
+ * build configuration where it is lower. Returns whether anything changed.
841
+ */
842
+ export function raiseFrameworkDeploymentTarget(
843
+ project: XcodeProject,
844
+ frameworkTargetUUID: string,
845
+ minimum: string
846
+ ): boolean {
847
+ const rawObjects = getRawProjectObjects(project);
848
+ const target = rawObjects.PBXNativeTarget?.[frameworkTargetUUID] as
849
+ | PbxNativeTarget
850
+ | undefined;
851
+ const configList = target?.buildConfigurationList
852
+ ? rawObjects.XCConfigurationList?.[target.buildConfigurationList]
853
+ : undefined;
854
+ const configs = rawObjects.XCBuildConfiguration ?? {};
855
+
856
+ let changed = false;
857
+ for (const ref of configList?.buildConfigurations ?? []) {
858
+ const uuid = getReferencedUuid(ref);
859
+ const buildSettings = uuid ? configs[uuid]?.buildSettings : undefined;
860
+ if (!buildSettings) {
861
+ continue;
862
+ }
863
+ const current = normalizeXcodeBuildSettingValue(
864
+ buildSettings.IPHONEOS_DEPLOYMENT_TARGET
865
+ );
866
+ if (!current || isVersionLower(current, minimum)) {
867
+ buildSettings.IPHONEOS_DEPLOYMENT_TARGET = minimum;
868
+ changed = true;
869
+ }
870
+ }
871
+ return changed;
872
+ }
@@ -5,15 +5,16 @@ import android.content.res.Configuration
5
5
  import com.callstack.reactnativebrownfield.OnJSBundleLoaded
6
6
  import com.callstack.reactnativebrownfield.ReactNativeBrownfield
7
7
  import com.facebook.react.PackageList
8
- import com.facebook.react.ReactHost
9
- import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
10
8
  import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
9
+ import com.facebook.react.views.view.setEdgeToEdgeFeatureFlagOn
11
10
 
12
11
  object ReactNativeHostManager {
13
12
  fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) {
14
- loadReactNative(application)
13
+ if (BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
14
+ setEdgeToEdgeFeatureFlagOn()
15
+ }
15
16
 
16
- val reactHost: ReactHost by lazy {
17
+ ReactNativeBrownfield.initialize(application, onJSBundleLoaded) {
17
18
  getDefaultReactHost(
18
19
  context = application,
19
20
  packageList = PackageList(application).packages,
@@ -24,12 +25,9 @@ object ReactNativeHostManager {
24
25
  jsRuntimeFactory = null
25
26
  )
26
27
  }
27
-
28
- ReactNativeBrownfield.initialize(application, reactHost, onJSBundleLoaded)
29
28
  }
30
29
 
31
30
  fun onConfigurationChanged(application: Application, newConfig: Configuration) {
32
31
  // no-op (kept for API symmetry with Expo variant)
33
32
  }
34
33
  }
35
-
@@ -47,7 +47,7 @@ reactBrownfield {
47
47
  missingDimensionStrategies = listOf({{MISSING_DIMENSION_STRATEGIES}})
48
48
  }
49
49
 
50
- react {
50
+ {{EXCLUDE_DEPENDENCIES_BLOCK}}react {
51
51
  autolinkLibrariesWithApp()
52
52
  }
53
53
 
@@ -0,0 +1,7 @@
1
+ // Keeps the generated ExpoModulesProvider linked into the framework, so Expo
2
+ // modules register when the host app starts React Native.
3
+ extension ReactNativeBrownfield {
4
+ public func ensureExpoModulesProvider() {
5
+ let _ = ExpoModulesProvider()
6
+ }
7
+ }
@@ -69,6 +69,13 @@ export interface BrownfieldPluginAndroidConfig {
69
69
  * @default []
70
70
  */
71
71
  missingDimensionStrategies?: string[];
72
+
73
+ /**
74
+ * Dependencies to keep out of the published AAR's POM and module.json, as
75
+ * "group:module" or "group" for a whole group, e.g. "com.caverock:androidsvg".
76
+ * @default []
77
+ */
78
+ excludeDependencies?: string[];
72
79
  }
73
80
 
74
81
  /**
package/src/index.ts CHANGED
@@ -96,11 +96,13 @@ const ReactNativeBrownfield = {
96
96
  ): { remove: () => void } => {
97
97
  const subscription = ReactNativeBrownfieldModule.onBrownfieldMessage(
98
98
  (payload) => {
99
+ let data: unknown;
99
100
  try {
100
- callback({ data: JSON.parse(payload.text) });
101
+ data = JSON.parse(payload.text);
101
102
  } catch {
102
- callback({ data: payload.text });
103
+ data = payload.text;
103
104
  }
105
+ callback({ data });
104
106
  }
105
107
  );
106
108
  return { remove: () => subscription.remove() };
@@ -26,6 +26,7 @@ import {
26
26
  addFrameworkTarget,
27
27
  addSourceFilesBuildPhase,
28
28
  copyBundleReactNativePhase,
29
+ raiseFrameworkDeploymentTarget,
29
30
  } from '../expo-config-plugin/ios/xcodeHelpers';
30
31
  import { createIosFramework } from '../expo-config-plugin/ios/withIosFrameworkFiles';
31
32
  import {
@@ -167,6 +168,35 @@ function resolveReactNativeVersion(projectRoot: string): string {
167
168
  return rnPkg.version;
168
169
  }
169
170
 
171
+ /**
172
+ * Major Expo SDK version when the app itself depends on `expo` (installed
173
+ * with `install-expo-modules`), otherwise undefined. Only the app's own
174
+ * package.json counts: in a monorepo, `expo` can resolve from a hoisted
175
+ * node_modules even though this app never set it up. Returns -1 when the
176
+ * dependency is declared but not installed, which modifyPodfile reports.
177
+ */
178
+ function resolveExpoMajor(projectRoot: string): number | undefined {
179
+ const pkg = JSON.parse(
180
+ fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')
181
+ );
182
+ if (!pkg.dependencies?.expo && !pkg.devDependencies?.expo) {
183
+ return undefined;
184
+ }
185
+
186
+ const localRequire: NodeRequire =
187
+ typeof require === 'undefined'
188
+ ? createRequire(path.join(process.cwd(), 'noop.js'))
189
+ : require;
190
+ try {
191
+ const expoPkg = localRequire(
192
+ localRequire.resolve('expo/package.json', { paths: [projectRoot] })
193
+ );
194
+ return Number.parseInt(String(expoPkg.version).split('.')[0] ?? '', 10);
195
+ } catch {
196
+ return -1;
197
+ }
198
+ }
199
+
170
200
  export async function scaffoldBrownfieldInRncCliProject(
171
201
  options: BrownfieldScaffoldOptions = {}
172
202
  ): Promise<void> {
@@ -202,6 +232,12 @@ export async function scaffoldBrownfieldInRncCliProject(
202
232
  : path.join(projectRoot, iosSourceDir || 'ios');
203
233
 
204
234
  const rnVersion = resolveReactNativeVersion(projectRoot);
235
+ const expoMajor = resolveExpoMajor(projectRoot);
236
+ if (expoMajor !== undefined) {
237
+ Logger.logInfo(
238
+ 'Expo modules detected. The iOS framework will start React Native through the Expo host.'
239
+ );
240
+ }
205
241
 
206
242
  const iosFrameworkName = options.iosFrameworkName ?? 'BrownfieldLib';
207
243
  const androidModuleName = options.androidModuleName ?? 'brownfieldlib';
@@ -265,6 +301,7 @@ export async function scaffoldBrownfieldInRncCliProject(
265
301
  useLocalGradlePlugin: false,
266
302
  useLocalMaven: false,
267
303
  missingDimensionStrategies: [],
304
+ excludeDependencies: [],
268
305
  },
269
306
  ios: null,
270
307
  debug: options.debug ?? false,
@@ -306,7 +343,9 @@ export async function scaffoldBrownfieldInRncCliProject(
306
343
  // an iOS deployment target directly.
307
344
  // TODO: derive from the Podfile `platform :ios` line or RN's
308
345
  // min_ios_version_supported, or expose a flag.
309
- deploymentTarget: '15.0',
346
+ // Expo SDK 56+ modules require iOS 16.4.
347
+ deploymentTarget:
348
+ expoMajor !== undefined && expoMajor >= 56 ? '16.4' : '15.0',
310
349
  // Standard CFBundleShortVersionString for the generated framework
311
350
  // (matches the Expo plugin default); unrelated to package versioning.
312
351
  frameworkVersion: '1',
@@ -330,13 +369,25 @@ export async function scaffoldBrownfieldInRncCliProject(
330
369
  projectName: path.basename(xcodeprojPath, '.xcodeproj'),
331
370
  };
332
371
 
333
- const { frameworkTargetUUID } = addFrameworkTarget(
372
+ const { frameworkTargetUUID, targetAlreadyExists } = addFrameworkTarget(
334
373
  project,
335
374
  modRequest,
336
375
  resolvedIosConfig.ios,
337
376
  { useExpoHost: false }
338
377
  );
339
378
 
379
+ // A target created before Expo was added keeps its old deployment target.
380
+ if (
381
+ targetAlreadyExists &&
382
+ expoMajor !== undefined &&
383
+ expoMajor >= 56 &&
384
+ raiseFrameworkDeploymentTarget(project, frameworkTargetUUID, '16.4')
385
+ ) {
386
+ Logger.logInfo(
387
+ `Raised the ${iosFrameworkName} deployment target to iOS 16.4, which Expo SDK 56 and newer require.`
388
+ );
389
+ }
390
+
340
391
  copyBundleReactNativePhase(project, frameworkTargetUUID);
341
392
  addSourceFilesBuildPhase(
342
393
  project,
@@ -355,10 +406,16 @@ export async function scaffoldBrownfieldInRncCliProject(
355
406
  if (!podfile) {
356
407
  throw new Error(`Missing ${podfilePath}`);
357
408
  }
358
- writeFileIfChanged(podfilePath, modifyPodfile(podfile, iosFrameworkName));
409
+ writeFileIfChanged(
410
+ podfilePath,
411
+ modifyPodfile(podfile, iosFrameworkName, expoMajor)
412
+ );
359
413
 
360
414
  currentStep = 'ios-sources';
361
- createIosFramework(iosDir, resolvedIosConfig, { useExpoHost: false });
415
+ createIosFramework(iosDir, resolvedIosConfig, {
416
+ useExpoHost: false,
417
+ withExpoModules: expoMajor !== undefined,
418
+ });
362
419
 
363
420
  // --- App package wiring: Brownfield deps + package:* scripts ---
364
421
  // The scaffolded Kotlin host imports com.callstack.reactnativebrownfield.*,
@@ -120,15 +120,17 @@ export function addBrownfieldDependencies(projectRoot: string): string[] {
120
120
 
121
121
  /**
122
122
  * Adds the `package:ios` / `package:android` scripts from the docs' manual
123
- * steps (settings come from the generated brownfield.config.json, so no flags
124
- * are duplicated here). Existing scripts with the same names are kept.
123
+ * steps (settings come from the generated brownfield.config.json, so no other
124
+ * flags are duplicated here). Existing scripts with the same names are kept.
125
125
  */
126
126
  export function addBrownfieldPackageScripts(projectRoot: string): string[] {
127
127
  const pkg = readPackageJson(projectRoot);
128
128
  pkg.scripts = pkg.scripts ?? {};
129
129
 
130
130
  const desiredScripts: Record<string, string> = {
131
- 'package:ios': 'brownfield package:ios',
131
+ // Also emit a local Swift package, so the host app adds one package
132
+ // instead of every XCFramework.
133
+ 'package:ios': 'brownfield package:ios --add-spm-package',
132
134
  'package:android': 'brownfield package:android',
133
135
  };
134
136