@callstack/react-native-brownfield 6.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/gradle-plugin/brownfield/gradle.properties +1 -1
  3. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt +41 -4
  4. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibraryNamespaceFallbackTest.kt +137 -0
  5. package/ios/Expo/ExpoHostRuntime.swift +3 -1
  6. package/ios/Expo/ExpoUpdatesStartup.swift +2 -1
  7. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  8. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js +2 -2
  9. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  10. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  11. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  12. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  13. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  14. package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  15. package/lib/commonjs/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  16. package/lib/commonjs/scaffold/index.js +1 -1
  17. package/lib/commonjs/scaffold/index.js.map +1 -1
  18. package/lib/commonjs/scaffold/projectFiles.js +1 -1
  19. package/lib/commonjs/scaffold/projectFiles.js.map +1 -1
  20. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  21. package/lib/module/expo-config-plugin/ios/podfileHelpers.js +2 -2
  22. package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  23. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  24. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  25. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  26. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  27. package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  28. package/lib/module/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  29. package/lib/module/scaffold/index.js +1 -1
  30. package/lib/module/scaffold/index.js.map +1 -1
  31. package/lib/module/scaffold/projectFiles.js +1 -1
  32. package/lib/module/scaffold/projectFiles.js.map +1 -1
  33. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  34. package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
  36. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
  38. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts +2 -2
  41. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts.map +1 -1
  42. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  43. package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  44. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
  45. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  46. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
  47. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  48. package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -1
  49. package/lib/typescript/module/src/scaffold/projectFiles.d.ts +2 -2
  50. package/lib/typescript/module/src/scaffold/projectFiles.d.ts.map +1 -1
  51. package/package.json +2 -2
  52. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
  53. package/src/expo-config-plugin/ios/podfileHelpers.ts +47 -13
  54. package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +11 -1
  55. package/src/expo-config-plugin/ios/xcodeHelpers.ts +67 -9
  56. package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  57. package/src/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  58. package/src/scaffold/index.ts +60 -4
  59. package/src/scaffold/projectFiles.ts +5 -3
@@ -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
-
@@ -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
+ }
@@ -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';
@@ -306,7 +342,9 @@ export async function scaffoldBrownfieldInRncCliProject(
306
342
  // an iOS deployment target directly.
307
343
  // TODO: derive from the Podfile `platform :ios` line or RN's
308
344
  // min_ios_version_supported, or expose a flag.
309
- deploymentTarget: '15.0',
345
+ // Expo SDK 56+ modules require iOS 16.4.
346
+ deploymentTarget:
347
+ expoMajor !== undefined && expoMajor >= 56 ? '16.4' : '15.0',
310
348
  // Standard CFBundleShortVersionString for the generated framework
311
349
  // (matches the Expo plugin default); unrelated to package versioning.
312
350
  frameworkVersion: '1',
@@ -330,13 +368,25 @@ export async function scaffoldBrownfieldInRncCliProject(
330
368
  projectName: path.basename(xcodeprojPath, '.xcodeproj'),
331
369
  };
332
370
 
333
- const { frameworkTargetUUID } = addFrameworkTarget(
371
+ const { frameworkTargetUUID, targetAlreadyExists } = addFrameworkTarget(
334
372
  project,
335
373
  modRequest,
336
374
  resolvedIosConfig.ios,
337
375
  { useExpoHost: false }
338
376
  );
339
377
 
378
+ // A target created before Expo was added keeps its old deployment target.
379
+ if (
380
+ targetAlreadyExists &&
381
+ expoMajor !== undefined &&
382
+ expoMajor >= 56 &&
383
+ raiseFrameworkDeploymentTarget(project, frameworkTargetUUID, '16.4')
384
+ ) {
385
+ Logger.logInfo(
386
+ `Raised the ${iosFrameworkName} deployment target to iOS 16.4, which Expo SDK 56 and newer require.`
387
+ );
388
+ }
389
+
340
390
  copyBundleReactNativePhase(project, frameworkTargetUUID);
341
391
  addSourceFilesBuildPhase(
342
392
  project,
@@ -355,10 +405,16 @@ export async function scaffoldBrownfieldInRncCliProject(
355
405
  if (!podfile) {
356
406
  throw new Error(`Missing ${podfilePath}`);
357
407
  }
358
- writeFileIfChanged(podfilePath, modifyPodfile(podfile, iosFrameworkName));
408
+ writeFileIfChanged(
409
+ podfilePath,
410
+ modifyPodfile(podfile, iosFrameworkName, expoMajor)
411
+ );
359
412
 
360
413
  currentStep = 'ios-sources';
361
- createIosFramework(iosDir, resolvedIosConfig, { useExpoHost: false });
414
+ createIosFramework(iosDir, resolvedIosConfig, {
415
+ useExpoHost: false,
416
+ withExpoModules: expoMajor !== undefined,
417
+ });
362
418
 
363
419
  // --- App package wiring: Brownfield deps + package:* scripts ---
364
420
  // 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