@callstack/brownfield-cli 3.8.1 → 3.10.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @callstack/brownfield-cli
2
2
 
3
+ ## 3.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#323](https://github.com/callstack/react-native-brownfield/pull/323) [`3456d3a`](https://github.com/callstack/react-native-brownfield/commit/3456d3aded18002475def4b79889979e76e1db5e) Thanks [@artus9033](https://github.com/artus9033)! - Support RN prebuilts in Brownfield, by default enabled in RN >= 0.84, opt-in in RN 0.83; or in Expo 55+ (Expo 54 is not supported).
8
+
9
+ Add `--use-prebuilt-rn-core` to `brownfield package:ios` so callers can opt into or out of React Native Apple prebuilt binaries; omitting the flag defers to version-aware defaults handled by Rock. The CLI rejects `--use-prebuilt-rn-core` when React Native is older than 0.81 or when the project is Expo SDK older than 55.
10
+
11
+ Fix brownfield framework dylib install names to use @rpath instead of hardcoded paths.
12
+
13
+ ## 3.9.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#326](https://github.com/callstack/react-native-brownfield/pull/326) [`80e6364`](https://github.com/callstack/react-native-brownfield/commit/80e6364d405d216b3e84a6297dfe346d2f01444b) Thanks [@artus9033](https://github.com/artus9033)! - feat: strip SO files by default, deprecate experimental option in favor of useStrippedSoFiles
18
+
3
19
  ## 3.8.1
4
20
 
5
21
  ### Patch Changes
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { ExampleUsage } from '../../shared/index.js';
3
+ export declare function parseUsePrebuiltRnCoreArgument(value: string | boolean): boolean;
3
4
  export declare const packageIosCommand: Command;
4
5
  export declare const packageIosExample: ExampleUsage;
5
6
  //# sourceMappingURL=packageIos.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAK/B,eAAO,MAAM,iBAAiB,SAgJ7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
