Skip to content

feat: Expo managed workflow support (iOS + Android isolation) - #42

Draft
CAMOBAP wants to merge 7 commits into
mainfrom
feat/expo-managed-workflow-support
Draft

feat: Expo managed workflow support (iOS + Android isolation)#42
CAMOBAP wants to merge 7 commits into
mainfrom
feat/expo-managed-workflow-support

Conversation

@CAMOBAP

@CAMOBAP CAMOBAP commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • iOS (Expo dev-client): Detects EXDevLauncherController.sourceUrl via KVO when the bundle URL is unavailable at startup, enabling the sandbox to load bundles served by expo-dev-client without any configuration
  • iOS factory: Auto-detects expo-modules-core at pod install time; when present, uses EXReactNativeFactory per sandbox so Expo modules are registered and an AppContext is created for each runtime
  • Android isolation: Each sandbox host already wraps its ReactApplicationContext with SandboxContextWrapper (scopes filesDir, cacheDir, sharedPreferences); Expo modules (ExpoModulesPackage) are now added to the sandbox package list when expo-modules-core is detected in node_modules
  • Zero user config: Detection is purely automatic — vanilla RN projects are unaffected; Expo integration appears only when expo-modules-core is in node_modules

Notes

iOS scoped AppContextConfig (per-origin document/cache dirs) is scaffolded in SandboxAppContextBridge.swift but not yet wired, because EXReactNativeFactory has no public hook to inject a custom config. Android already achieves equivalent scoping via SandboxContextWrapper. iOS scoped dirs are a follow-up item.

Test plan

  • apps/expo-demo builds and sandbox loads in Expo dev-client
  • Two sandboxes with different origins load independently
  • Vanilla RN demo (apps/expo) still builds and works unchanged
  • pod install in a non-Expo project does not pull expo-modules-core

🤖 Generated with Claude Code

CAMOBAP and others added 2 commits July 26, 2026 15:01
- Add runtime ObjC dispatch via objc_getClass/performSelector to derive
  the sandbox bundle URL from EXDevLauncherController.sourceUrl without
  a hard compile-time dependency on expo-dev-launcher headers
- Add KVO on EXDevLauncherController.sourceUrl in
  SandboxReactNativeViewComponentView to retry sandbox load once the
  Expo host bundle URL becomes available (set asynchronously after
  deep-link network check, ~1-2s after launch)
- Expose bundleURL as a public method on SandboxReactNativeDelegate
- Add apps/expo-demo: a minimal Expo SDK 54 / RN 0.81.4 managed-workflow
  app that exercises the sandbox inside an expo-dev-client build
- Remove orphaned apps/expo/ (abandoned WIP native build output, no
  package.json), replace with apps/expo-demo using managed workflow
- Pin metro extraNodeModules.react to workspace root to avoid bun
  hoisting a stale react@19.0.0 into the app's local node_modules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix stale comment referencing RCTJavaScriptDidLoadNotification (the
  implementation uses KVO, not the notification)
- Forward unhandled KVO observations to super in
  observeValueForKeyPath:ofObject:change:context:
- Remove duplicate #import <objc/runtime.h> in SandboxReactNativeDelegate.mm
- Use a typed static context pointer (kExpoSourceUrlKVOContext) for KVO
  registration/removal instead of nil, preventing accidental dispatch of
  superclass observations into our handler
- Correct infiniteLoop comment: hangs sandbox JS thread only, host app
  stays responsive (each sandbox runs in its own Hermes instance)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CAMOBAP and others added 2 commits July 29, 2026 09:26
Auto-detects expo-modules-core at build time so vanilla RN projects are unaffected:
- iOS (podspec): File.exist? check sets RNS_HAS_EXPO_MODULES=1 via pod_target_xcconfig
  and pulls in ios/Expo/**  sources + expo-modules-core/expo dependencies only when present
- Android (build.gradle): rootProject.file("node_modules/expo-modules-core").exists()
  gates the src/expo/java source set, expo-modules-core dependency, and HAS_EXPO_MODULES
  BuildConfig field

iOS isolation (gated by #if RNS_HAS_EXPO_MODULES):
- SandboxExpoFactory.mm: creates ExpoReactNativeFactory instead of RCTReactNativeFactory,
  enabling Expo module registration inside the sandbox RCTHost
- SandboxAppContextBridge.swift: Swift bridge that creates EXAppContext with a scoped
  AppContextConfig (documentDirectory/cacheDirectory under SandboxData/<sanitized-origin>/)
- SandboxReactNativeDelegate.mm: injects the scoped EXAppContext via
  ExpoBridgeModule.initWithAppContext: in getModuleInstanceFromClass: so Expo modules
  (FileSystem, SecureStore, etc.) read/write origin-namespaced directories

Android isolation:
- SandboxContextWrapper already scopes getFilesDir/getCacheDir/getSharedPreferences to the
  origin; AppContext receives this wrapped context so Expo modules are automatically scoped
- ExpoIntegration.kt provides a ModuleRegistryAdapter with the host project's Expo packages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… into Android sandbox

- SandboxExpoFactory.mm: use correct ObjC class name EXReactNativeFactory and
  import <Expo/ExpoReactNativeFactory.h> (ObjC header, not Swift-generated)
- SandboxAppContextBridge.swift: fix return type to AppContext (Swift name),
  not EXAppContext (ObjC alias)
- ExpoIntegration.kt: replace legacy ModuleRegistryAdapter with ExpoModulesPackage
- SandboxReactNativeDelegate.kt: wire Expo packages into sandbox host via
  reflection-guarded getExpoPackages() so the main source set never directly
  references the conditionally-compiled ExpoIntegration class
- React-Sandbox.podspec: refactor source_files to declarative ternary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@CAMOBAP
CAMOBAP force-pushed the feat/expo-managed-workflow-support branch from 764ba56 to b1c314e Compare July 29, 2026 08:32
@CAMOBAP CAMOBAP changed the title feat: Expo managed workflow (expo-dev-client) support for iOS feat: Expo managed workflow support (iOS + Android isolation) Jul 29, 2026
CAMOBAP and others added 3 commits July 29, 2026 11:49
…ft header import

ExpoReactNativeFactory is public but not open, so it cannot be subclassed
from Swift outside the Expo module. Replace the subclass approach with a
static factory method on SandboxExpoFactory that constructs and returns an
ExpoReactNativeFactory — composition instead of inheritance.

Also fix React_Sandbox-Swift.h import in SandboxReactNativeDelegate.mm
(was using wrong hyphenated name; CocoaPods generates underscore form).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ft header import

Implement iOS scoped AppContext via ObjC subclass of EXReactNativeFactory.
Override host:didInitializeRuntime: to replace [[EXAppContext alloc] init]
with [SandboxAppContextBridge createScopedAppContextForOrigin:origin] —
all other lines (scheduler binding, setRuntime:, setHostWrapper:,
registerNativeModules) are identical to Expo's implementation. Expo modules
registered via registerNativeModules all share the scoped AppContext, giving
them origin-namespaced document/cache directories automatically.

Remove the now-redundant ExpoBridgeModule interception from
SandboxReactNativeDelegate (it was creating a second, disconnected AppContext
for just one module). The factory override covers the whole runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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