@callstack/brownfield-cli 3.13.2 → 3.15.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 (29) hide show
  1. package/CHANGELOG.md +8 -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 +33 -2
  5. package/dist/brownfield/utils/createLocalSpmPackage.d.ts +2 -1
  6. package/dist/brownfield/utils/createLocalSpmPackage.d.ts.map +1 -1
  7. package/dist/brownfield/utils/createLocalSpmPackage.js +8 -2
  8. package/dist/brownfield/utils/emitExpoSupportXcframeworks.d.ts +9 -0
  9. package/dist/brownfield/utils/emitExpoSupportXcframeworks.d.ts.map +1 -0
  10. package/dist/brownfield/utils/emitExpoSupportXcframeworks.js +70 -0
  11. package/dist/brownfield/utils/normalizeCopiedXcframework.d.ts +3 -0
  12. package/dist/brownfield/utils/normalizeCopiedXcframework.d.ts.map +1 -0
  13. package/dist/brownfield/utils/normalizeCopiedXcframework.js +64 -0
  14. package/dist/brownfield/utils/prepareLocalSpmArtifacts.d.ts.map +1 -1
  15. package/dist/brownfield/utils/prepareLocalSpmArtifacts.js +2 -63
  16. package/dist/brownfield/utils/supportsPrebuiltExpo.d.ts +14 -0
  17. package/dist/brownfield/utils/supportsPrebuiltExpo.d.ts.map +1 -0
  18. package/dist/brownfield/utils/supportsPrebuiltExpo.js +20 -0
  19. package/dist/types.d.ts +2 -0
  20. package/dist/types.d.ts.map +1 -1
  21. package/package.json +6 -6
  22. package/schema.json +4 -0
  23. package/src/brownfield/commands/packageIos.ts +49 -2
  24. package/src/brownfield/utils/createLocalSpmPackage.ts +15 -1
  25. package/src/brownfield/utils/emitExpoSupportXcframeworks.ts +132 -0
  26. package/src/brownfield/utils/normalizeCopiedXcframework.ts +90 -0
  27. package/src/brownfield/utils/prepareLocalSpmArtifacts.ts +7 -90
  28. package/src/brownfield/utils/supportsPrebuiltExpo.ts +34 -0
  29. package/src/types.ts +3 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @callstack/brownfield-cli
2
2
 