1
+ {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAK5C,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAY/B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GAAG,OAAO,GACtB,OAAO,CAcT;AAOD,eAAO,MAAM,iBAAiB,SA6L3B,CAAC;AAEJ,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
@@ -2,22 +2,60 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { getBuildOptions, mergeFrameworks, } from '@rock-js/platform-apple-helpers';
4
4
  import { packageIosAction } from '@rock-js/plugin-brownfield-ios';
5
- import { colorLink, getReactNativeVersion, logger, relativeToCwd, } from '@rock-js/tools';
6
- import { Command } from 'commander';
5
+ import { RockError, colorLink, getReactNativeVersion, logger, relativeToCwd, } from '@rock-js/tools';
6
+ import { Command, Option } from 'commander';
7
7
  import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
8
8
  import { getProjectInfo } from '../utils/project.js';
9
+ import { supportsPrebuiltRNCore } from '../utils/supportsPrebuiltRNCore.js';
9
10
  import { actionRunner, curryOptions, ExampleUsage, } from '../../shared/index.js';
10
11
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
11
12
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
12
13
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
14
+ /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
15
+ const USE_PREBUILT_RN_CORE_HELP = 'Whether the Xcode build for packaging should use React Native Apple prebuilt binaries (via CocoaPods). ' +
16
+ '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. ' +
17
+ '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`). ' +
18
+ 'See the Brownfield iOS guide for details: https://oss.callstack.com/react-native-brownfield/docs/getting-started/ios#react-native-prebuilts';
19
+ export function parseUsePrebuiltRnCoreArgument(value) {
20
+ if (typeof value === 'boolean') {
21
+ return value;
22
+ }
23
+ const normalized = value.trim().toLowerCase();
24
+ if (normalized === 'true' || normalized === '1') {
25
+ return true;
26
+ }
27
+ if (normalized === 'false' || normalized === '0') {
28
+ return false;
29
+ }
30
+ throw new RockError(`Invalid value for --use-prebuilt-rn-core: expected true or false, received "${value}"`);
31
+ }
13
32
  export const packageIosCommand = curryOptions(new Command('package:ios').description('Build iOS XCFramework'), getBuildOptions({ platformName: 'ios' }).map((option) => option.name.startsWith('--build-folder')
14
33
  ? {
15
34
  ...option,
16
35
  description: option.description +
17
36
  " By default, the '.brownfield/build' path will be used.",
18
37
  }
19
- : option)).action(actionRunner(async (options) => {
38
+ : option))
39
+ .addOption(new Option('--use-prebuilt-rn-core [bool]', USE_PREBUILT_RN_CORE_HELP)
40
+ .preset(true)
41
+ .argParser(parseUsePrebuiltRnCoreArgument))
42
+ .action(actionRunner(async (options) => {
20
43
  const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
44
+ const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot });
45
+ // version-aware default when the flag is omitted (see ios.mdx "React Native Prebuilts")
46
+ options.usePrebuiltRnCore ??= prebuiltRNCoreSupport.supported
47
+ ? prebuiltRNCoreSupport.enabledByDefault
48
+ : false;
49
+ if (prebuiltRNCoreSupport) {
50
+ logger.info(`${options.usePrebuiltRnCore ? 'Using' : 'Not using'} prebuilt RN core`);
51
+ if (!prebuiltRNCoreSupport.enabledByDefault &&
52
+ !options.usePrebuiltRnCore) {
53
+ logger.info('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.');
54
+ }
55
+ }
56
+ if (options.usePrebuiltRnCore && !prebuiltRNCoreSupport.supported) {
57
+ throw new RockError(prebuiltRNCoreSupport.reason);
58
+ }
21
59
  await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
22
60
  if (!userConfig.project.ios) {
23
61
  throw new Error('iOS project not found.');
@@ -35,6 +73,7 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
35
73
  dotBrownfieldDir = path.join(projectRoot, dotBrownfieldDir);
36
74
  }
37
75
  options.buildFolder ??= path.join(dotBrownfieldDir, 'build');
76
+ options.verbose ??= logger.isVerbose();
38
77
  // The new_architecture.rb script scans Info.plist and fails on binary plist files,
39
78
  // which is the case for our XCFrameworks.
40
79
  // We're reusing the "build" directory which is excluded from the scan.
@@ -50,9 +89,9 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
50
89
  // e.g. '0.82' (note the missing patch component),
51
90
  // therefore we resolve it manually from RN's package.json using Rock's utils
52
91
  reactNativeVersion: getReactNativeVersion(projectRoot),
53
- usePrebuiltRNCore: false, // for brownfield, it is required to build RN from source
54
92
  packageDir, // the output directory for artifacts
55
93
  skipCache: true, // cache is dependent on existence of Rock config file
94
+ usePrebuiltRNCore: options.usePrebuiltRnCore,
56
95
  }, platformConfig);
57
96
  const reactBrownfieldXcframeworkPath = path.join(packageDir, 'ReactBrownfield.xcframework');
58
97
  if (fs.existsSync(reactBrownfieldXcframeworkPath)) {
@@ -14,6 +14,7 @@ export declare function getExpoConfigIfIsExpo(projectRoot: string): ExpoProjectC
14
14
  * @returns Whether the project is an Expo project
15
15
  */
16
16
  export declare function isExpoProject(projectRoot: string): boolean;
17
+ export declare function getExpoSdkMajor(projectRoot: string): number | null;
17
18
  /**
18
19
  * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
19
20
  */
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/project.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,UAAU,EACX,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAKjE,OAAO,EAEL,KAAK,aAAa,IAAI,iBAAiB,EACxC,MAAM,cAAc,CAAC;AAQtB;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,4BAMxD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAc1D;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,aAAa,EACb,UAAU,EAAE,EAAE,GAAG,EAAE,EACnB,WAAW,GACZ,EAAE;IACD,8CAA8C;IAC9C,aAAa,EAAE,aAAa,CAAC;IAE7B,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAE9B,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB,QAuBA;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,SAAS,KAAK,GAAG,SAAS,EAC/D,QAAQ,EAAE,QAAQ,GACjB;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC,CAeA;AAED,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,QAAQ,GACT,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAC;CAC7B,GAAG,UAAU,CAuBb;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,eAAe,EACrB,aAAa,EAAE,oBAAoB;;;EAQpC"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/project.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EACb,UAAU,EACX,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAKjE,OAAO,EAEL,KAAK,aAAa,IAAI,iBAAiB,EACxC,MAAM,cAAc,CAAC;AAQtB;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,4BAMxD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAc1D;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOlE;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,aAAa,EACb,UAAU,EAAE,EAAE,GAAG,EAAE,EACnB,WAAW,GACZ,EAAE;IACD,8CAA8C;IAC9C,aAAa,EAAE,aAAa,CAAC;IAE7B,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAE9B,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB,QAuBA;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,SAAS,KAAK,GAAG,SAAS,EAC/D,QAAQ,EAAE,QAAQ,GACjB;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC,CAeA;AAED,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,QAAQ,GACT,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAC;CAC7B,GAAG,UAAU,CAuBb;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,eAAe,EACrB,aAAa,EAAE,oBAAoB;;;EAQpC"}
@@ -36,6 +36,14 @@ export function isExpoProject(projectRoot) {
36
36
  ['dependencies', 'peerDependencies', 'devDependencies'].some((key) => JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))[key]?.expo);
