@callstack/react-native-brownfield 5.1.1 → 5.3.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 (107) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/gradle-plugin/brownfield/build.gradle.kts +67 -2
  3. package/gradle-plugin/brownfield/gradle/libs.versions.toml +5 -2
  4. package/gradle-plugin/brownfield/gradle/wrapper/gradle-wrapper.properties +1 -1
  5. package/gradle-plugin/brownfield/gradle.properties +1 -1
  6. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNBrownfieldPlugin.kt +35 -4
  7. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +6 -1
  8. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/shared/MergeClassesTask.kt +2 -0
  9. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/Extension.kt +2 -2
  10. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +155 -0
  11. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  12. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js +1 -1
  13. package/lib/commonjs/expo-config-plugin/android/withAndroidModuleFiles.js.map +1 -1
  14. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js +1 -1
  15. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  16. package/lib/commonjs/expo-config-plugin/ios/withBrownfieldIos.js +1 -1
  17. package/lib/commonjs/expo-config-plugin/ios/withBrownfieldIos.js.map +1 -1
  18. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  19. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  20. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  21. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  22. package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +35 -0
  23. package/lib/commonjs/expo-config-plugin/template/android/build.gradle.kts +1 -2
  24. package/lib/commonjs/expo-config-plugin/template/android/proguard-rules.pro +1 -1
  25. package/lib/commonjs/expo-config-plugin/template/ios/FrameworkInterface.vanilla.swift +12 -0
  26. package/lib/commonjs/expo-config-plugin/template/ios/PodfileTargetBlock.vanilla.rb +5 -0
  27. package/lib/commonjs/index.js.map +1 -1
  28. package/lib/commonjs/scaffold/index.js +2 -0
  29. package/lib/commonjs/scaffold/index.js.map +1 -0
  30. package/lib/commonjs/scaffold/projectFiles.js +2 -0
  31. package/lib/commonjs/scaffold/projectFiles.js.map +1 -0
  32. package/lib/commonjs/scaffold/xcode.d.js +2 -0
  33. package/lib/commonjs/scaffold/xcode.d.js.map +1 -0
  34. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  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/ios/podfileHelpers.js +1 -1
  38. package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  39. package/lib/module/expo-config-plugin/ios/withBrownfieldIos.js +1 -1
  40. package/lib/module/expo-config-plugin/ios/withBrownfieldIos.js.map +1 -1
  41. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  42. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  43. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  44. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  45. package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +35 -0
  46. package/lib/module/expo-config-plugin/template/android/build.gradle.kts +1 -2
  47. package/lib/module/expo-config-plugin/template/android/proguard-rules.pro +1 -1
  48. package/lib/module/expo-config-plugin/template/ios/FrameworkInterface.vanilla.swift +12 -0
  49. package/lib/module/expo-config-plugin/template/ios/PodfileTargetBlock.vanilla.rb +5 -0
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/scaffold/index.js +2 -0
  52. package/lib/module/scaffold/index.js.map +1 -0
  53. package/lib/module/scaffold/projectFiles.js +2 -0
  54. package/lib/module/scaffold/projectFiles.js.map +1 -0
  55. package/lib/module/scaffold/xcode.d.js +2 -0
  56. package/lib/module/scaffold/xcode.d.js.map +1 -0
  57. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  58. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +19 -1
  59. package/lib/typescript/commonjs/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts +4 -2
  61. package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withBrownfieldIos.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +11 -2
  64. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +6 -2
  66. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +3 -2
  68. package/lib/typescript/commonjs/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/src/scaffold/index.d.ts +23 -0
  71. package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts +63 -0
  73. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts.map +1 -0
  74. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  75. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts +19 -1
  76. package/lib/typescript/module/src/expo-config-plugin/android/withAndroidModuleFiles.d.ts.map +1 -1
  77. package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts +4 -2
  78. package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  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/withIosFrameworkFiles.d.ts +11 -2
  81. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  82. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +6 -2
  83. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  84. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts +3 -2
  85. package/lib/typescript/module/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.d.ts.map +1 -1
  86. package/lib/typescript/module/src/index.d.ts.map +1 -1
  87. package/lib/typescript/module/src/scaffold/index.d.ts +23 -0
  88. package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -0
  89. package/lib/typescript/module/src/scaffold/projectFiles.d.ts +63 -0
  90. package/lib/typescript/module/src/scaffold/projectFiles.d.ts.map +1 -0
  91. package/package.json +16 -2
  92. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
  93. package/src/expo-config-plugin/android/withAndroidModuleFiles.ts +62 -5
  94. package/src/expo-config-plugin/ios/podfileHelpers.ts +24 -6
  95. package/src/expo-config-plugin/ios/withBrownfieldIos.ts +5 -2
  96. package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +21 -6
  97. package/src/expo-config-plugin/ios/xcodeHelpers.ts +77 -5
  98. package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +35 -0
  99. package/src/expo-config-plugin/template/android/build.gradle.kts +1 -2
  100. package/src/expo-config-plugin/template/android/proguard-rules.pro +1 -1
  101. package/src/expo-config-plugin/template/ios/FrameworkInterface.vanilla.swift +12 -0
  102. package/src/expo-config-plugin/template/ios/PodfileTargetBlock.vanilla.rb +5 -0
  103. package/src/expo-config-plugin/types/android/BrownfieldPluginAndroidConfig.ts +3 -2
  104. package/src/index.ts +7 -0
  105. package/src/scaffold/index.ts +395 -0
  106. package/src/scaffold/projectFiles.ts +243 -0
  107. package/src/scaffold/xcode.d.ts +4 -0
