@callstack/brownfield-cli 5.3.0 → 6.0.1

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 (37) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/brownfield/commands/packageIos.d.ts +2 -0
  3. package/dist/brownfield/commands/packageIos.d.ts.map +1 -1
  4. package/dist/brownfield/commands/packageIos.js +102 -18
  5. package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts +4 -1
  6. package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.d.ts.map +1 -1
  7. package/dist/brownfield/utils/copyDebugBundleToSimulatorSlice.js +6 -1
  8. package/dist/brownfield/utils/destinationSdks.d.ts +36 -0
  9. package/dist/brownfield/utils/destinationSdks.d.ts.map +1 -0
  10. package/dist/brownfield/utils/destinationSdks.js +70 -0
  11. package/dist/brownfield/utils/emitExpoSupportXcframeworks.d.ts +9 -1
  12. package/dist/brownfield/utils/emitExpoSupportXcframeworks.d.ts.map +1 -1
  13. package/dist/brownfield/utils/emitExpoSupportXcframeworks.js +27 -5
  14. package/dist/brownfield/utils/expo.d.ts +7 -0
  15. package/dist/brownfield/utils/expo.d.ts.map +1 -1
  16. package/dist/brownfield/utils/expo.js +28 -1
  17. package/dist/brownfield/utils/project.d.ts +14 -0
  18. package/dist/brownfield/utils/project.d.ts.map +1 -1
  19. package/dist/brownfield/utils/project.js +47 -0
  20. package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts +4 -1
  21. package/dist/brownfield/utils/resolvePackagedFrameworkName.d.ts.map +1 -1
  22. package/dist/brownfield/utils/resolvePackagedFrameworkName.js +7 -3
  23. package/dist/brownfield/utils/supportsPrebuiltExpo.d.ts +20 -0
  24. package/dist/brownfield/utils/supportsPrebuiltExpo.d.ts.map +1 -1
  25. package/dist/brownfield/utils/supportsPrebuiltExpo.js +68 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +10 -0
  28. package/package.json +6 -6
  29. package/src/brownfield/commands/packageIos.ts +143 -49
  30. package/src/brownfield/utils/copyDebugBundleToSimulatorSlice.ts +10 -0
  31. package/src/brownfield/utils/destinationSdks.ts +122 -0
  32. package/src/brownfield/utils/emitExpoSupportXcframeworks.ts +48 -4
  33. package/src/brownfield/utils/expo.ts +38 -1
  34. package/src/brownfield/utils/project.ts +65 -0
  35. package/src/brownfield/utils/resolvePackagedFrameworkName.ts +16 -5
  36. package/src/brownfield/utils/supportsPrebuiltExpo.ts +98 -2
  37. package/src/index.ts +12 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @callstack/brownfield-cli
2
2
 