37
37
  return hasExpoConfig && dependsOnExpo;
38
38
  }
39
+ export function getExpoSdkMajor(projectRoot) {
40
+ const rawExpoVersion = getExpoConfigIfIsExpo(projectRoot)?.exp.sdkVersion;
41
+ if (!rawExpoVersion) {
42
+ return null;
43
+ }
44
+ const expoSdkMajor = parseInt(rawExpoVersion.split('.')[0], 10);
45
+ return Number.isFinite(expoSdkMajor) ? expoSdkMajor : null;
46
+ }
39
47
  /**
40
48
  * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
41
49
  */
@@ -0,0 +1,18 @@
1
+ /** Minimum RN version that can opt in to prebuilts via `--use-prebuilt-rn-core`. */
2
+ export declare const MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE = "0.81.0";
3
+ /** Minimum RN version where Brownfield enables prebuilts by default (vanilla projects). */
4
+ export declare const MIN_REACT_NATIVE_VERSION_FOR_PREBUILT_RN_CORE_BY_DEFAULT = "0.84.0";
5
+ export declare const MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT = 55;
6
+ export type PrebuiltRNCoreSupportResult = {
7
+ supported: true;
8
+ enabledByDefault: boolean;
9
+ reason?: never;
10
+ } | {
11
+ supported: false;
12
+ enabledByDefault?: never;
13
+ reason: string;
14
+ };
15
+ export declare function supportsPrebuiltRNCore({ projectRoot, }: {
16
+ projectRoot: string;
17
+ }): PrebuiltRNCoreSupportResult;
18
+ //# sourceMappingURL=supportsPrebuiltRNCore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supportsPrebuiltRNCore.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/supportsPrebuiltRNCore.ts"],"names":[],"mappings":"AAIA,oFAAoF;AACpF,eAAO,MAAM,oDAAoD,WAAW,CAAC;AAC7E,2FAA2F;AAC3F,eAAO,MAAM,wDAAwD,WAC3D,CAAC;AACX,eAAO,MAAM,kDAAkD,KAAK,CAAC;AAErE,MAAM,MAAM,2BAA2B,GACnC;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,gBAAgB,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,GAC9D;IAAE,SAAS,EAAE,KAAK,CAAC;IAAC,gBAAgB,CAAC,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE,wBAAgB,sBAAsB,CAAC,EACrC,WAAW,GACZ,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,2BAA2B,CA+C9B"}
@@ -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.8.1",
3
+ "version": "3.10.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,
@@ -27,6 +29,36 @@ import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieC
27
29
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
28
30
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
29
31
 
