Manual Setup
When to Use Manual Setup
Use this guide when bundle-drop login or bundle-drop init cannot safely update a customized project, or when an AI setup plan returns low confidence. For standard projects, prefer the reviewed CLI setup in Installation; it detects the project type, previews every change, and creates backups.
Manual setup changes how the native app chooses its JavaScript bundle at cold start. Preserve your existing Debug, package-list, lifecycle, and embedded-bundle behavior. After changing native or Expo configuration, create a new native binary before testing OTA updates.
Before You Edit Native Files
Install the package and make sure bundle.drop.config.js already exists. If setup failed before creating that file, complete the package, authentication, and project-config steps in Installation first.
React Native autolinking owns the Android package and iOS pod. Do not manually add BundleDropPackage, an Android Gradle project, or a manifest entry. Bare iOS projects still need pods installed:
npm install @gfean/react-native-bundle-drop
cd ios && pod install && cd ..Bare React Native Metro Setup
Bare React Native must resolve bundle-drop-config to the project config. If metro.config.js already contains custom behavior, merge only the resolver.extraNodeModules entry instead of replacing that behavior.
const path = require("path");
const { getDefaultConfig } = require("@react-native/metro-config");
const config = getDefaultConfig(__dirname);
config.resolver = config.resolver || {};
config.resolver.extraNodeModules = {
...(config.resolver.extraNodeModules || {}),
"bundle-drop-config": path.resolve(__dirname, "bundle.drop.config.js"),
};
module.exports = config;Android MainApplication
Choose the tab that matches the host already present in your app. Current Kotlin ReactHost apps must keep that shape. Do not add or recreate ReactNativeHost or DefaultReactNativeHost solely for Bundle Drop.
The resolver belongs in the native Release bundle path because native selection happens before JavaScript initializes. Returning the existing fallback in Debug keeps Metro in control.
import com.bundledrop.BundleDropModule
private fun getJSBundleFile(): String? {
if (BuildConfig.DEBUG) return null
return BundleDropModule.resolveJSBundleFile(applicationContext, null)
}
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages.apply {
// Preserve existing manually added packages.
},
jsBundleFilePath = getJSBundleFile(),
// Preserve every other existing host option.
)
}Keep the current template's loadReactNative(this) call and every existing package or host option. If your app already has custom bundle selection, evaluate it once and pass that result as fallback. When migrating from CodePush, replace the CodePush resolver; do not retain CodePush as Bundle Drop's fallback.
iOS AppDelegate
Add the locator to the object that already owns bundleURL(). In older Swift templates that is usually AppDelegate; in current factory-based templates it is commonly the nested ReactNativeDelegate. If sourceURL(for:) already delegates to bundleURL(), keep it unchanged.
import BundleDrop
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
return BundleDropLocator.bundleURL()
?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}Preserve a custom Metro bundle root and the app's existing embedded Release fallback. If the Objective-C delegate already has sourceURLForBridge: call [self bundleURL], put the Debug/Release selection in -bundleURL instead of duplicating it.
Expo Manual Setup
Expo projects use the Bundle Drop config plugin and Metro wrapper. Do not manually patch generated Android MainApplication or iOS AppDelegate files. Expo autolinking and the config plugin own those generated seams, and managed/CNG projects can keep ios/ and android/ absent until prebuild, expo run:*, or EAS Build.
Register the plugin exactly once in the authoritative Expo app configuration. Preserve every unrelated plugin and config value. For app.json, add the package to the existing expo.plugins array instead of replacing the file.
{
"expo": {
"plugins": [
"@gfean/react-native-bundle-drop"
]
}
}The dynamic-config tab is an insertion pattern, not a replacement app.config.* file. Apply the duplicate check inside the existing config function and keep its environment reads, computed values, plugins, and returned fields.
Preserve Expo's Metro defaults and wrap the final config:
const { getDefaultConfig } = require("expo/metro-config");
const { withBundleDropExpo } = require("@gfean/react-native-bundle-drop/metro");
module.exports = withBundleDropExpo(getDefaultConfig(__dirname), {
projectRoot: __dirname,
});Finally, add projectType: "expo" to the object already exported by bundle.drop.config.js. Do not replace the file: keep its generated serverUrl, organization, project, and per-platform runtime versions unchanged.
If the project uses a custom Metro config, pass that completed config to withBundleDropExpo rather than replacing it with the short example.
Remove Active expo-updates
If active expo-updates is present, stop: both systems cannot own native startup in the same binary. When the guided planner is available, prefer its reviewed migration preview:
npx bundle-drop init --project-type expo --migrate-expo-updatesIf guided setup is unavailable or returned low confidence, migrate manually:
- Remove the direct
expo-updatesdependency with the package manager already used by the project. - In the authoritative Expo config, remove every
expo-updatesplugin entry, whether it is a string or a tuple such as["expo-updates", { ... }]. Preserve every unrelated plugin, and keep the Bundle Drop plugin exactly once. - Remove only
updates.urlandupdates.enabledfrom the Expo config. Preserve otherupdatesfields; remove theupdatesobject only if it becomes empty. - Keep the Bundle Drop Metro wrapper and
projectType: "expo"configuration described above. - Generate and install a new native binary. An existing binary still contains the old startup owner.
npm uninstall expo-updates --legacy-peer-depsExpo Go does not include the Bundle Drop native adapter. Debug and development-client builds intentionally stay on Metro. Use a non-Debug build to test installed OTA startup:
npx expo run:ios --configuration Release
npx expo run:android --variant releaseInitialize and Verify
Native setup only chooses the startup bundle. You must also call BundleDrop.init once before app registration, or in the Expo Router root layout. Use the complete examples in Installation and Expo.
Run diagnostics for the project type and platform you changed:
npx bundle-drop doctor --project-type bare --platform android
npx bundle-drop doctor --project-type bare --platform iosThe doctor may warn until Android autolinking output or Podfile.lock exists. After integration, build and install a new native binary. Bump the affected platform's runtime version when the native compatibility boundary changes; see Runtime Version.
