| type | Reference | |||||
|---|---|---|---|---|---|---|
| title | TurboModule migration implementation workflow | |||||
| description | New Architecture–specific artifacts for Codegen specs, native module conversion, area harness setup, and coordinated break validation — extends the cross-package change authoring workflow. | |||||
| tags |
|
|||||
| timestamp | 2026-06-26 00:00:00 UTC |
Requirements for migrating one package (or one legacy native module within a multi-module package) from legacy NativeModules to Codegen TurboModules. Shared loop: change authoring workflow.
Policy: OKF documentation and commit policy.
| Topic | Document |
|---|---|
| Change authoring loop | change-authoring-workflow.md |
| Architectural decisions (why) | architecture-decisions.md — canonical owner; this doc is the how |
| Live phase/gate snapshots | migration-work-queue.md |
| Inventory, phase order | migration-work-queue.md |
| Work types, tiers, gates | change-authoring-workflow.md; term ids: iteration-vocabulary.md |
| E2e commands | running-e2e.md |
| Validation commands | validation-checklist.md |
| Reference implementation | packages/functions (PR #8603) |
In addition to change authoring gates:
| Gate | Requirement |
|---|---|
| Spec inventory | Native methods inventoried from Java/ObjC; mapped to typed spec using packages/<pkg>/lib/types/internal.ts where available |
| Codegen | specs/NativeRNFBTurbo*.ts + codegenConfig; generated android/ios artifacts committed (includesGeneratedCode: true) |
| Native shell | Android NativeRNFBTurbo* extends generated *Spec; iOS spec protocol + getTurboModule() |
| JS wiring | nativeModuleName → NativeRNFBTurbo*; turboModule: true; web shims via setReactNativeModule unchanged |
| Podspec / package | New-arch guard; exclude duplicate RN generated providers (see RNFBFunctions.podspec) |
| Implementation | Package Jest + unit-focused e2e on New Architecture build on every required platform (platform coverage gate). Native bridge / codegen / podspec / build.gradle touched → blocking before handoff; Jest-only does not close implementation_gate. Build or env failure keeps the gate open — handoff must include a platform matrix (exit code + pass counts + log path) or an explicit env blocker, not “Jest green”. |
| Review | Package e2e area-focused tier on frozen tree; no .only; native lint rows from validation checklist where touched |
| Event deferral | Legacy event proxy retained unless queue deferral discriminator triggers escalation |
One TurboModule spec per legacy native module — do not consolidate the specs in the first pass (database ×5, firestore ×4); each legacy module keeps its own NativeRNFBTurbo* spec and shell.
Commit granularity: one commit per package, not per spec. Splitting a multi-module package across several commits adds little value and is often impractical — the specs share codegenConfig, generated artifacts, podspec/build.gradle guards, and JS wiring that only compile and pass e2e together. Convert the whole package (all its legacy modules → all its specs) in a single implementation → independent-review → commit loop and land it as one feat(<pkg>)!: migrate <pkg> to TurboModules commit (breaking: New Architecture required). Per-spec commits are only warranted if a single legacy module is genuinely independently shippable and reviewable, which is rare. Multiple specs ≠ multiple commits.
Phase 0 app is the first package with two TurboModule specs in one codegenConfig: NativeRNFBTurboApp + NativeRNFBTurboUtils. Modular type parity is tracked separately in Phase 0.1 (configs/app.ts). Single codegenConfig.name = the aggregate library name RNFBAppTurboModules (not a module name; decision NewArch-AD-7); iOS modulesProvider maps each spec name to its ObjC shell (RNFBAppModule, RNFBUtilsModule). Android registers both shells in the package class. Method names must be unique across the package's specs (NewArch-AD-11). Reference: packages/app/package.json, ReactNativeFirebaseAppPackage.java.
- Inventory
@ReactMethod/RCT_EXPORT_METHODfrom existing Java/ObjC sources. - Draft
specs/NativeRNFBTurbo*.ts— strong types fromlib/types/internal.tsand firebase-js-sdk shapes;Object/ open maps only for genuinely dynamic payloads. Follow Codegen-safe names and types before running codegen. - Naming:
NativeRNFBTurbo*prefix (decision NewArch-AD-2). - Regenerate codegen output; commit under
android/.../generatedandios/generated— § Running codegen. - For Phase 0 (
app): include unified module resolver work inpackages/app(queue § reference pattern). - NewArch-AD-18 raw-resolver audit:
grepproduct code forgetReactNativeModule((excludepackages/app/lib/internal/nativeModule*.tsinfrastructure). For each hit, compare against the NewArch-AD-18 canonical exception table: existing row → confirm turbo module name + in-code// NewArch-AD-18 E<n>:comment; unlisted → bug (legacy name / should be wrapped) or new exception (add ADR row + rationale before merge). Every package migration PR must leave no unlisted raw call sites in that package's scope.
Phase 0 is a re-implementation pass under the matured ADRs — light in line count but necessary for correctness. Full checklist (also in queue P0 row):
| # | ADR / item | Work |
|---|---|---|
| 1 | NewArch-AD-7 | Rename functions codegenConfig.name → RNFBFunctionsTurboModules; regen + commit generated artifacts (do first — prevents wrong precedent for single-spec packages). Module name NativeRNFBTurboFunctions unchanged. |
| 2 | NewArch-AD-9 | iOS RNFBAppModule + RNFBUtilsModule: requiresMainQueueSetup → NO. |
| 3 | NewArch-AD-19 | Remove methodQueue = dispatch_get_main_queue() from both shells unless on-device validation proves a specific method needs it. |
| 4 | NewArch-AD-14 / NewArch-AD-14a | Replace eager wrap + flatten-onto-{}+Object.freeze with memoizing lazy Proxy + routing composite Proxy in nativeModule.ts. Drop dead multiModuleRoot[moduleName] boolean flags. |
| 5 | NewArch-AD-15 | Stop calling getConstants() on every resolve in withTurboConstants; memoize static constants only; Play Services → dynamic method (remove from utils constants typing when converted). |
| 6 | NewArch-AD-18 E6 | Fix messaging.isSupported(): NativeRNFBTurboUtils + androidGetPlayServicesStatus(). |
| 7 | NewArch-AD-18 E5, E7 | UtilsStatics.FilePath: memoized static utils accessor; app/lib/modular.ts meta/json/preferences → getAppModule(). |
| 8 | NewArch-AD-13 | Harness overrides: .gitignore tests/harness.overrides.js first, then harness.overrides.example.js + read hook in tests/app.js / tests/globals.js. |
| 9 | NewArch-AD-17.1 | Jest TurboModule enumeration/Proxy contract test including 2-host merge fixture — file packages/app/__tests__/nativeModuleContract.test.ts; scoped run: yarn tests:jest -- packages/app/__tests__/nativeModuleContract.test.ts (ADR §17.1). |
| 10 | Keep | NewArch-AD-8 for...in/Object.create, NewArch-AD-10 authDomains centralization, tests/globals.js NativeRNFBTurbo* proxy (E4). |
Out of scope for Phase 0 (deferred): NewArch-AD-18 E8–E10 (firestore/database/pnv until those packages migrate); dead legacy Java shells; iOS customAuthDomains rename.
Per package, repeat the functions shape:
-
specs/NativeRNFBTurbo*.ts— extendsTurboModule. -
package.jsoncodegenConfig—jsSrcsDir: "specs", androidjavaPackageName, iosmodulesProvider(one entry per spec; see multi-spec packages). -
Android codegen wiring —
react-native.config.js→platforms.android.cmakeListsPathpointing at the committed generated JNICMakeLists.txt(app package path differs fromfunctions: undersrc/reactnative/java/.../generated/jni/). Afteryarn codegen, verify CMake uses the compile macro for the monorepo React Native version:target_compile_optionson RN 0.78 (current test app),target_compile_reactnative_optionson RN 0.81+ (current@react-native/codegendefault).Forward compatibility: this is not a permanent fork. The committed generated
CMakeLists.txtis reproduced byyarn codegenagainst whatever@react-native/codegenthe monorepo resolves — when the test app moves to RN 0.81+, re-running codegen emitstarget_compile_reactnative_optionsand the artifact updates with it. There is no hand-maintained macro choice: the macro only has to match the RN version that generated it, andfunctionsalready proves the same pipeline regenerates cleanly across versions. Treat a macro mismatch as a stale committed artifact (re-run codegen), not as a code change to maintain. -
Android —
NativeRNFBTurbo*extends generated*Spec; register in package class. -
iOS —
.mmimplements spec;- (std::shared_ptr<TurboModule>)getTurboModule:returns generated JSI class. -
JS — update namespace config; no public API changes unless unavoidable.
-
Native business logic — prefer keeping ObjC++/Java shell + existing Swift helpers; language modernization is out of scope (queue rationale).
React Native Codegen turns specs/NativeRNFBTurbo*.ts into TypeScript, C++, Java, and ObjC++ artifacts. A name or type that is legal in TypeScript can still break generated native code. Check this list during spec authoring and before yarn codegen.
| Category | Do not use | Use instead | Why |
|---|---|---|---|
| Method names | delete |
deleteUser, deleteToken, deleteItem, etc. |
delete collides with generated C++ / native language keywords. |
| Type annotations | object |
A shaped object type, Object for truly dynamic Codegen maps, or a named interface |
RN Codegen rejects TypeScript object (TSObjectKeyword) in NativeModule specs. |
| Dynamic maps | Unshaped catch-all payloads when fields are known | A named interface with explicit fields |
Strong typed specs are easier to review and catch native/JS drift. |
| Any generated symbol | Language keywords / reserved identifiers (class, default, new, switch, case, try, catch, return, void, static, public, private, protected, namespace, operator, template, this, self) |
A domain-specific method or field name | Codegen emits bindings in multiple languages; avoid words reserved in any generated target. |
If Codegen requires a wrapper-object type such as Object, add a narrow lint disable in the spec file with a comment or local scope; do not "fix" it to primitive object, because that breaks Codegen. If a legacy method already uses a reserved word, rename the TurboModule spec method and keep the public JS API stable via the wrapper layer.
Repeat for every package migration. Skipping any row is the usual cause of TypeError: new Proxy target must be an Object (Android) or Native module NativeRNFBTurbo* is not registered (iOS/macOS) at runtime.
| # | Layer | Requirement | Reference |
|---|---|---|---|
| 1 | Spec | specs/NativeRNFBTurbo*.ts; module name = TurboModuleRegistry.get key. When JS reads this.native.<constant> (e.g. perf, in-app-messaging, app, utils), declare getConstants(): { … } in the spec — without it, codegen omits getConstants from the JSI method map and withTurboConstants has nothing to merge (runtime undefined on both platforms). |
app/specs/NativeRNFBTurboApp.ts |
| 2 | codegenConfig | name = aggregate library RNFB<Package>TurboModules (not the module name); includesGeneratedCode: true; ios.modulesProvider maps spec name → ObjC shell class |
firestore/package.json |
| 3 | codegen output | Regenerate and commit android/.../generated/ + ios/generated/ — see § Running codegen |
NewArch-AD-5 |
| 4 | react-native.config.js | platforms.android.cmakeListsPath → committed generated/jni/CMakeLists.txt — path is relative to package root and must match android:codegen outputPath (no duplicate android/ prefix) |
firestore/react-native.config.js |
| 5 | CMake macro | Generated CMakeLists.txt must use the macro for the test app's RN version (tests/package.json): target_compile_options on RN 0.78; target_compile_reactnative_options on RN 0.81+. Wrong macro → Android CMake configure fails or JNI never links. After yarn codegen, diff against functions (0.78) or re-run codegen when the test app upgrades. |
§ Android codegen wiring step 3 |
| 6 | Android shell | NativeRNFBTurbo* extends generated *Spec; only turbo shells registered in ReactPackage.createNativeModules. When the spec declares getConstants, implement protected getTypedExportedConstants() (generated *Spec owns public getConstants()), not a standalone public getConstants() override. |
perf/.../NativeRNFBTurboPerf.java |
| 7 | Android build.gradle | New-arch guard (newArchEnabled); sourceSets { java.excludes = ['**/generated/jni/**'] } when jni lives under java.srcDirs |
firestore/android/build.gradle |
| 8 | iOS shell | .mm: RCT_EXPORT_MODULE(NativeRNFBTurbo*) (exact spec name); getTurboModule: → NativeRNFBTurbo*SpecJSI; .h imports RNFB<Package>TurboModules.h and conforms to NativeRNFBTurbo*Spec (class extension or header — see functions). Constants: when the spec declares getConstants, use typed facebook::react::ModuleConstants<JS::NativeRNFBTurbo*::Constants::Builder> + [_RCTTypedModuleConstants newWithUnsafeDictionary:…] — not legacy - (NSDictionary *)getConstants / constantsToExport (JSI never surfaces those to TurboModuleRegistry.get). |
app/.../RNFBAppModule.mm |
| 9 | Podspec | Set pod_target_xcconfig HEADER_SEARCH_PATHS → ios/generated/RNFB<Package>TurboModules + ios/generated before install_modules_dependencies(s) (see firestore/RNFBFirestore.podspec). Assigning pod_target_xcconfig after install_modules_dependencies overwrites RN’s use_frameworks React-debug/Fabric header paths and breaks from-source iOS (react/timing/primitives.h → 'react/debug/flags.h' file not found). Also: new-arch guard; exclude_files for duplicate RN codegen providers (RCTModuleProviders.*, etc.). |
firestore/RNFBFirestore.podspec |
| 10 | JS | nativeModuleName → turbo name; turboModule: true; web shim registers turbo name |
firestore/lib/FirestoreModule.ts |
| 11 | jest.setup.ts | Mock NativeRNFBTurbo* (not legacy RNFB*Module) with methods on the correct host; include getConstants: () => ({ … }) when the package reads constants |
jest.setup.ts |
| 12 | Native rebuild | After steps 1–11: yarn tests:android:build and/or yarn tests:ios:build — Jest green alone does not prove native registration. Android: autolinkLibrariesFromCommand caches tests/android/build/generated/autolinking/autolinking.json keyed only on tests/{package.json,yarn.lock,react-native.config.js} — adding or changing a package react-native.config.js does not invalidate the cache; delete autolinking.json + *.sha under that folder (or touch tests/package.json) then rebuild, or JNI never links (Proxy target must be an Object). iOS: yarn tests:ios:build runs pod install + Xcode compile; required after codegen / modulesProvider / podspec changes. |
tests/android/settings.gradle |
Symptom → likely miss: Android Proxy target must be an object → rows 4–7 or 12 (CMake not linked / stale autolinking cache). JS reads undefined for this.native.isFoo / constant-backed getters on either platform → row 1 (spec missing getConstants), row 6 (Android getTypedExportedConstants), row 8 (iOS still using NSDictionary constants). Metro redbox Requiring unknown module "undefined" at app load (before Jet connects) → not a registration miss — stale JS/native toolchain; run checklist row 12 + Metro reset-cache; escalate to full toolchain refresh if it persists. iOS Native module NativeRNFBTurbo* is not registered → rows 2, 8, 9, 12. iOS unrecognized selector on turbo method → row 8 (legacy RCT_EXPORT_METHOD signatures instead of spec protocol). iOS build missing generated header → row 9 (HEADER_SEARCH_PATHS). iOS 'react/debug/flags.h' file not found via React-timing while compiling a package → row 9 (pod_target_xcconfig must precede install_modules_dependencies).
Package package.json scripts (yarn android:codegen / yarn ios:codegen) are convenience wrappers; cd packages/<pkg> && yarn ios:codegen often fails after a clean install (unknown command 'codegen') because @react-native-community/cli resolves from the test app workspace, not the library package cwd.
Canonical — from repo root, cd tests, one platform at a time; --outputPath must match the package's android:codegen / ios:codegen script (paths differ per package — copy from functions/package.json):
cd tests
npx @react-native-community/cli codegen \
--path ../packages/<pkg> \
--platform android \
--source library \
--outputPath ../packages/<pkg>/<android:codegen outputPath from package.json>
npx @react-native-community/cli codegen \
--path ../packages/<pkg> \
--platform ios \
--source library \
--outputPath ../packages/<pkg>/ios/generatedExample (perf Android): --outputPath ../packages/perf/android/src/reactnative/java/io/invertase/firebase/perf/generated.
After regen: commit generated dirs, then checklist row 12 (:build + Metro reset-cache before :test-cover).
Unit-focused tier per change authoring § implementation inner loop and TurboModule area harness below.
Shared infrastructure (already landed for functions):
packages/app/lib/internal/registry/nativeModule.ts—turboModuleflag, iOS null encodingpackages/app/lib/internal/nullSerialization.ts+ native interceptor
On a frozen tree — full change authoring § independent-review, plus:
- New Architecture build on each touched platform (native bridge change).
- Package e2e at area-focused tier with area harness applied locally before first
:test-cover. - iOS: exercise null-in-object payloads where the package passes option maps (RN #52802) — highest risk:
auth,firestore,database,storage.
Extends change authoring § harness narrowing.
Area setup (required for unit-focused and area-focused tiers): copy tests/harness.overrides.example.js to gitignored tests/harness.overrides.js — set modules to the package under migration and RNFBDebug: true. Load that package's full packages/<pkg>/e2e/*.e2e.js specs via committed require.context (running e2e § local overrides).
| Package | Harness modules key |
Typical e2e entry |
|---|---|---|
app |
app |
packages/app/e2e/ |
auth |
auth |
packages/auth/e2e/ |
firestore |
firestore |
packages/firestore/e2e/ (may coexist with Pipeline specs — load package module only) |
functions |
functions |
packages/functions/e2e/ (reference; already migrated) |
in-app-messaging |
inAppMessaging |
packages/in-app-messaging/e2e/ |
app-distribution |
appDistribution |
packages/app-distribution/e2e/ |
installations |
installations |
packages/installations/e2e/ |
perf |
perf |
packages/perf/e2e/ |
ml |
ml |
packages/ml/e2e/ |
| Others | match platformSupportedModules key in tests/app.js |
packages/<pkg>/e2e/ |
Harness key ≠ npm folder name for camelCase entries (inAppMessaging, appDistribution, remoteConfig, …). Always copy the string from tests/app.js platformSupportedModules — wrong keys load zero specs or the wrong package (Phase 2 review pitfall).
Sanity check: pass counts must match loaded scope — not full-app totals (running e2e § gate).
TurboModule NativeModules proxy: package e2e that reads turbo shells directly (e.g. events.e2e.js → NativeModules.NativeRNFBTurboApp) requires the harness proxy in tests/globals.js to route NativeRNFBTurbo* names through getReactNativeModule(), not only the legacy RNF* prefix. Without that, the proxy returns a no-op stub and event tests hang. Durable committed wiring — not part of overrides narrowing (NewArch-AD-13).
Push state (committed): full test app remains default for CI. Local :test-cover during migration uses area narrowing even when git has full harness.
Phase R / coordinated break: revert all narrowing; full tier 3-platform run before monorepo major ships (change authoring § pre-merge-validation).
Runs after every native package is on TurboModules (Phases 0–5), before Phase R. Queue rationale and scope discriminator: migration work queue § Phase S. This section owns the procedure.
What it fixes: Some RNFB methods are typed Promise<T> only because the legacy bridge made every call async — firebase-js-sdk's equivalent is synchronous. TurboModules support sync JSI methods, so those return to sync parity. These deltas are visible in compare:types configs as async-vs-sync differences.
Per-package loop (same change authoring work types; one focused commit per package):
gap-analysis— Read the package'scompare:typesconfig (.github/scripts/compare-types/configs/<pkg>.ts) for documented async-vs-sync differences. For each, apply the scope discriminator: keepPromise<T>if the native work has real latency (network/disk/keychain/token); only convert pure bridge-forced async.implementation— Declare the spec method sync (dropPromise) inspecs/NativeRNFBTurbo*.ts; re-runyarn codegen; update the native shell to return synchronously (no resolver); update JS (modular.ts) to call sync; update types inlib/. Unit-focused tier.independent-review— area-focused tier on a frozen tree; verify no consumer-visible behavior regressions beyond the intended async→sync change.documentation— Remove the now-resolved entry from the package'scompare:typesconfig (the difference is gone); note the API change in the migration guide for the coordinated major.commit—refactor(<pkg>): return sync parity for bridge-forced async APIs(orfeat(<pkg>):if the public type change is the headline). One commit per package.
Completion signal (per package): the corresponding async-vs-sync entry is removed from the package's compare:types config and yarn compare:types is clean for that package (validation checklist § type parity).
Caution: sync across JSI runs on the JS thread — never convert a method that does real I/O. When unsure, keep it async: over-converting is a perf/ANR risk; under-converting keeps the correct async signature but leaves a documented async→sync parity gap tracked in compare:types until resolved.
Per package (or per phase batch), same commit when user-facing:
User docs
- Migration guide update for coordinated New Architecture break (final phase only unless package already ships new-arch-only like
functions) - Package install notes if podspec/build requirements change
OKF bundle maintenance
- Update migration work queue gate rows when items close
- Record non-obvious codegen or null-serialization choices here or in queue historical notes — not only commit messages
feat(<pkg>)!: migrate <module-or-package> to TurboModules
Breaking change (!): TurboModule migration requires New Architecture; legacy bridge is removed per package.
Never stage: tests/harness.overrides.js, any .only, temporary sub-suite edits in tests/app.js.
Before git commit: validation evidence package recorded; coverage evidence package when lib/native bridge touched (change authoring § commit).
- macOS / web turbo name registration —
nativeModuleWeb.tsregisters JS-SDK shims by module name in the registry object initializer (not deferred to bottom-of-file calls) soNativeRNFBTurbo*names exist beforeRNFBNativeEventEmitterinstantiates during circular imports. WhenAPP_NATIVE_MODULEchanges (NewArch-AD-2), register both legacyRNFBAppModuleand turbo keys. Missing registration → macOS blank window /Native module NativeRNFBTurboApp is not registeredincom.facebook.react.log:javascript. - Events deferred (NewArch-AD-4) — keep
RNFBRCTEventEmitter/nativeEventsfan-out unless testing forces escalation. - Swift / ObjC interop — TurboModule shell stays ObjC++; follow functions podspec patterns for
use_frameworks!and non-framework builds. - Unified resolver (NewArch-AD-6) —
TurboModuleRegistry.getwithNativeModulesfallback inpackages/app; fallback removed at Phase R. requiresMainQueueSetup=NO(NewArch-AD-9) — iOS shells must returnNO;YESblocks sync methods and risks main-thread deadlock under TurboModules. AuditgetConstants/init/methodQueuefor genuine main-thread work as part of gap-analysis; dispatch only that work explicitly.- TurboModule JS enumeration & wrapping (NewArch-AD-8 + NewArch-AD-14) — prototype lazy loading means
Object.keys/ spread /Object.assign({}, host)break method access (XYZ is not a function). Enumerate withfor...in; the module surface is a memoizing lazyProxy(NewArch-AD-14), and multi-module packages use the routing composite Proxy (NewArch-AD-14a) — not a flattened-onto-{}+Object.freezeobject. Do not copy TurboModule hosts into{}. Use the wrapped surface by default; rawgetReactNativeModuleonly per NewArch-AD-18. - Android cross-module native shared state (NewArch-AD-10) — turbo shells may expose
public staticfields read by other packages' native code. Phase 0:NativeRNFBTurboApp.authDomains(populated oninitializeApp; read byRCTConvertFirebaseandReactNativeFirebaseAuthModule). Unregistered legacy bridge classes may delegate to the turbo shell — do not duplicate the map on legacy classes. - iOS auth-domain naming — iOS keeps historical
customAuthDomains+getCustomDomain:on the turbo shell (RNFBAppModule.mm); Android usesauthDomainsonNativeRNFBTurboApp. Same semantics; intentional cross-platform naming carry-over. - Spec Promise typing (Android) — Codegen Android methods take
Promiseargs even when the legacy bridge was sync void. Example: Play Services helpers inNativeRNFBTurboUtils— declarePromise<PlayServicesAvailability>/Promise<void>; native resolves the promise. - Android codegen output path families — always copy
android:codegenoutputPathfrom the targetpackage.json(do not assume one layout).src/reactnative/java/.../generated:app,firestore,perf,in-app-messaging,ml.src/main/java/.../generated:functions,installations,app-distribution.react-native.config.jscmakeListsPathmust match the same tree. - Duplicate generated trees — a wrong initial
outputPathor regen into a second folder leaves twogenerated/trees (e.g.src/main/java+src/reactnative/java, orfiam/+in_app_messaging/). Android then fails withduplicate class: NativeRNFBTurbo*Spec. Keep one canonical tree; delete stale dirs; align shell imports,build.gradlesourceSets, andcmakeListsPath. - Dead legacy shells (removed) — unregistered legacy Java bridge modules in
app,utils,in-app-messaging,installations,perf, andremote-configwere deleted after turbo-shell registration and turbo-path verification. Do not reintroduce duplicate shells alongside turbo registrars. - CMake macro vs test-app RN version — see registration checklist § row 5.
yarn codegenuses root@react-native/codegen; if it emitstarget_compile_reactnative_optionsbut the test app is still RN 0.78, replace withtarget_compile_options(copy fromfunctionsCMakeLists) before first:test-cover. - TurboModule constants (both platforms) — legacy bridge exposed constants as enumerable keys on the module object. TurboModules require the full chain in checklist rows 1, 6, 8: spec
getConstants()→ codegen → AndroidgetTypedExportedConstants→ iOS typedModuleConstants+_RCTTypedModuleConstants→withTurboConstantsinnativeModuleAndroidIos.ts. Skipping the spec or leaving iOS onNSDictionary *producesundefinedin constructors likethis._foo = this.native.isFooeven when methods work. Packages without constants (functions,installations, …) skip the constants rows. - Android autolinking cache — see checklist row 12.
npx react-native configcan show the correctcmakeListsPathwhiletests/android/build/generated/autolinking/autolinking.jsonis still stale. - Metro
Requiring unknown module "undefined"— Metro runtime error whenrequire()gets module idundefined. Not the same as native constantundefinedorProxy target must be an Object. Usually stale Metro cache and/or partial refresh after spec/codegen/lib/**/podspec changes while iOS/Android debug still loads a live bundle from:8081(running e2e § stale toolchain). A staticindex.bundlefetch can succeed while the simulator still shows the redbox — treat as toolchain staleness, not a missingimport. Escalation: full toolchain refresh. - Codegen CLI cwd — see § Running codegen. Do not debug
unknown command 'codegen'by patching package scripts; run fromtests/. - phone-number-verification — bypasses
createModuleNamespace; wire spec + resolver directly inmodular.ts.
Live phase status and arbiter gates: migration work queue (ephemeral).