All skills
expo avatar

/expo-brownfield

@ea892a7 official
by expoexpo/skills2.6k stars
156

Framework (OSS). Integrate Expo and React Native into an existing native iOS or Android app. Use for brownfield, embedding a React Native screen in SwiftUI/UIKit or Kotlin, or AAR/XCFramework packaging. Covers isolated and integrated approaches. For building or distributing a purely native app with EAS, use eas-app-stores.

Use this Skill: https://skilld.dev/gh/expo/skills/expo-brownfield

This session only. Nothing lands on disk.

referencestroubleshooting.md

≈2.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Brownfield Troubleshooting

Cross-cutting issues that apply to both the isolated and integrated approaches. For approach-specific setup, see ./brownfield-isolated.md or ./brownfield-integrated.md.

Build failures

Symptom: Gradle or Xcode build fails after a config change, dependency upgrade, or Expo SDK bump.

First inspect the failing build step and the dependency/configuration diff.

  • Integrated approach: keep the hand-maintained host intact. Run npx expo install --check in the JS project, apply SDK-matched native template changes selectively, then run bundle exec pod install (or pod install without Bundler) in the host's Podfile directory. Open the .xcworkspace. Neither prebuild nor prebuild --clean is a recovery step for this host: clean prebuild deletes native directories.
  • Isolated approach: rebuild the affected platform in the separate Expo producer, then replace the consumer's artifact and its accompanying dependencies. CNG regeneration belongs only in that producer, after checking that its native files are generated and reproducible.
  • For stale iOS build products, clean the affected target's build folder/DerivedData. Preserve Podfile.lock; deleting it changes dependency resolution and can hide the cause. Reinstall pods only when the error points to the pod installation.
  • For Android, clean the affected project's build outputs with its Gradle wrapper. Inspect publication coordinates and dependency resolution before removing a specific stale local Maven artifact; do not clear unrelated caches.

Missing autolinked Expo modules

Symptom: Compilation succeeds but a module throws "Native module cannot be null" / "Cannot find native module 'X'" at runtime.

  • Install with npx expo install <package> rather than plain yarn add — expo install picks the version compatible with the current SDK.
  • After installing a new module, rebuild the native app. Autolinking runs at native build time, not at JS bundle time.
  • For the isolated approach, you must re-run npx expo-brownfield build:android|ios after adding a module, and republish/re-embed the new artifact.

Metro connection

Symptom: "Could not connect to development server" / red screen on launch in debug.

  • Ensure the device or emulator can reach the dev machine. The Android emulator can talk to the host via 10.0.2.2; physical devices need a reachable LAN IP.
  • For physical Android devices on USB: adb reverse tcp:8081 tcp:8081.
  • Confirm Metro is actually running: npx expo start from the Expo project (or yarn start from the workspace root).
  • Verify the debug AndroidManifest.xml enables cleartext traffic — Android 9+ blocks HTTP by default. The debug variant should include android:usesCleartextTraffic="true" on <application>, or a network_security_config allowing the dev server.
  • iOS simulator: Metro should be reachable at localhost:8081. If it is not, check that ATS exceptions are still in place in Info.plist for localhost (the Expo template ships this by default).

iOS XCFramework signing (isolated approach)

Symptom: App launches but immediately crashes with "Library not loaded" or codesign errors during archive.

  • Inspect the actual output and generated Package.swift; the framework set depends on package version and source/prebuilt settings, not only the SDK major. Link all required binaries and embed/sign dynamic frameworks. Do not apply Embed & Sign to static binaries. See the artifact instructions.
  • The frameworks must be added to the app target, not a framework or extension target.
  • With Swift Package output (build:ios --package), inspect the manifest and link all required products. Precompiled builds on SDK 57 expose an aggregate product; other configurations and older packages can expose separate products. See version compatibility.

iOS architecture / simulator mismatch

Symptom: "Building for iOS Simulator, but the linked library was built for iOS" or "Undefined symbols for architecture arm64".

  • The XCFramework includes both device and simulator slices. If a slice is missing, rebuild on the missing platform. The expo-brownfield build:ios command produces both by default.
  • On Apple Silicon simulators, do not set EXCLUDED_ARCHS = arm64 for the simulator configuration — Apple Silicon simulators require arm64. The classic Rosetta-only exclusion is no longer correct.

Android mavenLocal() not found (isolated approach)

Symptom: Gradle reports "Could not find com.example:mybrownfield:1.0.0" even after a successful expo-brownfield build:android.

  • mavenLocal() must be declared under dependencyResolutionManagement { repositories { ... } } in settings.gradle.kts, not the deprecated top-level allprojects { repositories { ... } } block. The deprecated form is silently ignored when dependencyResolutionManagement is present.
  • Confirm the artifact actually landed in ~/.m2:
    find ~/.m2/repository -name "mybrownfield*"
  • Verify the group and libraryName in the consumer's dependency line match what the plugin config emitted.