32
+ /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
33
+ const USE_PREBUILT_RN_CORE_HELP =
34
+ 'Whether the Xcode build for packaging should use React Native Apple prebuilt binaries (via CocoaPods). ' +
35
+ '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. ' +
36
+ '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`). ' +
37
+ 'See the Brownfield iOS guide for details: https://oss.callstack.com/react-native-brownfield/docs/getting-started/ios#react-native-prebuilts';
38
+
39
+ export function parseUsePrebuiltRnCoreArgument(
40
+ value: string | boolean
41
+ ): boolean {
42
+ if (typeof value === 'boolean') {
43
+ return value;
44
+ }
45
+ const normalized = value.trim().toLowerCase();
46
+ if (normalized === 'true' || normalized === '1') {
47
+ return true;
48
+ }
49
+ if (normalized === 'false' || normalized === '0') {
50
+ return false;
51
+ }
52
+ throw new RockError(
53
+ `Invalid value for --use-prebuilt-rn-core: expected true or false, received "${value}"`
54
+ );
55
+ }
56
+
57
+ type PackageIosCliFlags = AppleBuildFlags & {
58
+ /** Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults). */
59
+ usePrebuiltRnCore?: boolean;
60
+ };
61
+
30
62
  export const packageIosCommand = curryOptions(
31
63
  new Command('package:ios').description('Build iOS XCFramework'),
32
64
  getBuildOptions({ platformName: 'ios' }).map((option) =>
@@ -39,139 +71,184 @@ export const packageIosCommand = curryOptions(
39
71
  }
40
72
  : option
41
73
  )
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
- {
74
+ )
75
+ .addOption(
76
+ new Option('--use-prebuilt-rn-core [bool]', USE_PREBUILT_RN_CORE_HELP)
77
+ .preset(true)
78
+ .argParser(parseUsePrebuiltRnCoreArgument)
79
+ )
80
+ .action(
81
+ actionRunner(async (options: PackageIosCliFlags) => {
82
+ const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
83
+
84
+ const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot });
85
+
86
+ // version-aware default when the flag is omitted (see ios.mdx "React Native Prebuilts")
87
+ options.usePrebuiltRnCore ??= prebuiltRNCoreSupport.supported
88
+ ? prebuiltRNCoreSupport.enabledByDefault
89
+ : false;
90
+
91
+ if (prebuiltRNCoreSupport) {
92
+ logger.info(
93
+ `${options.usePrebuiltRnCore ? 'Using' : 'Not using'} prebuilt RN core`
94
+ );
95
+
96
+ if (
97
+ !prebuiltRNCoreSupport.enabledByDefault &&
98
+ !options.usePrebuiltRnCore
99
+ ) {
100
+ logger.info(
101
+ '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.'
102
+ );
103
+ }
104
+ }
105
+
106
+ if (options.usePrebuiltRnCore && !prebuiltRNCoreSupport.supported) {
107
+ throw new RockError(prebuiltRNCoreSupport.reason);
108
+ }
109
+
110
+ await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
111
+
112
+ if (!userConfig.project.ios) {
113
+ throw new Error('iOS project not found.');
114
+ }
115
+
116
+ if (!userConfig.project.ios.xcodeProject) {
117
+ throw new Error('iOS Xcode project not found in the configuration.');
118
+ }
119
+
120
+ let dotBrownfieldDir = path.join(
121
+ // for Expo projects, platformConfig?.sourceDir == "", but for non-Expo projects, it's "ios"
122
+ ...(userConfig.project.ios.sourceDir.trim().length > 0
123
+ ? [userConfig.project.ios.sourceDir]
124
+ : [projectRoot, 'ios']),
125
+ '.brownfield'
126
+ );
127
+
128
+ // non-Expo projects have a relative sourceDir path, so we need to make it absolute
129
+ if (!path.isAbsolute(dotBrownfieldDir)) {
130
+ dotBrownfieldDir = path.join(projectRoot, dotBrownfieldDir);
131
+ }
132
+
133
+ options.buildFolder ??= path.join(dotBrownfieldDir, 'build');
134
+ options.verbose ??= logger.isVerbose();
135
+
136
+ // The new_architecture.rb script scans Info.plist and fails on binary plist files,
137
+ // which is the case for our XCFrameworks.
138
+ // We're reusing the "build" directory which is excluded from the scan.
139
+ // Reference: https://github.com/facebook/react-native/blob/490c5e8dcc6cdb19c334cc39e93a39a48ba71e96/packages/react-native/scripts/cocoapods/new_architecture.rb#L171
140
+ const packageDir = path.join(dotBrownfieldDir, 'package', 'build');
141
+ const configuration = options.configuration ?? 'Debug';
142
+
143
+ const { hasBrownie } = await runBrownieCodegenIfApplicable(
86
144
  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))}`
145
+ 'swift'
146
+ );
147
+ const { hasNavigation } =
148
+ await runNavigationCodegenIfApplicable(projectRoot);
149
+
150
+ await packageIosAction(
151
+ options,
152
+ {
153
+ projectRoot,
154
+ reactNativePath: userConfig.reactNativePath,
155
+ // below: the userConfig.reactNativeVersion may be a non-semver-format string,
156
+ // e.g. '0.82' (note the missing patch component),
157
+ // therefore we resolve it manually from RN's package.json using Rock's utils
158
+ reactNativeVersion: getReactNativeVersion(projectRoot),
159
+ packageDir, // the output directory for artifacts
160
+ skipCache: true, // cache is dependent on existence of Rock config file
161
+ usePrebuiltRNCore: options.usePrebuiltRnCore,
162
+ },
163
+ platformConfig
140
164
  );
141
- }
142
-
143
- if (hasNavigation) {
144
- 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))}`
165
+
166
+ const reactBrownfieldXcframeworkPath = path.join(
167
+ packageDir,
168
+ 'ReactBrownfield.xcframework'
171
169
  );
