@callstack/brownfield-cli 3.13.1 → 3.14.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 (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/brownfield/commands/packageAndroid.d.ts.map +1 -1
  3. package/dist/brownfield/commands/packageAndroid.js +3 -1
  4. package/dist/brownfield/commands/packageIos.d.ts +1 -0
  5. package/dist/brownfield/commands/packageIos.d.ts.map +1 -1
  6. package/dist/brownfield/commands/packageIos.js +37 -4
  7. package/dist/brownfield/commands/publishAndroid.d.ts.map +1 -1
  8. package/dist/brownfield/commands/publishAndroid.js +3 -1
  9. package/dist/brownfield/utils/createLocalSpmPackage.d.ts +2 -1
  10. package/dist/brownfield/utils/createLocalSpmPackage.d.ts.map +1 -1
  11. package/dist/brownfield/utils/createLocalSpmPackage.js +8 -2
  12. package/dist/brownfield/utils/emitExpoSupportXcframeworks.d.ts +9 -0
  13. package/dist/brownfield/utils/emitExpoSupportXcframeworks.d.ts.map +1 -0
  14. package/dist/brownfield/utils/emitExpoSupportXcframeworks.js +70 -0
  15. package/dist/brownfield/utils/normalizeCopiedXcframework.d.ts +3 -0
  16. package/dist/brownfield/utils/normalizeCopiedXcframework.d.ts.map +1 -0
  17. package/dist/brownfield/utils/normalizeCopiedXcframework.js +64 -0
  18. package/dist/brownfield/utils/prepareLocalSpmArtifacts.d.ts.map +1 -1
  19. package/dist/brownfield/utils/prepareLocalSpmArtifacts.js +2 -63
  20. package/dist/brownfield/utils/project.d.ts.map +1 -1
  21. package/dist/brownfield/utils/project.js +20 -9
  22. package/dist/brownfield/utils/supportsPrebuiltExpo.d.ts +14 -0
  23. package/dist/brownfield/utils/supportsPrebuiltExpo.d.ts.map +1 -0
  24. package/dist/brownfield/utils/supportsPrebuiltExpo.js +20 -0
  25. package/dist/brownie/config.js +1 -1
  26. package/dist/config.d.ts +4 -1
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +10 -7
  29. package/dist/expoPluginConfig.d.ts +74 -0
  30. package/dist/expoPluginConfig.d.ts.map +1 -0
  31. package/dist/expoPluginConfig.js +103 -0
  32. package/dist/types.d.ts +94 -3
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +12 -6
  35. package/schema.json +98 -9
  36. package/src/brownfield/commands/packageAndroid.ts +4 -1
  37. package/src/brownfield/commands/packageIos.ts +53 -4
  38. package/src/brownfield/commands/publishAndroid.ts +4 -1
  39. package/src/brownfield/utils/createLocalSpmPackage.ts +15 -1
  40. package/src/brownfield/utils/emitExpoSupportXcframeworks.ts +132 -0
  41. package/src/brownfield/utils/normalizeCopiedXcframework.ts +90 -0
  42. package/src/brownfield/utils/prepareLocalSpmArtifacts.ts +7 -90
  43. package/src/brownfield/utils/project.ts +33 -14
  44. package/src/brownfield/utils/supportsPrebuiltExpo.ts +34 -0
  45. package/src/brownie/config.ts +1 -1
  46. package/src/config.ts +12 -8
  47. package/src/expoPluginConfig.ts +195 -0
  48. package/src/types.ts +112 -3
@@ -17,7 +17,9 @@ import {
17
17
  import { Command, Option } from 'commander';
18
18
 
19
19
  import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
20
+ import { findProjectRoot } from '../utils/paths.js';
20
21
  import { getProjectInfo } from '../utils/project.js';
22
+ import { supportsPrebuiltExpo } from '../utils/supportsPrebuiltExpo.js';
21
23
  import { supportsPrebuiltRNCore } from '../utils/supportsPrebuiltRNCore.js';
22
24
  import {
23
25
  actionRunner,
@@ -31,7 +33,9 @@ import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkN
31
33
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
32
34
  import type { PackageIosOptions } from '../../types.js';
33
35
  import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js';
36
+ import { emitExpoSupportXcframeworks } from '../utils/emitExpoSupportXcframeworks.js';
34
37
  import { mergeBrownfieldConfigWithOptions } from '../../config.js';
38
+ import { removeCodeSignatureArtifacts } from '../utils/normalizeCopiedXcframework.js';
35
39
 
36
40
  /** Help text for `--use-prebuilt-rn-core` (keep in sync with docs/docs/docs/getting-started/ios.mdx, "React Native Prebuilts" section). */
37
41
  const USE_PREBUILT_RN_CORE_HELP =
@@ -40,7 +44,14 @@ const USE_PREBUILT_RN_CORE_HELP =
40
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`). ' +
41
45
  'See the Brownfield iOS guide for details: https://oss.callstack.com/react-native-brownfield/docs/getting-started/ios#react-native-prebuilts';
42
46
 
43
- 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,
44
55
  value: string | boolean
45
56
  ): boolean {
46
57
  if (typeof value === 'boolean') {
@@ -54,10 +65,22 @@ export function parseUsePrebuiltRnCoreArgument(
54
65
  return false;
55
66
  }
56
67
  throw new RockError(
57
- `Invalid value for --use-prebuilt-rn-core: expected true or false, received "${value}"`
68
+ `Invalid value for ${flagName}: expected true or false, received "${value}"`
58
69
  );
59
70
  }
60
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
+
61
84
  function getPackagedFrameworkResolutionFailureMessage({
62
85
  resolution,
63
86
  candidates,
@@ -88,6 +111,11 @@ export const packageIosCommand = curryOptions(
88
111
  .preset(true)
89
112
  .argParser(parseUsePrebuiltRnCoreArgument)
90
113
  )
114
+ .addOption(
115
+ new Option('--use-prebuilt-expo [bool]', USE_PREBUILT_EXPO_HELP)
116
+ .preset(true)
117
+ .argParser(parseUsePrebuiltExpoArgument)
118
+ )
91
119
  .addOption(
92
120
  new Option(
93
121
  '--add-spm-package',
@@ -97,16 +125,25 @@ export const packageIosCommand = curryOptions(
97
125
  .action(
98
126
  actionRunner(async (cliOptions: PackageIosOptions) => {
99
127
  const options = mergeBrownfieldConfigWithOptions(cliOptions, 'ios');
128
+ const projectRoot = findProjectRoot();
129
+
130
+ await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
100
131
 
101
- const { projectRoot, platformConfig, userConfig } = getProjectInfo('ios');
132
+ const { platformConfig, userConfig } = getProjectInfo('ios');
102
133
 
103
134
  const prebuiltRNCoreSupport = supportsPrebuiltRNCore({ projectRoot });
135
+ const prebuiltExpoSupport = supportsPrebuiltExpo({ projectRoot });
104
136
 
105
137
  // version-aware default when the flag is omitted (see ios.mdx "React Native Prebuilts")
106
138
  options.usePrebuiltRnCore ??= prebuiltRNCoreSupport.supported
107
139
  ? prebuiltRNCoreSupport.enabledByDefault
108
140
  : false;
109
141
 
142
+ // version-aware default when the flag is omitted
143
+ options.usePrebuiltExpo ??= prebuiltExpoSupport.supported
144
+ ? prebuiltExpoSupport.enabledByDefault
145
+ : false;
146
+
110
147
  if (prebuiltRNCoreSupport) {
111
148
  logger.info(
112
149
  `${options.usePrebuiltRnCore ? 'Using' : 'Not using'} prebuilt RN core`
@@ -126,7 +163,9 @@ export const packageIosCommand = curryOptions(
126
163
  throw new RockError(prebuiltRNCoreSupport.reason);
127
164
  }
128
165
 
129
- await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
166
+ if (options.usePrebuiltExpo && !prebuiltExpoSupport.supported) {
167
+ throw new RockError(prebuiltExpoSupport.reason);
168
+ }
130
169
 
131
170
  if (!userConfig.project.ios) {
132
171
  throw new Error('iOS project not found.');
@@ -182,6 +221,12 @@ export const packageIosCommand = curryOptions(
182
221
  platformConfig
183
222
  );
184
223
 
224
+ emitExpoSupportXcframeworks({
225
+ projectRoot,
226
+ packageDir,
227
+ usePrebuiltExpo: options.usePrebuiltExpo,
228
+ });
229
+
185
230
  const productsPath = path.join(options.buildFolder, 'Build', 'Products');
186
231
  const { frameworkName, resolution, candidates } =
187
232
  resolvePackagedFrameworkName({
@@ -242,6 +287,7 @@ export const packageIosCommand = curryOptions(
242
287
  packageDir,
243
288
  'ReactBrownfield.xcframework'
244
289
  );
290
+
245
291
  if (fs.existsSync(reactBrownfieldXcframeworkPath)) {
246
292
  // Strip the binary from ReactBrownfield.xcframework to make it interface-only.
247
293
  // This avoids duplicate symbols when consumer apps embed both BrownfieldLib
@@ -313,10 +359,13 @@ export const packageIosCommand = curryOptions(
313
359
  );
314
360
  }
315
361
 
362
+ removeCodeSignatureArtifacts(packageDir);
363
+
316
364
  if (options.addSpmPackage) {
317
365
  const { packageManifestPath } = createLocalSpmPackage({
318
366
  packageDir,
319
367
  frameworkName: frameworkName ?? undefined,
368
+ usePrebuiltExpo: options.usePrebuiltExpo,
320
369
  });
321
370
 
322
371
  logger.success(
@@ -12,6 +12,7 @@ import {
12
12
  ExampleUsage,
13
13
  } from '../../shared/index.js';
14
14
  import { getProjectInfo } from '../utils/project.js';
15
+ import { findProjectRoot } from '../utils/paths.js';
15
16
  import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
16
17
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
17
18
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
@@ -25,13 +26,15 @@ export const publishAndroidCommand = curryOptions(
25
26
  ).action(
26
27
  actionRunner(async (cliOptions: PublishLocalAarFlags) => {
27
28
  const options = mergeBrownfieldConfigWithOptions(cliOptions, 'android');
29
+ const projectRoot = findProjectRoot();
28
30
 
29
- const { projectRoot, platformConfig } = getProjectInfo('android');
30
31
  await runExpoPrebuildIfNeeded({
31
32
  projectRoot,
32
33
  platform: 'android',
33
34
  });
34
35
 
36
+ const { platformConfig } = getProjectInfo('android');
37
+
35
38
  await runBrownieCodegenIfApplicable(projectRoot, 'kotlin');
36
39
  await runNavigationCodegenIfApplicable(projectRoot);
37
40
 
@@ -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;
@@ -21,15 +21,46 @@ const cliConfig: typeof cliConfigImport =
21
21
  : // @ts-expect-error: interop default
22
22
  cliConfigImport.default;
23
23
 
24
+ function hasExpoAppConfig(projectRoot: string): boolean {
25
+ return (
26
+ fs.existsSync(path.join(projectRoot, 'app.json')) ||
27
+ fs.existsSync(path.join(projectRoot, 'app.config.js')) ||
28
+ fs.existsSync(path.join(projectRoot, 'app.config.ts')) ||
29
+ fs.existsSync(path.join(projectRoot, 'app.config.mjs'))
30
+ );
31
+ }
32
+
33
+ function projectDependsOnExpo(projectRoot: string): boolean {
34
+ const packageJsonPath = path.join(projectRoot, 'package.json');
35
+
36
+ if (!fs.existsSync(packageJsonPath)) {
37
+ return false;
38
+ }
39
+
40
+ const packageJson = JSON.parse(
41
+ fs.readFileSync(packageJsonPath, 'utf-8')
42
+ ) as Record<string, Record<string, string>>;
43
+
44
+ return ['dependencies', 'peerDependencies', 'devDependencies'].some(
45
+ (key) => packageJson[key]?.expo
46
+ );
47
+ }
48
+
24
49
  /**
25
50
  * Gets the Expo config if the project is an Expo project
26
51
  * @param projectRoot The project root path
27
52
  * @returns The Expo config if the project is an Expo project, null otherwise
28
53
  */
29
54
  export function getExpoConfigIfIsExpo(projectRoot: string) {
55
+ const hasAppConfig = hasExpoAppConfig(projectRoot);
56
+
30
57
  try {
31
58
  return getConfig(projectRoot, { skipSDKVersionRequirement: true });
32
- } catch {
59
+ } catch (error) {
60
+ if (hasAppConfig) {
61
+ throw error;
62
+ }
63
+
33
64
  return null;
34
65
  }
35
66
  }
@@ -41,19 +72,7 @@ export function getExpoConfigIfIsExpo(projectRoot: string) {
41
72
  * @returns Whether the project is an Expo project
42
73
  */
43
74
  export function isExpoProject(projectRoot: string): boolean {
44
- const hasExpoConfig = getExpoConfigIfIsExpo(projectRoot) !== null;
45
-
46
- // additionally, it is needed to check if the project depends on Expo packages explicitly
47
- // to prevent false positives in a monorepo setup
48
- const rnProjectRoot = findProjectRoot();
49
- const packageJsonPath = path.join(rnProjectRoot, 'package.json');
50
- const dependsOnExpo =
51
- fs.existsSync(packageJsonPath) &&
52
- ['dependencies', 'peerDependencies', 'devDependencies'].some(
53
- (key) => JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))[key]?.expo
54
- );
55
-
56
- return hasExpoConfig && dependsOnExpo;
75
+ return hasExpoAppConfig(projectRoot) && projectDependsOnExpo(projectRoot);
57
76
  }
58
77
 
59
78
  export function getExpoSdkMajor(projectRoot: string): number | null {
@@ -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
+ }
@@ -108,7 +108,7 @@ export function resolveBrownieCodegenConfig({
108
108
  const legacyConfig = hasLegacyConfig(projectRoot)
109
109
  ? loadConfig(projectRoot)
110
110
  : undefined;
111
- const brownfieldBrownie = loadBrownfieldConfig(projectRoot).brownie;
111
+ const brownfieldBrownie = loadBrownfieldConfig(projectRoot)?.brownie;
112
112
 
113
113
  if (legacyConfig !== undefined && (brownie ?? brownfieldBrownie)) {
114
114
  throw new Error(LEGACY_AND_NEW_BROWNIE_CONFIG_ERROR);