@callstack/brownfield-cli 3.9.0 → 3.11.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 (30) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/brownfield/commands/packageIos.d.ts +1 -0
  3. package/dist/brownfield/commands/packageIos.d.ts.map +1 -1
  4. package/dist/brownfield/commands/packageIos.js +97 -6
  5. package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts +8 -0
  6. package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts.map +1 -0
  7. package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.js +21 -0
  8. package/dist/brownfield/utils/createLocalSpmPackage.d.ts +10 -0
  9. package/dist/brownfield/utils/createLocalSpmPackage.d.ts.map +1 -0
  10. package/dist/brownfield/utils/createLocalSpmPackage.js +138 -0
  11. package/dist/brownfield/utils/prepareLocalSpmArtifacts.d.ts +8 -0
  12. package/dist/brownfield/utils/prepareLocalSpmArtifacts.d.ts.map +1 -0
  13. package/dist/brownfield/utils/prepareLocalSpmArtifacts.js +77 -0
  14. package/dist/brownfield/utils/project.d.ts +1 -0
  15. package/dist/brownfield/utils/project.d.ts.map +1 -1
  16. package/dist/brownfield/utils/project.js +8 -0
  17. package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts +14 -0
  18. package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts.map +1 -0
  19. package/dist/brownfield/utils/resolvePackagedFrameworkName.js +61 -0
  20. package/dist/brownfield/utils/supportsPrebuiltRNCore.d.ts +18 -0
  21. package/dist/brownfield/utils/supportsPrebuiltRNCore.d.ts.map +1 -0
  22. package/dist/brownfield/utils/supportsPrebuiltRNCore.js +36 -0
  23. package/package.json +6 -6
  24. package/src/brownfield/commands/packageIos.ts +288 -129
  25. package/src/brownfield/utils/copyDebugBundleToSimulatorSlice.ts +58 -0
  26. package/src/brownfield/utils/createLocalSpmPackage.ts +208 -0
  27. package/src/brownfield/utils/prepareLocalSpmArtifacts.ts +117 -0
  28. package/src/brownfield/utils/project.ts +9 -0
  29. package/src/brownfield/utils/resolvePackagedFrameworkName.ts +98 -0
  30. package/src/brownfield/utils/supportsPrebuiltRNCore.ts +67 -0