@@ -9,6 +9,7 @@ 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
13
  import {
13
14
  type AndroidManifestMetaDataEntry,
14
15
  type AndroidStringResourceEntry,
@@ -22,6 +23,12 @@ import {
22
23
  import { getHermesArtifact } from './utils/hermes';
23
24
  import { formatMissingDimensionStrategies } from './utils/formatHelpers';
24
25
 
26
+ /**
27
+ * Expo SDK 58+ Android library modules should not set `targetSdk`.
28
+ * Keep emitting it for older SDKs that still require the field.
29
+ */
30
+ export const EXPO_SDK_OMIT_TARGET_SDK_FROM_MAJOR = 58;
31
+
25
32
  function renderMissingDimensionStrategyBlock(
26
33
  missingDimensionStrategies: string[]
27
34
  ): string {
@@ -53,6 +60,27 @@ export function resolveTargetSdkVersionExpression(
53
60
  return 'resolveRootProjectInt("targetSdkVersion")';
54
61
  }
55
62
 
63
+ export function renderTargetSdkBlock(
64
+ config: ResolvedBrownfieldPluginConfigWithAndroid,
65
+ expoMajor?: number,
66
+ templateVariant: 'expo' | 'vanilla' = 'expo'
67
+ ): string {
68
+ // RN CLI (vanilla) projects target AGP 9, where library modules must not
69
+ // set `targetSdk` (compilation fails with "Unresolved reference 'targetSdk'").
70
+ if (templateVariant === 'vanilla') {
71
+ return '';
72
+ }
73
+
74
+ if (
75
+ expoMajor !== undefined &&
76
+ expoMajor >= EXPO_SDK_OMIT_TARGET_SDK_FROM_MAJOR
77
+ ) {
78
+ return '';
79
+ }
80
+
81
+ return `\n targetSdk = ${resolveTargetSdkVersionExpression(config)}`;
82
+ }
83
+
56
84
  /**
57
85
  * Creates the Android library module directory structure and files
58
86
  */
@@ -61,11 +89,23 @@ export function createAndroidModule({
61
89
  config,
62
90
  rnVersion,
63
91
  projectRoot,
92
+ expoMajor,
93
+ templateVariant = 'expo',
64
94
  }: {
65
95
  /**
66
96
  * Expo app root (used to detect optional dependencies such as expo-updates)
67
97
  */
68
98
  projectRoot?: string;
99
+
100
+ /**
101
+ * Source template flavor. 'vanilla' is used by the RN CLI scaffold for
102
+ * non-Expo projects; Expo keeps the default 'expo' variant.
103
+ * Note: expresses the same Expo-vs-vanilla axis as `useExpoHost` in the
104
+ * iOS helpers (withIosFrameworkFiles/xcodeHelpers), with opposite
105
+ * polarity: 'vanilla' here === `useExpoHost: false` there.
106
+ */
107
+ templateVariant?: 'expo' | 'vanilla';
108
+
69
109
  /**
70
110
  * The root Android directory path
71
111
  */
@@ -76,6 +116,11 @@ export function createAndroidModule({
76
116
  */
77
117
  rnVersion: string;
78
118
 
119
+ /**
120
+ * Expo SDK major version used to gate Android library `targetSdk`.
121
+ */
122
+ expoMajor?: number;
123
+
79
124
  /**
80
125
  * The resolved Brownfield plugin configuration
81
126
  */
@@ -89,7 +134,11 @@ export function createAndroidModule({
89
134
  const hermesArtifact = getHermesArtifact(rnVersion, projectRoot);
90
135
  const compileSdkVersionExpression =
91
136
  resolveCompileSdkVersionExpression(config);
92
- const targetSdkVersionExpression = resolveTargetSdkVersionExpression(config);
137
+ const targetSdkBlock = renderTargetSdkBlock(
138
+ config,
139
+ expoMajor,
140
+ templateVariant
141
+ );
93
142
  const minifyEnabled =
94
143
  (android as { minifyEnabled?: boolean }).minifyEnabled ?? false;
95
144
  const extraProguardRules =
@@ -112,7 +161,7 @@ export function createAndroidModule({
112
161
  content: renderTemplate('android', 'build.gradle.kts', {
113
162
  '{{PACKAGE_NAME}}': android.packageName,
114
163
  '{{MIN_SDK_VERSION}}': android.minSdkVersion.toString(),
115
- '{{TARGET_SDK_VERSION}}': targetSdkVersionExpression,
164
+ '{{TARGET_SDK_BLOCK}}': targetSdkBlock,
116
165
  '{{COMPILE_SDK_VERSION}}': compileSdkVersionExpression,
117
166
  '{{GROUP_ID}}': android.groupId,
118
167
  '{{ARTIFACT_ID}}': android.artifactId,
@@ -130,9 +179,15 @@ export function createAndroidModule({
130
179
  },
131
180
  {
132
181
  relativePath: `src/main/java/${config.android.packageName.replace(/\./g, '/')}/ReactNativeHostManager.kt`,
133
- content: renderTemplate('android', 'ReactNativeHostManager.post55.kt', {
134
- '{{PACKAGE_NAME}}': android.packageName,
135
- }),
182
+ content: renderTemplate(
183
+ 'android',
184
+ templateVariant === 'vanilla'
185
+ ? 'ReactNativeHostManager.vanilla.kt'
186
+ : 'ReactNativeHostManager.post55.kt',
187
+ {
188
+ '{{PACKAGE_NAME}}': android.packageName,
189
+ }
190
+ ),
136
191
  },
137
192
  {
138
193
  relativePath: 'consumer-rules.pro',
@@ -141,6 +196,7 @@ export function createAndroidModule({
141
196
  {
142
197
  relativePath: 'proguard-rules.pro',
143
198
  content: renderTemplate('android', 'proguard-rules.pro', {
199
+ '{{PACKAGE_NAME}}': android.packageName,
144
200
  '{{EXTRA_PROGUARD_RULES}}': extraProguardRulesText,
145
201
  }),
146
202
  },
@@ -292,6 +348,7 @@ export const withAndroidModuleFiles: ConfigPlugin<
292
348
  config: props,
293
349
  rnVersion,
294
350
  projectRoot: dangerousConfig.modRequest.projectRoot,
351
+ expoMajor: getExpoInfo(dangerousConfig).expoMajor,
295
352
  });
296
353
 
297
354
  return dangerousConfig;
@@ -54,13 +54,15 @@ function ensureExpoDefinesForSDK55AndAbove(
54
54
  * Modifies the Podfile to include the Brownfield framework target
55
55
  * @param podfile The original Podfile content
56
56
  * @param frameworkName The name of the framework target to add
57
- * @param expoMajor The major version of the Expo SDK
57
+ * @param expoMajor The major version of the Expo SDK. Omit for vanilla
58
+ * (non-Expo) projects: the vanilla target block is inserted and no
59
+ * Expo-only hooks are applied.
58
60
  * @returns The modified Podfile content
59
61
  */
60
62
  export function modifyPodfile(
61
63
  podfile: string,
62
64
  frameworkName: string,
63
- expoMajor: number
65
+ expoMajor?: number
64
66
  ): string {
65
67
  // check if the framework target is already included
66
68
  if (podfile.includes(`target '${frameworkName}'`)) {
@@ -73,9 +75,18 @@ export function modifyPodfile(
73
75
  Logger.logDebug(`Modifying Podfile for framework: ${frameworkName}`);
74
76
 
75
77
  // insert the framework target after the main target's "do"
76
- const frameworkTargetBlock = renderTemplate('ios', 'PodfileTargetBlock.rb', {
77
- '{{FRAMEWORK_NAME}}': frameworkName,
78
- });
78
+ // Vanilla means the caller passed no Expo SDK version at all (undefined).
79
+ // Negative values mean "Expo detected but unknown version" and must still
80
+ // hit the unsupported-version error path below, not the vanilla path.
81
+ const frameworkTargetBlock = renderTemplate(
82
+ 'ios',
83
+ typeof expoMajor === 'number'
84
+ ? 'PodfileTargetBlock.rb'
85
+ : 'PodfileTargetBlock.vanilla.rb',
86
+ {
87
+ '{{FRAMEWORK_NAME}}': frameworkName,
88
+ }
89
+ );
79
90
 
80
91
  // find insertion point after the first target's content begins, before the end of the target block
81
92
  const mainTargetMatch = podfile.match(
@@ -101,8 +112,15 @@ export function modifyPodfile(
101
112
 
102
113
  Logger.logDebug(`Added framework target "${frameworkName}" to Podfile`);
103
114
 
115
+ // 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
+ if (typeof expoMajor !== 'number') {
119
+ return modifiedPodfile;
120
+ }
121
+
104
122
  if (expoMajor < MIN_SUPPORTED_EXPO_SDK_MAJOR_VERSION) {
105
- const versionLabel = expoMajor < 0 ? 'unknown' : expoMajor.toString();
123
+ const versionLabel = expoMajor < 0 ? 'unknown' : String(expoMajor);
106
124
  throw new SourceModificationError(
107
125
  `Expo SDK ${versionLabel} is not supported. Please use Expo SDK ${MIN_SUPPORTED_EXPO_SDK_MAJOR_VERSION} or newer. For older versions, please see the matrix of supported versions: https://oss.callstack.com/react-native-brownfield/docs/getting-started/introduction#expo-version-compatibility`
108
126
  );
@@ -47,7 +47,8 @@ export const withBrownfieldIos: ConfigPlugin<
47
47
  const { frameworkTargetUUID, targetAlreadyExists } = addFrameworkTarget(
48
48
  project,
49
49
  modRequest,
50
- iosProps
50
+ iosProps,
51
+ { useExpoHost: true }
51
52
  );
52
53
 
53
54
  // Ensure Expo.plist is present in the framework resources phase when
@@ -73,7 +74,9 @@ export const withBrownfieldIos: ConfigPlugin<
73
74
  // copy the "Bundle React Native code and images" build phase from the main target to the framework target
74
75
  copyBundleReactNativePhase(project, frameworkTargetUUID);
75
76
 
76
- addSourceFilesBuildPhase(project, frameworkTargetUUID, iosProps);
77
+ addSourceFilesBuildPhase(project, frameworkTargetUUID, iosProps, {
78
+ useExpoHost: true,
79
+ });
77
80
 
78
81
  return xcodeConfig;
79
82
  });
@@ -16,14 +16,28 @@ import { renderTemplate } from '../template/engine';
16
16
  * @returns The list of framework source files
17
17
  */
18
18
  export function getFrameworkSourceFiles(
19
- ios: ResolvedBrownfieldPluginConfigWithIos['ios']
19
+ ios: ResolvedBrownfieldPluginConfigWithIos['ios'],
20
+ options?: {
21
+ /**
22
+ * Whether the packaged framework is expected to use the Expo host.
23
+ * This influences template selection for the generated framework sources.
24
+ * Note: expresses the same Expo-vs-vanilla axis as `templateVariant` in
25
+ * the Android helper (withAndroidModuleFiles.createAndroidModule), with
26
+ * opposite polarity: `false` here === 'vanilla' there.
27
+ */
28
+ useExpoHost?: boolean;
29
+ }
20
30
  ): RenderedTemplateFile[] {
31
+ const useExpoHost = options?.useExpoHost ?? true;
32
+
21
33
  return [
22
34
  {
23
35
  relativePath: `${ios.frameworkName}.swift`,
24
- content: renderTemplate('ios', 'FrameworkInterface.swift', {
25
- '{{BUNDLE_IDENTIFIER}}': ios.bundleIdentifier,
26
- }),
36
+ content: useExpoHost
37
+ ? renderTemplate('ios', 'FrameworkInterface.swift', {
38
+ '{{BUNDLE_IDENTIFIER}}': ios.bundleIdentifier,
39
+ })
40
+ : renderTemplate('ios', 'FrameworkInterface.vanilla.swift', {}),
27
41
  },
28
42
  {
29
43
  relativePath: 'Info.plist',
@@ -41,7 +55,8 @@ export function getFrameworkSourceFiles(
41
55
  */
42
56
  export function createIosFramework(
43
57
  iosDir: string,
44
- config: ResolvedBrownfieldPluginConfigWithIos
58
+ config: ResolvedBrownfieldPluginConfigWithIos,
59
+ options?: Parameters<typeof getFrameworkSourceFiles>[1]
45
60
  ) {
46
61
  const { ios } = config;
47
62
  const frameworkDir = path.join(iosDir, ios.frameworkName);
@@ -61,7 +76,7 @@ export function createIosFramework(
61
76
  }
62
77
 
63
78
  // write files
64
- for (const file of getFrameworkSourceFiles(ios)) {
79
+ for (const file of getFrameworkSourceFiles(ios, options)) {
65
80
  const filePath = path.join(frameworkDir, file.relativePath);
66
81
 
67
82
  fs.writeFileSync(filePath, file.content, 'utf8');
@@ -11,6 +11,43 @@ import type { ResolvedBrownfieldPluginIosConfig } from '../types';
11
11
  import { SourceModificationError } from '../errors/SourceModificationError';
12
12
  import { getFrameworkSourceFiles } from './withIosFrameworkFiles';
13
13
 
14
+ /**
15
+ * xcode@3.x addTarget() stores target names/comments quoted (e.g. name =
16
+ * "BrownfieldLib"), and the pbxproj parser keeps the quote characters on
17
+ * re-parse. That makes pbxTargetByName() (comment-based, unquoted) miss
18
+ * targets we previously wrote, so a second run would create a duplicate
19
+ * target. Match by unquoting the stored name as a fallback.
20
+ */
21
+ function findFrameworkTargetByQuotedName(
22
+ project: XcodeProject,
23
+ frameworkName: string
24
+ ): ReturnType<XcodeProject['pbxTargetByName']> {
25
+ const nativeTargets = getRawProjectObjects(project).PBXNativeTarget as
26
+ | Record<string, PbxNativeTarget & { isa?: string }>
27
+ | undefined;
28
+
29
+ for (const [key, target] of Object.entries(nativeTargets ?? {})) {
30
+ if (key.endsWith('_comment') || target?.isa !== 'PBXNativeTarget') {
31
+ continue;
32
+ }
33
+ const name = unquotePbxString(target.name);
34
+ if (name === frameworkName) {
35
+ return target as any;
36
+ }
37
+ }
38
+
39
+ return null;
40
+ }
41
+
42
+ function unquotePbxString(value: string | undefined): string | undefined {
43
+ if (typeof value !== 'string') {
44
+ return undefined;
45
+ }
46
+ return value.startsWith('"') && value.endsWith('"')
47
+ ? value.slice(1, -1)
48
+ : value;
49
+ }
50
+
14
51
  /**
15
52
  * Adds a new Framework target to the Xcode project for Brownfield packaging
16
53
  * @throws If target creation fails
@@ -20,7 +57,10 @@ import { getFrameworkSourceFiles } from './withIosFrameworkFiles';
20
57
  export function addFrameworkTarget(
21
58
  project: XcodeProject,
22
59
  modRequest: ModProps<XcodeProject>,
23
- options: ResolvedBrownfieldPluginIosConfig
60
+ options: ResolvedBrownfieldPluginIosConfig,
61
+ brownfieldOptions?: {
62
+ useExpoHost?: boolean;
63
+ }
24
64
  ): {
25
65
  frameworkTargetUUID: string;
26
66
  targetAlreadyExists: boolean;
@@ -28,7 +68,9 @@ export function addFrameworkTarget(
28
68
  const { frameworkName, bundleIdentifier } = options;
29
69
 
30
70
  // check if target already exists
31
- const existingTarget = project.pbxTargetByName(frameworkName);
71
+ const existingTarget =
72
+ project.pbxTargetByName(frameworkName) ??
73
+ findFrameworkTargetByQuotedName(project, frameworkName);
32
74
  if (existingTarget) {
33
75
  Logger.logDebug(
34
76
  `Framework target "${frameworkName}" already exists, skipping creation`
@@ -131,7 +173,7 @@ export function addFrameworkTarget(
131
173
  });
132
174
 
133
175
  // create the framework group in the project
134
- const filePaths = getFrameworkSourceFiles(options).map(
176
+ const filePaths = getFrameworkSourceFiles(options, brownfieldOptions).map(
135
177
  (file) => file.relativePath
136
178
  );
137
179
  const groupPath = path.join(modRequest.platformProjectRoot, frameworkName);
@@ -161,9 +203,12 @@ export function addFrameworkTarget(
161
203
  export function addSourceFilesBuildPhase(
162
204
  project: XcodeProject,
163
205
  frameworkTargetUUID: string,
164
- options: ResolvedBrownfieldPluginIosConfig
206
+ options: ResolvedBrownfieldPluginIosConfig,
207
+ brownfieldOptions?: {
208
+ useExpoHost?: boolean;
209
+ }
165
210
  ) {
166
- const filePaths = getFrameworkSourceFiles(options).map(
211
+ const filePaths = getFrameworkSourceFiles(options, brownfieldOptions).map(
167
212
  (file) => file.relativePath
168
213
  );
169
214
 
@@ -171,6 +216,26 @@ export function addSourceFilesBuildPhase(
171
216
  (filePath) => !filePath.endsWith('.plist')
172
217
  );
173
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
222
+ | Record<string, PbxNativeTarget>
223
+ | undefined;
224
+ const targetBuildPhases =
225
+ nativeTargets?.[frameworkTargetUUID]?.buildPhases ?? [];
226
+ const alreadyHasSourcePhase = targetBuildPhases.some((phase) =>
227
+ hasBuildPhaseComment(
228
+ { comment: typeof phase === 'string' ? undefined : phase.comment },
229
+ options.frameworkName
230
+ )
231
+ );
232
+ if (alreadyHasSourcePhase) {
233
+ Logger.logDebug(
234
+ `Sources build phase "${options.frameworkName}" already present on target ${frameworkTargetUUID}, skipping`
235
+ );
236
+ return;
237
+ }
238
+
174
239
  project.addBuildPhase(
175
240
  sourceFiles,
176
241
  'PBXSourcesBuildPhase',
@@ -613,6 +678,13 @@ export function copyBundleReactNativePhase(
613
678
  }
614
679
  );
615
680
 
681
+ // xcode's pbxShellScriptBuildPhaseObj only escapes quotes, leaving real
682
+ // newlines in the shellScript value — that produces an invalid pbxproj
683
+ // once the project is written. Encode both fields the same way the
684
+ // update path above does.
685
+ addedPhase.buildPhase.shellPath = encodePbxString(frameworkShellPath);
686
+ addedPhase.buildPhase.shellScript = encodePbxString(frameworkShellScript);
687
+
616
688
  if (existingPhase.showEnvVarsInLog !== undefined) {
617
689
  addedPhase.buildPhase.showEnvVarsInLog = existingPhase.showEnvVarsInLog;
618
690
  }
@@ -0,0 +1,35 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.app.Application
4
+ import android.content.res.Configuration
5
+ import com.callstack.reactnativebrownfield.OnJSBundleLoaded
6
+ import com.callstack.reactnativebrownfield.ReactNativeBrownfield
7
+ import com.facebook.react.PackageList
8
+ import com.facebook.react.ReactHost
9
+ import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
10
+ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
11
+
12
+ object ReactNativeHostManager {
13
+ fun initialize(application: Application, onJSBundleLoaded: OnJSBundleLoaded? = null) {
14
+ loadReactNative(application)
15
+
16
+ val reactHost: ReactHost by lazy {
17
+ getDefaultReactHost(
18
+ context = application,
19
+ packageList = PackageList(application).packages,
20
+ jsMainModulePath = "index",
21
+ jsBundleAssetPath = "index.android.bundle",
22
+ jsBundleFilePath = null,
23
+ useDevSupport = BuildConfig.DEBUG,
24
+ jsRuntimeFactory = null
25
+ )
26
+ }
27
+
28
+ ReactNativeBrownfield.initialize(application, reactHost, onJSBundleLoaded)
29
+ }
30
+
31
+ fun onConfigurationChanged(application: Application, newConfig: Configuration) {
32
+ // no-op (kept for API symmetry with Expo variant)
33
+ }
34
+ }
35
+
@@ -56,8 +56,7 @@ android {
56
56
  compileSdk = {{COMPILE_SDK_VERSION}}
57
57
 
58
58
  defaultConfig {
59
- minSdk = {{MIN_SDK_VERSION}}
60
- targetSdk = {{TARGET_SDK_VERSION}}
59
+ minSdk = {{MIN_SDK_VERSION}}{{TARGET_SDK_BLOCK}}
61
60
 
62
61
  buildConfigField(
63
62
  "boolean",
@@ -26,6 +26,6 @@
26
26
  -dontwarn java.lang.invoke.StringConcatFactory
27
27
 
28
28
  # Public API consumed from the Brownfield AAR.
29
- -keep class com.callstack.rnbrownfield.demo.expoapp56.ReactNativeHostManager { *; }
29
+ -keep class {{PACKAGE_NAME}}.ReactNativeHostManager { *; }
30
30
 
31
31
  {{EXTRA_PROGUARD_RULES}}
@@ -0,0 +1,12 @@
1
+ import Foundation
2
+
3
+ // Re-export the Brownfield runtime so the native host app resolves
4
+ // ReactNativeBrownfield.shared by importing only this framework, matching the
5
+ // manual guide's interface file (docs getting-started/ios.mdx).
6
+ @_exported import ReactBrownfield
7
+
8
+ // Initializes a Bundle instance that points at the framework target.
9
+ public let ReactNativeBundle = Bundle(for: InternalClassForBundle.self)
10
+
11
+ class InternalClassForBundle {}
12
+
@@ -0,0 +1,5 @@
1
+ # Brownfield framework target for packaging as XCFramework
2
+ target '{{FRAMEWORK_NAME}}' do
3
+ inherit! :complete
4
+ end
5
+
@@ -21,14 +21,15 @@ export interface BrownfieldPluginAndroidConfig {
21
21
  minSdkVersion?: number;
22
22
 
23
23
  /**
24
- * Target SDK version for the Android library
24
+ * Target SDK version for the Android library.
25
+ * Used on Expo SDK versions below 58; ignored on 58 and later.
25
26
  * @default inherited from the generated Expo app project when available
26
27
  */
27
28
  targetSdkVersion?: number;
28
29
 
29
30
  /**
30
31
  * Compile SDK version for the Android library
31
- * @default inherited from the generated Expo app project when available
32
+ * @default 37 on Expo SDK 58 and later, otherwise 35
32
33
  */
33
34
  compileSdkVersion?: number;
34
35
 
package/src/index.ts CHANGED
@@ -2,6 +2,13 @@ import { Platform } from 'react-native';
2
2
 
3
3
  import ReactNativeBrownfieldModule from './NativeReactNativeBrownfieldModule';
4
4
 
5
+ // NOTE: the scaffold API is intentionally NOT re-exported here. This module is
6
+ // the package's `react-native`/`source` entry point, so anything reachable from
7
+ // it is traversed by Metro when an app imports the package. The scaffold pulls
8
+ // in node:fs, node:path, xcode and the Community CLI, which cannot bundle.
9
+ // Node-side consumers import it from the dedicated subpath instead:
10
+ // `@callstack/react-native-brownfield/scaffold`.
11
+
5
12
  export type { BrownfieldConfig } from '@callstack/brownfield-cli/types';
6
13
 
7
14
  export interface MessageEvent {