Skip to content

fix: support Expo modules in React Native CLI apps - #487

Open
KisaneNeko wants to merge 10 commits into
callstack:mainfrom
KisaneNeko:fix/cli-skip-prebuild-bare-expo
Open

KisaneNeko wants to merge 10 commits into
callstack:mainfrom
KisaneNeko:fix/cli-skip-prebuild-bare-expo

Conversation

@KisaneNeko

@KisaneNeko KisaneNeko commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Expo modules didn't work in bare RN apps packaged with brownfield. Issues found and fixed:

  • expo prebuild wiped the brownfield module. The CLI ran prebuild for any project with Expo and an app.json, which regenerated the committed android/ and ios/ dirs without the library module. It now skips prebuild when the native dir is committed.
  • Expo modules didn't register on iOS. That needs the Expo host, which only the config plugin set up. create-react-native-brownfield now sets it up when the app depends on expo, also when re-run after adding Expo.
  • --use-prebuilt-expo didn't work in bare apps. It never set EXPO_USE_PRECOMPILED_MODULES, so pod install built Expo from source and packaging failed with "XCFramework not found". The CLI now sets it from the flag, and without the flag it respects an exported EXPO_USE_PRECOMPILED_MODULES=0 or Podfile.properties.json turning it off.
  • Precompiled builds failed with "cannot load underlying module". Caused by the SWIFT_ENABLE_EXPLICIT_MODULES = NO that RN's pod install writes into the project. The CLI now passes SWIFT_ENABLE_EXPLICIT_MODULES=YES for these builds.
  • Precompiled Expo crashed on a different RN version. The binaries are built for the RN version the SDK targets. The CLI now builds Expo from source on a mismatch, and warns if precompiled is forced.
  • Packaged expo-image dependencies were symlinks. With precompiled Expo, CocoaPods links SDWebImage and libavif into node_modules, and the CLI copied the links instead of the files. The package broke when moved off the build machine (e.g. committed to the host app's repo), and stripping signatures edited files in node_modules. Found while testing this, present since Expo 56 support (feat: add Expo 56 support #360). The CLI now copies the real files.
  • Re-running the setup command after pod install broke the next pod install. CocoaPods renames the framework's Sources phase, so the duplicate check missed it and added a second one ("no parent for object BrownfieldLib.swift"). This was on main already, but our docs now tell users to re-run the command after adding Expo. The check now matches any Sources phase on the target.

Docs: a new guide, Expo Modules in React Native CLI Apps, with the steps for these apps. The RN CLI setup guide and the iOS guide link to it, so apps without Expo can skip it.

Behavior change

Prebuild used to run for every Expo project. Now it only runs when ios/ or android/ is missing or gitignored. Two cases get no automatic prebuild anymore:

  • Expo apps that commit their generated native dirs. They need to run npx expo prebuild themselves after changing app.json or plugins.
  • Packaging outside a git repo (e.g. Docker, CI without .git) with native dirs already present.

Nothing fails in either case, but the package is built from native dirs that weren't regenerated. If that's a concern, we could add a flag to force prebuild. Happy to add it if you think committed CNG dirs are common.

Testing

  • RNApp with Expo SDK 58 and expo-image (demo changes not in this PR): works in AndroidApp and AppleApp.
  • Fresh bare app (RN 0.86 + Expo SDK 57): packages on the precompiled path, expo-image renders.
  • ExpoApp58: unchanged, still precompiled.
  • Packaged output moved to another folder, with node_modules/expo-image hidden, still builds and runs.
  • Unit tests pass (strip-framework-binary.test.ts fails on main too).

🤖 Generated with Claude Code

The CLI ran `expo prebuild` whenever it found Expo and an app.json. In a
React Native CLI app that installed Expo modules, prebuild regenerated the
committed android/ and ios/ directories and deleted the brownfield library
module, so packaging failed.

Only run prebuild when the native directory is missing or gitignored (CNG).

Also document the manual iOS step for these apps: Expo modules only register
through the Expo host runtime, which the config plugin enables in Expo apps.
…Expo

Expo modules only register through the Expo host runtime, which the config
plugin enables in Expo apps. create-react-native-brownfield now does the same
when the app's package.json depends on expo: the Podfile framework target gets
the Expo host env and the EXPO_SDK_GTE_55 define, and the framework interface
gets ensureExpoModulesProvider(). Re-running it after adding Expo switches an
existing framework target over.

Only the app's own package.json counts, so a hoisted expo in a monorepo does
not turn the Expo host on. The Podfile hook no longer assumes Expo's
podfile_properties exists.
@KisaneNeko KisaneNeko changed the title fix(cli): skip expo prebuild in React Native CLI apps using Expo modules fix: support Expo modules in React Native CLI apps Oct 13, 2026
Three things broke packaging with precompiled Expo modules outside Expo apps:

- `--use-prebuilt-expo` only picked which XCFrameworks to copy. It never told
  CocoaPods to use precompiled modules, which only Expo-generated Podfiles turn
  on, so a bare app failed with "XCFramework not found". The CLI now sets
  EXPO_USE_PRECOMPILED_MODULES from the flag.
- Older React Native versions wrote SWIFT_ENABLE_EXPLICIT_MODULES = NO into app
  projects. With precompiled Expo the React Native pods are static libraries,
  and with explicit modules off Xcode can't find their module maps when it
  verifies the framework's Swift interface ("cannot load underlying module").
  The CLI turns explicit modules on for these builds.
- Expo's precompiled modules are built against the React Native version the SDK
  targets, and crash at runtime on another minor version. The CLI now defaults
  to building Expo from source in that case and warns if it's forced.
@KisaneNeko
KisaneNeko marked this pull request as ready for review October 13, 2026 14:57
With precompiled Expo, CocoaPods symlinks expo-image's dependency
XCFrameworks into node_modules. The package step copied the links, so
the output broke when moved to another machine, and stripping signatures
modified the files in node_modules.
- warn when git can't tell whether the native dir is gitignored
- make the Expo host Podfile edit handle CRLF, comments and fail loudly
- respect Podfile.properties.json and an exported opt-out for precompiled Expo
- keep a user-provided SWIFT_ENABLE_EXPLICIT_MODULES value
- add tests for the new defaults
CocoaPods renames the framework target's Sources phase comment to
"Sources", so the idempotency check missed it and a re-run added a
second phase, which broke the next pod install.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant