Skip to content

fix(app,database): resolve platform native module when bundlers apply package exports - #9361

Merged
mikehardy merged 3 commits into
invertase:mainfrom
AndriiSerheniuk:fix/database-off-package-exports
Oct 16, 2026
Merged

mikehardy merged 3 commits into
invertase:mainfrom
AndriiSerheniuk:fix/database-off-package-exports

Conversation

@AndriiSerheniuk

Copy link
Copy Markdown
Contributor

Description

With a bundler that applies package exports — Re.Pack (Rspack/webpack) with enablePackageExports: true — every Realtime Database unsubscribe throws on Android and iOS:

TypeError: undefined is not a function
  at DatabaseSyncTree.removeRegistration  (this.native.off(registrationObj.key, registration))

Calling the unsubscribe returned by onValue(...), calling ref.off(), or unmounting a component whose effect cleanup unsubscribes all throw, and the native listener is never detached.

Cause. DatabaseSyncTree imports getReactNativeModule from the subpath @react-native-firebase/app/dist/module/internal/nativeModule. packages/app/package.json maps exports["./dist/module/internal/*"] to the literal file ./dist/module/internal/*.js. An exports target is an exact file, so the bundler does not try nativeModule.android.js / nativeModule.ios.js: it loads nativeModule.js, which re-exports nativeModuleWeb. The web getReactNativeModule returns the JS fallback that database/lib/index.ts registers, and on Android and iOS that fallback (web/RNFBDatabaseModule.android.ts / .ios.ts) is {}, so .off is undefined.

Subscribing works because DatabaseQuery.on goes through the module's native composite, built in app/lib/internal/registry/nativeModule.ts, which imports ../nativeModule relatively; a relative import gets the platform file.

Who is affected.

  • Re.Pack 5 with enablePackageExports: true. On React Native 0.87 that is every Re.Pack app: @react-native/asset-utils ships exports without main, so a stock Re.Pack app does not build with package exports off.
  • Metro is not affected. dist/module/package.json ({"type":"module"}) is the nearest package.json for these files and has no exports, so Metro resolves them as files and picks the platform variant.
  • Other getReactNativeModule callers that use the same subpath fail the same way under these bundlers: firestore (setLogLevel via internal/staticNativeModule.ts), messaging, phone-number-verification. The setReactNativeModule importers (analytics, app-check, auth, database, firestore, functions, in-app-messaging, remote-config, storage) only write to the web registry, which is harmless on native, and are switched for consistency.

Fix.

  • app/lib/internal/index.ts re-exports getReactNativeModule and setReactNativeModule from ./nativeModule. The build keeps that relative import extensionless, so every bundler resolves it per platform: nativeModule.android / .ios on native, nativeModule.js → nativeModuleWeb on web and macOS, as before.
  • The 13 packages/*/lib files that imported @react-native-firebase/app/dist/module/internal/nativeModule import from @react-native-firebase/app/dist/module/internal instead.
  • A dependency-cruiser rule, satellites-no-platform-split-subpath, fails lint:deps when a package outside app imports app/lib/internal/nativeModule directly.
  • phone-number-verification/__tests__/nativeModuleContract.test.ts injects its fixture through TurboModuleRegistry.get, like the other nativeModuleContract tests. It previously registered the fixture in the web registry, which only reached the library because Jest resolved the same subpath to the web file too.

Related issues

None found.

Release Summary

Fix Realtime Database unsubscribe (off()) and other native-module lookups throwing undefined is not a function on Android and iOS when the bundler applies package exports (Re.Pack).

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
    • Other (macOS, web)
  • My change includes tests;
    • e2e tests added or updated in packages/\*\*/e2e
    • jest tests added or updated in packages/\*\*/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan

Device repro. Minimal app: React Native 0.87.1 default template, @react-native-firebase/app + database 26.4.0. One button: onValue(ref(getDatabase(), path), …), then call the returned unsubscribe after the first value. The fix was applied to the installed dist files exactly as this PR builds them.

Bundler (default config) Android (Pixel 7a) iOS (iPhone 15 Pro simulator, iOS 17.5)
Metro, 26.4.0 unsubscribe OK unsubscribe OK
Re.Pack 5.4.0 + Rspack 2.2.2, enablePackageExports: true, 26.4.0 TypeError: undefined is not a function TypeError: undefined is not a function
Re.Pack, with this fix unsubscribe OK unsubscribe OK
Metro, with this fix unsubscribe OK unsubscribe OK