172
- }
173
- })
174
- );
170
+ if (fs.existsSync(reactBrownfieldXcframeworkPath)) {
171
+ // Strip the binary from ReactBrownfield.xcframework to make it interface-only.
172
+ // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
173
+ // (which contains ReactBrownfield symbols) and ReactBrownfield.xcframework.
174
+ stripFrameworkBinary(reactBrownfieldXcframeworkPath);
175
+ }
176
+
177
+ if (hasBrownie) {
178
+ const productsPath = path.join(
179
+ options.buildFolder,
180
+ 'Build',
181
+ 'Products'
182
+ );
183
+ const brownieOutputPath = path.join(packageDir, 'Brownie.xcframework');
184
+
185
+ await mergeFrameworks({
186
+ sourceDir: userConfig.project.ios.sourceDir,
187
+ frameworkPaths: [
188
+ path.join(
189
+ productsPath,
190
+ `${configuration}-iphoneos`,
191
+ 'Brownie',
192
+ 'Brownie.framework'
193
+ ),
194
+ path.join(
195
+ productsPath,
196
+ `${configuration}-iphonesimulator`,
197
+ 'Brownie',
198
+ 'Brownie.framework'
199
+ ),
200
+ ],
201
+ outputPath: brownieOutputPath,
202
+ });
203
+
204
+ // Strip the binary from Brownie.xcframework to make it interface-only.
205
+ // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
206
+ // (which contains Brownie symbols) and Brownie.xcframework.
207
+ stripFrameworkBinary(brownieOutputPath);
208
+
209
+ logger.success(
210
+ `Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`
211
+ );
212
+ }
213
+
214
+ if (hasNavigation) {
215
+ const productsPath = path.join(
216
+ options.buildFolder,
217
+ 'Build',
218
+ 'Products'
219
+ );
220
+ const brownfieldNavigationOutputPath = path.join(
221
+ packageDir,
222
+ 'BrownfieldNavigation.xcframework'
223
+ );
224
+
225
+ await mergeFrameworks({
226
+ sourceDir: userConfig.project.ios.sourceDir,
227
+ frameworkPaths: [
228
+ path.join(
229
+ productsPath,
230
+ `${configuration}-iphoneos`,
231
+ 'BrownfieldNavigation',
232
+ 'BrownfieldNavigation.framework'
233
+ ),
234
+ path.join(
235
+ productsPath,
236
+ `${configuration}-iphonesimulator`,
237
+ 'BrownfieldNavigation',
238
+ 'BrownfieldNavigation.framework'
239
+ ),
240
+ ],
241
+ outputPath: brownfieldNavigationOutputPath,
242
+ });
243
+
244
+ stripFrameworkBinary(brownfieldNavigationOutputPath);
245
+
246
+ logger.success(
247
+ `BrownfieldNavigation.xcframework created at ${colorLink(relativeToCwd(brownfieldNavigationOutputPath))}`
248
+ );
249
+ }
250
+ })
251
+ );
175
252
 