Module name mismatch

Symptom: The native view loads but renders a blank screen, with "Application 'X' has not been registered" in the JS logs.

  • The moduleName passed to ReactNativeViewController(moduleName: "main") (iOS) or returned from getMainComponentName() (Android) must equal the name passed to AppRegistry.registerComponent("main", () => App) in the JS entry point.
  • The default Expo template registers "main". If you changed the registration, update every native call site.

Monorepo: autolinking can't find the Expo project

Symptom: Gradle or CocoaPods fails resolving Expo modules even though they are installed.

  • Android (integrated): set root = file("../../my-project") (or the correct relative path) inside the react { ... } block in app/build.gradle, and explicitly set the project root in settings.gradle before expoAutolinking.useExpoModules().
  • iOS (integrated): set :app_path in use_react_native! to the absolute path of the Expo project root. Optionally pass EXPO_PROJECT_ROOT=/abs/path to pod install.
  • Confirm node_modules/ is installed at the workspace root (yarn install from the monorepo root, not from the Expo project subdirectory).

After upgrading Expo SDK

First check the selected SDK's Node, Xcode, and minimum OS requirements in version compatibility. An unsupported compiler or older deployment target is not repaired by clearing caches. If the brownfield setup stops building after an SDK upgrade:

  • Re-run npx expo install --fix in the Expo project to align native module versions.
  • Isolated: regenerate only the CNG-owned producer if needed, rebuild its artifact, and update the host dependency. Integrated: apply native upgrade diffs to the existing host and reinstall pods; preserve its source files and project configuration.
  • Compare the new templates/expo-template-bare-minimum for the target SDK against your customized native files — Expo occasionally changes Gradle plugin names, Podfile helpers, or AppDelegate entry points across SDKs.

Result missing, duplicate callbacks, or a sheet that will not close

  • Check the module registration and per-presentation request ID. Root props need an explicit JS entry point; do not assume a Router route receives native initialProps directly.
  • Attach the host listener before mounting RN and supply required startup data through initial props. For live updates, verify subscription readiness and use acknowledgements where delivery matters; messages are not a durable queue.
  • Remove only this feature's listeners on completion, cancellation, and host dismissal. Dispatch UI changes to the main thread.
  • popToNative() depends on the native wrapper: the SDK 55 UIKit controller pops navigation, and its SwiftUI wrapper separately calls dismiss(). Custom integrated containers need their own handler. Check which wrapper is actually mounted, or let the host close it on a result/close message. See feature integration.

Source: SKILL.md on GitHub

No alerts16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive documentation and guidelines for integrating Expo and React Native features into existing native iOS and Android apps using either isolated or integrated architectures. It includes a specific utility command to submit feedback to Expo via npx. No security issues or malicious behaviors were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

Signed by skilld at ea892a7. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 3 weeks ago
  • expo
  • react-native
  • brownfield
  • ios
  • android
  • native-integration
  • aar
  • xcframework
  • kotlin
  • swift

README badge

README badge for expo/skills/expo-brownfield

Integrates React Native and Expo into an existing native iOS or Android app using either isolated (AAR/XCFramework) or integrated (Gradle/CocoaPods) approaches. Choose isolated if the native team needs no Node tooling or RN lives in a separate repo; choose integrated if one team owns both codebases and wants hot reload in the native build.

Generated from the current SKILL.md.

What is the difference between isolated and integrated approaches?
Isolated builds React Native as a prebuilt AAR or XCFramework that the native team consumes without needing Node or RN tooling installed. Integrated adds React Native sources directly to the existing Gradle and CocoaPods build, requiring a single team to manage both native and RN code together.
Which approach should I use if my native and React Native teams are separate?
Choose isolated. It lets the iOS/Android team consume React Native as a regular library dependency without installing Node, Yarn, or the React Native build toolchain, and allows the codebases to live in separate repositories and release independently.
What is the minimum Expo SDK version for brownfield?
Expo SDK 55 or later. Earlier SDKs lack the required ExpoReactHostFactory and ExpoReactNativeFactory entry points and the current autolinking surface. Always pin the SDK explicitly when creating the Expo project.
Do both approaches require Node.js and Yarn?
Yes, Node.js (LTS) and Yarn are required in the environment that builds the React Native side. The integrated approach additionally requires CocoaPods on iOS; the isolated approach does not.

Generated from the current SKILL.md. These answers refresh after source changes.