Module that DatabaseSyncTree reaches in each bundle:

  • Metro, before and after: internal/nativeModule.android.js / internal/nativeModule.ios.js
  • Re.Pack before: internal/nativeModule.js (→ nativeModuleWeb.js)
  • Re.Pack after: internal/index.js → internal/nativeModule.android.js / internal/nativeModule.ios.js; nativeModuleWeb.js is no longer in the native bundle

Repo checks.

  • yarn lerna:prepare, yarn lint:js, yarn lint:deps, yarn tsc:compile, yarn tsc:compile:consumer: pass.
  • yarn lint:deps with the old import restored in database/lib/DatabaseSyncTree.ts fails with satellites-no-platform-split-subpath.
  • yarn tests:jest: all suites pass except app/__tests__/iosSimulatorHelpers.test.js, which needs the iOS 27 e2e simulator and fails identically on main on this machine.
  • E2e was not run locally (no Xcode 27 / iOS 27 runtime here); relying on CI.

🔥

@CLAassistant

CLAassistant commented Oct 7, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@AndriiSerheniuk
AndriiSerheniuk force-pushed the fix/database-off-package-exports branch from cefb1cb to 973fa83 Compare October 8, 2026 06:18
AndriiSerheniuk and others added 3 commits October 16, 2026 11:47
… package exports

Packages imported getReactNativeModule and setReactNativeModule from the @react-native-firebase/app/dist/module/internal/nativeModule subpath. The app package exports map resolves that subpath to the exact file nativeModule.js, so bundlers that apply package exports (Re.Pack) load the web resolver on Android and iOS. DatabaseSyncTree then gets the empty web fallback and every Realtime Database off() throws 'undefined is not a function'.

Re-export both functions from the app internal index, where the relative ./nativeModule import resolves per platform, import them from there, and add a dependency-cruiser rule that rejects the subpath import outside app.
Keep the getReactNativeModule e2e import on the platform-resolving internal entry instead of the package-exports nativeModule subpath.
Keep the e2e harness resolver import on the platform-resolving internal entry for consistency with the package-exports fix.
@mikehardy
mikehardy force-pushed the fix/database-off-package-exports branch from 973fa83 to 40ec7b8 Compare October 16, 2026 18:47

@mikehardy mikehardy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @AndriiSerheniuk — clear root-cause write-up and the right fix: re-export getReactNativeModule / setReactNativeModule from the app internal barrel so bundlers that honor package exports (Re.Pack) resolve the platform files instead of the web stub. The dependency-cruiser guard and PNV contract-test update are good follow-through.

Maintainer follow-ups on the branch (rebased onto current main):

  • switch the leftover App e2e resolver import off the …/internal/nativeModule subpath
  • same consistency change for the e2e harness tests/globals.js import

Local verification: Android/iOS full e2e green on the rebased tip; app-area e2e after the globals change also green. Approving.

@mikehardy mikehardy added the Workflow: Pending Merge Waiting on CI or similar label Oct 16, 2026
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.77%. Comparing base (a5c6839) to head (40ec7b8).
⚠️ Report is 65 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9361      +/-   ##
============================================
+ Coverage     70.86%   74.77%   +3.91%     
- Complexity     2243     2286      +43     
============================================
  Files           446      624     +178     
  Lines         25532    41119   +15587     
  Branches       4343     5228     +885     
============================================
+ Hits          18091    30741   +12650     
- Misses         6136     8789    +2653     
- Partials       1305     1589     +284     
Flag Coverage Δ
android-native 70.53% <ø> (+1.17%) ⬆️
e2e-ts-android 53.49% <ø> (-0.35%) ⬇️
e2e-ts-ios 52.91% <ø> (?)
e2e-ts-macos 49.36% <ø> (-0.42%) ⬇️
ios-native 78.90% <ø> (?)
ios-ruby 100.00% <ø> (?)
jest 50.02% <100.00%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mikehardy
mikehardy merged commit 4d4127f into invertase:main Oct 16, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Workflow: Pending Merge Waiting on CI or similar

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants