Skip to content

feat: Expo support (managed workflow, dev-client, and bare) for iOS and Android - #45

Open
eumaninho54 wants to merge 4 commits into
callstackincubator:mainfrom
eumaninho54:feat/expo-support
Open

feat: Expo support (managed workflow, dev-client, and bare) for iOS and Android#45
eumaninho54 wants to merge 4 commits into
callstackincubator:mainfrom
eumaninho54:feat/expo-support

Conversation

@eumaninho54

Copy link
Copy Markdown

Summary

Adds Expo support (managed workflow with expo-dev-client, and bare) on both iOS and Android, in dev and production builds. Related to #13.

Two other approaches were already attempted for this (#42, #15). This PR takes a different path: instead of resolving the sandbox's bundle URL natively during boot, it resolves it in JS, after the host is already running.

The problem

In dev, the sandbox needs to know the Metro URL to fetch its own bundle. On RN CLI, RCTBundleURLProvider already has this because it's the same code that booted the host. Under Expo's dev-client, a different launcher (expo-dev-launcher) resolves that URL instead, so RCTBundleURLProvider never gets configured, and the sandbox has nothing to read from.

#42 solves this by reaching into EXDevLauncherController at runtime (objc_getClass/performSelector, no compile-time header dependency) and using KVO to wait for sourceUrl to become available, since it's set asynchronously a second or two after launch. It's iOS-only, gated behind #if DEBUG, and the derived query string is hardcoded (platform=ios&dev=true&hot=false), so it doesn't track the actual dev state.

#15 takes a different angle: swap between ExpoReactNativeFactoryDelegate and RCTDefaultReactNativeFactoryDelegate via #ifdef EXPO_MODULE, linking ExpoModulesCore conditionally. That solves a different piece (which native factory to use for a bare Expo app) but doesn't actually derive the dev Metro URL. It's also iOS-only and never finished.

This approach

The sandbox's bundle URL is resolved in JS, using NativeModules.SourceCode.getConstants().scriptURL from the host that's already running. That value always reflects wherever the host bundle actually loaded from, no matter which launcher resolved it (RN CLI, expo-dev-launcher, whatever comes next). Since JS only runs after the host has booted, there's no timing race to work around, and no dependency on Expo's internal classes.

  • No native code changes needed for the Expo case itself
  • Works the same for managed and bare
  • Works on both iOS and Android (previous attempts only covered iOS)
  • Forwards only the query params that affect how Metro serves the bundle (platform, dev, hot, minify, etc.), reading them from the host's actual URL instead of hardcoding them, so it doesn't break under --no-dev or similar
  • Strips launcher-specific params (like Expo Router's transform.routerRoot) that would otherwise break the sandbox bundle

Also fixed along the way

  • NativeModules.SourceCode doesn't come with its constants pre-flattened under New Architecture/bridgeless, only a getConstants() method. Needed for the above to actually work.
  • Android was reusing a cached copy of any remote bundle by URL. In dev, the same URL can serve different content across sessions (Metro), so this could silently serve stale JS. Only non-dev remote bundles still use the cache now.

Test plan

Added apps/expo-demo (from #42, adjusted for this fork).

  • iOS, managed workflow + expo-dev-client, Debug: sandbox loads, hot reload works without a rebuild
  • Android, managed workflow + expo-dev-client, Debug: same
  • iOS, Release (RN 0.83.10): no crash, sandbox waits on a bundle as expected given the known limitation above
  • Android, Release (RN 0.83.10): same

Derives the sandbox's dev bundle URL in JS from the host's own
resolved script URL (NativeModules.SourceCode.scriptURL) instead of
guessing it natively. Works the same regardless of which launcher
stood up the host (RN CLI, Expo dev-client, bare), so Expo managed
workflow works out of the box with no native changes.
Pulled apps/expo-demo in from upstream PR callstackincubator#42 (Expo managed workflow
demo with expo-dev-client). Bumped @types/react to match the
workspace version and fix a duplicate-types JSX error.
Under New Architecture / bridgeless, NativeModules.SourceCode doesn't
come with constants pre-flattened onto the object, only a
getConstants() method. Confirmed working end to end on iOS and
Android in an Expo managed workflow app with expo-dev-client.
downloadRemoteBundle reused a cached copy of any http(s) bundle by
URL, but a dev bundle served by Metro (identified by dev=true in the
query string) changes content without the URL changing. Only remote
bundles without dev=true (e.g. a CDN-hosted production bundle) still
use the cache.

Confirmed on device: edited sandbox JS, reloaded without a native
rebuild, sandbox picked up the change instead of serving stale JS.
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