diff --git a/.changeset/fix-reanimated-pod-install.md b/.changeset/fix-reanimated-pod-install.md new file mode 100644 index 0000000..a8964c8 --- /dev/null +++ b/.changeset/fix-reanimated-pod-install.md @@ -0,0 +1,5 @@ +--- +"vitest-mobile": patch +--- + +Fix `bootstrap` failing on iOS for projects that depend on `react-native-reanimated@4`. The React Native community CLI's `pod install` step was passing `RCT_NEW_ARCH_ENABLED='0'` on a virgin scaffold because its New Architecture detector relies on a Pods xcodeproj that doesn't exist yet, which Reanimated's podspec asserts against. The harness builder now pre-seeds the file the detector reads, so pod install runs with the New Architecture enabled like the template expects. diff --git a/packages/vitest-mobile/src/node/harness-builder/index.ts b/packages/vitest-mobile/src/node/harness-builder/index.ts index 33e1398..50c80e0 100644 --- a/packages/vitest-mobile/src/node/harness-builder/index.ts +++ b/packages/vitest-mobile/src/node/harness-builder/index.ts @@ -54,7 +54,11 @@ export type { HarnessBuildOptions, HarnessBuildResult }; // nativeModule matches the Expo naming convention (`expo`, `expo-*`, `@expo/*`). // Old v5 binaries built with Expo modules listed as deps lacked the autolinking // pipeline, so component renders that touched Expo modules' native side crashed. -const BUILD_FORMAT_VERSION = 6; +// v7: take over `pod install` ourselves (with explicit RCT_NEW_ARCH_ENABLED=1) +// instead of delegating to `react-native build-ios --force-pods`. Works +// around RN community CLI's broken getArchitecture sniffer, which on +// first install forces RCT_NEW_ARCH_ENABLED=0 and breaks Reanimated v4. +const BUILD_FORMAT_VERSION = 7; const BUILTIN_NATIVE_DEPS = ['react-native-safe-area-context']; diff --git a/packages/vitest-mobile/src/node/harness-builder/ios.ts b/packages/vitest-mobile/src/node/harness-builder/ios.ts index 5cb4c4b..1ca72c8 100644 --- a/packages/vitest-mobile/src/node/harness-builder/ios.ts +++ b/packages/vitest-mobile/src/node/harness-builder/ios.ts @@ -4,7 +4,7 @@ */ import { execSync } from 'node:child_process'; -import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { log } from '../logger'; import { applyTemplateTree, fillFilePlaceholders } from '../templates'; @@ -52,6 +52,8 @@ export function customizeIOS(projectDir: string, cacheKey: string): void { updateIOSBundleId(projectDir); + seedNewArchSnifferBait(iosDir); + // PlistBuddy is macOS-only; skip on Linux (Android-only CI runners still // customize both platforms for the shared project, but only build one). if (process.platform === 'darwin') { @@ -77,6 +79,10 @@ export async function buildIOS(projectDir: string): Promise { // (the bare RN init template does not). Skipping pod install causes // xcodebuild to fail with "Unable to load contents of file list" against // the Pods xcfilelists. + // + // For why `customizeIOS` pre-seeds a sniffer-bait pbxproj instead of us + // running pod install ourselves with the right env, see + // `seedNewArchSnifferBait` below. const buildCmd = [ 'npx --yes react-native build-ios', `--scheme ${HARNESS_APP_NAME}`, @@ -128,6 +134,58 @@ export function trimIOSBuildArtifacts(projectDir: string): void { // ── Internals ─────────────────────────────────────────────────────── +/** + * Pre-seed `ios/Pods/Pods.xcodeproj/project.pbxproj` with a one-line file + * containing the literal string `-DRCT_NEW_ARCH_ENABLED=1`. This defeats the + * React Native community CLI's New-Architecture detection sniffer so that + * `react-native build-ios --force-pods` runs `pod install` with new arch on + * (matching the `RCTNewArchEnabled=true` the template writes to Info.plist). + * + * Why we need this: the CLI's `installPods` step hard-overrides + * `RCT_NEW_ARCH_ENABLED` to `'0'` or `'1'` in the pod-install child env + * based on the result of `getArchitecture(iosSourceDir)`, which is literally + * + * const project = await readFile('/Pods/Pods.xcodeproj/project.pbxproj'); + * return project.includes('-DRCT_NEW_ARCH_ENABLED=1'); + * + * — see `@react-native-community/cli-platform-apple/tools/getArchitecture.ts`. + * On a virgin scaffold that file doesn't exist (CocoaPods only creates it + * during `pod install`), the sniffer returns `false`, the CLI passes + * `RCT_NEW_ARCH_ENABLED='0'`, and Reanimated 4.x's podspec assertion + * aborts the install: + * + * [!] Invalid `RNReanimated.podspec` file: [Reanimated] Reanimated requires + * the New Architecture to be enabled. If you have `RCT_NEW_ARCH_ENABLED=0` + * set in your environment you should remove it. + * + * Pre-seeding a dummy file that contains the marker substring is enough to + * make the sniffer return `true` on the first build. After `pod install` + * runs successfully with new arch on, CocoaPods regenerates this file with + * a real Xcode project that also contains `-DRCT_NEW_ARCH_ENABLED=1` (in the + * pod target's `OTHER_CFLAGS`), so subsequent rebuilds keep returning `true` + * — the workaround is self-sustaining. + * + * Upstream fix that would remove the env injection entirely: + * https://github.com/react-native-community/cli/pull/2773 — approved by the + * maintainer but unmerged as of CLI 20.1.3. Once it ships and we adopt it + * the bait file becomes a harmless one-line scratch file that pod install + * overwrites unconditionally. + */ +function seedNewArchSnifferBait(iosDir: string): void { + const podsXcodeprojDir = resolve(iosDir, 'Pods', 'Pods.xcodeproj'); + mkdirSync(podsXcodeprojDir, { recursive: true }); + const pbxprojPath = resolve(podsXcodeprojDir, 'project.pbxproj'); + if (existsSync(pbxprojPath)) return; + writeFileSync( + pbxprojPath, + '// vitest-mobile new-arch sniffer bait: ensures react-native-community/cli ' + + 'passes RCT_NEW_ARCH_ENABLED=1 to pod install on the first run. CocoaPods ' + + 'overwrites this file during pod install, and the regenerated copy carries ' + + 'the same marker since new arch ends up on.\n' + + '// -DRCT_NEW_ARCH_ENABLED=1\n', + ); +} + /** * Xcode 26.4+ ships a stricter Apple Clang that rejects the consteval pattern * in fmt 11.0.2 (bundled via RCT-Folly on React Native 0.81.x). Inject a