3
+ ## 3.15.0
4
+
5
+ ## 3.14.0
6
+
7
+ ### Minor Changes
8
+
9
+ - [#360](https://github.com/callstack/react-native-brownfield/pull/360) [`10ff597`](https://github.com/callstack/react-native-brownfield/commit/10ff597c057c04aa093b4e5ae8c5334b169d3d56) Thanks [@adamTrz](https://github.com/adamTrz)! - Expo SDK 56 support
10
+
3
11
  ## 3.13.2
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { ExampleUsage } from '../../shared/index.js';
3
3
  export declare function parseUsePrebuiltRnCoreArgument(value: string | boolean): boolean;
4
+ export declare function parseUsePrebuiltExpoArgument(value: string | boolean): boolean;
4
5
  export declare const packageIosCommand: Command;
5
6
  export declare const packageIosExample: ExampleUsage;
6
7
  //# 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,EAAU,MAAM,WAAW,CAAC;AAM5C,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAiB/B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GAAG,OAAO,GACtB,OAAO,CAcT;AAcD,eAAO,MAAM,iBAAiB,SAqQ3B,CAAC;AAEJ,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
1
+ {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAO5C,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AA4C/B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GAAG,OAAO,GACtB,OAAO,CAET;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,GAAG,OAAO,GACtB,OAAO,CAET;AAcD,eAAO,MAAM,iBAAiB,SA8R3B,CAAC;AAEJ,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
@@ -7,6 +7,7 @@ import { Command, Option } from 'commander';
7
7
  import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
8
8
  import { findProjectRoot } from '../utils/paths.js';
9
9
  import { getProjectInfo } from '../utils/project.js';
10
+ import { supportsPrebuiltExpo } from '../utils/supportsPrebuiltExpo.js';
10
11
  import { supportsPrebuiltRNCore } from '../utils/supportsPrebuiltRNCore.js';
11
12
  import { actionRunner, curryOptions, ExampleUsage, } from '../../shared/index.js';
12
13
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
@@ -15,13 +16,19 @@ import { copyDebugBundleToSimulatorSlice } from '../utils/copyDebugBundleToSimul
15
16
  import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkName.js';
16
17
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
17
18
  import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js';
19
+ import { emitExpoSupportXcframeworks } from '../utils/emitExpoSupportXcframeworks.js';
18
20
  import { mergeBrownfieldConfigWithOptions } from '../../config.js';
21
+ import { removeCodeSignatureArtifacts } from '../utils/normalizeCopiedXcframework.js';
19
22
  /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
20
23
  const USE_PREBUILT_RN_CORE_HELP = 'Whether the Xcode build for packaging should use React Native Apple prebuilt binaries (via CocoaPods). ' +
21
24
  '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. ' +
22
25
  '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`). ' +
23
26
  'See the Brownfield iOS guide for details: https://oss.callstack.com/react-native-brownfield/docs/getting-started/ios#react-native-prebuilts';
24
- export function parseUsePrebuiltRnCoreArgument(value) {
27
+ /** Help text for `--use-prebuilt-expo`. */
28
+ const USE_PREBUILT_EXPO_HELP = 'Whether the Xcode build for packaging should use prebuilt Expo support XCFrameworks. ' +
29
+ 'If you omit this flag, Brownfield follows version-aware defaults for supported Expo projects. ' +
30
+ 'Pass true or false to force either behavior. Use the flag without a value as shorthand for true (same as `--use-prebuilt-expo true`).';
31
+ function parseBooleanOptionArgument(flagName, value) {
25
32
  if (typeof value === 'boolean') {
26
33
  return value;
27
34
  }
@@ -32,7 +39,13 @@ export function parseUsePrebuiltRnCoreArgument(value) {
32
39
  if (normalized === 'false' || normalized === '0') {
33
40
  return false;
34
41
  }
35
- throw new RockError(`Invalid value for --use-prebuilt-rn-core: expected true or false, received "${value}"`);
42
+ throw new RockError(`Invalid value for ${flagName}: expected true or false, received "${value}"`);
43
+ }
44
+ export function parseUsePrebuiltRnCoreArgument(value) {
45
+ return parseBooleanOptionArgument('--use-prebuilt-rn-core', value);
46
+ }
47
+ export function parseUsePrebuiltExpoArgument(value) {
48
+ return parseBooleanOptionArgument('--use-prebuilt-expo', value);
36
49
  }
37
50
  function getPackagedFrameworkResolutionFailureMessage({ resolution, candidates, }) {
38
51
  return resolution === 'ambiguous'
@@ -49,6 +62,9 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
49
62
  .addOption(new Option('--use-prebuilt-rn-core [bool]', USE_PREBUILT_RN_CORE_HELP)
50
63
  .preset(true)
51
64
  .argParser(parseUsePrebuiltRnCoreArgument))
65
+ .addOption(new Option('--use-prebuilt-expo [bool]', USE_PREBUILT_EXPO_HELP)
66
+ .preset(true)
67
+ .argParser(parseUsePrebuiltExpoArgument))
52
68
  .addOption(new Option('--add-spm-package', 'Generate a local Swift Package Manager manifest next to the packaged XCFramework outputs'))
53
69
  .action(actionRunner(async (cliOptions) => {
54
70
  const options = mergeBrownfieldConfigWithOptions(cliOptions, 'ios');
@@ -56,10 +72,15 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
56
72
  await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
57
73
  const { platformConfig, userConfig } = getProjectInfo('ios');
58
74
  const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot });
75
+ const prebuiltExpoSupport = supportsPrebuiltExpo({ projectRoot });
59
76
  // version-aware default when the flag is omitted (see ios.mdx "React Native Prebuilts")
60
77
  options.usePrebuiltRnCore ??= prebuiltRNCoreSupport.supported
61
78
  ? prebuiltRNCoreSupport.enabledByDefault
62
79
  : false;
80
+ // version-aware default when the flag is omitted
81
+ options.usePrebuiltExpo ??= prebuiltExpoSupport.supported
82
+ ? prebuiltExpoSupport.enabledByDefault
83
+ : false;
63
84
  if (prebuiltRNCoreSupport) {
64
85
  logger.info(`${options.usePrebuiltRnCore ? 'Using' : 'Not using'} prebuilt RN core`);
65
86
  if (!prebuiltRNCoreSupport.enabledByDefault &&
@@ -70,6 +91,9 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
70
91
  if (options.usePrebuiltRnCore && !prebuiltRNCoreSupport.supported) {
71
92
  throw new RockError(prebuiltRNCoreSupport.reason);
72
93
  }
94
+ if (options.usePrebuiltExpo && !prebuiltExpoSupport.supported) {
95
+ throw new RockError(prebuiltExpoSupport.reason);
96
+ }
73
97
  if (!userConfig.project.ios) {
74
98
  throw new Error('iOS project not found.');
75
99
  }
@@ -106,6 +130,11 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
106
130
  skipCache: true, // cache is dependent on existence of Rock config file
107
131
  usePrebuiltRNCore: options.usePrebuiltRnCore,
108
132
  }, platformConfig);
133
+ emitExpoSupportXcframeworks({
134
+ projectRoot,
135
+ packageDir,
136
+ usePrebuiltExpo: options.usePrebuiltExpo,
137
+ });
109
138
  const productsPath = path.join(options.buildFolder, 'Build', 'Products');
110
139
  const { frameworkName, resolution, candidates } = resolvePackagedFrameworkName({
111
140
  explicitScheme: options.scheme,
@@ -178,10 +207,12 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
178
207
  stripFrameworkBinary(brownfieldNavigationOutputPath);
179
208
  logger.success(`BrownfieldNavigation.xcframework created at ${colorLink(relativeToCwd(brownfieldNavigationOutputPath))}`);
180
209
  }
210
+ removeCodeSignatureArtifacts(packageDir);
181
211
  if (options.addSpmPackage) {
182
212
  const { packageManifestPath } = createLocalSpmPackage({
183
213
  packageDir,
184
214
  frameworkName: frameworkName ?? undefined,
215
+ usePrebuiltExpo: options.usePrebuiltExpo,
185
216
  });
186
217
  logger.success(`Local SPM package manifest created at ${colorLink(relativeToCwd(packageManifestPath))}`);
187
218
  logger.info(`Add the local package folder in Xcode: ${colorLink(relativeToCwd(packageDir))}`);
@@ -1,10 +1,11 @@
1
1
  type CreateLocalSpmPackageOptions = {
2
2
  packageDir: string;
3
3
  frameworkName?: string;
4
+ usePrebuiltExpo?: boolean;
4
5
  };
5
6
  type CreateLocalSpmPackageResult = {
6
7
  packageManifestPath: string;
7
8
  };
8
- export declare function createLocalSpmPackage({ packageDir, frameworkName, }: CreateLocalSpmPackageOptions): CreateLocalSpmPackageResult;
9
+ export declare function createLocalSpmPackage({ packageDir, frameworkName, usePrebuiltExpo, }: CreateLocalSpmPackageOptions): CreateLocalSpmPackageResult;
9
10
  export {};
10
11
  //# sourceMappingURL=createLocalSpmPackage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createLocalSpmPackage.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/createLocalSpmPackage.ts"],"names":[],"mappings":"AAQA,KAAK,4BAA4B,GAAG;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,2BAA2B,GAAG;IACjC,mBAAmB,EAAE,MAAM,CAAC;CAC7B,CAAC;AAuJF,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,aAAa,GACd,EAAE,4BAA4B,GAAG,2BAA2B,CAsC5D"}
1
+ {"version":3,"file":"createLocalSpmPackage.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/createLocalSpmPackage.ts"],"names":[],"mappings":"AAYA,KAAK,4BAA4B,GAAG;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,KAAK,2BAA2B,GAAG;IACjC,mBAAmB,EAAE,MAAM,CAAC;CAC7B,CAAC;AA0JF,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,aAAa,EACb,eAAsB,GACvB,EAAE,4BAA4B,GAAG,2BAA2B,CA2C5D"}
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { prepareLocalSpmArtifacts, SPM_ARTIFACTS_DIR_NAME, } from './prepareLocalSpmArtifacts.js';
4
+ import { ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES, getExpoSupportXcframeworkNames, } from './emitExpoSupportXcframeworks.js';
4
5
  const RESERVED_FRAMEWORK_NAMES = new Set([
5
6
  'hermes',
6
7
  'hermesvm',
@@ -9,6 +10,7 @@ const RESERVED_FRAMEWORK_NAMES = new Set([
9
10
  'BrownfieldNavigation',
10
11
  'React',
11
12
  'ReactNativeDependencies',
13
+ ...ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES,
12
14
  ]);
13
15
  function requireXcframework(packageDir, name) {
14
16
  const xcframeworkPath = path.join(packageDir, `${name}.xcframework`);
@@ -71,7 +73,9 @@ ${binaryTargets}
71
73
  `;
72
74
  }
73
75
  function renderReadme({ packageName, libraryName, targetNames, }) {
74
- const frameworks = targetNames.map((targetName) => `- \`${targetName}\``).join('\n');
76
+ const frameworks = targetNames
77
+ .map((targetName) => `- \`${targetName}\``)
78
+ .join('\n');
75
79
  return `# ${packageName}
76
80
 
77
81
  This is a generated local Swift Package Manager package for the packaged React Native brownfield artifacts.
@@ -103,14 +107,16 @@ If Xcode builds your host app but the simulator installation fails, check the ho
103
107
  This folder is generated by \`brownfield package:ios --add-spm-package\`. Re-run that command whenever the packaged XCFrameworks change so this package stays in sync.
104
108
  `;
105
109
  }
106
- export function createLocalSpmPackage({ packageDir, frameworkName, }) {
110
+ export function createLocalSpmPackage({ packageDir, frameworkName, usePrebuiltExpo = true, }) {
107
111
  const resolvedFrameworkName = resolveAppFrameworkName(packageDir, frameworkName);
112
+ const expoSupportXcframeworkNames = getExpoSupportXcframeworkNames(usePrebuiltExpo);
108
113
  const targetNames = [
109
114
  resolvedFrameworkName,
110
115
  requireHermesXcframework(packageDir),
111
116
  requireXcframework(packageDir, 'ReactBrownfield'),
112
117
  optionalXcframework(packageDir, 'Brownie'),
113
118
  optionalXcframework(packageDir, 'BrownfieldNavigation'),
119
+ ...expoSupportXcframeworkNames.map((frameworkName) => optionalXcframework(packageDir, frameworkName)),
114
120
  optionalXcframework(packageDir, 'React'),
115
121
  optionalXcframework(packageDir, 'ReactNativeDependencies'),
116
122
  ].filter((targetName) => targetName !== null);
@@ -0,0 +1,9 @@
1
+ export declare const EXPO_SUPPORT_XCFRAMEWORK_NAMES: readonly ["ExpoModulesJSI", "ExpoFileSystem", "ExpoFont", "ExpoModulesCore", "ExpoImage", "ExpoModulesWorklets"];
2
+ export declare const ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES: readonly ["ExpoModulesJSI", "ExpoFileSystem", "ExpoFont", "ExpoModulesCore", "ExpoImage", "ExpoModulesWorklets", "SDWebImage", "SDWebImageSVGCoder", "SDWebImageWebPCoder", "SDWebImageAVIFCoder", "libavif"];
3
+ export declare function getExpoSupportXcframeworkNames(usePrebuiltExpo: boolean): readonly ["ExpoModulesJSI", "ExpoFileSystem", "ExpoFont", "ExpoModulesCore", "ExpoImage", "ExpoModulesWorklets", "SDWebImage", "SDWebImageSVGCoder", "SDWebImageWebPCoder", "SDWebImageAVIFCoder", "libavif"] | readonly ["ExpoModulesJSI"];
4
+ export declare function emitExpoSupportXcframeworks({ projectRoot, packageDir, usePrebuiltExpo, }: {
5
+ projectRoot: string;
6
+ packageDir: string;
7
+ usePrebuiltExpo?: boolean;
8
+ }): boolean;
9
+ //# sourceMappingURL=emitExpoSupportXcframeworks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitExpoSupportXcframeworks.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/emitExpoSupportXcframeworks.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,8BAA8B,kHAOjC,CAAC;AAEX,eAAO,MAAM,kCAAkC,+MAGrC,CAAC;AAEX,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,OAAO,+OAItE;AA8CD,wBAAgB,2BAA2B,CAAC,EAC1C,WAAW,EACX,UAAU,EACV,eAAsB,GACvB,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,WAkCA"}
@@ -0,0 +1,70 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { RockError } from '@rock-js/tools';
4
+ import { normalizeCopiedXcframework } from './normalizeCopiedXcframework.js';
5
+ import { getExpoSdkMajor, isExpoProject } from './project.js';
6
+ import { MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO } from './supportsPrebuiltExpo.js';
7
+ /**
8
+ * Swift support XCFrameworks
9
+ *
10
+ * These are required for the Expo Image module to work.
11
+ *
12
+ * TODO: This needs investigation and maybe a discussion with the Expo team.
13
+ */
14
+ const SWIFT_SUPPORT_XCFRAMEWORK_NAMES = [
15
+ 'SDWebImage',
16
+ 'SDWebImageSVGCoder',
17
+ 'SDWebImageWebPCoder',
18
+ 'SDWebImageAVIFCoder',
19
+ 'libavif',
20
+ ];
21
+ export const EXPO_SUPPORT_XCFRAMEWORK_NAMES = [
22
+ 'ExpoModulesJSI',
23
+ 'ExpoFileSystem',
24
+ 'ExpoFont',
25
+ 'ExpoModulesCore',
26
+ 'ExpoImage',
27
+ 'ExpoModulesWorklets'
28
+ ];
29
+ export const ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES = [
30
+ ...EXPO_SUPPORT_XCFRAMEWORK_NAMES,
31
+ ...SWIFT_SUPPORT_XCFRAMEWORK_NAMES,
32
+ ];
33
+ export function getExpoSupportXcframeworkNames(usePrebuiltExpo) {
34
+ return usePrebuiltExpo
35
+ ? ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES
36
+ : ['ExpoModulesJSI'];
37
+ }
38
+ function resolveExpoFrameworkSourcePath(projectRoot, frameworkName) {
39
+ if (frameworkName === 'ExpoModulesJSI') {
40
+ return path.join(projectRoot, 'node_modules', 'expo-modules-jsi', 'apple', 'Products', 'ExpoModulesJSI.xcframework');
41
+ }
42
+ if (SWIFT_SUPPORT_XCFRAMEWORK_NAMES.includes(frameworkName)) {
43
+ return path.join(projectRoot, 'ios', 'Pods', 'ExpoImage', `${frameworkName}.xcframework`);
44
+ }
45
+ if (EXPO_SUPPORT_XCFRAMEWORK_NAMES.includes(frameworkName)) {
46
+ return path.join(projectRoot, 'ios', 'Pods', frameworkName, `${frameworkName}.xcframework`);
47
+ }
48
+ throw new RockError(`Unsupported Expo XCFramework: ${frameworkName}`);
49
+ }
50
+ export function emitExpoSupportXcframeworks({ projectRoot, packageDir, usePrebuiltExpo = true, }) {
51
+ if (!isExpoProject(projectRoot)) {
52
+ return false;
53
+ }
54
+ const expoSdkMajor = getExpoSdkMajor(projectRoot);
55
+ if (expoSdkMajor === null ||
56
+ expoSdkMajor < MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO) {
57
+ return false;
58
+ }
59
+ for (const frameworkName of getExpoSupportXcframeworkNames(usePrebuiltExpo)) {
60
+ const sourcePath = resolveExpoFrameworkSourcePath(projectRoot, frameworkName);
61
+ if (!fs.existsSync(sourcePath)) {
62
+ throw new RockError(`Expected Expo SDK ${MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO}+ XCFramework not found: ${frameworkName}.xcframework at ${path.relative(projectRoot, sourcePath)}`);
63
+ }
64
+ const destinationPath = path.join(packageDir, `${frameworkName}.xcframework`);
65
+ fs.rmSync(destinationPath, { recursive: true, force: true });
66
+ fs.cpSync(sourcePath, destinationPath, { recursive: true });
67
+ normalizeCopiedXcframework(destinationPath);
68
+ }
69
+ return true;
70
+ }
@@ -0,0 +1,3 @@
1
+ export declare function removeCodeSignatureArtifacts(dirPath: string): void;
2
+ export declare function normalizeCopiedXcframework(xcframeworkPath: string): void;
3
+ //# sourceMappingURL=normalizeCopiedXcframework.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeCopiedXcframework.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/normalizeCopiedXcframework.ts"],"names":[],"mappings":"AAIA,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,QAkB3D;AAyCD,wBAAgB,0BAA0B,CAAC,eAAe,EAAE,MAAM,QA0BjE"}
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ export function removeCodeSignatureArtifacts(dirPath) {
5
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
6
+ const entryPath = path.join(dirPath, entry.name);
7
+ if (entry.isDirectory()) {
8
+ if (entry.name === '_CodeSignature') {
9
+ fs.rmSync(entryPath, { recursive: true, force: true });
10
+ continue;
11
+ }
12
+ removeCodeSignatureArtifacts(entryPath);
13
+ continue;
14
+ }
15
+ if (entry.name === 'CodeResources') {
16
+ fs.rmSync(entryPath, { force: true });
17
+ }
18
+ }
19
+ }
20
+ function resolveFrameworkExecutablePath(frameworkDir, frameworkName) {
21
+ const directExecutablePath = path.join(frameworkDir, frameworkName);
22
+ if (fs.existsSync(directExecutablePath)) {
23
+ return directExecutablePath;
24
+ }
25
+ const versionedExecutablePath = path.join(frameworkDir, 'Versions', 'Current', frameworkName);
26
+ if (fs.existsSync(versionedExecutablePath)) {
27
+ return versionedExecutablePath;
28
+ }
29
+ return null;
30
+ }
31
+ function removeExecutableSignature(executablePath) {
32
+ try {
33
+ execFileSync('codesign', ['--remove-signature', executablePath], {
34
+ stdio: 'pipe',
35
+ });
36
+ }
37
+ catch (error) {
38
+ if (error instanceof Error &&
39
+ error.message.includes('code object is not signed at all')) {
40
+ return;
41
+ }
42
+ throw error;
43
+ }
44
+ }
45
+ export function normalizeCopiedXcframework(xcframeworkPath) {
46
+ removeCodeSignatureArtifacts(xcframeworkPath);
47
+ for (const sliceName of fs.readdirSync(xcframeworkPath)) {
48
+ const slicePath = path.join(xcframeworkPath, sliceName);
49
+ if (!fs.statSync(slicePath).isDirectory()) {
50
+ continue;
51
+ }
52
+ for (const entry of fs.readdirSync(slicePath)) {
53
+ if (!entry.endsWith('.framework')) {
54
+ continue;
55
+ }
56
+ const frameworkName = path.basename(entry, '.framework');
57
+ const frameworkDir = path.join(slicePath, entry);
58
+ const executablePath = resolveFrameworkExecutablePath(frameworkDir, frameworkName);
59
+ if (executablePath) {
60
+ removeExecutableSignature(executablePath);
61
+ }
62
+ }
63
+ }
64
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"prepareLocalSpmArtifacts.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/prepareLocalSpmArtifacts.ts"],"names":[],"mappings":"AAIA,KAAK,+BAA+B,GAAG;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAyFtD,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,WAAW,GACZ,EAAE,+BAA+B,UAejC"}
1
+ {"version":3,"file":"prepareLocalSpmArtifacts.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/prepareLocalSpmArtifacts.ts"],"names":[],"mappings":"AAKA,KAAK,+BAA+B,GAAG;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAEtD,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,WAAW,GACZ,EAAE,+BAA+B,UAkBjC"}
@@ -1,68 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { execFileSync } from 'node:child_process';
3
+ import { normalizeCopiedXcframework } from './normalizeCopiedXcframework.js';
4
4
  export const SPM_ARTIFACTS_DIR_NAME = 'spm-artifacts';
5
- function removeCodeSignatureArtifacts(dirPath) {
6
- for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
7
- const entryPath = path.join(dirPath, entry.name);
8
- if (entry.isDirectory()) {
9
- if (entry.name === '_CodeSignature') {
10
- fs.rmSync(entryPath, { recursive: true, force: true });
11
- continue;
12
- }
13
- removeCodeSignatureArtifacts(entryPath);
14
- continue;
15
- }
16
- if (entry.name === 'CodeResources') {
17
- fs.rmSync(entryPath, { force: true });
18
- }
19
- }
20
- }
21
- function resolveFrameworkExecutablePath(frameworkDir, frameworkName) {
22
- const directExecutablePath = path.join(frameworkDir, frameworkName);
23
- if (fs.existsSync(directExecutablePath)) {
24
- return directExecutablePath;
25
- }
26
- const versionedExecutablePath = path.join(frameworkDir, 'Versions', 'Current', frameworkName);
27
- if (fs.existsSync(versionedExecutablePath)) {
28
- return versionedExecutablePath;
29
- }
30
- return null;
31
- }
32
- function removeExecutableSignature(executablePath) {
33
- try {
34
- execFileSync('codesign', ['--remove-signature', executablePath], {
35
- stdio: 'pipe',
36
- });
37
- }
38
- catch (error) {
39
- if (error instanceof Error &&
40
- error.message.includes('code object is not signed at all')) {
41
- return;
42
- }
43
- throw error;
44
- }
45
- }
46
- function normalizeCopiedXcframeworkSignature(xcframeworkPath) {
47
- removeCodeSignatureArtifacts(xcframeworkPath);
48
- for (const sliceName of fs.readdirSync(xcframeworkPath)) {
49
- const slicePath = path.join(xcframeworkPath, sliceName);
50
- if (!fs.statSync(slicePath).isDirectory()) {
51
- continue;
52
- }
53
- for (const entry of fs.readdirSync(slicePath)) {
54
- if (!entry.endsWith('.framework')) {
55
- continue;
56
- }
57
- const frameworkName = path.basename(entry, '.framework');
58
- const frameworkDir = path.join(slicePath, entry);
59
- const executablePath = resolveFrameworkExecutablePath(frameworkDir, frameworkName);
60
- if (executablePath) {
61
- removeExecutableSignature(executablePath);
62
- }
63
- }
64
- }
65
- }
66
5
  export function prepareLocalSpmArtifacts({ packageDir, targetNames, }) {
67
6
  const spmArtifactsDir = path.join(packageDir, SPM_ARTIFACTS_DIR_NAME);
68
7
  fs.rmSync(spmArtifactsDir, { recursive: true, force: true });
@@ -71,7 +10,7 @@ export function prepareLocalSpmArtifacts({ packageDir, targetNames, }) {
71
10
  const sourcePath = path.join(packageDir, `${targetName}.xcframework`);
72
11
  const destinationPath = path.join(spmArtifactsDir, `${targetName}.xcframework`);
73
12
  fs.renameSync(sourcePath, destinationPath);
74
- normalizeCopiedXcframeworkSignature(destinationPath);
13
+ normalizeCopiedXcframework(destinationPath);
75
14
  }
76
15
  return spmArtifactsDir;
77
16
  }
@@ -0,0 +1,14 @@
1
+ export declare const MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO = 56;
2
+ export type PrebuiltExpoSupportResult = {
3
+ supported: true;
4
+ enabledByDefault: boolean;
5
+ reason?: never;
6
+ } | {
7
+ supported: false;
8
+ enabledByDefault?: never;
9
+ reason: string;
10
+ };
11
+ export declare function supportsPrebuiltExpo({ projectRoot, }: {
12
+ projectRoot: string;
13
+ }): PrebuiltExpoSupportResult;
14
+ //# sourceMappingURL=supportsPrebuiltExpo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supportsPrebuiltExpo.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/supportsPrebuiltExpo.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oCAAoC,KAAK,CAAC;AAEvD,MAAM,MAAM,yBAAyB,GACjC;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,oBAAoB,CAAC,EACnC,WAAW,GACZ,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,yBAAyB,CAqB5B"}
@@ -0,0 +1,20 @@
1
+ import { getExpoSdkMajor, isExpoProject } from './project.js';
2
+ export const MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO = 56;
3
+ export function supportsPrebuiltExpo({ projectRoot, }) {
4
+ if (!isExpoProject(projectRoot)) {
5
+ return { supported: true, enabledByDefault: false };
6
+ }
7
+ const expoSdkMajor = getExpoSdkMajor(projectRoot);
8
+ if (expoSdkMajor === null ||
9
+ expoSdkMajor < MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO) {
10
+ const sdkLabel = expoSdkMajor === null ? 'unknown' : String(expoSdkMajor);
11
+ return {
12
+ supported: false,
13
+ reason: `--use-prebuilt-expo is unsupported in Expo SDK ${sdkLabel}: packaging brownfield with Expo prebuilts requires Expo SDK ${MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO} or newer.`,
14
+ };
15
+ }
16
+ return {
17
+ supported: true,
18
+ enabledByDefault: true,
19
+ };
20
+ }
package/dist/types.d.ts CHANGED
@@ -23,6 +23,8 @@ export type BrownieConfig = {
23
23
  export type PackageIosOptions = Omit<AppleBuildFlags, 'archive' | 'local'> & {
24
24
  /** Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults). */
25
25
  usePrebuiltRnCore?: boolean;
26
+ /** Set when `--use-prebuilt-expo` is passed; omitted when the flag is absent (Rock applies Expo version defaults). */
27
+ usePrebuiltExpo?: boolean;
26
28
  /** When set, generate a local Swift Package Manager manifest next to the packaged XCFramework outputs. */
27
29
  addSpmPackage?: boolean;
28
30
  };
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,eAAe,EAChB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,UAAU,IAAI,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAErF,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC;IAC5C;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,OAAO,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG;IAC3E,uHAAuH;IACvH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B,0GAA0G;IAC1G,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG,uBAAuB,GACnE,OAAO,CAAC,eAAe,CAAC,CAAC;AAC3B,MAAM,MAAM,+BAA+B,GAAG,uBAAuB,GACnE,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAChC,MAAM,MAAM,2BAA2B,GAAG,uBAAuB,GAC/D,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE7B;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC;IAE1D;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,IAAI,CACxC,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,oBAAoB,CAAC,EACxD,MAAM,uBAAuB,CAC9B,GAAG;IACF;;OAEG;IACH,IAAI,CAAC,EAAE,2BAA2B,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,OAAO,CAAC,iBAAiB,CAAC,EAC1B,MAAM,uBAAuB,CAC9B,GAAG;IACF;;OAEG;IACH,IAAI,CAAC,EAAE,uBAAuB,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,wBAAwB,GACrD,uBAAuB,GACvB,OAAO,CAAC;IACN;;OAEG;IACH,OAAO,EAAE,uBAAuB,CAAC;IAEjC;;OAEG;IACH,GAAG,EAAE,mBAAmB,CAAC;IAEzB;;OAEG;IACH,OAAO,EAAE,aAAa,CAAC;CACxB,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,eAAe,EAChB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,UAAU,IAAI,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAErF,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC;IAC5C;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,OAAO,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG;IAC3E,uHAAuH;IACvH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B,sHAAsH;IACtH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,0GAA0G;IAC1G,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG,uBAAuB,GACnE,OAAO,CAAC,eAAe,CAAC,CAAC;AAC3B,MAAM,MAAM,+BAA+B,GAAG,uBAAuB,GACnE,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAChC,MAAM,MAAM,2BAA2B,GAAG,uBAAuB,GAC/D,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE7B;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC;IAE1D;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,IAAI,CACxC,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,oBAAoB,CAAC,EACxD,MAAM,uBAAuB,CAC9B,GAAG;IACF;;OAEG;IACH,IAAI,CAAC,EAAE,2BAA2B,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,OAAO,CAAC,iBAAiB,CAAC,EAC1B,MAAM,uBAAuB,CAC9B,GAAG;IACF;;OAEG;IACH,IAAI,CAAC,EAAE,uBAAuB,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,wBAAwB,GACrD,uBAAuB,GACvB,OAAO,CAAC;IACN;;OAEG;IACH,OAAO,EAAE,uBAAuB,CAAC;IAEjC;;OAEG;IACH,GAAG,EAAE,mBAAmB,CAAC;IAEzB;;OAEG;IACH,OAAO,EAAE,aAAa,CAAC;CACxB,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/brownfield-cli",
3
- "version": "3.13.2",
3
+ "version": "3.15.0",
4
4
  "license": "MIT",
5
5
  "author": "Artur Morys-Magiera <artus9033@gmail.com>",
6
6
  "bin": {
@@ -88,11 +88,11 @@
88
88
  "@expo/config": "^12.0.13",
89
89
  "@react-native-community/cli-config": "^20.0.0",
90
90
  "@react-native-community/cli-config-android": "^20.0.0",
91
- "@rock-js/platform-android": "^0.13.5",
92
- "@rock-js/platform-apple-helpers": "^0.13.5",
93
- "@rock-js/plugin-brownfield-android": "^0.13.5",
94
- "@rock-js/plugin-brownfield-ios": "^0.13.5",
95
- "@rock-js/tools": "^0.13.5",
91
+ "@rock-js/platform-android": "^0.14.0",
92
+ "@rock-js/platform-apple-helpers": "^0.14.0",
93
+ "@rock-js/plugin-brownfield-android": "^0.14.0",
94
+ "@rock-js/plugin-brownfield-ios": "^0.14.0",
95
+ "@rock-js/tools": "^0.14.0",
96
96
  "ajv": "^8.20.0",
97
97
  "commander": "^14.0.3",
98
98
  "quicktype-core": "^23.2.6",
package/schema.json CHANGED
@@ -170,6 +170,10 @@
170
170
  "description": "Xcode target to use.",
171
171
  "type": "string"
172
172
  },
173
+ "usePrebuiltExpo": {
174
+ "description": "Set when `--use-prebuilt-expo` is passed; omitted when the flag is absent (Rock applies Expo version defaults).",
175
+ "type": "boolean"
176
+ },
173
177
  "usePrebuiltRnCore": {
174
178
  "description": "Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults).",
175
179
  "type": "boolean"
@@ -19,6 +19,7 @@ import { Command, Option } from 'commander';
19
19
  import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
20
20
  import { findProjectRoot } from '../utils/paths.js';
21
21
  import { getProjectInfo } from '../utils/project.js';
22
+ import { supportsPrebuiltExpo } from '../utils/supportsPrebuiltExpo.js';
22
23
  import { supportsPrebuiltRNCore } from '../utils/supportsPrebuiltRNCore.js';
23
24
  import {
24
25
  actionRunner,
@@ -32,7 +33,9 @@ import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkN
32
33
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
33
34
  import type { PackageIosOptions } from '../../types.js';
34
35
  import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js';
36
+ import { emitExpoSupportXcframeworks } from '../utils/emitExpoSupportXcframeworks.js';
35
37
  import { mergeBrownfieldConfigWithOptions } from '../../config.js';
38
+ import { removeCodeSignatureArtifacts } from '../utils/normalizeCopiedXcframework.js';
36
39
 
37
40
  /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
38
41
  const USE_PREBUILT_RN_CORE_HELP =
@@ -41,7 +44,14 @@ const USE_PREBUILT_RN_CORE_HELP =
41
44
  '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`). ' +
42
45
  'See the Brownfield iOS guide for details: https://oss.callstack.com/react-native-brownfield/docs/getting-started/ios#react-native-prebuilts';
43
46
 
44
- export function parseUsePrebuiltRnCoreArgument(
47
+ /** Help text for `--use-prebuilt-expo`. */
48
+ const USE_PREBUILT_EXPO_HELP =
49
+ 'Whether the Xcode build for packaging should use prebuilt Expo support XCFrameworks. ' +
50
+ 'If you omit this flag, Brownfield follows version-aware defaults for supported Expo projects. ' +
51
+ 'Pass true or false to force either behavior. Use the flag without a value as shorthand for true (same as `--use-prebuilt-expo true`).';
52
+
53
+ function parseBooleanOptionArgument(
54
+ flagName: string,
45
55
  value: string | boolean
46
56
  ): boolean {
47
57
  if (typeof value === 'boolean') {
@@ -55,10 +65,22 @@ export function parseUsePrebuiltRnCoreArgument(
55
65
  return false;
56
66
  }
57
67
  throw new RockError(
58
- `Invalid value for --use-prebuilt-rn-core: expected true or false, received "${value}"`
68
+ `Invalid value for ${flagName}: expected true or false, received "${value}"`
59
69
  );
60
70
  }
61
71
 
72
+ export function parseUsePrebuiltRnCoreArgument(
73
+ value: string | boolean
74
+ ): boolean {
75
+ return parseBooleanOptionArgument('--use-prebuilt-rn-core', value);
76
+ }
77
+
78
+ export function parseUsePrebuiltExpoArgument(
79
+ value: string | boolean
80
+ ): boolean {
81
+ return parseBooleanOptionArgument('--use-prebuilt-expo', value);
82
+ }
83
+
62
84
  function getPackagedFrameworkResolutionFailureMessage({
63
85
  resolution,
64
86
  candidates,
@@ -89,6 +111,11 @@ export const packageIosCommand = curryOptions(
89
111
  .preset(true)
90
112
  .argParser(parseUsePrebuiltRnCoreArgument)
91
113
  )
114
+ .addOption(
115
+ new Option('--use-prebuilt-expo [bool]', USE_PREBUILT_EXPO_HELP)
116
+ .preset(true)
117
+ .argParser(parseUsePrebuiltExpoArgument)
118
+ )
92
119
  .addOption(
93
120
  new Option(
94
121
  '--add-spm-package',
@@ -105,12 +132,18 @@ export const packageIosCommand = curryOptions(
105
132
  const { platformConfig, userConfig } = getProjectInfo('ios');
106
133
 
107
134
  const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot });
135
+ const prebuiltExpoSupport = supportsPrebuiltExpo({ projectRoot });
108
136
 
109
137
  // version-aware default when the flag is omitted (see ios.mdx "React Native Prebuilts")
110
138
  options.usePrebuiltRnCore ??= prebuiltRNCoreSupport.supported
111
139
  ? prebuiltRNCoreSupport.enabledByDefault
112
140
  : false;
113
141
 
142
+ // version-aware default when the flag is omitted
143
+ options.usePrebuiltExpo ??= prebuiltExpoSupport.supported
144
+ ? prebuiltExpoSupport.enabledByDefault
145
+ : false;
146
+
114
147
  if (prebuiltRNCoreSupport) {
115
148
  logger.info(
116
149
  `${options.usePrebuiltRnCore ? 'Using' : 'Not using'} prebuilt RN core`
@@ -130,6 +163,10 @@ export const packageIosCommand = curryOptions(
130
163
  throw new RockError(prebuiltRNCoreSupport.reason);
131
164
  }
132
165
 
166
+ if (options.usePrebuiltExpo && !prebuiltExpoSupport.supported) {
167
+ throw new RockError(prebuiltExpoSupport.reason);
168
+ }
169
+
133
170
  if (!userConfig.project.ios) {
134
171
  throw new Error('iOS project not found.');
135
172
  }
@@ -184,6 +221,12 @@ export const packageIosCommand = curryOptions(
184
221
  platformConfig
185
222
  );
186
223
 
224
+ emitExpoSupportXcframeworks({
225
+ projectRoot,
226
+ packageDir,
227
+ usePrebuiltExpo: options.usePrebuiltExpo,
228
+ });
229
+
187
230
  const productsPath = path.join(options.buildFolder, 'Build', 'Products');
188
231
  const { frameworkName, resolution, candidates } =
189
232
  resolvePackagedFrameworkName({
@@ -244,6 +287,7 @@ export const packageIosCommand = curryOptions(
244
287
  packageDir,
245
288
  'ReactBrownfield.xcframework'
246
289
  );
290
+
247
291
  if (fs.existsSync(reactBrownfieldXcframeworkPath)) {
248
292
  // Strip the binary from ReactBrownfield.xcframework to make it interface-only.
249
293
  // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
@@ -315,10 +359,13 @@ export const packageIosCommand = curryOptions(
315
359
  );
316
360
  }
317
361
 
362
+ removeCodeSignatureArtifacts(packageDir);
363
+
318
364
  if (options.addSpmPackage) {
319
365
  const { packageManifestPath } = createLocalSpmPackage({
320
366
  packageDir,
321
367
  frameworkName: frameworkName ?? undefined,
368
+ usePrebuiltExpo: options.usePrebuiltExpo,
322
369
  });
323
370
 
324
371
  logger.success(
@@ -5,10 +5,15 @@ import {
5
5
  prepareLocalSpmArtifacts,
6
6
  SPM_ARTIFACTS_DIR_NAME,
7
7
  } from './prepareLocalSpmArtifacts.js';
8
+ import {
9
+ ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES,
10
+ getExpoSupportXcframeworkNames,
11
+ } from './emitExpoSupportXcframeworks.js';
8
12
 
9
13
  type CreateLocalSpmPackageOptions = {
10
14
  packageDir: string;
11
15
  frameworkName?: string;
16
+ usePrebuiltExpo?: boolean;
12
17
  };
13
18
 
14
19
  type CreateLocalSpmPackageResult = {
@@ -23,6 +28,7 @@ const RESERVED_FRAMEWORK_NAMES = new Set([
23
28
  'BrownfieldNavigation',
24
29
  'React',
25
30
  'ReactNativeDependencies',
31
+ ...ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES,
26
32
  ]);
27
33
 
28
34
  function requireXcframework(packageDir: string, name: string) {
@@ -130,7 +136,9 @@ function renderReadme({
130
136
  libraryName: string;
131
137
  targetNames: string[];
132
138
  }) {
133
- const frameworks = targetNames.map((targetName) => `- \`${targetName}\``).join('\n');
139
+ const frameworks = targetNames
140
+ .map((targetName) => `- \`${targetName}\``)
141
+ .join('\n');
134
142
 
135
143
  return `# ${packageName}
136
144
 
@@ -167,17 +175,23 @@ This folder is generated by \`brownfield package:ios --add-spm-package\`. Re-run
167
175
  export function createLocalSpmPackage({
168
176
  packageDir,
169
177
  frameworkName,
178
+ usePrebuiltExpo = true,
170
179
  }: CreateLocalSpmPackageOptions): CreateLocalSpmPackageResult {
171
180
  const resolvedFrameworkName = resolveAppFrameworkName(
172
181
  packageDir,
173
182
  frameworkName
174
183
  );
184
+ const expoSupportXcframeworkNames =
185
+ getExpoSupportXcframeworkNames(usePrebuiltExpo);
175
186
  const targetNames = [
176
187
  resolvedFrameworkName,
177
188
  requireHermesXcframework(packageDir),
178
189
  requireXcframework(packageDir, 'ReactBrownfield'),
179
190
  optionalXcframework(packageDir, 'Brownie'),
180
191
  optionalXcframework(packageDir, 'BrownfieldNavigation'),
192
+ ...expoSupportXcframeworkNames.map((frameworkName) =>
193
+ optionalXcframework(packageDir, frameworkName)
194
+ ),
181
195
  optionalXcframework(packageDir, 'React'),
182
196
  optionalXcframework(packageDir, 'ReactNativeDependencies'),
183
197
  ].filter((targetName): targetName is string => targetName !== null);
@@ -0,0 +1,132 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { RockError } from '@rock-js/tools';
5
+
6
+ import { normalizeCopiedXcframework } from './normalizeCopiedXcframework.js';
7
+ import { getExpoSdkMajor, isExpoProject } from './project.js';
8
+
9
+ import { MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO } from './supportsPrebuiltExpo.js';
10
+
11
+ /**
12
+ * Swift support XCFrameworks
13
+ *
14
+ * These are required for the Expo Image module to work.
15
+ *
16
+ * TODO: This needs investigation and maybe a discussion with the Expo team.
17
+ */
18
+ const SWIFT_SUPPORT_XCFRAMEWORK_NAMES = [
19
+ 'SDWebImage',
20
+ 'SDWebImageSVGCoder',
21
+ 'SDWebImageWebPCoder',
22
+ 'SDWebImageAVIFCoder',
23
+ 'libavif',
24
+ ] as const;
25
+
26
+ export const EXPO_SUPPORT_XCFRAMEWORK_NAMES = [
27
+ 'ExpoModulesJSI',
28
+ 'ExpoFileSystem',
29
+ 'ExpoFont',
30
+ 'ExpoModulesCore',
31
+ 'ExpoImage',
32
+ 'ExpoModulesWorklets'
33
+ ] as const;
34
+
35
+ export const ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES = [
36
+ ...EXPO_SUPPORT_XCFRAMEWORK_NAMES,
37
+ ...SWIFT_SUPPORT_XCFRAMEWORK_NAMES,
38
+ ] as const;
39
+
40
+ export function getExpoSupportXcframeworkNames(usePrebuiltExpo: boolean) {
41
+ return usePrebuiltExpo
42
+ ? ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES
43
+ : (['ExpoModulesJSI'] as const);
44
+ }
45
+
46
+ type SwiftSupportXcframeworkName =
47
+ (typeof SWIFT_SUPPORT_XCFRAMEWORK_NAMES)[number];
48
+
49
+ type ExpoSupportXcframeworkName =
50
+ (typeof EXPO_SUPPORT_XCFRAMEWORK_NAMES)[number];
51
+
52
+ function resolveExpoFrameworkSourcePath(
53
+ projectRoot: string,
54
+ frameworkName: SwiftSupportXcframeworkName | ExpoSupportXcframeworkName
55
+ ) {
56
+ if (frameworkName === 'ExpoModulesJSI') {
57
+ return path.join(
58
+ projectRoot,
59
+ 'node_modules',
60
+ 'expo-modules-jsi',
61
+ 'apple',
62
+ 'Products',
63
+ 'ExpoModulesJSI.xcframework'
64
+ );
65
+ }
66
+
67
+ if (SWIFT_SUPPORT_XCFRAMEWORK_NAMES.includes(frameworkName as SwiftSupportXcframeworkName)) {
68
+ return path.join(
69
+ projectRoot,
70
+ 'ios',
71
+ 'Pods',
72
+ 'ExpoImage',
73
+ `${frameworkName}.xcframework`
74
+ );
75
+ }
76
+
77
+ if (EXPO_SUPPORT_XCFRAMEWORK_NAMES.includes(frameworkName as ExpoSupportXcframeworkName)) {
78
+ return path.join(
79
+ projectRoot,
80
+ 'ios',
81
+ 'Pods',
82
+ frameworkName,
83
+ `${frameworkName}.xcframework`
84
+ );
85
+ }
86
+
87
+ throw new RockError(`Unsupported Expo XCFramework: ${frameworkName}`);
88
+ }
89
+
90
+ export function emitExpoSupportXcframeworks({
91
+ projectRoot,
92
+ packageDir,
93
+ usePrebuiltExpo = true,
94
+ }: {
95
+ projectRoot: string;
96
+ packageDir: string;
97
+ usePrebuiltExpo?: boolean;
98
+ }) {
99
+ if (!isExpoProject(projectRoot)) {
100
+ return false;
101
+ }
102
+
103
+ const expoSdkMajor = getExpoSdkMajor(projectRoot);
104
+ if (
105
+ expoSdkMajor === null ||
106
+ expoSdkMajor < MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO
107
+ ) {
108
+ return false;
109
+ }
110
+
111
+ for (const frameworkName of getExpoSupportXcframeworkNames(usePrebuiltExpo)) {
112
+ const sourcePath = resolveExpoFrameworkSourcePath(
113
+ projectRoot,
114
+ frameworkName
115
+ );
116
+ if (!fs.existsSync(sourcePath)) {
117
+ throw new RockError(
118
+ `Expected Expo SDK ${MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO}+ XCFramework not found: ${frameworkName}.xcframework at ${path.relative(projectRoot, sourcePath)}`
119
+ );
120
+ }
121
+
122
+ const destinationPath = path.join(
123
+ packageDir,
124
+ `${frameworkName}.xcframework`
125
+ );
126
+ fs.rmSync(destinationPath, { recursive: true, force: true });
127
+ fs.cpSync(sourcePath, destinationPath, { recursive: true });
128
+ normalizeCopiedXcframework(destinationPath);
129
+ }
130
+
131
+ return true;
132
+ }
@@ -0,0 +1,90 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+
5
+ export function removeCodeSignatureArtifacts(dirPath: string) {
6
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
7
+ const entryPath = path.join(dirPath, entry.name);
8
+
9
+ if (entry.isDirectory()) {
10
+ if (entry.name === '_CodeSignature') {
11
+ fs.rmSync(entryPath, { recursive: true, force: true });
12
+ continue;
13
+ }
14
+
15
+ removeCodeSignatureArtifacts(entryPath);
16
+ continue;
17
+ }
18
+
19
+ if (entry.name === 'CodeResources') {
20
+ fs.rmSync(entryPath, { force: true });
21
+ }
22
+ }
23
+ }
24
+
25
+ function resolveFrameworkExecutablePath(
26
+ frameworkDir: string,
27
+ frameworkName: string
28
+ ) {
29
+ const directExecutablePath = path.join(frameworkDir, frameworkName);
30
+ if (fs.existsSync(directExecutablePath)) {
31
+ return directExecutablePath;
32
+ }
33
+
34
+ const versionedExecutablePath = path.join(
35
+ frameworkDir,
36
+ 'Versions',
37
+ 'Current',
38
+ frameworkName
39
+ );
40
+ if (fs.existsSync(versionedExecutablePath)) {
41
+ return versionedExecutablePath;
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ function removeExecutableSignature(executablePath: string) {
48
+ try {
49
+ execFileSync('codesign', ['--remove-signature', executablePath], {
50
+ stdio: 'pipe',
51
+ });
52
+ } catch (error) {
53
+ if (
54
+ error instanceof Error &&
55
+ error.message.includes('code object is not signed at all')
56
+ ) {
57
+ return;
58
+ }
59
+
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ export function normalizeCopiedXcframework(xcframeworkPath: string) {
65
+ removeCodeSignatureArtifacts(xcframeworkPath);
66
+
67
+ for (const sliceName of fs.readdirSync(xcframeworkPath)) {
68
+ const slicePath = path.join(xcframeworkPath, sliceName);
69
+ if (!fs.statSync(slicePath).isDirectory()) {
70
+ continue;
71
+ }
72
+
73
+ for (const entry of fs.readdirSync(slicePath)) {
74
+ if (!entry.endsWith('.framework')) {
75
+ continue;
76
+ }
77
+
78
+ const frameworkName = path.basename(entry, '.framework');
79
+ const frameworkDir = path.join(slicePath, entry);
80
+ const executablePath = resolveFrameworkExecutablePath(
81
+ frameworkDir,
82
+ frameworkName
83
+ );
84
+
85
+ if (executablePath) {
86
+ removeExecutableSignature(executablePath);
87
+ }
88
+ }
89
+ }
90
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { execFileSync } from 'node:child_process';
3
+
4
+ import { normalizeCopiedXcframework } from './normalizeCopiedXcframework.js';
4
5
 
5
6
  type PrepareLocalSpmArtifactsOptions = {
6
7
  packageDir: string;
@@ -9,93 +10,6 @@ type PrepareLocalSpmArtifactsOptions = {
9
10
 
10
11
  export const SPM_ARTIFACTS_DIR_NAME = 'spm-artifacts';
11
12
 
12
- function removeCodeSignatureArtifacts(dirPath: string) {
13
- for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
14
- const entryPath = path.join(dirPath, entry.name);
15
-
16
- if (entry.isDirectory()) {
17
- if (entry.name === '_CodeSignature') {
18
- fs.rmSync(entryPath, { recursive: true, force: true });
19
- continue;
20
- }
21
-
22
- removeCodeSignatureArtifacts(entryPath);
23
- continue;
24
- }
25
-
26
- if (entry.name === 'CodeResources') {
27
- fs.rmSync(entryPath, { force: true });
28
- }
29
- }
30
- }
31
-
32
- function resolveFrameworkExecutablePath(
33
- frameworkDir: string,
34
- frameworkName: string
35
- ) {
36
- const directExecutablePath = path.join(frameworkDir, frameworkName);
37
- if (fs.existsSync(directExecutablePath)) {
38
- return directExecutablePath;
39
- }
40
-
41
- const versionedExecutablePath = path.join(
42
- frameworkDir,
43
- 'Versions',
44
- 'Current',
45
- frameworkName
46
- );
47
- if (fs.existsSync(versionedExecutablePath)) {
48
- return versionedExecutablePath;
49
- }
50
-
51
- return null;
52
- }
53
-
54
- function removeExecutableSignature(executablePath: string) {
55
- try {
56
- execFileSync('codesign', ['--remove-signature', executablePath], {
57
- stdio: 'pipe',
58
- });
59
- } catch (error) {
60
- if (
61
- error instanceof Error &&
62
- error.message.includes('code object is not signed at all')
63
- ) {
64
- return;
65
- }
66
-
67
- throw error;
68
- }
69
- }
70
-
71
- function normalizeCopiedXcframeworkSignature(xcframeworkPath: string) {
72
- removeCodeSignatureArtifacts(xcframeworkPath);
73
-
74
- for (const sliceName of fs.readdirSync(xcframeworkPath)) {
75
- const slicePath = path.join(xcframeworkPath, sliceName);
76
- if (!fs.statSync(slicePath).isDirectory()) {
77
- continue;
78
- }
79
-
80
- for (const entry of fs.readdirSync(slicePath)) {
81
- if (!entry.endsWith('.framework')) {
82
- continue;
83
- }
84
-
85
- const frameworkName = path.basename(entry, '.framework');
86
- const frameworkDir = path.join(slicePath, entry);
87
- const executablePath = resolveFrameworkExecutablePath(
88
- frameworkDir,
89
- frameworkName
90
- );
91
-
92
- if (executablePath) {
93
- removeExecutableSignature(executablePath);
94
- }
95
- }
96
- }
97
- }
98
-
99
13
  export function prepareLocalSpmArtifacts({
100
14
  packageDir,
101
15
  targetNames,
@@ -107,10 +21,13 @@ export function prepareLocalSpmArtifacts({
107
21
 
108
22
  for (const targetName of targetNames) {
109
23
  const sourcePath = path.join(packageDir, `${targetName}.xcframework`);
110
- const destinationPath = path.join(spmArtifactsDir, `${targetName}.xcframework`);
24
+ const destinationPath = path.join(
25
+ spmArtifactsDir,
26
+ `${targetName}.xcframework`
27
+ );
111
28
 
112
29
  fs.renameSync(sourcePath, destinationPath);
113
- normalizeCopiedXcframeworkSignature(destinationPath);
30
+ normalizeCopiedXcframework(destinationPath);
114
31
  }
115
32
 
116
33
  return spmArtifactsDir;
@@ -0,0 +1,34 @@
1
+ import { getExpoSdkMajor, isExpoProject } from './project.js';
2
+
3
+ export const MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO = 56;
4
+
5
+ export type PrebuiltExpoSupportResult =
6
+ | { supported: true; enabledByDefault: boolean; reason?: never }
7
+ | { supported: false; enabledByDefault?: never; reason: string };
8
+
9
+ export function supportsPrebuiltExpo({
10
+ projectRoot,
11
+ }: {
12
+ projectRoot: string;
13
+ }): PrebuiltExpoSupportResult {
14
+ if (!isExpoProject(projectRoot)) {
15
+ return { supported: true, enabledByDefault: false };
16
+ }
17
+
18
+ const expoSdkMajor = getExpoSdkMajor(projectRoot);
19
+ if (
20
+ expoSdkMajor === null ||
21
+ expoSdkMajor < MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO
22
+ ) {
23
+ const sdkLabel = expoSdkMajor === null ? 'unknown' : String(expoSdkMajor);
24
+ return {
25
+ supported: false,
26
+ reason: `--use-prebuilt-expo is unsupported in Expo SDK ${sdkLabel}: packaging brownfield with Expo prebuilts requires Expo SDK ${MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO} or newer.`,
27
+ };
28
+ }
29
+
30
+ return {
31
+ supported: true,
32
+ enabledByDefault: true,
33
+ };
34
+ }
package/src/types.ts CHANGED
@@ -33,6 +33,9 @@ export type PackageIosOptions = Omit<AppleBuildFlags, 'archive' | 'local'> & {
33
33
  /** Set when `--use-prebuilt-rn-core` is passed; omitted when the flag is absent (Rock applies RN version defaults). */
34
34
  usePrebuiltRnCore?: boolean;
35
35
 
36
+ /** Set when `--use-prebuilt-expo` is passed; omitted when the flag is absent (Rock applies Expo version defaults). */
37
+ usePrebuiltExpo?: boolean;
38
+
36
39
  /** When set, generate a local Swift Package Manager manifest next to the packaged XCFramework outputs. */
37
40
  addSpmPackage?: boolean;
38
41
  };