3
+ ## 6.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#487](https://github.com/callstack/react-native-brownfield/pull/487) [`79817d1`](https://github.com/callstack/react-native-brownfield/commit/79817d1a21b0cb28ae63d41e935c57af8e7db39f) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - Support Expo modules in React Native CLI apps. `brownfield package:*` no longer runs `expo prebuild` on projects with committed `android/` and `ios/` directories, which used to delete the brownfield library module. Prebuild now runs only when the native directory is missing or gitignored. If you commit the generated directories of an Expo app, run `npx expo prebuild` yourself after changing `app.json` or config plugins. `create-react-native-brownfield` now detects Expo and sets up the iOS framework to use the Expo runtime. On a re-run after adding Expo, it also raises an existing framework target to iOS 16.4.
8
+
9
+ `--use-prebuilt-expo` now also turns Expo's precompiled modules on or off for `pod install`, and turns on Swift explicit modules so packaging works in projects that still carry the old `SWIFT_ENABLE_EXPLICIT_MODULES = NO` workaround. It defaults to building Expo from source when the app's React Native version doesn't match the one Expo's precompiled modules were built for.
10
+
11
+ `package:ios` now copies the expo-image dependency XCFrameworks (SDWebImage and libavif) as real files instead of symlinks into `node_modules`, so the package works when moved to another machine.
12
+
13
+ Re-running `create-react-native-brownfield` after `pod install` no longer adds a second Sources build phase to the framework target, which made the next `pod install` fail.
14
+
15
+ The `package:ios` script that `create-react-native-brownfield` adds now passes `--add-spm-package`, so the iOS app adds one local Swift package instead of every XCFramework. Existing scripts aren't changed.
16
+
17
+ - [#485](https://github.com/callstack/react-native-brownfield/pull/485) [`69fe3e3`](https://github.com/callstack/react-native-brownfield/commit/69fe3e3f05a49036cae1f3564a47ef3eed186155) Thanks [@hurali97](https://github.com/hurali97)! - respect `--destination` in `package:ios` when merging XCFrameworks
18
+
19
+ ## 6.0.0
20
+
3
21
  ## 5.3.0
4
22
 
5
23
  ## 5.2.0
@@ -3,5 +3,7 @@ import { ExampleUsage } from '../../shared/index.js';
3
3
  export declare function parseUsePrebuiltRnCoreArgument(value: string | boolean): boolean;
4
4
  export declare function parseUsePrebuiltExpoArgument(value: string | boolean): boolean;
5
5
  export declare const packageIosCommand: Command;
6
+ /** Whether `--use-prebuilt-expo` was chosen (CLI or brownfield.config.json) rather than inferred. */
7
+ export declare function isUsePrebuiltExpoExplicit(value: boolean | undefined): value is boolean;
6
8
  export declare const packageIosExample: ExampleUsage;
7
9
  //# 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;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"}
1
+ {"version":3,"file":"packageIos.d.ts","sourceRoot":"","sources":["../../../src/brownfield/commands/packageIos.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAU5C,OAAO,EAGL,YAAY,EACb,MAAM,uBAAuB,CAAC;AAgD/B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GAAG,OAAO,GACtB,OAAO,CAET;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAE7E;AAcD,eAAO,MAAM,iBAAiB,SAkX3B,CAAC;AAEJ,qGAAqG;AACrG,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,oBAEnE;AAED,eAAO,MAAM,iBAAiB,cAG7B,CAAC"}
@@ -6,13 +6,14 @@ import { RockError, colorLink, getReactNativeVersion, logger, relativeToCwd, } f
6
6
  import { Command, Option } from 'commander';
7
7
  import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
8
8
  import { findProjectRoot } from '../utils/paths.js';
9
- import { getProjectInfo } from '../utils/project.js';
10
- import { supportsPrebuiltExpo } from '../utils/supportsPrebuiltExpo.js';
9
+ import { getProjectInfo, isExpoProject } from '../utils/project.js';
10
+ import { isPrecompiledExpoDisabledByPodfile, supportsPrebuiltExpo, } from '../utils/supportsPrebuiltExpo.js';
11
11
  import { supportsPrebuiltRNCore } from '../utils/supportsPrebuiltRNCore.js';
12
12
  import { actionRunner, curryOptions, ExampleUsage, } from '../../shared/index.js';
13
13
  import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
14
14
  import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
15
15
  import { copyDebugBundleToSimulatorSlice } from '../utils/copyDebugBundleToSimulatorSlice.js';
16
+ import { collectFrameworkPaths, resolveDestinationSdks, } from '../utils/destinationSdks.js';
16
17
  import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkName.js';
17
18
  import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
18
19
  import { createLocalSpmPackage } from '../utils/createLocalSpmPackage.js';
@@ -68,6 +69,9 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
68
69
  .addOption(new Option('--add-spm-package', 'Generate a local Swift Package Manager manifest next to the packaged XCFramework outputs'))
69
70
  .action(actionRunner(async (cliOptions) => {
70
71
  const options = mergeBrownfieldConfigWithOptions(cliOptions, 'ios');
72
+ // captured before the version-aware default below, so that a value from
73
+ // brownfield.config.json counts as explicit
74
+ const usePrebuiltExpoExplicit = isUsePrebuiltExpoExplicit(options.usePrebuiltExpo);
71
75
  const projectRoot = findProjectRoot();
72
76
  await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
73
77
  const { platformConfig, userConfig } = getProjectInfo('ios');
@@ -77,10 +81,46 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
77
81
  options.usePrebuiltRnCore ??= prebuiltRNCoreSupport.supported
78
82
  ? prebuiltRNCoreSupport.enabledByDefault
79
83
  : false;
80
- // version-aware default when the flag is omitted
81
- options.usePrebuiltExpo ??= prebuiltExpoSupport.supported
82
- ? prebuiltExpoSupport.enabledByDefault
83
- : false;
84
+ const usePrebuiltExpoWasPassed = options.usePrebuiltExpo !== undefined;
85
+ const precompiledExpoDisabledByPodfile = isExpoProject(projectRoot) &&
86
+ isPrecompiledExpoDisabledByPodfile(projectRoot);
87
+ if (precompiledExpoDisabledByPodfile) {
88
+ if (options.usePrebuiltExpo) {
89
+ logger.warn('ios/Podfile.properties.json sets EXPO_USE_PRECOMPILED_MODULES to "false", so pod install builds Expo modules from source and --use-prebuilt-expo will fail. Remove that property or pass --use-prebuilt-expo false.');
90
+ }
91
+ else if (!usePrebuiltExpoWasPassed) {
92
+ logger.info('ios/Podfile.properties.json turns Expo precompiled modules off. Building Expo modules from source.');
93
+ options.usePrebuiltExpo = false;
94
+ }
95
+ }
96
+ else if (!usePrebuiltExpoWasPassed &&
97
+ isExpoProject(projectRoot) &&
98
+ process.env.EXPO_USE_PRECOMPILED_MODULES === '0') {
99
+ // Respect an exported opt-out, the same way Expo's Podfile does.
100
+ logger.info('EXPO_USE_PRECOMPILED_MODULES is set to 0. Building Expo modules from source.');
101
+ options.usePrebuiltExpo = false;
102
+ }
103
+ const usePrebuiltExpoWasChosen = usePrebuiltExpoWasPassed || options.usePrebuiltExpo !== undefined;
104
+ // version-aware default when the flag is omitted; Expo's precompiled
105
+ // modules also need the prebuilt React Native core
106
+ if (options.usePrebuiltExpo === undefined) {
107
+ options.usePrebuiltExpo = prebuiltExpoSupport.supported
108
+ ? prebuiltExpoSupport.enabledByDefault && !!options.usePrebuiltRnCore
109
+ : false;
110
+ if (prebuiltExpoSupport.enabledByDefault &&
111
+ !options.usePrebuiltRnCore) {
112
+ logger.info('Expo precompiled modules need the prebuilt React Native core, which is off. Building Expo modules from source.');
113
+ }
114
+ }
115
+ if (prebuiltExpoSupport.supported &&
116
+ prebuiltExpoSupport.defaultDisabledReason) {
117
+ if (usePrebuiltExpoWasChosen && options.usePrebuiltExpo) {
118
+ logger.warn(`${prebuiltExpoSupport.defaultDisabledReason} The packaged app will likely crash at runtime. Pass --use-prebuilt-expo false to build Expo modules from source.`);
119
+ }
120
+ else if (!usePrebuiltExpoWasChosen) {
121
+ logger.info(`${prebuiltExpoSupport.defaultDisabledReason} Building Expo modules from source.`);
122
+ }
123
+ }
84
124
  if (prebuiltRNCoreSupport) {
85
125
  logger.info(`${options.usePrebuiltRnCore ? 'Using' : 'Not using'} prebuilt RN core`);
86
126
  if (!prebuiltRNCoreSupport.enabledByDefault &&
@@ -94,6 +134,25 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
94
134
  if (options.usePrebuiltExpo && !prebuiltExpoSupport.supported) {
95
135
  throw new RockError(prebuiltExpoSupport.reason);
96
136
  }
137
+ if (isExpoProject(projectRoot)) {
138
+ // Read by Expo's autolinking during `pod install`. Expo-generated
139
+ // Podfiles default it to on, but a React Native CLI app's Podfile
140
+ // never sets it, so the flag has to drive it.
141
+ process.env.EXPO_USE_PRECOMPILED_MODULES = options.usePrebuiltExpo
142
+ ? '1'
143
+ : '0';
144
+ }
145
+ if (options.usePrebuiltExpo &&
146
+ !options.extraParams?.some((param) => param.startsWith('SWIFT_ENABLE_EXPLICIT_MODULES='))) {
147
+ // Precompiled Expo builds the React Native pods as static libraries.
148
+ // With explicit modules off (a leftover older React Native versions
149
+ // wrote into app projects), Xcode cannot find their module maps when
150
+ // verifying the framework's Swift interface.
151
+ options.extraParams = [
152
+ ...(options.extraParams ?? []),
153
+ 'SWIFT_ENABLE_EXPLICIT_MODULES=YES',
154
+ ];
155
+ }
97
156
  if (!userConfig.project.ios) {
98
157
  throw new Error('iOS project not found.');
99
158
  }
@@ -117,6 +176,9 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
117
176
  // Reference: https://github.com/facebook/react-native/blob/490c5e8dcc6cdb19c334cc39e93a39a48ba71e96/packages/react-native/scripts/cocoapods/new_architecture.rb#L171
118
177
  const packageDir = path.join(dotBrownfieldDir, 'package', 'build');
119
178
  const configuration = options.configuration ?? 'Debug';
179
+ // `--destination` narrows which slices Xcode emits, so every path below has to
180
+ // look at those slices only instead of assuming a device + simulator pair
181
+ const sdks = resolveDestinationSdks(options.destination);
120
182
  const { hasBrownie } = await runBrownieCodegenIfApplicable(projectRoot, 'swift');
121
183
  const { hasNavigation } = await runNavigationCodegenIfApplicable(projectRoot);
122
184
  await packageIosAction(options, {
@@ -134,12 +196,18 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
134
196
  projectRoot,
135
197
  packageDir,
136
198
  usePrebuiltExpo: options.usePrebuiltExpo,
199
+ usePrebuiltExpoExplicit,
200
+ // keep the generated SPM manifest consistent with the emitted XCFrameworks
201
+ onDegradeToSource: () => {
202
+ options.usePrebuiltExpo = false;
203
+ },
137
204
  });
138
205
  const productsPath = path.join(options.buildFolder, 'Build', 'Products');
139
206
  const { frameworkName, resolution, candidates } = resolvePackagedFrameworkName({
140
207
  explicitScheme: options.scheme,
141
208
  productsPath,
142
209
  configuration,
210
+ sdks,
143
211
  });
144
212
  if (!frameworkName && options.addSpmPackage) {
145
213
  throw new RockError(`Cannot generate local SPM package: ${getPackagedFrameworkResolutionFailureMessage({
@@ -152,15 +220,18 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
152
220
  productsPath,
153
221
  configuration,
154
222
  frameworkName,
223
+ sdks,
155
224
  });
156
225
  if (configuration.includes('Debug')) {
157
226
  // Re-merge only Debug frameworks so the simulator slice includes main.jsbundle.
158
227
  await mergeFrameworks({
159
228
  sourceDir: userConfig.project.ios.sourceDir,
160
- frameworkPaths: [
161
- path.join(productsPath, `${configuration}-iphoneos`, `${frameworkName}.framework`),
162
- path.join(productsPath, `${configuration}-iphonesimulator`, `${frameworkName}.framework`),
163
- ],
229
+ frameworkPaths: collectFrameworkPaths({
230
+ productsPath,
231
+ configuration,
232
+ sdks,
233
+ frameworkName,
234
+ }),
164
235
  outputPath: path.join(packageDir, `${frameworkName}.xcframework`),
165
236
  });
166
237
  }
@@ -182,10 +253,13 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
182
253
  const brownieOutputPath = path.join(packageDir, 'Brownie.xcframework');
183
254
  await mergeFrameworks({
184
255
  sourceDir: userConfig.project.ios.sourceDir,
185
- frameworkPaths: [
186
- path.join(productsPath, `${configuration}-iphoneos`, 'Brownie', 'Brownie.framework'),
187
- path.join(productsPath, `${configuration}-iphonesimulator`, 'Brownie', 'Brownie.framework'),
188
- ],
256
+ frameworkPaths: collectFrameworkPaths({
257
+ productsPath,
258
+ configuration,
259
+ sdks,
260
+ frameworkName: 'Brownie',
261
+ productSubDir: 'Brownie',
262
+ }),
189
263
  outputPath: brownieOutputPath,
190
264
  });
191
265
  // Strip the binary from Brownie.xcframework to make it interface-only.
@@ -198,10 +272,13 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
198
272
  const brownfieldNavigationOutputPath = path.join(packageDir, 'BrownfieldNavigation.xcframework');
199
273
  await mergeFrameworks({
200
274
  sourceDir: userConfig.project.ios.sourceDir,
201
- frameworkPaths: [
202
- path.join(productsPath, `${configuration}-iphoneos`, 'BrownfieldNavigation', 'BrownfieldNavigation.framework'),
203
- path.join(productsPath, `${configuration}-iphonesimulator`, 'BrownfieldNavigation', 'BrownfieldNavigation.framework'),
204
- ],
275
+ frameworkPaths: collectFrameworkPaths({
276
+ productsPath,
277
+ configuration,
278
+ sdks,
279
+ frameworkName: 'BrownfieldNavigation',
280
+ productSubDir: 'BrownfieldNavigation',
281
+ }),
205
282
  outputPath: brownfieldNavigationOutputPath,
206
283
  });
207
284
  stripFrameworkBinary(brownfieldNavigationOutputPath);
@@ -218,5 +295,12 @@ export const packageIosCommand = curryOptions(new Command('package:ios').descrip
218
295
  logger.info(`Add the local package folder in Xcode: ${colorLink(relativeToCwd(packageDir))}`);
219
296
  logger.info('In Xcode, choose File > Add Package Dependencies..., click Add Local..., and select that folder.');
220
297
  }
298
+ // The dependency prints its own "Success" before the post-build steps above,
299
+ // so log a final line marking the true end of the run.
300
+ logger.success(`package:ios finished. Artifacts are in ${colorLink(relativeToCwd(packageDir))}`);
221
301
  }));
302
+ /** Whether `--use-prebuilt-expo` was chosen (CLI or brownfield.config.json) rather than inferred. */
303
+ export function isUsePrebuiltExpoExplicit(value) {
304
+ return value !== undefined;
305
+ }
222
306
  export const packageIosExample = new ExampleUsage('package:ios --scheme BrownfieldLib --configuration Release', "Build iOS XCFramework for 'BrownfieldLib' scheme in Release configuration");
@@ -1,8 +1,11 @@
1
+ import { type AppleSdk } from './destinationSdks.js';
1
2
  interface CopyDebugBundleToSimulatorSliceOptions {
2
3
  productsPath: string;
3
4
  configuration: string;
4
5
  frameworkName: string;
6
+ /** Slices the build produced; defaults to both. */
7
+ sdks?: AppleSdk[];
5
8
  }
6
- export declare function copyDebugBundleToSimulatorSlice({ productsPath, configuration, frameworkName, }: CopyDebugBundleToSimulatorSliceOptions): void;
9
+ export declare function copyDebugBundleToSimulatorSlice({ productsPath, configuration, frameworkName, sdks, }: CopyDebugBundleToSimulatorSliceOptions): void;
7
10
  export {};
8
11
  //# sourceMappingURL=copyDebugBundleToSimulatorSlice.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"copyDebugBundleToSimulatorSlice.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/copyDebugBundleToSimulatorSlice.ts"],"names":[],"mappings":"AAKA,UAAU,sCAAsC;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,+BAA+B,CAAC,EAC9C,YAAY,EACZ,aAAa,EACb,aAAa,GACd,EAAE,sCAAsC,QA0CxC"}
1
+ {"version":3,"file":"copyDebugBundleToSimulatorSlice.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/copyDebugBundleToSimulatorSlice.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErE,UAAU,sCAAsC;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED,wBAAgB,+BAA+B,CAAC,EAC9C,YAAY,EACZ,aAAa,EACb,aAAa,EACb,IAAqB,GACtB,EAAE,sCAAsC,QA+CxC"}
@@ -1,10 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { colorLink, logger, relativeToCwd } from '@rock-js/tools';
4
- export function copyDebugBundleToSimulatorSlice({ productsPath, configuration, frameworkName, }) {
4
+ import { ALL_APPLE_SDKS } from './destinationSdks.js';
5
+ export function copyDebugBundleToSimulatorSlice({ productsPath, configuration, frameworkName, sdks = ALL_APPLE_SDKS, }) {
5
6
  if (!configuration.includes('Debug')) {
6
7
  return;
7
8
  }
9
+ // nothing to copy across when only one slice was built
10
+ if (!sdks.includes('iphoneos') || !sdks.includes('iphonesimulator')) {
11
+ return;
12
+ }
8
13
  const deviceBundlePath = path.join(productsPath, `${configuration}-iphoneos`, `${frameworkName}.framework`, 'main.jsbundle');
9
14
  const simulatorFrameworkPath = path.join(productsPath, `${configuration}-iphonesimulator`, `${frameworkName}.framework`);
10
15
  const simulatorBundlePath = path.join(simulatorFrameworkPath, 'main.jsbundle');
@@ -0,0 +1,36 @@
1
+ /** Xcode SDK a build product directory belongs to (`Release-iphoneos`, `Release-iphonesimulator`). */
2
+ export type AppleSdk = 'iphoneos' | 'iphonesimulator';
3
+ /** Both slices, in the order Rock's `packageIosAction` builds them. */
4
+ export declare const ALL_APPLE_SDKS: AppleSdk[];
5
+ /**
6
+ * SDK slices a `package:ios` run produces for the given `--destination` values.
7
+ *
8
+ * Omitting `--destination` builds both slices, matching Rock's default.
9
+ */
10
+ export declare function resolveDestinationSdks(destination?: string[] | null): AppleSdk[];
11
+ /**
12
+ * Whether `directoryPath` holds a build product `mergeFrameworks` can consume:
13
+ * either a real `.framework` bundle or the static library it synthesizes one from.
14
+ */
15
+ export declare function hasFrameworkBuildProduct(directoryPath: string, frameworkName: string): boolean;
16
+ interface CollectFrameworkPathsOptions {
17
+ productsPath: string;
18
+ configuration: string;
19
+ sdks: AppleSdk[];
20
+ frameworkName: string;
21
+ /** Sub-directory of the configuration products dir, e.g. `Brownie` for a Pod target. */
22
+ productSubDir?: string;
23
+ }
24
+ /**
25
+ * `.framework` paths to merge into an XCFramework, limited to the slices that were
26
+ * actually built. Slices with no build product on disk are dropped, so a
27
+ * `--destination simulator` run merges the simulator slice alone instead of failing
28
+ * on a missing `-iphoneos` directory.
29
+ *
30
+ * Throws when no slice has a build product: passing an empty list to
31
+ * `mergeFrameworks` would surface as xcodebuild's opaque
32
+ * "at least one framework or library must be specified".
33
+ */
34
+ export declare function collectFrameworkPaths({ productsPath, configuration, sdks, frameworkName, productSubDir, }: CollectFrameworkPathsOptions): string[];
35
+ export {};
36
+ //# sourceMappingURL=destinationSdks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"destinationSdks.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/destinationSdks.ts"],"names":[],"mappings":"AAKA,sGAAsG;AACtG,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,iBAAiB,CAAC;AAEtD,uEAAuE;AACvE,eAAO,MAAM,cAAc,EAAE,QAAQ,EAAoC,CAAC;AA4B1E;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,GAC5B,QAAQ,EAAE,CASZ;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,WAMtB;AAED,UAAU,4BAA4B;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,wFAAwF;IACxF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,aAAa,EACb,aAAa,GACd,EAAE,4BAA4B,GAAG,MAAM,EAAE,CA2BzC"}
@@ -0,0 +1,70 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { RockError } from '@rock-js/tools';
4
+ /** Both slices, in the order Rock's `packageIosAction` builds them. */
5
+ export const ALL_APPLE_SDKS = ['iphoneos', 'iphonesimulator'];
6
+ /**
7
+ * Map a single `--destination` value onto the SDK(s) its build products land in.
8
+ *
9
+ * Mirrors `resolveDestination` in `@rock-js/platform-apple-helpers`: the aliases
10
+ * `device`/`simulator` plus any raw `xcodebuild -destination` string. A value we
11
+ * cannot classify (`id=<udid>`, `name=iPhone 17`) may resolve to either SDK, so it
12
+ * widens to both rather than silently dropping a slice.
13
+ */
14
+ function resolveSdksForDestination(destination) {
15
+ const normalized = destination.trim().toLowerCase();
16
+ if (normalized === 'device') {
17
+ return ['iphoneos'];
18
+ }
19
+ if (normalized === 'simulator' || normalized.includes('simulator')) {
20
+ return ['iphonesimulator'];
21
+ }
22
+ if (normalized.includes('platform=ios')) {
23
+ return ['iphoneos'];
24
+ }
25
+ return ALL_APPLE_SDKS;
26
+ }
27
+ /**
28
+ * SDK slices a `package:ios` run produces for the given `--destination` values.
29
+ *
30
+ * Omitting `--destination` builds both slices, matching Rock's default.
31
+ */
32
+ export function resolveDestinationSdks(destination) {
33
+ if (!destination || destination.length === 0) {
34
+ return ALL_APPLE_SDKS;
35
+ }
36
+ const sdks = new Set(destination.flatMap(resolveSdksForDestination));
37
+ // keep a stable order regardless of the order the flags were passed in
38
+ return ALL_APPLE_SDKS.filter((sdk) => sdks.has(sdk));
39
+ }
40
+ /**
41
+ * Whether `directoryPath` holds a build product `mergeFrameworks` can consume:
42
+ * either a real `.framework` bundle or the static library it synthesizes one from.
43
+ */
44
+ export function hasFrameworkBuildProduct(directoryPath, frameworkName) {
45
+ return (fs.existsSync(path.join(directoryPath, `${frameworkName}.framework`)) ||
46
+ fs.existsSync(path.join(directoryPath, `lib${frameworkName}.a`)));
47
+ }
48
+ /**
49
+ * `.framework` paths to merge into an XCFramework, limited to the slices that were
50
+ * actually built. Slices with no build product on disk are dropped, so a
51
+ * `--destination simulator` run merges the simulator slice alone instead of failing
52
+ * on a missing `-iphoneos` directory.
53
+ *
54
+ * Throws when no slice has a build product: passing an empty list to
55
+ * `mergeFrameworks` would surface as xcodebuild's opaque
56
+ * "at least one framework or library must be specified".
57
+ */
58
+ export function collectFrameworkPaths({ productsPath, configuration, sdks, frameworkName, productSubDir, }) {
59
+ const searchedDirectories = sdks.map((sdk) => path.join(productsPath, `${configuration}-${sdk}`, ...(productSubDir ? [productSubDir] : [])));
60
+ const frameworkPaths = searchedDirectories
61
+ .filter((directoryPath) => hasFrameworkBuildProduct(directoryPath, frameworkName))
62
+ .map((directoryPath) => path.join(directoryPath, `${frameworkName}.framework`));
63
+ if (frameworkPaths.length === 0) {
64
+ throw new RockError(`Could not find a build product for ${frameworkName} in the ${configuration} configuration. ` +
65
+ `Looked for ${frameworkName}.framework or lib${frameworkName}.a in:\n` +
66
+ searchedDirectories.map((dir) => ` - ${dir}`).join('\n') +
67
+ `\nIf the build produced an .app instead of a framework, the wrong scheme was built; pass --scheme with your brownfield framework scheme.`);
68
+ }
69
+ return frameworkPaths;
70
+ }
@@ -1,9 +1,17 @@
1
1
  export declare const EXPO_SUPPORT_XCFRAMEWORK_NAMES: readonly ["ExpoModulesJSI", "ExpoFileSystem", "ExpoFont", "ExpoModulesCore", "ExpoImage", "ExpoModulesWorklets"];
2
2
  export declare const ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES: readonly ["ExpoModulesJSI", "ExpoFileSystem", "ExpoFont", "ExpoModulesCore", "ExpoImage", "ExpoModulesWorklets", "SDWebImage", "SDWebImageSVGCoder", "SDWebImageWebPCoder", "SDWebImageAVIFCoder", "libavif"];
3
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, }: {
4
+ export declare function emitExpoSupportXcframeworks({ projectRoot, packageDir, usePrebuiltExpo, usePrebuiltExpoExplicit, onDegradeToSource, }: {
5
5
  projectRoot: string;
6
6
  packageDir: string;
7
7
  usePrebuiltExpo?: boolean;
8
+ /**
9
+ * Whether `usePrebuiltExpo` was chosen by the user (CLI flag or brownfield.config.json)
10
+ * rather than inferred from the Expo SDK version. When inferred, missing prebuilts
11
+ * degrade to building Expo modules from source instead of throwing.
12
+ */
13
+ usePrebuiltExpoExplicit?: boolean;
14
+ /** Called when an inferred `usePrebuiltExpo` was degraded to `false` because prebuilts are absent */
15
+ onDegradeToSource?: () => void;
8
16
  }): boolean;
9
17
  //# sourceMappingURL=emitExpoSupportXcframeworks.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"emitExpoSupportXcframeworks.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/emitExpoSupportXcframeworks.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,8BAA8B,kHAOjC,CAAC;AAEX,eAAO,MAAM,kCAAkC,+MAGrC,CAAC;AAEX,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,OAAO,+OAItE;AAsDD,wBAAgB,2BAA2B,CAAC,EAC1C,WAAW,EACX,UAAU,EACV,eAAsB,EACtB,uBAA8B,EAC9B,iBAAiB,GAClB,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,qGAAqG;IACrG,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC,WAwDA"}
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { RockError } from '@rock-js/tools';
3
+ import { logger, RockError } from '@rock-js/tools';
4
4
  import { normalizeCopiedXcframework } from './normalizeCopiedXcframework.js';
5
- import { getExpoSdkMajor, isExpoProject } from './project.js';
5
+ import { getExpoIosUsePrecompiledModules, getExpoSdkMajor, isExpoProject, } from './project.js';
6
6
  import { MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO } from './supportsPrebuiltExpo.js';
7
7
  /**
8
8
  * Swift support XCFrameworks
@@ -47,7 +47,13 @@ function resolveExpoFrameworkSourcePath(projectRoot, frameworkName) {
47
47
  }
48
48
  throw new RockError(`Unsupported Expo XCFramework: ${frameworkName}`);
49
49
  }
50
- export function emitExpoSupportXcframeworks({ projectRoot, packageDir, usePrebuiltExpo = true, }) {
50
+ function getPrebuiltExpoRemedy(projectRoot) {
51
+ const cause = getExpoIosUsePrecompiledModules(projectRoot) === false
52
+ ? 'expo-build-properties sets ios.usePrecompiledModules to false, so prebuilt Expo XCFrameworks are never produced. '
53
+ : '';
54
+ return `${cause}Either re-run with --use-prebuilt-expo false to build Expo modules from source, or run \`pod install\` (with ios.usePrecompiledModules enabled) to materialize the prebuilts.`;
55
+ }
56
+ export function emitExpoSupportXcframeworks({ projectRoot, packageDir, usePrebuiltExpo = true, usePrebuiltExpoExplicit = true, onDegradeToSource, }) {
51
57
  if (!isExpoProject(projectRoot)) {
52
58
  return false;
53
59
  }
@@ -56,14 +62,30 @@ export function emitExpoSupportXcframeworks({ projectRoot, packageDir, usePrebui
56
62
  expoSdkMajor < MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO) {
57
63
  return false;
58
64
  }
65
+ if (usePrebuiltExpo && !usePrebuiltExpoExplicit) {
66
+ const missingPrebuilts = ALL_EXPO_SUPPORT_XCFRAMEWORK_NAMES.filter((frameworkName) => !fs.existsSync(resolveExpoFrameworkSourcePath(projectRoot, frameworkName)));
67
+ // ExpoModulesJSI ships in node_modules and is not a Pods prebuilt; only degrade
68
+ // when Pods-provided prebuilts are absent.
69
+ if (missingPrebuilts.some((name) => name !== 'ExpoModulesJSI')) {
70
+ logger.warn(`Prebuilt Expo XCFrameworks were not found (${missingPrebuilts.join(', ')}). ${getPrebuiltExpoRemedy(projectRoot)} Continuing without prebuilt Expo (--use-prebuilt-expo false); pass --use-prebuilt-expo true to make this an error.`);
71
+ usePrebuiltExpo = false;
72
+ onDegradeToSource?.();
73
+ }
74
+ }
59
75
  for (const frameworkName of getExpoSupportXcframeworkNames(usePrebuiltExpo)) {
60
76
  const sourcePath = resolveExpoFrameworkSourcePath(projectRoot, frameworkName);
61
77
  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)}`);
78
+ throw new RockError(`Expected Expo SDK ${MIN_EXPO_SDK_MAJOR_FOR_PREBUILT_EXPO}+ XCFramework not found: ${frameworkName}.xcframework at ${path.relative(projectRoot, sourcePath)}. ${getPrebuiltExpoRemedy(projectRoot)}`);
63
79
  }
64
80
  const destinationPath = path.join(packageDir, `${frameworkName}.xcframework`);
65
81
  fs.rmSync(destinationPath, { recursive: true, force: true });
66
- fs.cpSync(sourcePath, destinationPath, { recursive: true });
82
+ // With precompiled Expo, CocoaPods symlinks expo-image's dependencies into
83
+ // node_modules. Copy what the link points to, so the package is portable
84
+ // and normalizing it doesn't modify node_modules.
85
+ fs.cpSync(fs.realpathSync(sourcePath), destinationPath, {
86
+ recursive: true,
87
+ verbatimSymlinks: true,
88
+ });
67
89
  normalizeCopiedXcframework(destinationPath);
68
90
  }
69
91
  return true;
@@ -1,4 +1,11 @@
1
1
  type ExpoPlatform = 'ios' | 'android';
2
+ /**
3
+ * Whether the platform's native project is generated by `expo prebuild` (Continuous Native
4
+ * Generation) rather than maintained by hand. A bare React Native app that installed Expo modules
5
+ * commits its native directories; running prebuild there would wipe them (including the
6
+ * brownfield library module), so prebuild only runs when the directory is missing or gitignored.
7
+ */
8
+ export declare function usesContinuousNativeGeneration(projectRoot: string, platform: ExpoPlatform): boolean;
2
9
  export declare function runExpoPrebuildIfNeeded({ projectRoot, platform, }: {
3
10
  projectRoot: string;
4
11
  platform: ExpoPlatform;
@@ -1 +1 @@
1
- {"version":3,"file":"expo.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/expo.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG,KAAK,GAAG,SAAS,CAAC;AAqCtC,wBAAsB,uBAAuB,CAAC,EAC5C,WAAW,EACX,QAAQ,GACT,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,YAAY,CAAC;CACxB,oBAqBA"}
1
+ {"version":3,"file":"expo.d.ts","sourceRoot":"","sources":["../../../src/brownfield/utils/expo.ts"],"names":[],"mappings":"AAQA,KAAK,YAAY,GAAG,KAAK,GAAG,SAAS,CAAC;AAmDtC;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,YAAY,GACrB,OAAO,CAGT;AAED,wBAAsB,uBAAuB,CAAC,EAC5C,WAAW,EACX,QAAQ,GACT,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,YAAY,CAAC;CACxB,oBA4BA"}
@@ -1,4 +1,6 @@
1
- import { spawn } from 'node:child_process';
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
2
4
  import { logger } from '@rock-js/tools';
3
5
  import { isExpoProject } from './project.js';
4
6
  async function spawnCommand({ command, args, cwd, }) {
@@ -22,10 +24,35 @@ async function spawnCommand({ command, args, cwd, }) {
22
24
  function getNpxCommand() {
23
25
  return process.platform === 'win32' ? 'npx.cmd' : 'npx';
24
26
  }
27
+ function isGitIgnored(targetPath) {
28
+ // `git check-ignore` exits 0 when ignored, 1 when not, 128 outside a repo.
29
+ const result = spawnSync('git', ['check-ignore', '-q', targetPath], {
30
+ cwd: path.dirname(targetPath),
31
+ stdio: 'ignore',
32
+ });
33
+ if (result.status !== 0 && result.status !== 1) {
34
+ logger.warn(`Couldn't check with git whether ${path.basename(targetPath)}/ is gitignored, so it's treated as committed and expo prebuild is skipped. If this project uses Continuous Native Generation, run \`npx expo prebuild\` before packaging.`);
35
+ }
36
+ return result.status === 0;
37
+ }
38
+ /**
39
+ * Whether the platform's native project is generated by `expo prebuild` (Continuous Native
40
+ * Generation) rather than maintained by hand. A bare React Native app that installed Expo modules
41
+ * commits its native directories; running prebuild there would wipe them (including the
42
+ * brownfield library module), so prebuild only runs when the directory is missing or gitignored.
43
+ */
44
+ export function usesContinuousNativeGeneration(projectRoot, platform) {
45
+ const nativeDir = path.join(projectRoot, platform);
46
+ return !fs.existsSync(nativeDir) || isGitIgnored(nativeDir);
47
+ }
25
48
  export async function runExpoPrebuildIfNeeded({ projectRoot, platform, }) {
26
49
  if (!isExpoProject(projectRoot)) {
27
50
  return false;
28
51
  }
52
+ if (!usesContinuousNativeGeneration(projectRoot, platform)) {
53
+ logger.info(`Expo modules detected in a project with a committed ${platform} directory. Skipping expo prebuild.`);
54
+ return false;
55
+ }
29
56
  logger.info(`Expo project detected. Running expo prebuild for ${platform}...`);
30
57
  const args = ['expo', 'prebuild', '--platform', platform];
31
58
  if (platform === 'ios') {
@@ -14,6 +14,20 @@ 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
+ /**
18
+ * Reads `ios.usePrecompiledModules` from the `expo-build-properties` plugin entry.
19
+ *
20
+ * `getExpoConfigIfIsExpo` uses `skipPlugins: true`, which makes `@expo/config`
21
+ * delete `exp.plugins`, so the plugins array is read from `rootConfig` (the raw
22
+ * static app.json / app.config.json) instead. `rootConfig` is the raw *static*
23
+ * config only, so a dynamic config (app.config.js/ts) exposes no plugins that
24
+ * way; for those we retry once with plugins enabled, which keeps the plugins
25
+ * array. That retry can throw for plugins that are not loadable as Node modules
26
+ * (the reason `skipPlugins` is set in the first place), so it is best-effort.
27
+ *
28
+ * @returns the explicit boolean value, or `undefined` if it is not set or not resolvable
29
+ */
30
+ export declare function getExpoIosUsePrecompiledModules(projectRoot: string): boolean | undefined;
17
31
  export declare function getExpoSdkMajor(projectRoot: string): number | null;
18
32
  /**
19
33
  * Fills the RNC CLI project config from the Expo config by mutating the passed in `options.projectConfig` object in place
@@ -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;AAiCtB;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,4BAkBxD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE1D;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"}
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;AAiCtB;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,4BAkBxD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE1D;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,+BAA+B,CAC7C,WAAW,EAAE,MAAM,GAClB,OAAO,GAAG,SAAS,CA8CrB;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"}
@@ -53,6 +53,53 @@ export function getExpoConfigIfIsExpo(projectRoot) {
53
53
  export function isExpoProject(projectRoot) {
54
54
  return hasExpoAppConfig(projectRoot) && projectDependsOnExpo(projectRoot);
55
55
  }
56
+ /**
57
+ * Reads `ios.usePrecompiledModules` from the `expo-build-properties` plugin entry.
58
+ *
59
+ * `getExpoConfigIfIsExpo` uses `skipPlugins: true`, which makes `@expo/config`
60
+ * delete `exp.plugins`, so the plugins array is read from `rootConfig` (the raw
61
+ * static app.json / app.config.json) instead. `rootConfig` is the raw *static*
62
+ * config only, so a dynamic config (app.config.js/ts) exposes no plugins that
63
+ * way; for those we retry once with plugins enabled, which keeps the plugins
64
+ * array. That retry can throw for plugins that are not loadable as Node modules
65
+ * (the reason `skipPlugins` is set in the first place), so it is best-effort.
66
+ *
67
+ * @returns the explicit boolean value, or `undefined` if it is not set or not resolvable
68
+ */
69
+ export function getExpoIosUsePrecompiledModules(projectRoot) {
70
+ const config = getExpoConfigIfIsExpo(projectRoot);
71
+ if (!config) {
72
+ return undefined;
73
+ }
74
+ const rootConfig = config.rootConfig;
75
+ let plugins = config.exp.plugins ??
76
+ rootConfig?.expo?.plugins ??
77
+ rootConfig?.plugins;
78
+ if (!Array.isArray(plugins)) {
79
+ // Dynamic config: no static plugins array exists. Retry with plugins
80
+ // enabled, which preserves `exp.plugins`. Best-effort — plugin resolution
81
+ // can throw, and an unreadable config is treated the same as an unset value.
82
+ try {
83
+ plugins = getConfig(projectRoot, { skipSDKVersionRequirement: true }).exp.plugins;
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ if (!Array.isArray(plugins)) {
90
+ return undefined;
91
+ }
92
+ for (const plugin of plugins) {
93
+ if (!Array.isArray(plugin) || plugin[0] !== 'expo-build-properties') {
94
+ continue;
95
+ }
96
+ const value = plugin[1]?.ios?.usePrecompiledModules;
97
+ if (typeof value === 'boolean') {
98
+ return value;
99
+ }
100
+ }
101
+ return undefined;
102
+ }
56
103
  export function getExpoSdkMajor(projectRoot) {
57
104
  const rawExpoVersion = getExpoConfigIfIsExpo(projectRoot)?.exp.sdkVersion;
58
105
  if (!rawExpoVersion) {