176
253
  export const packageIosExample = new ExampleUsage(
177
254
  'package:ios --scheme BrownfieldLib --configuration Release',
@@ -56,6 +56,15 @@ export function isExpoProject(projectRoot: string): boolean {
56
56
  return hasExpoConfig && dependsOnExpo;
57
57
  }
58
58
 
59
+ export function getExpoSdkMajor(projectRoot: string): number | null {
60
+ const rawExpoVersion = getExpoConfigIfIsExpo(projectRoot)?.exp.sdkVersion;
61
+ if (!rawExpoVersion) {
62
+ return null;
63
+ }
64
+ const expoSdkMajor = parseInt(rawExpoVersion.split('.')[0], 10);
65
+ return Number.isFinite(expoSdkMajor) ? expoSdkMajor : null;
66
+ }
67
+
59
68
  /**
60
69
  * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
61
70
  */
@@ -0,0 +1,67 @@
1
+ import { getReactNativeVersion, versionCompare } from '@rock-js/tools';
2
+
3
+ import { getExpoSdkMajor, isExpoProject } from './project.js';
4
+
5
+ /** Minimum RN version that can opt in to prebuilts via `--use-prebuilt-rn-core`. */
6
+ export const MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE = '0.81.0';
7
+ /** Minimum RN version where Brownfield enables prebuilts by default (vanilla projects). */
8
+ export const MIN_REACT_NATIVE_VERSION_FOR_PREBUILT_RN_CORE_BY_DEFAULT =
9
+ '0.84.0';
10
+ export const MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT = 55;
11
+
12
+ export type PrebuiltRNCoreSupportResult =
13
+ | { supported: true; enabledByDefault: boolean; reason?: never }
14
+ | { supported: false; enabledByDefault?: never; reason: string };
15
+
16
+ export function supportsPrebuiltRNCore({
17
+ projectRoot,
18
+ }: {
19
+ projectRoot: string;
20
+ }): PrebuiltRNCoreSupportResult {
21
+ const reactNativeVersion = getReactNativeVersion(projectRoot);
22
+
23
+ if (reactNativeVersion === 'unknown') {
24
+ return {
25
+ supported: false,
26
+ reason:
27
+ 'Cannot use --use-prebuilt-rn-core: unable to resolve the installed react-native version.',
28
+ };
29
+ }
30
+
31
+ if (
32
+ versionCompare(
33
+ reactNativeVersion,
34
+ MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE
35
+ ) < 0
36
+ ) {
37
+ return {
38
+ supported: false,
39
+ reason: `--use-prebuilt-rn-core requires React Native ${MIN_REACT_NATIVE_VERSION_FOR_OPT_IN_PREBUILT_RN_CORE} or newer (found ${reactNativeVersion}).`,
40
+ };
41
+ }
42
+
43
+ if (isExpoProject(projectRoot)) {
44
+ const expoSdkMajor = getExpoSdkMajor(projectRoot);
45
+
46
+ if (
47
+ expoSdkMajor === null ||
48
+ expoSdkMajor < MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_RN_CORE_BY_DEFAULT
49
+ ) {
50
+ const sdkLabel = expoSdkMajor === null ? 'unknown' : String(expoSdkMajor);
51
+ return {
52
+ supported: false,
53
+ 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.`,
54
+ };
55
+ }
56
+
57
+ return { supported: true, enabledByDefault: true };
58
+ }
59
+
60
+ const enabledByDefault =
61
+ versionCompare(
62
+ reactNativeVersion,
63
+ MIN_REACT_NATIVE_VERSION_FOR_PREBUILT_RN_CORE_BY_DEFAULT
64
+ ) >= 0;
65
+
66
+ return { supported: true, enabledByDefault };
67
+ }