@@ -0,0 +1,36 @@
1
+ import { getReactNativeVersion, versionCompare } from '@rock-js/tools';
2
+ import { getExpoSdkMajor, isExpoProject } from './project.js';
3
+ /** Minimum RN version that can opt in to prebuilts via `--use-prebuilt-rn-core`. */
4
+ export const MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE = '0.81.0';
5
+ /** Minimum RN version where Brownfield enables prebuilts by default (vanilla projects). */
6
+ export const MIN_REACT_NATIVE_VERSION_FOR_PREBUILT_RN_CORE_BY_DEFAULT = '0.84.0';
7
+ export const MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT = 55;
8
+ export function supportsPrebuiltRNCore({ projectRoot, }) {
9
+ const reactNativeVersion = getReactNativeVersion(projectRoot);
10
+ if (reactNativeVersion === 'unknown') {
11
+ return {
12
+ supported: false,
13
+ reason: 'Cannot use --use-prebuilt-rn-core: unable to resolve the installed react-native version.',
14
+ };
15
+ }
16
+ if (versionCompare(reactNativeVersion, MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE) < 0) {
17
+ return {
18
+ supported: false,
19
+ reason: `--use-prebuilt-rn-core requires React Native ${MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE} or newer (found ${reactNativeVersion}).`,
20
+ };
21
+ }
22
+ if (isExpoProject(projectRoot)) {
23
+ const expoSdkMajor = getExpoSdkMajor(projectRoot);
24
+ if (expoSdkMajor === null ||
25
+ expoSdkMajor < MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT) {
26
+ const sdkLabel = expoSdkMajor === null ? 'unknown' : String(expoSdkMajor);
27
+ return {
28
+ supported: false,
29
+ reason: `--use-prebuilt-rn-core is unsupported in Expo SDK ${sdkLabel}: packaging brownfield with prebuilts requires Expo SDK ${MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT} or newer.`,
30
+ };
31
+ }
32
+ return { supported: true, enabledByDefault: true };
33
+ }
34
+ const enabledByDefault = versionCompare(reactNativeVersion, MIN_REACT_NATIVE_VERSION_FOR_PREBUILT_RN_CORE_BY_DEFAULT) >= 0;
35
+ return { supported: true, enabledByDefault };
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/brownfield-cli",
3
- "version": "3.9.0",
3
+ "version": "3.11.0",
4
4
  "license": "MIT",
5
5
  "author": "Artur Morys-Magiera <artus9033@gmail.com>",
6
6
  "bin": {
@@ -76,11 +76,11 @@
76
76
  "@expo/config": "^12.0.13",
77
77
  "@react-native-community/cli-config": "^20.0.0",
78
78
  "@react-native-community/cli-config-android": "^20.0.0",
79
- "@rock-js/platform-android": "^0.12.12",
80
- "@rock-js/platform-apple-helpers": "^0.12.12",
81
- "@rock-js/plugin-brownfield-android": "^0.12.12",
82
- "@rock-js/plugin-brownfield-ios": "^0.12.12",
83
- "@rock-js/tools": "^0.12.12",
79
+ "@rock-js/platform-android": "^0.13.3",
80
+ "@rock-js/platform-apple-helpers": "^0.13.3",
81
+ "@rock-js/plugin-brownfield-android": "^0.13.3",
82
+ "@rock-js/plugin-brownfield-ios": "^0.13.3",
83
+ "@rock-js/tools": "^0.13.3",
84
84
  "commander": "^14.0.3",
85
85
  "quicktype-core": "^23.2.6",
86
86
  "quicktype-typescript-input": "^23.2.6",
@@ -8,16 +8,18 @@ import {
8
8
  } from '@rock-js/platform-apple-helpers';
9
9
  import { packageIosAction } from '@rock-js/plugin-brownfield-ios';
10
10
  import {
11
+ RockError,
11
12
  colorLink,
12
13
  getReactNativeVersion,
13
14
  logger,
14
15
  relativeToCwd,
15
16
  } from '@rock-js/tools';
16
17
 
17
- import { Command } from 'commander';
18
+ import { Command, Option } from 'commander';
18
19
 
19
20
  import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
20
21
  import { getProjectInfo } from '../utils/project.js';
22
+ import { supportsPrebuiltRNCore } from '../utils/supportsPrebuiltRNCore.js';
21
23
  import {
22
24
  actionRunner,
23
25
  curryOptions,
@@ -25,7 +27,54 @@ import {
25
27
  } from '../../shared/index.js';
26
28
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
27
29
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
30
+ import { copyDebugBundleToSimulatorSlice } from '../utils/copyDebugBundleToSimulatorSlice.js';
31
+ import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkName.js';
28
32
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
33
+ import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js';
34
+
35
+ /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
36
+ const USE_PREBUILT_RN_CORE_HELP =
37
+ 'Whether the Xcode build for packaging should use React Native Apple prebuilt binaries (via CocoaPods). ' +
38
+ 'If you omit this flag, Brownfield follows version-aware defaults: for React Native 0.84 and newer, prebuilts are enabled by default; for RN 0.83, they are disabled unless you opt in. ' +
39
+ 'Pass true or false to force either behavior. Use the flag without a value as shorthand for true (same as `--use-prebuilt-rn-core true`). ' +
40
+ 'See the Brownfield iOS guide for details: https://oss.callstack.com/react-native-brownfield/docs/getting-started/ios#react-native-prebuilts';
41
+
42
+ export function parseUsePrebuiltRnCoreArgument(
43
+ value: string | boolean
44
+ ): boolean {
45
+ if (typeof value === 'boolean') {
46
+ return value;
47
+ }
48
+ const normalized = value.trim().toLowerCase();
49
+ if (normalized === 'true' || normalized === '1') {
50
+ return true;
51
+ }
52
+ if (normalized === 'false' || normalized === '0') {
53
+ return false;
54
+ }
55
+ throw new RockError(
56
+ `Invalid value for --use-prebuilt-rn-core: expected true or false, received "${value}"`
57
+ );
58
+ }
59
+
60
+ function getPackagedFrameworkResolutionFailureMessage({
61
+ resolution,
62
+ candidates,
63
+ }: {
64
+ resolution: string | null | undefined;
65
+ candidates?: string[];
66
+ }) {
67
+ return resolution === 'ambiguous'
68
+ ? `found multiple bundled framework candidates (${candidates?.join(', ') ?? 'none'}); pass --scheme explicitly`
69
+ : 'could not resolve the packaged framework output automatically; pass --scheme explicitly';
70
+ }
71
+
72
+ type PackageIosCliFlags = AppleBuildFlags & {
73
+ /** Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults). */
74
+ usePrebuiltRnCore?: boolean;
75
+ /** When set, generate a local Swift Package Manager manifest next to the packaged XCFramework outputs. */
76
+ addSpmPackage?: boolean;
77
+ };
29
78
 
30
79
  export const packageIosCommand = curryOptions(
31
80
  new Command('package:ios').description('Build iOS XCFramework'),
@@ -39,139 +88,249 @@ export const packageIosCommand = curryOptions(
39
88
  }
40
89
  : option
41
90
  )
42
- ).action(
43
- actionRunner(async (options: AppleBuildFlags) => {
44
- const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
45
- await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
46
-
47
- if (!userConfig.project.ios) {
48
- throw new Error('iOS project not found.');
49
- }
50
-
51
- if (!userConfig.project.ios.xcodeProject) {
52
- throw new Error('iOS Xcode project not found in the configuration.');
53
- }
54
-
55
- let dotBrownfieldDir = path.join(
56
- // for Expo projects, platformConfig?.sourceDir == "", but for non-Expo projects, it's "ios"
57
- ...(userConfig.project.ios.sourceDir.trim().length > 0
58
- ? [userConfig.project.ios.sourceDir]
59
- : [projectRoot, 'ios']),
60
- '.brownfield'
61
- );
62
-
63
- // non-Expo projects have a relative sourceDir path, so we need to make it absolute
64
- if (!path.isAbsolute(dotBrownfieldDir)) {
65
- dotBrownfieldDir = path.join(projectRoot, dotBrownfieldDir);
66
- }
67
-
68
- options.buildFolder ??= path.join(dotBrownfieldDir, 'build');
69
-
70
- // The new_architecture.rb script scans Info.plist and fails on binary plist files,
71
- // which is the case for our XCFrameworks.
72
- // We're reusing the "build" directory which is excluded from the scan.
73
- // Reference: https://github.com/facebook/react-native/blob/490c5e8dcc6cdb19c334cc39e93a39a48ba71e96/packages/react-native/scripts/cocoapods/new_architecture.rb#L171
74
- const packageDir = path.join(dotBrownfieldDir, 'package', 'build');
75
- const configuration = options.configuration ?? 'Debug';
76
-
77
- const { hasBrownie } = await runBrownieCodegenIfApplicable(
78
- projectRoot,
79
- 'swift'
80
- );
81
- const { hasNavigation } = await runNavigationCodegenIfApplicable(projectRoot);
82
-
83
- await packageIosAction(
84
- options,
85
- {
91
+ )
92
+ .addOption(
93
+ new Option('--use-prebuilt-rn-core [bool]', USE_PREBUILT_RN_CORE_HELP)
94
+ .preset(true)
95
+ .argParser(parseUsePrebuiltRnCoreArgument)
96
+ )
97
+ .addOption(
98
+ new Option(
99
+ '--add-spm-package',
100
+ 'Generate a local Swift Package Manager manifest next to the packaged XCFramework outputs'
101
+ )
102
+ )
103
+ .action(
104
+ actionRunner(async (options: PackageIosCliFlags) => {
105
+ const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
106
+
107
+ const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot });
108
+
109
+ // version-aware default when the flag is omitted (see ios.mdx "React Native Prebuilts")
110
+ options.usePrebuiltRnCore ??= prebuiltRNCoreSupport.supported
111
+ ? prebuiltRNCoreSupport.enabledByDefault
112
+ : false;
113
+
114
+ if (prebuiltRNCoreSupport) {
115
+ logger.info(
116
+ `${options.usePrebuiltRnCore ? 'Using' : 'Not using'} prebuilt RN core`
117
+ );
118
+
119
+ if (
120
+ !prebuiltRNCoreSupport.enabledByDefault &&
121
+ !options.usePrebuiltRnCore
122
+ ) {
123
+ logger.info(
124
+ 'Your environment supports prebuilt RN Core as an opt-in feature, but it is disabled by default. Pass --use-prebuilt-rn-core to enable it.'
125
+ );
126
+ }
127
+ }
128
+
129
+ if (options.usePrebuiltRnCore && !prebuiltRNCoreSupport.supported) {
130
+ throw new RockError(prebuiltRNCoreSupport.reason);
131
+ }
132
+
133
+ await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
134
+
135
+ if (!userConfig.project.ios) {
136
+ throw new Error('iOS project not found.');
137
+ }
138
+
139
+ if (!userConfig.project.ios.xcodeProject) {
140
+ throw new Error('iOS Xcode project not found in the configuration.');
141
+ }
142
+
143
+ let dotBrownfieldDir = path.join(
144
+ // for Expo projects, platformConfig?.sourceDir == "", but for non-Expo projects, it's "ios"
145
+ ...(userConfig.project.ios.sourceDir.trim().length > 0
146
+ ? [userConfig.project.ios.sourceDir]
147
+ : [projectRoot, 'ios']),
148
+ '.brownfield'
149
+ );
150
+
151
+ // non-Expo projects have a relative sourceDir path, so we need to make it absolute
152
+ if (!path.isAbsolute(dotBrownfieldDir)) {
153
+ dotBrownfieldDir = path.join(projectRoot, dotBrownfieldDir);
154
+ }
155
+
156
+ options.buildFolder ??= path.join(dotBrownfieldDir, 'build');
157
+ options.verbose ??= logger.isVerbose();
158
+
159
+ // The new_architecture.rb script scans Info.plist and fails on binary plist files,
160
+ // which is the case for our XCFrameworks.
161
+ // We're reusing the "build" directory which is excluded from the scan.
162
+ // Reference: https://github.com/facebook/react-native/blob/490c5e8dcc6cdb19c334cc39e93a39a48ba71e96/packages/react-native/scripts/cocoapods/new_architecture.rb#L171
163
+ const packageDir = path.join(dotBrownfieldDir, 'package', 'build');
164
+ const configuration = options.configuration ?? 'Debug';
165
+
166
+ const { hasBrownie } = await runBrownieCodegenIfApplicable(
86
167
  projectRoot,
87
- reactNativePath: userConfig.reactNativePath,
88
- // below: the userConfig.reactNativeVersion may be a non-semver-format string,
89
- // e.g. '0.82' (note the missing patch component),
90
- // therefore we resolve it manually from RN's package.json using Rock's utils
91
- reactNativeVersion: getReactNativeVersion(projectRoot),
92
- usePrebuiltRNCore: false, // for brownfield, it is required to build RN from source
93
- packageDir, // the output directory for artifacts
94
- skipCache: true, // cache is dependent on existence of Rock config file
95
- },
96
- platformConfig
97
- );
98
-
99
- const reactBrownfieldXcframeworkPath = path.join(
100
- packageDir,
101
- 'ReactBrownfield.xcframework'
102
- );
103
- if (fs.existsSync(reactBrownfieldXcframeworkPath)) {
104
- // Strip the binary from ReactBrownfield.xcframework to make it interface-only.
105
- // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
106
- // (which contains ReactBrownfield symbols) and ReactBrownfield.xcframework.
107
- stripFrameworkBinary(reactBrownfieldXcframeworkPath);
108
- }
109
-
110
- if (hasBrownie) {
111
- const productsPath = path.join(options.buildFolder, 'Build', 'Products');
112
- const brownieOutputPath = path.join(packageDir, 'Brownie.xcframework');
113
-
114
- await mergeFrameworks({
115
- sourceDir: userConfig.project.ios.sourceDir,
116
- frameworkPaths: [
117
- path.join(
118
- productsPath,
119
- `${configuration}-iphoneos`,
120
- 'Brownie',
121
- 'Brownie.framework'
122
- ),
123
- path.join(
124
- productsPath,
125
- `${configuration}-iphonesimulator`,
126
- 'Brownie',
127
- 'Brownie.framework'
128
- ),
129
- ],
130
- outputPath: brownieOutputPath,
131
- });
132
-
133
- // Strip the binary from Brownie.xcframework to make it interface-only.
134
- // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
135
- // (which contains Brownie symbols) and Brownie.xcframework.
136
- stripFrameworkBinary(brownieOutputPath);
137
-
138
- logger.success(
139
- `Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`
168
+ 'swift'
169
+ );
170
+ const { hasNavigation } =
171
+ await runNavigationCodegenIfApplicable(projectRoot);
172
+
173
+ await packageIosAction(
174
+ options,
175
+ {
176
+ projectRoot,
177
+ reactNativePath: userConfig.reactNativePath,
178
+ // below: the userConfig.reactNativeVersion may be a non-semver-format string,
179
+ // e.g. '0.82' (note the missing patch component),
180
+ // therefore we resolve it manually from RN's package.json using Rock's utils
181
+ reactNativeVersion: getReactNativeVersion(projectRoot),
182
+ packageDir, // the output directory for artifacts
183
+ skipCache: true, // cache is dependent on existence of Rock config file
184
+ usePrebuiltRNCore: options.usePrebuiltRnCore,
185
+ },
186
+ platformConfig
140
187
  );
141
- }
142
188
 
143
- if (hasNavigation) {
144
189
  const productsPath = path.join(options.buildFolder, 'Build', 'Products');
145
- const brownfieldNavigationOutputPath = path.join(packageDir, 'BrownfieldNavigation.xcframework');
146
-
147
- await mergeFrameworks({
148
- sourceDir: userConfig.project.ios.sourceDir,
149
- frameworkPaths: [
150
- path.join(
151
- productsPath,
152
- `${configuration}-iphoneos`,
153
- 'BrownfieldNavigation',
154
- 'BrownfieldNavigation.framework'
155
- ),
156
- path.join(
157
- productsPath,
158
- `${configuration}-iphonesimulator`,
159
- 'BrownfieldNavigation',
160
- 'BrownfieldNavigation.framework'
161
- ),
162
- ],
163
- outputPath: brownfieldNavigationOutputPath,
164
- });
165
-
166
-
167
- stripFrameworkBinary(brownfieldNavigationOutputPath);
168
-
169
- logger.success(
170
- `BrownfieldNavigation.xcframework created at ${colorLink(relativeToCwd(brownfieldNavigationOutputPath))}`
190
+ const { frameworkName, resolution, candidates } =
191
+ resolvePackagedFrameworkName({
192
+ explicitScheme: options.scheme,
193
+ productsPath,
194
+ configuration,
195
+ });
196
+
197
+ if (!frameworkName && options.addSpmPackage) {
198
+ throw new RockError(
199
+ `Cannot generate local SPM package: ${getPackagedFrameworkResolutionFailureMessage({
200
+ resolution,
201
+ candidates,
202
+ })}`
203
+ );
204
+ }
205
+
206
+ if (frameworkName) {
207
+ copyDebugBundleToSimulatorSlice({
208
+ productsPath,
209
+ configuration,
210
+ frameworkName,
211
+ });
212
+
213
+ if (configuration.includes('Debug')) {
214
+ // Re-merge only Debug frameworks so the simulator slice includes main.jsbundle.
215
+ await mergeFrameworks({
216
+ sourceDir: userConfig.project.ios.sourceDir,
217
+ frameworkPaths: [
218
+ path.join(
219
+ productsPath,
220
+ `${configuration}-iphoneos`,
221
+ `${frameworkName}.framework`
222
+ ),
223
+ path.join(
224
+ productsPath,
225
+ `${configuration}-iphonesimulator`,
226
+ `${frameworkName}.framework`
227
+ ),
228
+ ],
229
+ outputPath: path.join(packageDir, `${frameworkName}.xcframework`),
230
+ });
231
+ }
232
+ } else if (configuration.includes('Debug')) {
233
+ logger.warn(
234
+ `Skipping Debug simulator JS bundle copy: ${getPackagedFrameworkResolutionFailureMessage({
235
+ resolution,
236
+ candidates,
237
+ })}`
238
+ );
239
+ }
240
+
241
+ const reactBrownfieldXcframeworkPath = path.join(
242
+ packageDir,
243
+ 'ReactBrownfield.xcframework'
171
244
  );
172
- }
173
- })
174
- );
245
+ if (fs.existsSync(reactBrownfieldXcframeworkPath)) {
246
+ // Strip the binary from ReactBrownfield.xcframework to make it interface-only.
247
+ // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
248
+ // (which contains ReactBrownfield symbols) and ReactBrownfield.xcframework.
249
+ stripFrameworkBinary(reactBrownfieldXcframeworkPath);
250
+ }
251
+
252
+ if (hasBrownie) {
253
+ const brownieOutputPath = path.join(packageDir, 'Brownie.xcframework');
254
+
255
+ await mergeFrameworks({
256
+ sourceDir: userConfig.project.ios.sourceDir,
257
+ frameworkPaths: [
258
+ path.join(
259
+ productsPath,
260
+ `${configuration}-iphoneos`,
261
+ 'Brownie',
262
+ 'Brownie.framework'
263
+ ),
264
+ path.join(
265
+ productsPath,
266
+ `${configuration}-iphonesimulator`,
267
+ 'Brownie',
268
+ 'Brownie.framework'
269
+ ),
270
+ ],
271
+ outputPath: brownieOutputPath,
272
+ });
273
+
274
+ // Strip the binary from Brownie.xcframework to make it interface-only.
275
+ // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
276
+ // (which contains Brownie symbols) and Brownie.xcframework.
277
+ stripFrameworkBinary(brownieOutputPath);
278
+
279
+ logger.success(
280
+ `Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`
281
+ );
282
+ }
283
+
284
+ if (hasNavigation) {
285
+ const brownfieldNavigationOutputPath = path.join(
286
+ packageDir,
287
+ 'BrownfieldNavigation.xcframework'
288
+ );
289
+
290
+ await mergeFrameworks({
291
+ sourceDir: userConfig.project.ios.sourceDir,
292
+ frameworkPaths: [
293
+ path.join(
294
+ productsPath,
295
+ `${configuration}-iphoneos`,
296
+ 'BrownfieldNavigation',
297
+ 'BrownfieldNavigation.framework'
298
+ ),
299
+ path.join(
300
+ productsPath,
301
+ `${configuration}-iphonesimulator`,
302
+ 'BrownfieldNavigation',
303
+ 'BrownfieldNavigation.framework'
304
+ ),
305
+ ],
306
+ outputPath: brownfieldNavigationOutputPath,
307
+ });
308
+
309
+ stripFrameworkBinary(brownfieldNavigationOutputPath);
310
+
311
+ logger.success(
312
+ `BrownfieldNavigation.xcframework created at ${colorLink(relativeToCwd(brownfieldNavigationOutputPath))}`
313
+ );
314
+ }
315
+
316
+ if (options.addSpmPackage) {
317
+ const { packageManifestPath } = createLocalSpmPackage({
318
+ packageDir,
319
+ frameworkName: frameworkName ?? undefined,
320
+ });
321
+
322
+ logger.success(
323
+ `Local SPM package manifest created at ${colorLink(relativeToCwd(packageManifestPath))}`
324
+ );
325
+ logger.info(
326
+ `Add the local package folder in Xcode: ${colorLink(relativeToCwd(packageDir))}`
327
+ );
328
+ logger.info(
329
+ "In Xcode, choose File > Add Package Dependencies..., click Add Local..., and select that folder."
330
+ );
331
+ }
332
+ })
333
+ );
175
334
 
176
335
  export const packageIosExample = new ExampleUsage(
177
336
  'package:ios --scheme BrownfieldLib --configuration Release',
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { colorLink, logger, relativeToCwd } from '@rock-js/tools';
5
+
6
+ interface CopyDebugBundleToSimulatorSliceOptions {
7
+ productsPath: string;
8
+ configuration: string;
9
+ frameworkName: string;
10
+ }
11
+
12
+ export function copyDebugBundleToSimulatorSlice({
13
+ productsPath,
14
+ configuration,
15
+ frameworkName,
16
+ }: CopyDebugBundleToSimulatorSliceOptions) {
17
+ if (!configuration.includes('Debug')) {
18
+ return;
19
+ }
20
+
21
+ const deviceBundlePath = path.join(
22
+ productsPath,
23
+ `${configuration}-iphoneos`,
24
+ `${frameworkName}.framework`,
25
+ 'main.jsbundle'
26
+ );
27
+
28
+ const simulatorFrameworkPath = path.join(
29
+ productsPath,
30
+ `${configuration}-iphonesimulator`,
31
+ `${frameworkName}.framework`
32
+ );
33
+
34
+ const simulatorBundlePath = path.join(
35
+ simulatorFrameworkPath,
36
+ 'main.jsbundle'
37
+ );
38
+
39
+ if (!fs.existsSync(deviceBundlePath)) {
40
+ logger.warn(
41
+ `Skipping simulator JS bundle copy: missing ${relativeToCwd(deviceBundlePath)}`
42
+ );
43
+ return;
44
+ }
45
+
46
+ if (!fs.existsSync(simulatorFrameworkPath)) {
47
+ logger.warn(
48
+ `Skipping simulator JS bundle copy: missing ${relativeToCwd(simulatorFrameworkPath)}`
49
+ );
50
+ return;
51
+ }
52
+
53
+ fs.copyFileSync(deviceBundlePath, simulatorBundlePath);
54
+
55
+ logger.success(
56
+ `Copied Debug JS bundle to simulator slice at ${colorLink(relativeToCwd(simulatorBundlePath))}`
57
+ );
58
+ }