diff --git a/.changeset/yellow-poems-go.md b/.changeset/yellow-poems-go.md
new file mode 100644
index 00000000..872005c8
--- /dev/null
+++ b/.changeset/yellow-poems-go.md
@@ -0,0 +1,9 @@
+---
+'@callstack/brownfield-navigation': minor
+'@callstack/brownfield-cli': minor
+'brownfield': minor
+'@callstack/brownie': minor
+'@callstack/react-native-brownfield': minor
+---
+
+add brownfield navigation
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index 942b1d3e..3d22c1b4 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -20,6 +20,10 @@ runs:
run: yarn install
shell: bash
+ - name: Build packages
+ run: yarn build
+ shell: bash
+
- name: Restore Turbo cache
if: inputs.restore-turbo-cache == 'true'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
diff --git a/apps/AndroidApp/app/src/main/AndroidManifest.xml b/apps/AndroidApp/app/src/main/AndroidManifest.xml
index bda9be43..8be33948 100644
--- a/apps/AndroidApp/app/src/main/AndroidManifest.xml
+++ b/apps/AndroidApp/app/src/main/AndroidManifest.xml
@@ -13,6 +13,14 @@
android:supportsRtl="true"
android:theme="@style/Theme.AndroidBrownfieldApp"
android:usesCleartextTraffic="true">
+
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "Referrals",
+ style = MaterialTheme.typography.headlineMedium
+ )
+ Text(
+ text = "Opened from BrownfieldNavigation.navigateToReferrals(userId).",
+ textAlign = TextAlign.Center
+ )
+ Text(
+ text = "userId: $userId",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ Button(onClick = { finish() }) {
+ Text("Go back")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ const val EXTRA_USER_ID = "extra_user_id"
+ }
+}
diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/SettingsActivity.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/SettingsActivity.kt
new file mode 100644
index 00000000..fd127452
--- /dev/null
+++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/SettingsActivity.kt
@@ -0,0 +1,53 @@
+package com.callstack.brownfield.android.example
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme
+
+class SettingsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ setContent {
+ AndroidBrownfieldAppTheme {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "Settings",
+ style = MaterialTheme.typography.headlineMedium
+ )
+ Text(
+ text = "Opened from BrownfieldNavigation.navigateToSettings().",
+ textAlign = TextAlign.Center
+ )
+ Button(onClick = { finish() }) {
+ Text("Go back")
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj
index 94258f6d..78323de0 100644
--- a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj
+++ b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj
@@ -7,14 +7,16 @@
objects = {
/* Begin PBXBuildFile section */
- 7926B0E22F4E5A6400694E68 /* BrownfieldLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */; };
- 7926B0E32F4E5A6400694E68 /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
- 7926B0E42F4E5A6600694E68 /* hermesvm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */; };
- 7926B0E52F4E5A6600694E68 /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
- 7926B0E62F4E5A6700694E68 /* ReactBrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */; };
- 7926B0E72F4E5A6700694E68 /* ReactBrownfield.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
- 7926B0E82F4E5A6800694E68 /* Brownie.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */; };
- 7926B0E92F4E5A6800694E68 /* Brownie.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 614B23922F50633200CB6363 /* BrownfieldLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */; };
+ 614B23932F50633200CB6363 /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 614B23942F50633200CB6363 /* Brownie.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238E2F50633200CB6363 /* Brownie.xcframework */; };
+ 614B23952F50633200CB6363 /* Brownie.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238E2F50633200CB6363 /* Brownie.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 614B23962F50633200CB6363 /* BrownfieldNavigation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */; };
+ 614B23972F50633200CB6363 /* BrownfieldNavigation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 614B23982F50633200CB6363 /* hermesvm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23902F50633200CB6363 /* hermesvm.xcframework */; };
+ 614B23992F50633200CB6363 /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23902F50633200CB6363 /* hermesvm.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 614B239A2F50633200CB6363 /* ReactBrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23912F50633200CB6363 /* ReactBrownfield.xcframework */; };
+ 614B239B2F50633200CB6363 /* ReactBrownfield.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23912F50633200CB6363 /* ReactBrownfield.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -24,10 +26,11 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
- 7926B0E92F4E5A6800694E68 /* Brownie.xcframework in Embed Frameworks */,
- 7926B0E52F4E5A6600694E68 /* hermesvm.xcframework in Embed Frameworks */,
- 7926B0E72F4E5A6700694E68 /* ReactBrownfield.xcframework in Embed Frameworks */,
- 7926B0E32F4E5A6400694E68 /* BrownfieldLib.xcframework in Embed Frameworks */,
+ 614B23972F50633200CB6363 /* BrownfieldNavigation.xcframework in Embed Frameworks */,
+ 614B23952F50633200CB6363 /* Brownie.xcframework in Embed Frameworks */,
+ 614B239B2F50633200CB6363 /* ReactBrownfield.xcframework in Embed Frameworks */,
+ 614B23992F50633200CB6363 /* hermesvm.xcframework in Embed Frameworks */,
+ 614B23932F50633200CB6363 /* BrownfieldLib.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -35,10 +38,11 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
- 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = ""; };
- 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = ""; };
- 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = ""; };
- 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = ""; };
+ 614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = ""; };
+ 614B238E2F50633200CB6363 /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = ""; };
+ 614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldNavigation.xcframework; path = package/BrownfieldNavigation.xcframework; sourceTree = ""; };
+ 614B23902F50633200CB6363 /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = ""; };
+ 614B23912F50633200CB6363 /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = ""; };
793C76A72EEBF938008A2A34 /* Brownfield Apple App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -55,25 +59,35 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 7926B0E82F4E5A6800694E68 /* Brownie.xcframework in Frameworks */,
- 7926B0E62F4E5A6700694E68 /* ReactBrownfield.xcframework in Frameworks */,
- 7926B0E42F4E5A6600694E68 /* hermesvm.xcframework in Frameworks */,
- 7926B0E22F4E5A6400694E68 /* BrownfieldLib.xcframework in Frameworks */,
+ 614B23962F50633200CB6363 /* BrownfieldNavigation.xcframework in Frameworks */,
+ 614B23942F50633200CB6363 /* Brownie.xcframework in Frameworks */,
+ 614B239A2F50633200CB6363 /* ReactBrownfield.xcframework in Frameworks */,
+ 614B23982F50633200CB6363 /* hermesvm.xcframework in Frameworks */,
+ 614B23922F50633200CB6363 /* BrownfieldLib.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 6108E5322F40A26800EA8FA1 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */,
+ 614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */,
+ 614B238E2F50633200CB6363 /* Brownie.xcframework */,
+ 614B23902F50633200CB6363 /* hermesvm.xcframework */,
+ 614B23912F50633200CB6363 /* ReactBrownfield.xcframework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
793C769E2EEBF938008A2A34 = {
isa = PBXGroup;
children = (
793C76A92EEBF938008A2A34 /* Brownfield Apple App */,
+ 6108E5322F40A26800EA8FA1 /* Frameworks */,
793C76A82EEBF938008A2A34 /* Products */,
- 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */,
- 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */,
- 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */,
- 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */,
);
sourceTree = "";
};
diff --git a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift
index f4eab6a8..22e2e83f 100644
--- a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift
+++ b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift
@@ -2,6 +2,8 @@ import BrownfieldLib
import Brownie
import ReactBrownfield
import SwiftUI
+import UIKit
+import BrownfieldNavigation
class AppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?
@@ -18,6 +20,55 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
}
+public class RNNavigationDelegate: BrownfieldNavigationDelegate {
+ public func navigateToSettings() {
+ present(SettingsScreen())
+ }
+
+ public func navigateToReferrals(_ userId: String) {
+ present(ReferralsScreen(userId: userId))
+ }
+
+ private func present(_ view: Content) {
+ DispatchQueue.main.async {
+ let hostingController = UIHostingController(rootView: view)
+
+ guard let topController = UIApplication.shared.topMostViewController() else {
+ return
+ }
+
+ if let navigationController = topController.navigationController {
+ navigationController.pushViewController(hostingController, animated: true)
+ return
+ }
+
+ let navigationController = UINavigationController(rootViewController: hostingController)
+ topController.present(navigationController, animated: true)
+ }
+ }
+}
+
+private extension UIApplication {
+ func topMostViewController(
+ base: UIViewController? = UIApplication.shared.connectedScenes
+ .compactMap { $0 as? UIWindowScene }
+ .flatMap { $0.windows }
+ .first(where: { $0.isKeyWindow })?.rootViewController
+ ) -> UIViewController? {
+ if let navigationController = base as? UINavigationController {
+ return topMostViewController(base: navigationController.visibleViewController)
+ }
+ if let tabBarController = base as? UITabBarController,
+ let selected = tabBarController.selectedViewController {
+ return topMostViewController(base: selected)
+ }
+ if let presented = base?.presentedViewController {
+ return topMostViewController(base: presented)
+ }
+ return base
+ }
+}
+
@main
struct BrownfieldAppleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@@ -28,6 +79,10 @@ struct BrownfieldAppleApp: App {
print("React Native has been loaded")
}
+ BrownfieldNavigationManager.shared.setDelegate(
+ navigationDelegate: RNNavigationDelegate()
+ )
+
#if USE_EXPO_HOST
ReactNativeBrownfield.shared.ensureExpoModulesProvider()
#endif
diff --git a/apps/AppleApp/Brownfield Apple App/components/ReferralsScreen.swift b/apps/AppleApp/Brownfield Apple App/components/ReferralsScreen.swift
new file mode 100644
index 00000000..e1f37d1b
--- /dev/null
+++ b/apps/AppleApp/Brownfield Apple App/components/ReferralsScreen.swift
@@ -0,0 +1,28 @@
+import SwiftUI
+
+
+struct ReferralsScreen: View {
+ let userId: String
+
+ var body: some View {
+ VStack(spacing: 16) {
+ Text("Referrals")
+ .font(.title2)
+ .fontWeight(.semibold)
+
+ Text("User ID")
+ .foregroundStyle(.secondary)
+ Text(userId)
+ .font(.body.monospaced())
+ .textSelection(.enabled)
+
+ Button("Share referral link") {
+ // Placeholder action for the sample app.
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .padding()
+ .navigationTitle("Referrals")
+ }
+}
\ No newline at end of file
diff --git a/apps/AppleApp/Brownfield Apple App/components/SettingsScreen.swift b/apps/AppleApp/Brownfield Apple App/components/SettingsScreen.swift
new file mode 100644
index 00000000..d5f584c3
--- /dev/null
+++ b/apps/AppleApp/Brownfield Apple App/components/SettingsScreen.swift
@@ -0,0 +1,22 @@
+import SwiftUI
+
+struct SettingsScreen: View {
+ var body: some View {
+ Form {
+ Section("Preferences") {
+ Toggle("Enable notifications", isOn: .constant(true))
+ Toggle("Dark mode", isOn: .constant(false))
+ }
+
+ Section("About") {
+ HStack {
+ Text("Version")
+ Spacer()
+ Text("1.0.0")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ .navigationTitle("Settings")
+ }
+}
\ No newline at end of file
diff --git a/apps/AppleApp/prepareXCFrameworks.js b/apps/AppleApp/prepareXCFrameworks.js
index 9c6a2be5..f161180d 100644
--- a/apps/AppleApp/prepareXCFrameworks.js
+++ b/apps/AppleApp/prepareXCFrameworks.js
@@ -79,6 +79,7 @@ const validNames = [
'Brownie.xcframework',
'hermesvm.xcframework',
'ReactBrownfield.xcframework',
+ 'BrownfieldNavigation.xcframework',
];
for (const file of fs.readdirSync(targetPackagePath)) {
diff --git a/apps/ExpoApp/RNApp.tsx b/apps/ExpoApp/RNApp.tsx
index a87add22..1d578d96 100644
--- a/apps/ExpoApp/RNApp.tsx
+++ b/apps/ExpoApp/RNApp.tsx
@@ -1,6 +1,7 @@
import { SafeAreaView } from 'react-native-safe-area-context';
-import { StyleSheet, Text, View } from 'react-native';
+import { Button, StyleSheet, Text, View } from 'react-native';
import Counter from './components/counter';
+import BrownfieldNavigation from '@callstack/brownfield-navigation';
export default function RNApp() {
return (
@@ -9,6 +10,15 @@ export default function RNApp() {
+
+
);
diff --git a/apps/ExpoApp/brownfield.navigation.ts b/apps/ExpoApp/brownfield.navigation.ts
new file mode 100644
index 00000000..8bc90246
--- /dev/null
+++ b/apps/ExpoApp/brownfield.navigation.ts
@@ -0,0 +1,12 @@
+export interface BrownfieldNavigationSpec {
+ /**
+ * Navigate to the native settings screen
+ */
+ navigateToSettings(): void;
+
+ /**
+ * Navigate to the native referrals screen
+ * @param userId - The user's unique identifier
+ */
+ navigateToReferrals(userId: string): void;
+}
diff --git a/apps/ExpoApp/package.json b/apps/ExpoApp/package.json
index 9244f052..c469f44f 100644
--- a/apps/ExpoApp/package.json
+++ b/apps/ExpoApp/package.json
@@ -16,6 +16,7 @@
"brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release"
},
"dependencies": {
+ "@callstack/brownfield-navigation": "workspace:^",
"@callstack/brownie": "workspace:^",
"@callstack/react-native-brownfield": "workspace:^",
"@expo/vector-icons": "^15.0.3",
diff --git a/apps/RNApp/brownfield.navigation.ts b/apps/RNApp/brownfield.navigation.ts
new file mode 100644
index 00000000..8bc90246
--- /dev/null
+++ b/apps/RNApp/brownfield.navigation.ts
@@ -0,0 +1,12 @@
+export interface BrownfieldNavigationSpec {
+ /**
+ * Navigate to the native settings screen
+ */
+ navigateToSettings(): void;
+
+ /**
+ * Navigate to the native referrals screen
+ * @param userId - The user's unique identifier
+ */
+ navigateToReferrals(userId: string): void;
+}
diff --git a/apps/RNApp/ios/Podfile.lock b/apps/RNApp/ios/Podfile.lock
index 73debef9..a9c0d5b3 100644
--- a/apps/RNApp/ios/Podfile.lock
+++ b/apps/RNApp/ios/Podfile.lock
@@ -1,5 +1,33 @@
PODS:
- boost (1.84.0)
+ - BrownfieldNavigation (3.0.0):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
- Brownie (3.0.0):
- boost
- DoubleConversion
@@ -2530,6 +2558,7 @@ PODS:
DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
+ - "BrownfieldNavigation (from `../node_modules/@callstack/brownfield-navigation`)"
- "Brownie (from `../node_modules/@callstack/brownie`)"
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
@@ -2616,6 +2645,8 @@ SPEC REPOS:
EXTERNAL SOURCES:
boost:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
+ BrownfieldNavigation:
+ :path: "../node_modules/@callstack/brownfield-navigation"
Brownie:
:path: "../node_modules/@callstack/brownie"
DoubleConversion:
@@ -2772,7 +2803,8 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
- Brownie: 914f0a636f17d04fa6a1cd1d057d1439d868918f
+ BrownfieldNavigation: 12a34a451661d8f685beebab19d4ba7b43efc409
+ Brownie: 981350e32e072e5b55b624eb8810ba9bbc9683d9
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
FBLazyVector: 0aa6183b9afe3c31fc65b5d1eeef1f3c19b63bfa
@@ -2788,68 +2820,68 @@ SPEC CHECKSUMS:
React-Core: 956ac86b4d9b0c0fd9a14b9cc533aa297bb501c0
React-CoreModules: 3a8d39778cf9eeca40e419814e875da1a8e29855
React-cxxreact: db275765e1eb08f038599fb44114cf57ee0d18cd
- React-debug: 1dfa1d1cbd93bdaffa3b140190829f9fd9e27985
- React-defaultsnativemodule: 35f353ba06901fb5e374bc56e750fde05cbb05b9
- React-domnativemodule: cf9e1b1b520ce0e66396c2744b3eb6d419711c13
- React-Fabric: c0b0c1ad70476d354b3da9fef96094f7b37804da
- React-FabricComponents: 8c6861c5233cf0d5685cee301a979313090e2f57
- React-FabricImage: cef8883d2fb6c892003fefcad261d2898adbe926
- React-featureflags: 0e2b969019c2b118de64a6d4c55ef7c05f2b0f1d
- React-featureflagsnativemodule: e1ef619d14fe0a68d4783b32293309dbb13ef2a5
- React-graphics: 0fc6b7acaff7161bda05bf8bffceacc2b0b4e38d
+ React-debug: c8356d908286b1dc4cf90cd0977227dd61b7b1eb
+ React-defaultsnativemodule: df1a41d072194c96d0077dd30ee8d5d452397f26
+ React-domnativemodule: 8abd63d26685a5c1c88c8ccc902876dc9c0e2d6f
+ React-Fabric: 1dea7e164181d7d688cfbd70a6e5f026e2df6bf5
+ React-FabricComponents: 2a6f81481fa240a9239536402d72823f9d642925
+ React-FabricImage: 513940cfd43193d3befb45dba9911f936bd74df7
+ React-featureflags: bc1d980ff8356b931cd87c16700a39aaede1ed5a
+ React-featureflagsnativemodule: 4f7beedf0c241c44dcffc51e52a6178b5e0d541d
+ React-graphics: 69311413b44b6d228dbc705d8ce57ad0a4d55daf
React-hermes: b454b9352bc26e638704d103009f659a125b86d3
- React-idlecallbacksnativemodule: 35ab292f8404c469744db5a5dd5f0f27f95e5ebf
- React-ImageManager: 3312c550ebcf6b7d911d9993082adcb3e1407ce8
- React-jserrorhandler: 2a7f2d94566f05f8cb82288afd46bc0fd8b2ffc7
+ React-idlecallbacksnativemodule: d15d469a152b7677d184a9538fae0744692e4575
+ React-ImageManager: cce591e16cc6fa63ad5d45de012b4ddf31fd21e9
+ React-jserrorhandler: 05fb248a535148a7eec94c786bd0e9e1413c6b3a
React-jsi: 7aa265cf8372d8385ccc7935729e76d27e694dfe
React-jsiexecutor: 8dd53bebfb3bc12f0541282aa4c858a433914e37
- React-jsinspector: f89b9ae62a4e2f6035b452442ef20a7f98f9cb27
- React-jsinspectorcdp: 44e46c1473a8deecf7b188389ed409be83fb3cc7
- React-jsinspectornetwork: dc9524f6e3d7694b1b6f4bd22dedad8ccc2c0a80
- React-jsinspectortracing: 0166ebbdfb125936a5d231895de3c11a19521dfc
- React-jsitooling: 34692514ec8d8735938eda3677808a58f41c925b
- React-jsitracing: a598dae84a87f8013635d09c5e7884023bda8501
+ React-jsinspector: 0f62d1ffa7242033a1106f0af9f83ec12a381401
+ React-jsinspectorcdp: 5ae22d48dcf03812cd4f8c4a6fd7c7204cd8789d
+ React-jsinspectornetwork: 9052eb6bbd876bfdafa1605874dd848511236844
+ React-jsinspectortracing: 6d89a5caab7b86947607cf654fc94cf1c31f8330
+ React-jsitooling: ecbd81f751b79ba748d4d0d54445da1b53e363fd
+ React-jsitracing: 8068734240da604902fead29287dc21b820bc7d3
React-logger: 500f2fa5697d224e63c33d913c8a4765319e19bf
- React-Mapbuffer: 06d59c448da7e34eb05b3fb2189e12f6a30fec57
- React-microtasksnativemodule: d1ee999dc9052e23f6488b730fa2d383a4ea40e5
- react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
- React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0
+ React-Mapbuffer: 4c50cf6af44286015a20a5995d5321f625c93459
+ React-microtasksnativemodule: a84b9331106616ab1fa36de9ae555718d4bbdcf5
+ react-native-safe-area-context: 0a3b034bb63a5b684dd2f5fffd3c90ef6ed41ee8
+ React-NativeModulesApple: efd0906463c79d9b86197dbcf0d58358dff8c5ed
React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb
React-perflogger: 2e229bf33e42c094fd64516d89ec1187a2b79b5b
- React-performancecdpmetrics: 05ba4bd83f36acf192071bb5d9c8f45faf04d140
- React-performancetimeline: bfc96fcd2b79f7489dd54e3df4cba186dd8dd141
+ React-performancecdpmetrics: fd9bbc52960c6aa008fdae263849eb14411ae13e
+ React-performancetimeline: 16eaea3f8be5d42eb3bf8a261d87df2fe7e6e111
React-RCTActionSheet: 2399bb6cc8adaef2e5850878102fea2ad1788a0e
React-RCTAnimation: d1deb6946e83e22a795a7d0148b94faad8851644
React-RCTAppDelegate: 10b35d5cec3f8653f6de843ae800b3ba8050b801
React-RCTBlob: 85150378edc42862d7c13ff2502693f32b174f91
- React-RCTFabric: 736f9da3ad57e2cef5fa4c132999933a89bb8378
- React-RCTFBReactNativeSpec: 705ec584758966950a31fa235539b57523059837
+ React-RCTFabric: f57a14a48756480a7c96670d633cb39692eed453
+ React-RCTFBReactNativeSpec: 725c3bb08b2f86741df136455960f2b58dd8f6e4
React-RCTImage: bb6cbdc22698b3afc8eb8d81ef03ee840d24c6f6
React-RCTLinking: e8b006d101c45651925de3e82189f03449eedfe7
React-RCTNetwork: 7999731af05ec8f591cbc6ad4e29d79e209c581a
- React-RCTRuntime: 99d8a2a17747866fb972561cdb205afe9b26d369
+ React-RCTRuntime: cdbbadadafcad5836fb0616073d7011c39c30ffd
React-RCTSettings: 839f334abb92e917bc24322036081ffe15c84086
React-RCTText: 272f60e9a5dbfd14c348c85881ee7d5c7749a67c
React-RCTVibration: 1ffa30a21e2227be3afe28d657ac8e6616c91bae
- React-rendererconsistency: 3c3e198aba0255524ed7126aa812d22ce750d217
- React-renderercss: 6b3ce3dfadf991937ae3229112be843ef1438c32
- React-rendererdebug: baf9e1daa07ac7f9aca379555126d29f963ba38b
- React-RuntimeApple: 4136aee89257894955ef09e9f9ef74f0c27596be
- React-RuntimeCore: e9a743d7de4bbd741b16e10b26078d815d6513ab
- React-runtimeexecutor: 781e292362066af82fa2478d95c6b0e374421844
- React-RuntimeHermes: 6ab3c2847516769fc860d711814f1735859cad74
- React-runtimescheduler: 824c83a5fd68b35396de6d4f2f9ae995daac861b
- React-timing: 1ebc7102dd52a3edcc63534686bb156e12648411
- React-utils: abf37b162f560cd0e3e5d037af30bb796512246d
- React-webperformancenativemodule: 50a57c713a90d27ae3ab947a6c9c8859bcb49709
+ React-rendererconsistency: a51dcbe4b3c1159413cfdb85abace6a5c871a4b3
+ React-renderercss: 5fdc31a529021337e7eac6f1e9bf4410947b877e
+ React-rendererdebug: 8427d2e5d1b7e39971c9c59e55bbfcb7884a942f
+ React-RuntimeApple: 9ba3723a539ed1701b8ba08dc317f1255c269a37
+ React-RuntimeCore: 61b10d50472e29cd1ec98aba797d0d8d4f325283
+ React-runtimeexecutor: 8e5135a09dcb012a15a025dc514361c927ea5db9
+ React-RuntimeHermes: f06c7288967d0209fc075e5eabd5e851580047e9
+ React-runtimescheduler: bd92275b3a847c71d10210ae89a8e04dba076630
+ React-timing: 91f11a6537770b698eb8152e4669012992710b27
+ React-utils: f06ff240e06e2bd4b34e48f1b34cac00866e8979
+ React-webperformancenativemodule: b3398f8175fa96d992c071b1fa59bd6f9646b840
ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176
- ReactBrownfield: 3137236180ff93a812c6303974607eddb58b183d
- ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9
- ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424
- RNScreens: ffbb0296608eb3560de641a711bbdb663ed1f6b4
+ ReactBrownfield: 03a2fd2f61109c00810b8d82af6f8907095191ed
+ ReactCodegen: 0bce2d209e2e802589f4c5ff76d21618200e74cb
+ ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654
+ RNScreens: d6413aeb1878cdafd3c721e2c5218faf5d5d3b13
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- Yoga: 8e01cef9947ca77f0477a098f0b32848a8e448c6
+ Yoga: 526f25666395d30c297d53154398ffd249eaf9e1
PODFILE CHECKSUM: 7c116a16dd0744063c8c6293dbfc638c9d447c19
-COCOAPODS: 1.16.2
+COCOAPODS: 1.15.2
diff --git a/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj b/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj
index 2d019141..e44dfe4c 100644
--- a/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj
+++ b/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj
@@ -8,14 +8,14 @@
/* Begin PBXBuildFile section */
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
- 4BDFB2DEC3FA2A466B4399CC /* libPods-RNApp-BrownfieldLib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 74384ED30C65F0559B7E3845 /* libPods-RNApp-BrownfieldLib.a */; };
+ 6B3BDCA6FCD1F7343AD80D81 /* Pods_RNApp_BrownfieldLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 268998CB3D3CADCB5FA172FE /* Pods_RNApp_BrownfieldLib.framework */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
79BD1EE92EEBFB76003AA29F /* BrownfieldLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */; };
79BD1EEA2EEBFB76003AA29F /* BrownfieldLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
79F35E8C2EEC1D4500E64860 /* BrownfieldLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F35E8A2EEC1D4500E64860 /* BrownfieldLib.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
C66C2A65406C527E9529D08F /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
- EA23C7C5CE3406CF601EEF2A /* libPods-RNApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 368B138CEEDA7E1E2280A031 /* libPods-RNApp.a */; };
+ E8B11DC20B38EEBB30BD63A7 /* Pods_RNApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A046CFF3C8059D9B1E159A5 /* Pods_RNApp.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -47,15 +47,15 @@
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RNApp/Images.xcassets; sourceTree = ""; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RNApp/Info.plist; sourceTree = ""; };
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = RNApp/PrivacyInfo.xcprivacy; sourceTree = ""; };
- 368B138CEEDA7E1E2280A031 /* libPods-RNApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNApp.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 268998CB3D3CADCB5FA172FE /* Pods_RNApp_BrownfieldLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp_BrownfieldLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B4392A12AC88292D35C810B /* Pods-RNApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp.debug.xcconfig"; path = "Target Support Files/Pods-RNApp/Pods-RNApp.debug.xcconfig"; sourceTree = ""; };
5709B34CF0A7D63546082F79 /* Pods-RNApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp.release.xcconfig"; path = "Target Support Files/Pods-RNApp/Pods-RNApp.release.xcconfig"; sourceTree = ""; };
- 74384ED30C65F0559B7E3845 /* libPods-RNApp-BrownfieldLib.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNApp-BrownfieldLib.a"; sourceTree = BUILT_PRODUCTS_DIR; };
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = RNApp/AppDelegate.swift; sourceTree = ""; };
79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BrownfieldLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
79F35E8A2EEC1D4500E64860 /* BrownfieldLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrownfieldLib.swift; sourceTree = ""; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = RNApp/LaunchScreen.storyboard; sourceTree = ""; };
8A02E03D9F74B585B0A8F7F7 /* Pods-RNApp-BrownfieldLib.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp-BrownfieldLib.debug.xcconfig"; path = "Target Support Files/Pods-RNApp-BrownfieldLib/Pods-RNApp-BrownfieldLib.debug.xcconfig"; sourceTree = ""; };
+ 8A046CFF3C8059D9B1E159A5 /* Pods_RNApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D8C030F60E402FD6CFBB3904 /* Pods-RNApp-BrownfieldLib.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp-BrownfieldLib.release.xcconfig"; path = "Target Support Files/Pods-RNApp-BrownfieldLib/Pods-RNApp-BrownfieldLib.release.xcconfig"; sourceTree = ""; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
@@ -66,7 +66,7 @@
buildActionMask = 2147483647;
files = (
79BD1EE92EEBFB76003AA29F /* BrownfieldLib.framework in Frameworks */,
- EA23C7C5CE3406CF601EEF2A /* libPods-RNApp.a in Frameworks */,
+ E8B11DC20B38EEBB30BD63A7 /* Pods_RNApp.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -74,7 +74,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 4BDFB2DEC3FA2A466B4399CC /* libPods-RNApp-BrownfieldLib.a in Frameworks */,
+ 6B3BDCA6FCD1F7343AD80D81 /* Pods_RNApp_BrownfieldLib.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -97,8 +97,8 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
- 368B138CEEDA7E1E2280A031 /* libPods-RNApp.a */,
- 74384ED30C65F0559B7E3845 /* libPods-RNApp-BrownfieldLib.a */,
+ 8A046CFF3C8059D9B1E159A5 /* Pods_RNApp.framework */,
+ 268998CB3D3CADCB5FA172FE /* Pods_RNApp_BrownfieldLib.framework */,
);
name = Frameworks;
sourceTree = "";
diff --git a/apps/RNApp/package.json b/apps/RNApp/package.json
index 07ea82c4..2cec0ab0 100644
--- a/apps/RNApp/package.json
+++ b/apps/RNApp/package.json
@@ -13,9 +13,11 @@
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
- "codegen": "brownfield codegen"
+ "codegen": "brownfield codegen",
+ "codegen:navigation": "brownfield navigation:codegen brownfield.navigation.ts"
},
"dependencies": {
+ "@callstack/brownfield-navigation": "workspace:^",
"@callstack/brownie": "workspace:^",
"@callstack/react-native-brownfield": "workspace:^",
"@react-navigation/native": "^7.0.15",
diff --git a/apps/RNApp/src/HomeScreen.tsx b/apps/RNApp/src/HomeScreen.tsx
index 75638c26..c93f2a5c 100644
--- a/apps/RNApp/src/HomeScreen.tsx
+++ b/apps/RNApp/src/HomeScreen.tsx
@@ -10,7 +10,7 @@ import {
} from 'react-native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import ReactNativeBrownfield from '@callstack/react-native-brownfield';
-import type { MessageEvent } from '@callstack/react-native-brownfield';
+import BrownfieldNavigation from '@callstack/brownfield-navigation';
import { getRandomTheme } from './utils';
import type { RootStackParamList } from './navigation/RootStack';
@@ -171,6 +171,18 @@ export function HomeScreen({
title="Go back"
/>
+
+ BrownfieldNavigation.navigateToSettings()}
+ color={colors.secondary}
+ title="Open native settings"
+ />
+
+ BrownfieldNavigation.navigateToReferrals('user-123')}
+ color={colors.secondary}
+ title="Open native referrals"
+ />
);
}
diff --git a/docs/docs/docs/api-reference/_meta.json b/docs/docs/docs/api-reference/_meta.json
index 85b242b5..e529790b 100644
--- a/docs/docs/docs/api-reference/_meta.json
+++ b/docs/docs/docs/api-reference/_meta.json
@@ -4,6 +4,11 @@
"name": "react-native-brownfield",
"label": "ReactNativeBrownfield"
},
+ {
+ "type": "dir",
+ "name": "brownfield-navigation.mdx",
+ "label": "BrownfieldNavigation"
+ },
{
"type": "dir",
"name": "brownie",
diff --git a/docs/docs/docs/api-reference/brownfield-navigation.mdx/_meta.json b/docs/docs/docs/api-reference/brownfield-navigation.mdx/_meta.json
new file mode 100644
index 00000000..cea09ab7
--- /dev/null
+++ b/docs/docs/docs/api-reference/brownfield-navigation.mdx/_meta.json
@@ -0,0 +1,22 @@
+[
+ {
+ "type": "file",
+ "name": "brownfield-navigation",
+ "label": "Overview"
+ },
+ {
+ "type": "file",
+ "name": "setup-and-codegen",
+ "label": "Setup and Codegen"
+ },
+ {
+ "type": "file",
+ "name": "native-integration",
+ "label": "Native Integration"
+ },
+ {
+ "type": "file",
+ "name": "javascript-usage",
+ "label": "JavaScript Usage"
+ }
+]
diff --git a/docs/docs/docs/api-reference/brownfield-navigation.mdx/brownfield-navigation.mdx b/docs/docs/docs/api-reference/brownfield-navigation.mdx/brownfield-navigation.mdx
new file mode 100644
index 00000000..af03abed
--- /dev/null
+++ b/docs/docs/docs/api-reference/brownfield-navigation.mdx/brownfield-navigation.mdx
@@ -0,0 +1,26 @@
+# Brownfield Navigation
+
+`@callstack/brownfield-navigation` lets React Native code call into your host app navigation.
+You define a TypeScript contract once, generate native bridge files, and implement that contract in Android/iOS delegates.
+
+## How It Works
+
+1. You create a `brownfield.navigation.ts` spec in your React Native app.
+2. You run `brownfield-navigation-codegen` to generate native bridge/delegate files.
+3. Native host code implements `BrownfieldNavigationDelegate`.
+4. You register the delegate at startup.
+5. JavaScript calls `BrownfieldNavigation.()`.
+
+## Guides
+
+- [Setup and Codegen](/docs/api-reference/brownfield-navigation.mdx/setup-and-codegen)
+- [Native Integration](/docs/api-reference/brownfield-navigation.mdx/native-integration)
+- [JavaScript Usage](/docs/api-reference/brownfield-navigation.mdx/javascript-usage)
+
+## Example Projects
+
+Use these apps as end-to-end references:
+
+- `apps/RNApp` for the spec file and JavaScript calls
+- `apps/AndroidApp` for Android delegate implementation
+- `apps/AppleApp` for iOS delegate implementation
diff --git a/docs/docs/docs/api-reference/brownfield-navigation.mdx/javascript-usage.mdx b/docs/docs/docs/api-reference/brownfield-navigation.mdx/javascript-usage.mdx
new file mode 100644
index 00000000..5f29855a
--- /dev/null
+++ b/docs/docs/docs/api-reference/brownfield-navigation.mdx/javascript-usage.mdx
@@ -0,0 +1,61 @@
+# JavaScript Usage
+
+Use the generated JavaScript API after native delegate registration is complete.
+
+## Import and call methods
+
+```ts
+import BrownfieldNavigation from '@callstack/brownfield-navigation';
+
+BrownfieldNavigation.navigateToSettings();
+BrownfieldNavigation.navigateToReferrals('user-123');
+```
+
+## Example screen usage
+
+```tsx
+import { Button, View } from 'react-native';
+import BrownfieldNavigation from '@callstack/brownfield-navigation';
+
+export function NativeLinks() {
+ return (
+
+ BrownfieldNavigation.navigateToSettings()}
+ />
+ BrownfieldNavigation.navigateToReferrals('user-123')}
+ />
+
+ );
+}
+```
+
+## Best practices
+
+- Keep JS method names aligned with actual native destinations.
+- Pass stable, explicit params (`userId`, IDs, flags) rather than derived UI state.
+- Add runtime guards in app startup so delegate registration always happens first.
+
+## When to run codegen again
+
+Rerun codegen if any of these change:
+
+- Method names
+- Method parameters
+- Method return types
+
+```bash
+yarn brownfield:navigation-codegen
+```
+
+## Common errors
+
+- **`undefined is not a function` on method call**:
+ method was changed in spec, but app was not regenerated/rebuilt.
+- **Native crash on method call**:
+ delegate was not registered before JS usage.
+- **Method exists but does nothing**:
+ native delegate method was generated but not implemented in host app.
diff --git a/docs/docs/docs/api-reference/brownfield-navigation.mdx/native-integration.mdx b/docs/docs/docs/api-reference/brownfield-navigation.mdx/native-integration.mdx
new file mode 100644
index 00000000..901b2796
--- /dev/null
+++ b/docs/docs/docs/api-reference/brownfield-navigation.mdx/native-integration.mdx
@@ -0,0 +1,99 @@
+# Native Integration
+
+After codegen, implement the generated delegate interface in your host app and register it before JavaScript uses the module.
+
+## Android
+
+### 1) Implement `BrownfieldNavigationDelegate`
+
+Implement the generated delegate methods in your host `Activity` (or another class with access to navigation context):
+
+```kotlin
+import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
+import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationDelegate
+
+class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
+ override fun navigateToSettings() {
+ startActivity(Intent(this, SettingsActivity::class.java))
+ }
+
+ override fun navigateToReferrals(userId: String) {
+ startActivity(
+ Intent(this, ReferralsActivity::class.java)
+ .putExtra(ReferralsActivity.EXTRA_USER_ID, userId)
+ )
+ }
+}
+```
+
+### 2) Register the delegate during startup
+
+Register before any React Native screen can call `BrownfieldNavigation.*`:
+
+```kotlin
+import android.os.Bundle
+import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationManager
+
+override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ BrownfieldNavigationManager.setDelegate(this)
+ // Initialize React Native host
+}
+```
+
+## iOS
+
+### 1) Implement `BrownfieldNavigationDelegate`
+
+```swift
+import BrownfieldNavigation
+import SwiftUI
+import UIKit
+
+public final class RNNavigationDelegate: BrownfieldNavigationDelegate {
+ public func navigateToSettings() {
+ present(SettingsScreen())
+ }
+
+ public func navigateToReferrals(_ userId: String) {
+ present(ReferralsScreen(userId: userId))
+ }
+
+ private func present(_ view: Content) {
+ DispatchQueue.main.async {
+ let hostingController = UIHostingController(rootView: view)
+ UIApplication.shared.topMostViewController()?
+ .present(hostingController, animated: true)
+ }
+ }
+}
+```
+
+### 2) Register the delegate at app startup
+
+```swift
+import BrownfieldNavigation
+
+@main
+struct BrownfieldAppleApp: App {
+ init() {
+ BrownfieldNavigationManager.shared.setDelegate(
+ navigationDelegate: RNNavigationDelegate()
+ )
+ }
+}
+```
+
+## Lifecycle Requirements
+
+- Register delegate before rendering JS that might call the module.
+- Keep navigation on main/UI thread.
+- Re-register delegate if your host object is recreated.
+- Treat missing delegate as a startup bug: runtime calls require a registered delegate.
+
+## Troubleshooting
+
+- **Method added in TS but not visible natively**: rerun codegen and rebuild.
+- **Calls crash on app launch**: verify delegate registration happens before RN route rendering.
+- **Wrong screen opens**: check native delegate method wiring and params mapping.
diff --git a/docs/docs/docs/api-reference/brownfield-navigation.mdx/setup-and-codegen.mdx b/docs/docs/docs/api-reference/brownfield-navigation.mdx/setup-and-codegen.mdx
new file mode 100644
index 00000000..c87e840f
--- /dev/null
+++ b/docs/docs/docs/api-reference/brownfield-navigation.mdx/setup-and-codegen.mdx
@@ -0,0 +1,91 @@
+# Setup and Codegen
+
+This guide covers the required setup to define your navigation contract and generate bridge files for `@callstack/brownfield-navigation`.
+
+## Prerequisites
+
+Before you start:
+
+- Your React Native app has `@callstack/brownfield-navigation` installed.
+- Your app has Babel dependencies available (`@babel/core`, `@react-native/babel-preset`), which are used during codegen.
+- You know where your app root is (the folder containing your app `package.json`).
+
+## 1) Create `brownfield.navigation.ts`
+
+Create a new file named `brownfield.navigation.ts` in your React Native app root.
+
+Example:
+
+```ts
+// brownfield.navigation.ts
+export interface BrownfieldNavigationSpec {
+ /**
+ * Navigate to a native Settings screen.
+ */
+ navigateToSettings(): void;
+
+ /**
+ * Navigate to a native Referrals screen.
+ */
+ navigateToReferrals(userId: string): void;
+}
+```
+
+### Supported method signatures
+
+- Method name: any valid TypeScript identifier.
+- Params: typed and optional params are supported (for example `userId?: string`).
+- Return type: `void` is the common and recommended type for navigation actions.
+- Interface name: `BrownfieldNavigationSpec` (or `Spec`) is supported by the parser.
+
+:::info
+Prefer simple synchronous navigation methods (`void`).
+
+Async Promise-based methods are not currently supported by the generated native code on iOS and Android — they will compile, but the generated implementations will reject with a `not_implemented` error.
+:::
+
+## 2) Add a codegen script
+
+Add a script to your app `package.json`:
+
+```json
+{
+ "scripts": {
+ "brownfield:navigation-codegen": "brownfield navigation:codegen"
+ }
+}
+```
+
+## 3) Run codegen
+
+From your app root:
+
+```bash
+yarn brownfield:navigation-codegen
+```
+
+## 4) What gets generated
+
+Codegen updates `@callstack/brownfield-navigation` with your contract:
+
+- `src/NativeBrownfieldNavigation.ts` (TurboModule spec)
+- `src/index.ts` (JavaScript API surface)
+- `ios/BrownfieldNavigationDelegate.swift` (delegate protocol)
+- `ios/NativeBrownfieldNavigation.mm` (native module implementation)
+- Android delegate/module files under:
+ `android/src/main/java/com/callstack/nativebrownfieldnavigation/`
+
+## 5) Regenerate when contract changes
+
+Any time you add, remove, or rename methods in `brownfield.navigation.ts`, rerun:
+
+```bash
+yarn brownfield:navigation-codegen
+```
+
+Then recompile native apps.
+
+## Next
+
+- Continue with [Native Integration](/docs/api-reference/brownfield-navigation.mdx/native-integration)
+- Then wire JS calls from [JavaScript Usage](/docs/api-reference/brownfield-navigation.mdx/javascript-usage)
diff --git a/packages/brownfield-navigation/BrownfieldNavigation.podspec b/packages/brownfield-navigation/BrownfieldNavigation.podspec
new file mode 100644
index 00000000..ac83dccf
--- /dev/null
+++ b/packages/brownfield-navigation/BrownfieldNavigation.podspec
@@ -0,0 +1,22 @@
+require 'json'
+
+package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
+
+Pod::Spec.new do |spec|
+ spec.name = "BrownfieldNavigation"
+ spec.version = package['version']
+ spec.summary = package['description']
+ spec.license = package['license']
+
+ spec.authors = package['author']
+ spec.homepage = package['homepage']
+ spec.platform = :ios, "14.0"
+
+ spec.source = { :git => "git@github.com:callstack/react-native-brownfield.git", :tag => "#{spec.version}" }
+ spec.source_files = "ios/**/*.{h,m,mm,swift}"
+ spec.pod_target_xcconfig = {
+ 'DEFINES_MODULE' => 'YES',
+ }
+
+ install_modules_dependencies(spec)
+end
diff --git a/packages/brownfield-navigation/android/build.gradle b/packages/brownfield-navigation/android/build.gradle
new file mode 100644
index 00000000..00e7ba33
--- /dev/null
+++ b/packages/brownfield-navigation/android/build.gradle
@@ -0,0 +1,67 @@
+buildscript {
+ ext.BrownfieldNavigation = [
+ kotlinVersion: "2.0.21",
+ minSdkVersion: 24,
+ compileSdkVersion: 36,
+ targetSdkVersion: 36
+ ]
+
+ ext.getExtOrDefault = { prop ->
+ if (rootProject.ext.has(prop)) {
+ return rootProject.ext.get(prop)
+ }
+
+ return BrownfieldNavigation[prop]
+ }
+
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath "com.android.tools.build:gradle:8.7.2"
+ // noinspection DifferentKotlinGradleVersion
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
+ }
+}
+
+
+apply plugin: "com.android.library"
+apply plugin: "kotlin-android"
+
+apply plugin: "com.facebook.react"
+
+android {
+ namespace "com.callstack.nativebrownfieldnavigation"
+
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
+
+ defaultConfig {
+ minSdkVersion getExtOrDefault("minSdkVersion")
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
+ }
+
+ buildFeatures {
+ buildConfig true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ lint {
+ disable "GradleCompatible"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation "com.facebook.react:react-android"
+}
diff --git a/packages/brownfield-navigation/android/src/main/AndroidManifest.xml b/packages/brownfield-navigation/android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..f8b39136
--- /dev/null
+++ b/packages/brownfield-navigation/android/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/BrownfieldNavigationDelegate.kt b/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/BrownfieldNavigationDelegate.kt
new file mode 100644
index 00000000..780059a3
--- /dev/null
+++ b/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/BrownfieldNavigationDelegate.kt
@@ -0,0 +1,3 @@
+package com.callstack.nativebrownfieldnavigation
+
+interface BrownfieldNavigationDelegate
diff --git a/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/BrownfieldNavigationManager.kt b/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/BrownfieldNavigationManager.kt
new file mode 100644
index 00000000..da85becb
--- /dev/null
+++ b/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/BrownfieldNavigationManager.kt
@@ -0,0 +1,14 @@
+package com.callstack.nativebrownfieldnavigation
+
+object BrownfieldNavigationManager {
+ private var navigationDelegate: BrownfieldNavigationDelegate? = null
+
+ fun setDelegate(navigationDelegate: BrownfieldNavigationDelegate) {
+ this.navigationDelegate = navigationDelegate
+ }
+
+ fun getDelegate(): BrownfieldNavigationDelegate {
+ return navigationDelegate
+ ?: throw IllegalStateException("BrownfieldNavigation delegate is not set.")
+ }
+}
diff --git a/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/NativeBrownfieldNavigationModule.kt b/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/NativeBrownfieldNavigationModule.kt
new file mode 100644
index 00000000..f118105a
--- /dev/null
+++ b/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/NativeBrownfieldNavigationModule.kt
@@ -0,0 +1,18 @@
+package com.callstack.nativebrownfieldnavigation
+
+import android.util.Log
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactMethod
+
+class NativeBrownfieldNavigationModule(
+ reactContext: ReactApplicationContext
+) : NativeBrownfieldNavigationSpec(reactContext) {
+ @ReactMethod
+ override fun temporary() {
+ Log.d(NAME, "temporary")
+ }
+
+ companion object {
+ const val NAME = "NativeBrownfieldNavigation"
+ }
+}
diff --git a/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/NativeBrownfieldNavigationPackage.kt b/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/NativeBrownfieldNavigationPackage.kt
new file mode 100644
index 00000000..af5cebd1
--- /dev/null
+++ b/packages/brownfield-navigation/android/src/main/java/com/callstack/nativebrownfieldnavigation/NativeBrownfieldNavigationPackage.kt
@@ -0,0 +1,31 @@
+package com.callstack.nativebrownfieldnavigation
+
+import com.facebook.react.BaseReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.module.model.ReactModuleInfo
+import com.facebook.react.module.model.ReactModuleInfoProvider
+import java.util.HashMap
+
+class NativeBrownfieldNavigationPackage : BaseReactPackage() {
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
+ return if (name == NativeBrownfieldNavigationModule.NAME) {
+ NativeBrownfieldNavigationModule(reactContext)
+ } else {
+ null
+ }
+ }
+
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
+ mapOf(
+ NativeBrownfieldNavigationModule.NAME to ReactModuleInfo(
+ name = NativeBrownfieldNavigationModule.NAME,
+ className = NativeBrownfieldNavigationModule.NAME,
+ canOverrideExistingModule = false,
+ needsEagerInit = false,
+ isCxxModule = false,
+ isTurboModule = true
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/packages/brownfield-navigation/babel.config.js b/packages/brownfield-navigation/babel.config.js
new file mode 100644
index 00000000..f7b3da3b
--- /dev/null
+++ b/packages/brownfield-navigation/babel.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ presets: ['module:@react-native/babel-preset'],
+};
diff --git a/packages/brownfield-navigation/bob.config.js b/packages/brownfield-navigation/bob.config.js
new file mode 100644
index 00000000..534fcec3
--- /dev/null
+++ b/packages/brownfield-navigation/bob.config.js
@@ -0,0 +1,26 @@
+module.exports = {
+ source: 'src',
+ output: 'lib',
+ targets: [
+ [
+ 'commonjs',
+ {
+ esm: true,
+ configFile: true,
+ },
+ ],
+ [
+ 'module',
+ {
+ esm: true,
+ configFile: true,
+ },
+ ],
+ [
+ 'typescript',
+ {
+ project: 'tsconfig.build.json',
+ },
+ ],
+ ],
+};
diff --git a/packages/brownfield-navigation/eslint.config.mjs b/packages/brownfield-navigation/eslint.config.mjs
new file mode 100644
index 00000000..79e239c3
--- /dev/null
+++ b/packages/brownfield-navigation/eslint.config.mjs
@@ -0,0 +1,4 @@
+import eslintRnConfig from '../../eslint.config.rn.mjs';
+
+/** @type {import('eslint').Linter.Config[]} */
+export default eslintRnConfig;
diff --git a/packages/brownfield-navigation/ios/BrownfieldNavigationDelegate.swift b/packages/brownfield-navigation/ios/BrownfieldNavigationDelegate.swift
new file mode 100644
index 00000000..f4b2828d
--- /dev/null
+++ b/packages/brownfield-navigation/ios/BrownfieldNavigationDelegate.swift
@@ -0,0 +1,5 @@
+import Foundation
+
+@objc public protocol BrownfieldNavigationDelegate: AnyObject {
+
+}
diff --git a/packages/brownfield-navigation/ios/BrownfieldNavigationManager.swift b/packages/brownfield-navigation/ios/BrownfieldNavigationManager.swift
new file mode 100644
index 00000000..33e82419
--- /dev/null
+++ b/packages/brownfield-navigation/ios/BrownfieldNavigationManager.swift
@@ -0,0 +1,21 @@
+//
+// BrownfieldNavigationManager.swift
+//
+// Created by Hur Ali on 10/02/2026.
+//
+
+public class BrownfieldNavigationManager: NSObject {
+ @objc public static let shared = BrownfieldNavigationManager()
+ private var navigationDelegate: BrownfieldNavigationDelegate?
+
+ public func setDelegate(navigationDelegate: BrownfieldNavigationDelegate) {
+ self.navigationDelegate = navigationDelegate
+ }
+
+ @objc public func getDelegate() -> BrownfieldNavigationDelegate {
+ guard let delegate = navigationDelegate else {
+ fatalError("BrownfieldNavigationDelegate not set. Call setDelegate() before using navigation.")
+ }
+ return delegate
+ }
+}
diff --git a/packages/brownfield-navigation/ios/NativeBrownfieldNavigation.h b/packages/brownfield-navigation/ios/NativeBrownfieldNavigation.h
new file mode 100644
index 00000000..dd7a61a8
--- /dev/null
+++ b/packages/brownfield-navigation/ios/NativeBrownfieldNavigation.h
@@ -0,0 +1,15 @@
+//
+// NativeBrownfieldNavigation.h
+//
+// Created by Hur Ali on 10/02/2026.
+//
+
+#ifdef __cplusplus
+
+#import
+
+@interface NativeBrownfieldNavigation : NSObject
+
+@end
+
+#endif
diff --git a/packages/brownfield-navigation/ios/NativeBrownfieldNavigation.mm b/packages/brownfield-navigation/ios/NativeBrownfieldNavigation.mm
new file mode 100644
index 00000000..d92e3ef0
--- /dev/null
+++ b/packages/brownfield-navigation/ios/NativeBrownfieldNavigation.mm
@@ -0,0 +1,26 @@
+#import "NativeBrownfieldNavigation.h"
+
+#if __has_include("BrownfieldNavigation/BrownfieldNavigation-Swift.h")
+#import "BrownfieldNavigation/BrownfieldNavigation-Swift.h"
+#else
+#import "BrownfieldNavigation-Swift.h"
+#endif
+
+@implementation NativeBrownfieldNavigation
+
+- (void)temporary {
+ NSLog(@"temporary");
+}
+
+- (std::shared_ptr)getTurboModule:
+ (const facebook::react::ObjCTurboModule::InitParams &)params
+{
+ return std::make_shared(params);
+}
+
++ (NSString *)moduleName
+{
+ return @"NativeBrownfieldNavigation";
+}
+
+@end
diff --git a/packages/brownfield-navigation/package.json b/packages/brownfield-navigation/package.json
new file mode 100644
index 00000000..9c04a065
--- /dev/null
+++ b/packages/brownfield-navigation/package.json
@@ -0,0 +1,92 @@
+{
+ "name": "@callstack/brownfield-navigation",
+ "version": "3.0.0",
+ "license": "MIT",
+ "author": "Hur Ali",
+ "description": "Brownfield navigation helpers for React Native",
+ "homepage": "https://github.com/callstack/react-native-brownfield",
+ "main": "lib/commonjs/index",
+ "module": "lib/module/index",
+ "types": "./lib/typescript/commonjs/src/index.d.ts",
+ "react-native": "src/index",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./lib/typescript/module/src/index.d.ts",
+ "default": "./lib/module/index.js"
+ },
+ "require": {
+ "types": "./lib/typescript/commonjs/src/index.d.ts",
+ "default": "./lib/commonjs/index.js"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "bin": {
+ "brownfield": "lib/commonjs/scripts/brownfield.js"
+ },
+ "scripts": {
+ "lint": "eslint .",
+ "typecheck": "tsc --noEmit",
+ "build": "bob build",
+ "dev": "nodemon --watch src --ext js,ts,json --exec \"bob build\"",
+ "build:brownfield": "yarn run build",
+ "prepack": "cp ../../README.md ./README.md",
+ "postpack": "rm ./README.md"
+ },
+ "files": [
+ "src",
+ "lib",
+ "android",
+ "ios",
+ "*.podspec",
+ "!ios/build",
+ "!android/build",
+ "!android/gradle",
+ "!android/gradlew",
+ "!android/gradlew.bat",
+ "!android/local.properties",
+ "!**/__tests__",
+ "!**/__fixtures__",
+ "!**/__mocks__",
+ "!**/.*",
+ "README.md"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@callstack/brownfield-cli": "workspace:^"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/preset-env": "^7.25.3",
+ "@babel/runtime": "^7.25.0",
+ "@react-native/babel-preset": "0.82.1",
+ "@types/jest": "^30.0.0",
+ "@types/react": "^19.1.1",
+ "eslint": "^9.28.0",
+ "globals": "^16.2.0",
+ "import": "^0.0.6",
+ "nodemon": "^3.1.11",
+ "react": "19.1.1",
+ "react-native": "0.82.1",
+ "react-native-builder-bob": "^0.40.17",
+ "typescript": "5.9.3"
+ },
+ "codegenConfig": {
+ "name": "NativeBrownfieldNavigation",
+ "type": "modules",
+ "jsSrcsDir": "./src",
+ "android": {
+ "javaPackageName": "com.callstack.nativebrownfieldnavigation"
+ }
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/packages/brownfield-navigation/src/NativeBrownfieldNavigation.ts b/packages/brownfield-navigation/src/NativeBrownfieldNavigation.ts
new file mode 100644
index 00000000..5d4ed84d
--- /dev/null
+++ b/packages/brownfield-navigation/src/NativeBrownfieldNavigation.ts
@@ -0,0 +1,9 @@
+import { TurboModuleRegistry, type TurboModule } from 'react-native';
+
+export interface Spec extends TurboModule {
+ temporary(): void;
+}
+
+export default TurboModuleRegistry.getEnforcing(
+ 'NativeBrownfieldNavigation'
+);
diff --git a/packages/brownfield-navigation/src/index.ts b/packages/brownfield-navigation/src/index.ts
new file mode 100644
index 00000000..5af516da
--- /dev/null
+++ b/packages/brownfield-navigation/src/index.ts
@@ -0,0 +1,9 @@
+import NativeBrownfieldNavigation from './NativeBrownfieldNavigation';
+
+const BrownfieldNavigation = {
+ temporary: () => {
+ NativeBrownfieldNavigation.temporary();
+ },
+};
+
+export default BrownfieldNavigation;
diff --git a/packages/brownfield-navigation/src/scripts/brownfield.ts b/packages/brownfield-navigation/src/scripts/brownfield.ts
new file mode 100644
index 00000000..0b38f185
--- /dev/null
+++ b/packages/brownfield-navigation/src/scripts/brownfield.ts
@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+
+import { runCLI } from '@callstack/brownfield-cli';
+
+runCLI(process.argv);
diff --git a/packages/brownfield-navigation/tsconfig.build.json b/packages/brownfield-navigation/tsconfig.build.json
new file mode 100644
index 00000000..1c66acf6
--- /dev/null
+++ b/packages/brownfield-navigation/tsconfig.build.json
@@ -0,0 +1,3 @@
+{
+ "extends": "./tsconfig"
+}
diff --git a/packages/brownfield-navigation/tsconfig.json b/packages/brownfield-navigation/tsconfig.json
new file mode 100644
index 00000000..fd9e8623
--- /dev/null
+++ b/packages/brownfield-navigation/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "./lib/typescript"
+ }
+}
diff --git a/packages/cli/package.json b/packages/cli/package.json
index ff12257d..0578e73b 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -32,6 +32,11 @@
"types": "./dist/brownie/index.d.ts",
"default": "./dist/brownie/index.js"
},
+ "./navigation": {
+ "source": "./src/navigation/index.ts",
+ "types": "./dist/navigation/index.d.ts",
+ "default": "./dist/navigation/index.js"
+ },
"./package.json": "./package.json"
},
"scripts": {
diff --git a/packages/cli/src/brownfield/commands/packageAndroid.ts b/packages/cli/src/brownfield/commands/packageAndroid.ts
index 8c4f45c7..6852f3c5 100644
--- a/packages/cli/src/brownfield/commands/packageAndroid.ts
+++ b/packages/cli/src/brownfield/commands/packageAndroid.ts
@@ -14,6 +14,7 @@ import {
import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
import { getProjectInfo } from '../utils/project.js';
import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
+import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
export const packageAndroidCommand = curryOptions(
new Command('package:android').description('Build Android AAR'),
@@ -31,6 +32,7 @@ export const packageAndroidCommand = curryOptions(
});
await runBrownieCodegenIfApplicable(projectRoot, 'kotlin');
+ await runNavigationCodegenIfApplicable(projectRoot);
await packageAarAction({
projectRoot,
diff --git a/packages/cli/src/brownfield/commands/packageIos.ts b/packages/cli/src/brownfield/commands/packageIos.ts
index 0047a3d6..d01e8ca5 100644
--- a/packages/cli/src/brownfield/commands/packageIos.ts
+++ b/packages/cli/src/brownfield/commands/packageIos.ts
@@ -23,6 +23,7 @@ import {
ExampleUsage,
} from '../../shared/index.js';
import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
+import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
export const packageIosCommand = curryOptions(
@@ -76,6 +77,7 @@ export const packageIosCommand = curryOptions(
projectRoot,
'swift'
);
+ const { hasNavigation } = await runNavigationCodegenIfApplicable(projectRoot);
await packageIosAction(
options,
@@ -116,7 +118,7 @@ export const packageIosCommand = curryOptions(
outputPath: brownieOutputPath,
});
- // Strip the binary from Brownie.xcframework to make it interface-only.
+ // Strip the binary from Browniexcframework to make it interface-only.
// This avoids duplicate symbols when consumer apps embed both BrownfieldLib
// (which contains Brownie symbols) and Brownie.xcframework.
stripFrameworkBinary(brownieOutputPath);
@@ -125,6 +127,37 @@ export const packageIosCommand = curryOptions(
`Brownie.xcframework created at ${colorLink(relativeToCwd(brownieOutputPath))}`
);
}
+
+ if (hasNavigation) {
+ const productsPath = path.join(options.buildFolder, 'Build', 'Products');
+ const brownfieldNavigationOutputPath = path.join(packageDir, 'BrownfieldNavigation.xcframework');
+
+ await mergeFrameworks({
+ sourceDir: userConfig.project.ios.sourceDir,
+ frameworkPaths: [
+ path.join(
+ productsPath,
+ `${configuration}-iphoneos`,
+ 'BrownfieldNavigation',
+ 'BrownfieldNavigation.framework'
+ ),
+ path.join(
+ productsPath,
+ `${configuration}-iphonesimulator`,
+ 'BrownfieldNavigation',
+ 'BrownfieldNavigation.framework'
+ ),
+ ],
+ outputPath: brownfieldNavigationOutputPath,
+ });
+
+
+ stripFrameworkBinary(brownfieldNavigationOutputPath);
+
+ logger.success(
+ `BrownfieldNavigation.xcframework created at ${colorLink(relativeToCwd(brownfieldNavigationOutputPath))}`
+ );
+ }
})
);
diff --git a/packages/cli/src/brownfield/commands/publishAndroid.ts b/packages/cli/src/brownfield/commands/publishAndroid.ts
index 81c26a7d..6edb6746 100644
--- a/packages/cli/src/brownfield/commands/publishAndroid.ts
+++ b/packages/cli/src/brownfield/commands/publishAndroid.ts
@@ -14,6 +14,7 @@ import {
import { getProjectInfo } from '../utils/project.js';
import { runExpoPrebuildIfNeeded } from '../utils/expo.js';
import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
+import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
export const publishAndroidCommand = curryOptions(
new Command('publish:android').description(
@@ -29,6 +30,7 @@ export const publishAndroidCommand = curryOptions(
});
await runBrownieCodegenIfApplicable(projectRoot, 'kotlin');
+ await runNavigationCodegenIfApplicable(projectRoot);
await publishLocalAarAction({
projectRoot,
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index 53e05d9c..867a3dbb 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -11,6 +11,9 @@ import brownfieldCommands, {
import brownieCommands, {
groupName as brownieCommandsGroupName,
} from './brownie/index.js';
+import navigationCommands, {
+ groupName as navigationCommandsGroupName,
+} from './navigation/index.js';
const program = new Command();
@@ -72,6 +75,7 @@ function registrationHelper(
registrationHelper(brownfieldCommands, brownfieldCommandsGroupName);
registrationHelper(brownieCommands, brownieCommandsGroupName);
+registrationHelper(navigationCommands, navigationCommandsGroupName);
program.commandsGroup('Utility commands').helpCommand('help [command]');
diff --git a/packages/cli/src/navigation/__tests__/generators.test.ts b/packages/cli/src/navigation/__tests__/generators.test.ts
new file mode 100644
index 00000000..a4be1532
--- /dev/null
+++ b/packages/cli/src/navigation/__tests__/generators.test.ts
@@ -0,0 +1,95 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { generateKotlinDelegate, generateKotlinModule } from '../generators/android.js';
+import { generateObjCImplementation, generateSwiftDelegate } from '../generators/ios.js';
+import {
+ generateIndexDts,
+ generateIndexTs,
+ generateTurboModuleSpec,
+} from '../generators/ts.js';
+import type { MethodSignature } from '../types.js';
+
+const methods: MethodSignature[] = [
+ {
+ name: 'openScreen',
+ params: [
+ { name: 'route', type: 'string', optional: false },
+ { name: 'params', type: 'Object', optional: true },
+ ],
+ returnType: 'void',
+ isAsync: false,
+ },
+];
+
+describe('navigation code generators', () => {
+ it('generates TurboModule spec and index files', () => {
+ const turboModuleSpec = generateTurboModuleSpec(methods);
+ const indexTs = generateIndexTs(methods);
+ const indexDts = generateIndexDts(methods);
+
+ expect(turboModuleSpec).toContain('export interface Spec extends TurboModule');
+ expect(turboModuleSpec).toContain('openScreen(route: string, params?: Object): void;');
+
+ expect(indexTs).toContain('openScreen: (route: string, params?: Object)');
+ expect(indexTs).toContain('NativeBrownfieldNavigation.openScreen(route, params)');
+
+ expect(indexDts).toContain('openScreen: (route: string, params?: Object) => void;');
+ });
+
+ it('generates iOS bindings for sync methods', () => {
+ const swiftDelegate = generateSwiftDelegate(methods);
+ const objcImplementation = generateObjCImplementation(methods);
+
+ expect(swiftDelegate).toContain('@objc public protocol BrownfieldNavigationDelegate');
+ expect(swiftDelegate).toContain('@objc func openScreen(_ route: String, params params: [String: Any]?)');
+
+ expect(objcImplementation).toContain('- (void)openScreen:(NSString *)route params:(NSDictionary * _Nullable)params');
+ expect(objcImplementation).toContain(
+ '[[[BrownfieldNavigationManager shared] getDelegate] openScreen:route params:params];'
+ );
+ });
+
+ it('generates Android bindings for sync methods', () => {
+ const kotlinPackageName = 'com.callstack.nativebrownfieldnavigation';
+ const kotlinDelegate = generateKotlinDelegate(methods, kotlinPackageName);
+ const kotlinModule = generateKotlinModule(methods, kotlinPackageName);
+
+ expect(kotlinDelegate).toContain(`package ${kotlinPackageName}`);
+ expect(kotlinDelegate).toContain('fun openScreen(route: String, params: ReadableMap?)');
+
+ expect(kotlinModule).toContain('import com.facebook.react.bridge.ReadableMap');
+ expect(kotlinModule).toContain(
+ 'override fun openScreen(route: String, params: ReadableMap?)'
+ );
+ expect(kotlinModule).toContain(
+ 'BrownfieldNavigationManager.getDelegate().openScreen(route, params)'
+ );
+ });
+});
+
+describe('transpileWithConsumerBabel dependency errors', () => {
+ afterEach(() => {
+ vi.resetModules();
+ vi.unmock('node:module');
+ });
+
+ it('throws a clear error when @babel/core cannot be resolved', async () => {
+ vi.doMock('node:module', () => ({
+ createRequire: () => ({
+ resolve: () => {
+ throw new Error('module not found');
+ },
+ }),
+ }));
+
+ const { transpileWithConsumerBabel } = await import('../generators/ts.js');
+
+ expect(() =>
+ transpileWithConsumerBabel(
+ 'const value: string = "hello"; export default value;',
+ '/tmp/project',
+ '/tmp/package'
+ )
+ ).toThrow('Could not resolve "@babel/core". Install it in your app devDependencies.');
+ });
+});
diff --git a/packages/cli/src/navigation/__tests__/models.test.ts b/packages/cli/src/navigation/__tests__/models.test.ts
new file mode 100644
index 00000000..90df9532
--- /dev/null
+++ b/packages/cli/src/navigation/__tests__/models.test.ts
@@ -0,0 +1,124 @@
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+const { addSourceMock, quicktypeMock } = vi.hoisted(() => ({
+ addSourceMock: vi.fn(async (_source: unknown) => undefined),
+ quicktypeMock: vi.fn(async ({ lang }: { lang: 'swift' | 'kotlin' }) => ({
+ lines:
+ lang === 'swift'
+ ? ['public struct UserProfile {}', 'public struct SessionResult {}']
+ : ['data class UserProfile()', 'data class SessionResult()'],
+ })),
+}));
+
+vi.mock('quicktype-core', () => ({
+ FetchingJSONSchemaStore: class {},
+ InputData: class {
+ addInput(): void {}
+ },
+ JSONSchemaInput: class {
+ async addSource(source: unknown): Promise {
+ await addSourceMock(source);
+ }
+ },
+ quicktype: quicktypeMock,
+}));
+
+vi.mock('quicktype-typescript-input', () => ({
+ schemaForTypeScriptSources: () => ({
+ schema: JSON.stringify({
+ definitions: {
+ UserProfile: { type: 'object' },
+ SessionResult: { type: 'object' },
+ InternalOnly: { type: 'object' },
+ },
+ }),
+ }),
+}));
+
+import { generateNavigationModels } from '../generators/models.js';
+import type { MethodSignature } from '../types.js';
+
+function createTempSpecFile(contents: string): string {
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'navigation-models-'));
+ const specPath = path.join(tempDir, 'brownfield.navigation.ts');
+ fs.writeFileSync(specPath, contents);
+ return specPath;
+}
+
+function cleanupTempSpecFile(specPath: string): void {
+ fs.rmSync(path.dirname(specPath), { recursive: true, force: true });
+}
+
+describe('generateNavigationModels', () => {
+ const tempSpecFiles: string[] = [];
+
+ afterEach(() => {
+ for (const specPath of tempSpecFiles) {
+ cleanupTempSpecFile(specPath);
+ }
+ tempSpecFiles.length = 0;
+ vi.clearAllMocks();
+ });
+
+ it('generates models for complex referenced types', async () => {
+ const specPath = createTempSpecFile(`
+ export interface BrownfieldNavigationSpec {
+ openProfile(profile: UserProfile): void;
+ }
+
+ export interface UserProfile {}
+ `);
+ tempSpecFiles.push(specPath);
+
+ const methods: MethodSignature[] = [
+ {
+ name: 'openProfile',
+ params: [{ name: 'profile', type: 'UserProfile', optional: false }],
+ returnType: 'void',
+ isAsync: false,
+ },
+ ];
+
+ const models = await generateNavigationModels({
+ specPath,
+ methods,
+ kotlinPackageName: 'com.callstack.nativebrownfieldnavigation',
+ });
+
+ expect(models.modelTypeNames).toEqual(['UserProfile']);
+ expect(models.swiftModels).toContain('public struct UserProfile');
+ expect(models.kotlinModels).toContain('data class UserProfile');
+ expect(addSourceMock).toHaveBeenCalled();
+ expect(quicktypeMock).toHaveBeenCalledTimes(2);
+ });
+
+ it('skips model generation when no complex types are referenced', async () => {
+ const specPath = createTempSpecFile(`
+ export interface BrownfieldNavigationSpec {
+ open(route: string): void;
+ }
+ `);
+ tempSpecFiles.push(specPath);
+
+ const methods: MethodSignature[] = [
+ {
+ name: 'open',
+ params: [{ name: 'route', type: 'string', optional: false }],
+ returnType: 'void',
+ isAsync: false,
+ },
+ ];
+
+ const models = await generateNavigationModels({
+ specPath,
+ methods,
+ kotlinPackageName: 'com.callstack.nativebrownfieldnavigation',
+ });
+
+ expect(models).toEqual({ modelTypeNames: [] });
+ expect(quicktypeMock).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/cli/src/navigation/__tests__/parser.test.ts b/packages/cli/src/navigation/__tests__/parser.test.ts
new file mode 100644
index 00000000..575bad42
--- /dev/null
+++ b/packages/cli/src/navigation/__tests__/parser.test.ts
@@ -0,0 +1,90 @@
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { afterEach, describe, expect, it } from 'vitest';
+
+import { parseNavigationSpec } from '../parser.js';
+
+function createTempSpecFile(contents: string): string {
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'navigation-parser-'));
+ const specPath = path.join(tempDir, 'brownfield.navigation.ts');
+ fs.writeFileSync(specPath, contents);
+ return specPath;
+}
+
+function cleanupTempSpecFile(specPath: string): void {
+ fs.rmSync(path.dirname(specPath), { recursive: true, force: true });
+}
+
+describe('parseNavigationSpec', () => {
+ const tempSpecFiles: string[] = [];
+
+ afterEach(() => {
+ for (const specPath of tempSpecFiles) {
+ cleanupTempSpecFile(specPath);
+ }
+ tempSpecFiles.length = 0;
+ });
+
+ it('parses methods from BrownfieldNavigationSpec interface', () => {
+ const specPath = createTempSpecFile(`
+ export interface BrownfieldNavigationSpec {
+ openScreen(route: string, params?: Object): void;
+ }
+ `);
+ tempSpecFiles.push(specPath);
+
+ const methods = parseNavigationSpec(specPath);
+
+ expect(methods).toEqual([
+ {
+ name: 'openScreen',
+ params: [
+ { name: 'route', type: 'string', optional: false },
+ { name: 'params', type: 'Object', optional: true },
+ ],
+ returnType: 'void',
+ isAsync: false,
+ },
+ ]);
+ });
+
+ it('falls back to Spec interface when BrownfieldNavigationSpec is absent', () => {
+ const specPath = createTempSpecFile(`
+ export interface Spec {
+ presentModal(id: string): number;
+ }
+ `);
+ tempSpecFiles.push(specPath);
+
+ const methods = parseNavigationSpec(specPath);
+
+ expect(methods).toEqual([
+ {
+ name: 'presentModal',
+ params: [{ name: 'id', type: 'string', optional: false }],
+ returnType: 'number',
+ isAsync: false,
+ },
+ ]);
+ });
+
+ it('throws when no valid spec interface is present', () => {
+ const specPath = createTempSpecFile(`
+ export interface NavigationSpec {
+ noOp(): void;
+ }
+ `);
+ tempSpecFiles.push(specPath);
+
+ expect(() => parseNavigationSpec(specPath)).toThrow(
+ 'Could not find BrownfieldNavigationSpec or Spec interface in spec file'
+ );
+ });
+
+ it('throws when spec file does not exist', () => {
+ expect(() => parseNavigationSpec('/tmp/non-existent-navigation-spec.ts')).toThrow(
+ 'Spec file not found: /tmp/non-existent-navigation-spec.ts'
+ );
+ });
+});
diff --git a/packages/cli/src/navigation/__tests__/runner.integration.test.ts b/packages/cli/src/navigation/__tests__/runner.integration.test.ts
new file mode 100644
index 00000000..0fa3de1e
--- /dev/null
+++ b/packages/cli/src/navigation/__tests__/runner.integration.test.ts
@@ -0,0 +1,127 @@
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+let mockedNavigationPackagePath = '';
+
+vi.mock('../config', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ getNavigationPackagePath: vi.fn(() => mockedNavigationPackagePath),
+ };
+});
+
+vi.mock('../generators/ts', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ transpileWithConsumerBabel: vi.fn(() => '"use strict";module.exports={};'),
+ };
+});
+
+vi.mock('../generators/models', () => ({
+ generateNavigationModels: vi.fn(async () => ({ modelTypeNames: [] })),
+}));
+
+import { runNavigationCodegen } from '../runner.js';
+
+function createTempProject(): { projectRoot: string; packageRoot: string } {
+ const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'navigation-runner-'));
+ const packageRoot = path.join(projectRoot, 'mock-navigation-package');
+
+ fs.mkdirSync(path.join(packageRoot, 'src'), { recursive: true });
+ fs.mkdirSync(path.join(packageRoot, 'lib', 'commonjs'), { recursive: true });
+ fs.mkdirSync(path.join(packageRoot, 'lib', 'module'), { recursive: true });
+ fs.mkdirSync(
+ path.join(packageRoot, 'lib', 'typescript', 'commonjs', 'src'),
+ { recursive: true }
+ );
+ fs.mkdirSync(path.join(packageRoot, 'lib', 'typescript', 'module', 'src'), {
+ recursive: true,
+ });
+ fs.mkdirSync(path.join(packageRoot, 'ios'), { recursive: true });
+ fs.mkdirSync(
+ path.join(
+ packageRoot,
+ 'android',
+ 'src',
+ 'main',
+ 'java',
+ 'com',
+ 'callstack',
+ 'nativebrownfieldnavigation'
+ ),
+ { recursive: true }
+ );
+
+ return { projectRoot, packageRoot };
+}
+
+describe('runNavigationCodegen integration', () => {
+ const tempProjectRoots: string[] = [];
+
+ afterEach(() => {
+ for (const projectRoot of tempProjectRoots) {
+ fs.rmSync(projectRoot, { recursive: true, force: true });
+ }
+ tempProjectRoots.length = 0;
+ mockedNavigationPackagePath = '';
+ });
+
+ it('runs full codegen flow and writes generated artifacts', async () => {
+ const { projectRoot, packageRoot } = createTempProject();
+ tempProjectRoots.push(projectRoot);
+ mockedNavigationPackagePath = packageRoot;
+
+ fs.writeFileSync(
+ path.join(projectRoot, 'brownfield.navigation.ts'),
+ `
+ export interface BrownfieldNavigationSpec {
+ openScreen(route: string, params?: Object): void;
+ fetchToken(userId: string): Promise;
+ }
+ `
+ );
+
+ await runNavigationCodegen({ projectRoot });
+
+ const turboModuleSpecPath = path.join(
+ packageRoot,
+ 'src',
+ 'NativeBrownfieldNavigation.ts'
+ );
+ const indexTsPath = path.join(packageRoot, 'src', 'index.ts');
+ const swiftDelegatePath = path.join(
+ packageRoot,
+ 'ios',
+ 'BrownfieldNavigationDelegate.swift'
+ );
+ const kotlinModulePath = path.join(
+ packageRoot,
+ 'android',
+ 'src',
+ 'main',
+ 'java',
+ 'com',
+ 'callstack',
+ 'nativebrownfieldnavigation',
+ 'NativeBrownfieldNavigationModule.kt'
+ );
+
+ expect(fs.existsSync(turboModuleSpecPath)).toBe(true);
+ expect(fs.existsSync(indexTsPath)).toBe(true);
+ expect(fs.existsSync(swiftDelegatePath)).toBe(true);
+ expect(fs.existsSync(kotlinModulePath)).toBe(true);
+
+ expect(fs.readFileSync(turboModuleSpecPath, 'utf8')).toContain('openScreen');
+ expect(fs.readFileSync(indexTsPath, 'utf8')).toContain('fetchToken');
+ expect(fs.readFileSync(swiftDelegatePath, 'utf8')).toContain(
+ '@objc public protocol BrownfieldNavigationDelegate'
+ );
+ expect(fs.readFileSync(kotlinModulePath, 'utf8')).toContain(
+ 'class NativeBrownfieldNavigationModule'
+ );
+ });
+});
diff --git a/packages/cli/src/navigation/commands/codegen.ts b/packages/cli/src/navigation/commands/codegen.ts
new file mode 100644
index 00000000..ce0c935a
--- /dev/null
+++ b/packages/cli/src/navigation/commands/codegen.ts
@@ -0,0 +1,57 @@
+import { Command } from 'commander';
+import { intro, outro } from '@rock-js/tools';
+
+import { actionRunner } from '../../shared/index.js';
+import { runNavigationCodegen } from '../runner.js';
+
+interface RunNavigationCodegenCommandOptions {
+ dryRun?: boolean;
+}
+
+interface NavigationCodegenActionOptions {
+ specPath?: string;
+ dryRun?: boolean;
+}
+
+export async function runNavigationCodegenCommand({
+ specPath,
+ dryRun = false,
+}: NavigationCodegenActionOptions): Promise {
+ intro('Running Brownfield Navigation codegen');
+ await runNavigationCodegen({
+ specPath,
+ dryRun,
+ });
+ outro('Done!');
+}
+
+export const navigationCodegenCommand = new Command('navigation:codegen')
+ .description(
+ 'Generate Brownfield Navigation native bindings from brownfield.navigation.ts'
+ )
+ .argument(
+ '[specPath]',
+ 'Path to navigation spec file (defaults to brownfield.navigation.ts)'
+ )
+ .option('--dry-run', 'Print generated code without writing files')
+ .action(
+ actionRunner(
+ async (
+ ...args: Array
+ ) => {
+ const specPath = typeof args[0] === 'string' ? args[0] : undefined;
+ const options =
+ args.find(
+ (
+ arg
+ ): arg is RunNavigationCodegenCommandOptions =>
+ typeof arg === 'object' && arg !== null && 'dryRun' in arg
+ ) ?? {};
+
+ await runNavigationCodegenCommand({
+ specPath,
+ dryRun: Boolean(options.dryRun),
+ });
+ }
+ )
+ );
diff --git a/packages/cli/src/navigation/commands/index.ts b/packages/cli/src/navigation/commands/index.ts
new file mode 100644
index 00000000..4ea54b84
--- /dev/null
+++ b/packages/cli/src/navigation/commands/index.ts
@@ -0,0 +1 @@
+export * from './codegen.js';
diff --git a/packages/cli/src/navigation/config.ts b/packages/cli/src/navigation/config.ts
new file mode 100644
index 00000000..4bd09d41
--- /dev/null
+++ b/packages/cli/src/navigation/config.ts
@@ -0,0 +1,35 @@
+import path from 'node:path';
+import { createRequire } from 'node:module';
+
+export const NAVIGATION_PACKAGE_NAME = '@callstack/brownfield-navigation';
+export const DEFAULT_SPEC_FILENAME = 'brownfield.navigation.ts';
+export const DEFAULT_ANDROID_JAVA_PACKAGE =
+ 'com.callstack.nativebrownfieldnavigation';
+
+export function isNavigationInstalled(
+ projectRoot: string = process.cwd()
+): boolean {
+ const require = createRequire(path.join(projectRoot, 'package.json'));
+ try {
+ require.resolve(`${NAVIGATION_PACKAGE_NAME}/package.json`);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function getNavigationPackagePath(
+ projectRoot: string = process.cwd()
+): string {
+ const require = createRequire(path.join(projectRoot, 'package.json'));
+ try {
+ const packageJsonPath = require.resolve(
+ `${NAVIGATION_PACKAGE_NAME}/package.json`
+ );
+ return path.dirname(packageJsonPath);
+ } catch {
+ throw new Error(
+ `${NAVIGATION_PACKAGE_NAME} is not installed. Run 'npm install ${NAVIGATION_PACKAGE_NAME}' or 'yarn add ${NAVIGATION_PACKAGE_NAME}'`
+ );
+ }
+}
diff --git a/packages/cli/src/navigation/generators/android.ts b/packages/cli/src/navigation/generators/android.ts
new file mode 100644
index 00000000..540db7fe
--- /dev/null
+++ b/packages/cli/src/navigation/generators/android.ts
@@ -0,0 +1,127 @@
+import type { MethodSignature } from '../types.js';
+
+const TS_TO_KOTLIN_TYPE: Record = {
+ string: 'String',
+ number: 'Double',
+ boolean: 'Boolean',
+ void: 'Unit',
+ Object: 'ReadableMap',
+};
+
+function mapTsTypeToKotlin(tsType: string, optional: boolean = false): string {
+ if (tsType.startsWith('Promise<')) {
+ const inner = tsType.slice(8, -1);
+ return mapTsTypeToKotlin(inner, optional);
+ }
+
+ const mapped = TS_TO_KOTLIN_TYPE[tsType];
+ if (mapped) {
+ return optional ? `${mapped}?` : mapped;
+ }
+
+ return optional ? 'Any?' : 'Any';
+}
+
+export function generateKotlinDelegate(
+ methods: MethodSignature[],
+ kotlinPackageName: string
+): string {
+ const methodSignatures = methods
+ .map((method) => {
+ const params = method.params
+ .map(
+ (param) =>
+ `${param.name}: ${mapTsTypeToKotlin(param.type, param.optional)}`
+ )
+ .join(', ');
+ const returnType =
+ method.returnType === 'void'
+ ? ''
+ : `: ${mapTsTypeToKotlin(method.returnType, false)}`;
+ return ` fun ${method.name}(${params})${returnType}`;
+ })
+ .join('\n');
+
+ return `package ${kotlinPackageName}
+
+interface BrownfieldNavigationDelegate {
+${methodSignatures}
+}
+`;
+}
+
+export function generateKotlinModule(
+ methods: MethodSignature[],
+ kotlinPackageName: string
+): string {
+ const hasAsyncMethod = methods.some((method) => method.isAsync);
+ const hasObjectType = methods.some(
+ (method) =>
+ method.returnType.includes('Object') ||
+ method.params.some((param) => param.type === 'Object')
+ );
+
+ const methodImplementations = methods
+ .map((method) =>
+ method.isAsync
+ ? generateAsyncKotlinMethod(method)
+ : generateSyncKotlinMethod(method)
+ )
+ .join('\n\n');
+
+ return `package ${kotlinPackageName}
+
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactMethod${
+ hasAsyncMethod ? '\nimport com.facebook.react.bridge.Promise' : ''
+ }${hasObjectType ? '\nimport com.facebook.react.bridge.ReadableMap' : ''}
+
+class NativeBrownfieldNavigationModule(
+ reactContext: ReactApplicationContext
+) : NativeBrownfieldNavigationSpec(reactContext) {
+${methodImplementations}
+
+ companion object {
+ const val NAME = "NativeBrownfieldNavigation"
+ }
+}
+`;
+}
+
+function generateSyncKotlinMethod(method: MethodSignature): string {
+ const params = method.params
+ .map((param) => `${param.name}: ${mapTsTypeToKotlin(param.type, param.optional)}`)
+ .join(', ');
+ const args = method.params.map((param) => param.name).join(', ');
+
+ const signature = ` @ReactMethod\n override fun ${method.name}(${params})${
+ method.returnType === 'void'
+ ? ''
+ : `: ${mapTsTypeToKotlin(method.returnType, false)}`
+ }`;
+
+ if (method.returnType === 'void') {
+ return `${signature} {
+ BrownfieldNavigationManager.getDelegate().${method.name}(${args})
+ }`;
+ }
+
+ return `${signature} {
+ return BrownfieldNavigationManager.getDelegate().${method.name}(${args})
+ }`;
+}
+
+function generateAsyncKotlinMethod(method: MethodSignature): string {
+ const paramsWithTypes = method.params
+ .map((param) => `${param.name}: ${mapTsTypeToKotlin(param.type, param.optional)}`)
+ .join(', ');
+ const params =
+ paramsWithTypes.length > 0
+ ? `${paramsWithTypes}, promise: Promise`
+ : 'promise: Promise';
+
+ return ` @ReactMethod
+ override fun ${method.name}(${params}) {
+ promise.reject("not_implemented", "${method.name} is not implemented")
+ }`;
+}
diff --git a/packages/cli/src/navigation/generators/ios.ts b/packages/cli/src/navigation/generators/ios.ts
new file mode 100644
index 00000000..778dfcfd
--- /dev/null
+++ b/packages/cli/src/navigation/generators/ios.ts
@@ -0,0 +1,169 @@
+import type { MethodSignature } from '../types.js';
+
+const TS_TO_OBJC_TYPE: Record = {
+ string: 'NSString *',
+ number: 'double',
+ boolean: 'BOOL',
+ void: 'void',
+ Object: 'NSDictionary *',
+};
+
+const TS_TO_SWIFT_TYPE: Record = {
+ string: 'String',
+ number: 'Double',
+ boolean: 'Bool',
+ void: 'Void',
+ Object: '[String: Any]',
+};
+
+function mapTsTypeToObjC(tsType: string, nullable: boolean = false): string {
+ if (tsType.startsWith('Promise<')) {
+ return 'void';
+ }
+
+ const mapped = TS_TO_OBJC_TYPE[tsType];
+ if (mapped) {
+ if (nullable && mapped.includes('*')) {
+ return mapped.replace(' *', ' * _Nullable');
+ }
+ return mapped;
+ }
+
+ return nullable ? 'id _Nullable' : 'id';
+}
+
+function mapTsTypeToSwift(tsType: string, optional: boolean = false): string {
+ if (tsType.startsWith('Promise<')) {
+ const inner = tsType.slice(8, -1);
+ return mapTsTypeToSwift(inner, optional);
+ }
+
+ const mapped = TS_TO_SWIFT_TYPE[tsType];
+ if (mapped) {
+ return optional ? `${mapped}?` : mapped;
+ }
+
+ return optional ? 'Any?' : 'Any';
+}
+
+export function generateSwiftDelegate(methods: MethodSignature[]): string {
+ const protocolMethods = methods
+ .map((method) => {
+ const params = method.params
+ .map((param, index) => {
+ const swiftType = mapTsTypeToSwift(param.type, param.optional);
+ const label = index === 0 ? '_' : param.name;
+ return `${label} ${param.name}: ${swiftType}`;
+ })
+ .join(', ');
+
+ const returnType =
+ method.returnType === 'void'
+ ? ''
+ : ` -> ${mapTsTypeToSwift(method.returnType, false)}`;
+
+ return ` @objc func ${method.name}(${params})${returnType}`;
+ })
+ .join('\n');
+
+ return `import Foundation
+
+@objc public protocol BrownfieldNavigationDelegate: AnyObject {
+${protocolMethods}
+}
+`;
+}
+
+export function generateObjCImplementation(methods: MethodSignature[]): string {
+ const methodImplementations = methods
+ .map((method) =>
+ method.isAsync ? generateAsyncObjCMethod(method) : generateSyncObjCMethod(method)
+ )
+ .join('\n\n');
+
+ return `#import "NativeBrownfieldNavigation.h"
+
+#if __has_include("BrownfieldNavigation/BrownfieldNavigation-Swift.h")
+#import "BrownfieldNavigation/BrownfieldNavigation-Swift.h"
+#else
+#import "BrownfieldNavigation-Swift.h"
+#endif
+
+@implementation NativeBrownfieldNavigation
+
+${methodImplementations}
+
+- (std::shared_ptr)getTurboModule:
+ (const facebook::react::ObjCTurboModule::InitParams &)params
+{
+ return std::make_shared(params);
+}
+
++ (NSString *)moduleName
+{
+ return @"NativeBrownfieldNavigation";
+}
+
+@end
+`;
+}
+
+function generateSyncObjCMethod(method: MethodSignature): string {
+ const { name, params, returnType } = method;
+
+ let signature = `- (${mapTsTypeToObjC(returnType)})${name}`;
+ if (params.length > 0) {
+ signature += params
+ .map((param, index) => {
+ const prefix = index === 0 ? ':' : ` ${param.name}:`;
+ return `${prefix}(${mapTsTypeToObjC(param.type, param.optional)})${param.name}`;
+ })
+ .join('');
+ }
+
+ let delegateCall = `[[[BrownfieldNavigationManager shared] getDelegate] ${name}`;
+ if (params.length > 0) {
+ delegateCall += params
+ .map((param, index) => {
+ const label = index === 0 ? '' : param.name;
+ return `${label}:${param.name}`;
+ })
+ .join(' ');
+ }
+ delegateCall += ']';
+
+ const returnPrefix = returnType === 'void' ? '' : 'return ';
+
+ return `${signature} {
+ ${returnPrefix}${delegateCall};
+}`;
+}
+
+function generateAsyncObjCMethod(method: MethodSignature): string {
+ const { name, params } = method;
+
+ let signature = `- (void)${name}`;
+ const allParams: Array<{ name: string; type: string; optional: boolean }> = [
+ ...params,
+ { name: 'resolve', type: 'RCTPromiseResolveBlock', optional: false },
+ { name: 'reject', type: 'RCTPromiseRejectBlock', optional: false },
+ ];
+
+ signature += ':';
+ signature += allParams
+ .map((param, index) => {
+ const prefix = index === 0 ? '' : param.name;
+ const type =
+ param.type === 'RCTPromiseResolveBlock'
+ ? 'RCTPromiseResolveBlock'
+ : param.type === 'RCTPromiseRejectBlock'
+ ? 'RCTPromiseRejectBlock'
+ : mapTsTypeToObjC(param.type, param.optional);
+ return `${prefix}(${type})${param.name}`;
+ })
+ .join(' ');
+
+ return `${signature} {
+ reject(@"not_implemented", @"${name} is not implemented", nil);
+}`;
+}
diff --git a/packages/cli/src/navigation/generators/models.ts b/packages/cli/src/navigation/generators/models.ts
new file mode 100644
index 00000000..0d96a8fb
--- /dev/null
+++ b/packages/cli/src/navigation/generators/models.ts
@@ -0,0 +1,170 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import {
+ FetchingJSONSchemaStore,
+ InputData,
+ JSONSchemaInput,
+ quicktype,
+} from 'quicktype-core';
+import { schemaForTypeScriptSources } from 'quicktype-typescript-input';
+
+import type { MethodSignature } from '../types.js';
+
+export interface NavigationModelsOptions {
+ specPath: string;
+ methods: MethodSignature[];
+ kotlinPackageName: string;
+}
+
+export interface GeneratedNavigationModels {
+ swiftModels?: string;
+ kotlinModels?: string;
+ modelTypeNames: string[];
+}
+
+const SKIP_TYPE_TOKENS = new Set([
+ 'Array',
+ 'Date',
+ 'Map',
+ 'Object',
+ 'Promise',
+ 'ReadonlyArray',
+ 'Record',
+ 'Set',
+ 'any',
+ 'boolean',
+ 'false',
+ 'null',
+ 'number',
+ 'object',
+ 'string',
+ 'true',
+ 'undefined',
+ 'unknown',
+ 'void',
+]);
+
+function collectReferencedTypes(methods: MethodSignature[]): Set {
+ const referenced = new Set();
+
+ for (const method of methods) {
+ const typeTexts = [
+ method.returnType,
+ ...method.params.map((param) => param.type),
+ ];
+ for (const typeText of typeTexts) {
+ const matches = typeText.match(/\b[A-Za-z_]\w*\b/g);
+ if (!matches) {
+ continue;
+ }
+ for (const match of matches) {
+ if (!SKIP_TYPE_TOKENS.has(match)) {
+ referenced.add(match);
+ }
+ }
+ }
+ }
+
+ return referenced;
+}
+
+async function generateModelsForLanguage({
+ typeNames,
+ schema,
+ lang,
+ kotlinPackageName,
+}: {
+ typeNames: string[];
+ schema: object;
+ lang: 'swift' | 'kotlin';
+ kotlinPackageName: string;
+}): Promise {
+ const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());
+
+ for (const typeName of typeNames) {
+ const rootSchema = JSON.parse(JSON.stringify(schema)) as {
+ $ref?: string;
+ };
+ rootSchema.$ref = `#/definitions/${typeName}`;
+
+ await schemaInput.addSource({
+ name: typeName,
+ schema: JSON.stringify(rootSchema),
+ });
+ }
+
+ const inputData = new InputData();
+ inputData.addInput(schemaInput);
+
+ const rendererOptions =
+ lang === 'swift'
+ ? {
+ 'access-level': 'public',
+ 'mutable-properties': 'true',
+ initializers: 'false',
+ 'swift-5-support': 'true',
+ }
+ : {
+ framework: 'just-types',
+ package: kotlinPackageName,
+ };
+
+ const { lines } = await quicktype({
+ inputData,
+ lang,
+ rendererOptions,
+ });
+
+ return lines.join('\n');
+}
+
+export async function generateNavigationModels({
+ specPath,
+ methods,
+ kotlinPackageName,
+}: NavigationModelsOptions): Promise {
+ const absoluteSpecPath = path.resolve(process.cwd(), specPath);
+
+ if (!fs.existsSync(absoluteSpecPath)) {
+ throw new Error(`Spec file not found: ${absoluteSpecPath}`);
+ }
+
+ const schemaData = schemaForTypeScriptSources([absoluteSpecPath]);
+ if (!schemaData.schema) {
+ throw new Error('Failed to generate schema from TypeScript spec');
+ }
+
+ const parsedSchema = JSON.parse(schemaData.schema) as {
+ definitions?: Record;
+ };
+ const referencedTypes = collectReferencedTypes(methods);
+ const definitions = parsedSchema.definitions ?? {};
+ const modelTypeNames = [...referencedTypes].filter((typeName) =>
+ Object.hasOwn(definitions, typeName)
+ );
+
+ if (modelTypeNames.length === 0) {
+ return { modelTypeNames: [] };
+ }
+
+ const [swiftModels, kotlinModels] = await Promise.all([
+ generateModelsForLanguage({
+ typeNames: modelTypeNames,
+ schema: parsedSchema,
+ lang: 'swift',
+ kotlinPackageName,
+ }),
+ generateModelsForLanguage({
+ typeNames: modelTypeNames,
+ schema: parsedSchema,
+ lang: 'kotlin',
+ kotlinPackageName,
+ }),
+ ]);
+
+ return {
+ swiftModels,
+ kotlinModels,
+ modelTypeNames,
+ };
+}
diff --git a/packages/cli/src/navigation/generators/ts.ts b/packages/cli/src/navigation/generators/ts.ts
new file mode 100644
index 00000000..6d70eba4
--- /dev/null
+++ b/packages/cli/src/navigation/generators/ts.ts
@@ -0,0 +1,123 @@
+import path from 'node:path';
+import { createRequire } from 'node:module';
+
+import type { MethodSignature } from '../types.js';
+
+export function generateTurboModuleSpec(methods: MethodSignature[]): string {
+ const methodSignatures = methods
+ .map((method) => {
+ const params = method.params
+ .map((param) => `${param.name}${param.optional ? '?' : ''}: ${param.type}`)
+ .join(', ');
+ return ` ${method.name}(${params}): ${method.returnType};`;
+ })
+ .join('\n');
+
+ return `import { TurboModuleRegistry, type TurboModule } from 'react-native';
+
+export interface Spec extends TurboModule {
+${methodSignatures}
+}
+
+export default TurboModuleRegistry.getEnforcing(
+ 'NativeBrownfieldNavigation'
+);
+`;
+}
+
+export function generateIndexTs(methods: MethodSignature[]): string {
+ const functionImplementations = methods
+ .map((method) => {
+ const params = method.params
+ .map((param) => `${param.name}${param.optional ? '?' : ''}: ${param.type}`)
+ .join(', ');
+ const args = method.params.map((param) => param.name).join(', ');
+ const returnType = method.returnType === 'void' ? '' : `: ${method.returnType}`;
+
+ if (method.isAsync) {
+ return ` ${method.name}: async (${params})${returnType} => {
+ return NativeBrownfieldNavigation.${method.name}(${args});
+ }`;
+ }
+
+ return ` ${method.name}: (${params})${returnType} => {
+ NativeBrownfieldNavigation.${method.name}(${args});
+ }`;
+ })
+ .join(',\n');
+
+ return `import NativeBrownfieldNavigation from './NativeBrownfieldNavigation';
+
+const BrownfieldNavigation = {
+${functionImplementations},
+};
+
+export default BrownfieldNavigation;
+`;
+}
+
+export function transpileWithConsumerBabel(
+ tsCode: string,
+ projectRoot: string,
+ packageRoot: string
+): string {
+ const nodeRequire = createRequire(path.join(projectRoot, 'package.json'));
+ const moduleCandidates = [projectRoot, packageRoot];
+
+ function resolveOrThrow(moduleName: string): string {
+ for (const modulePath of moduleCandidates) {
+ try {
+ return nodeRequire.resolve(moduleName, { paths: [modulePath] });
+ } catch {
+ // Continue with remaining candidates.
+ }
+ }
+
+ throw new Error(
+ `Could not resolve "${moduleName}". Install it in your app devDependencies.`
+ );
+ }
+
+ const babelCorePath = resolveOrThrow('@babel/core');
+ const rnPresetPath = resolveOrThrow('@react-native/babel-preset');
+ const babelCore = nodeRequire(babelCorePath) as {
+ transformSync: (
+ source: string,
+ options: Record
+ ) => { code?: string | null } | null;
+ };
+
+ const transformed = babelCore.transformSync(tsCode, {
+ filename: 'index.ts',
+ babelrc: false,
+ configFile: false,
+ comments: false,
+ compact: true,
+ minified: true,
+ presets: [[rnPresetPath, {}]],
+ });
+
+ if (!transformed?.code) {
+ throw new Error('Babel transpilation failed for generated index.ts');
+ }
+
+ return transformed.code;
+}
+
+export function generateIndexDts(methods: MethodSignature[]): string {
+ const methodSignatures = methods
+ .map((method) => {
+ const params = method.params
+ .map((param) => `${param.name}${param.optional ? '?' : ''}: ${param.type}`)
+ .join(', ');
+ return ` ${method.name}: (${params}) => ${method.returnType};`;
+ })
+ .join('\n');
+
+ return `declare const BrownfieldNavigation: {
+${methodSignatures}
+};
+
+export default BrownfieldNavigation;
+`;
+}
diff --git a/packages/cli/src/navigation/helpers/runNavigationCodegenIfApplicable.ts b/packages/cli/src/navigation/helpers/runNavigationCodegenIfApplicable.ts
new file mode 100644
index 00000000..e0a97cc8
--- /dev/null
+++ b/packages/cli/src/navigation/helpers/runNavigationCodegenIfApplicable.ts
@@ -0,0 +1,17 @@
+import { isNavigationInstalled } from '../config.js';
+import { isNavigationSpecPresent } from '../spec-discovery.js';
+import { runNavigationCodegen } from '../runner.js';
+
+export async function runNavigationCodegenIfApplicable(
+ projectRoot: string,
+ specPath?: string
+): Promise<{ hasNavigation: boolean; hasSpec: boolean }> {
+ const hasNavigation = isNavigationInstalled(projectRoot);
+ const hasSpec = hasNavigation && isNavigationSpecPresent(specPath, projectRoot);
+
+ if (hasSpec) {
+ await runNavigationCodegen({ specPath, projectRoot });
+ }
+
+ return { hasNavigation, hasSpec };
+}
diff --git a/packages/cli/src/navigation/index.ts b/packages/cli/src/navigation/index.ts
new file mode 100644
index 00000000..153602f2
--- /dev/null
+++ b/packages/cli/src/navigation/index.ts
@@ -0,0 +1,10 @@
+import { styleText } from 'node:util';
+
+import * as Commands from './commands/index.js';
+
+export const groupName = `${styleText(['bold', 'blueBright'], '@callstack/brownfield-navigation')}${styleText('whiteBright', ' - native codegen utilities for Brownfield Navigation')}`;
+
+export { Commands };
+export type * from './types.js';
+export * from './runner.js';
+export default Commands;
diff --git a/packages/cli/src/navigation/parser.ts b/packages/cli/src/navigation/parser.ts
new file mode 100644
index 00000000..8d1c7b1d
--- /dev/null
+++ b/packages/cli/src/navigation/parser.ts
@@ -0,0 +1,43 @@
+import fs from 'node:fs';
+import { Project } from 'ts-morph';
+
+import type { MethodParam, MethodSignature } from './types.js';
+
+export function parseNavigationSpec(specPath: string): MethodSignature[] {
+ if (!fs.existsSync(specPath)) {
+ throw new Error(`Spec file not found: ${specPath}`);
+ }
+
+ const project = new Project({ skipAddingFilesFromTsConfig: true });
+ const sourceFile = project.addSourceFileAtPath(specPath);
+ const specInterface =
+ sourceFile.getInterface('BrownfieldNavigationSpec') ??
+ sourceFile.getInterface('Spec');
+
+ if (!specInterface) {
+ throw new Error(
+ 'Could not find BrownfieldNavigationSpec or Spec interface in spec file'
+ );
+ }
+
+ return specInterface.getMethods().map((method): MethodSignature => {
+ const name = method.getName();
+ const params: MethodParam[] = method.getParameters().map((param) => {
+ const typeNode = param.getTypeNode();
+ return {
+ name: param.getName(),
+ type: typeNode?.getText() ?? 'unknown',
+ optional: param.isOptional(),
+ };
+ });
+ const returnTypeNode = method.getReturnTypeNode();
+ const returnType = returnTypeNode?.getText() ?? 'void';
+
+ return {
+ name,
+ params,
+ returnType,
+ isAsync: returnType.startsWith('Promise<'),
+ };
+ });
+}
diff --git a/packages/cli/src/navigation/runner.ts b/packages/cli/src/navigation/runner.ts
new file mode 100644
index 00000000..bcde6ad4
--- /dev/null
+++ b/packages/cli/src/navigation/runner.ts
@@ -0,0 +1,256 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+import { logger } from '@rock-js/tools';
+import {
+ DEFAULT_ANDROID_JAVA_PACKAGE,
+ getNavigationPackagePath,
+} from './config.js';
+import { parseNavigationSpec } from './parser.js';
+import { resolveNavigationSpecPath } from './spec-discovery.js';
+import {
+ generateIndexDts,
+ generateIndexTs,
+ generateTurboModuleSpec,
+ transpileWithConsumerBabel,
+} from './generators/ts.js';
+import {
+ generateObjCImplementation,
+ generateSwiftDelegate,
+} from './generators/ios.js';
+import {
+ generateKotlinDelegate,
+ generateKotlinModule,
+} from './generators/android.js';
+import { generateNavigationModels } from './generators/models.js';
+import type { GeneratedNavigationArtifacts } from './types.js';
+
+interface RunNavigationCodegenOptions {
+ specPath?: string;
+ dryRun?: boolean;
+ projectRoot?: string;
+}
+
+interface NavigationOutputPaths {
+ turboModuleSpec: string;
+ navigationTs: string;
+ commonjsIndexJs: string;
+ moduleIndexJs: string;
+ commonjsIndexDts: string;
+ moduleIndexDts: string;
+ swiftDelegate: string;
+ swiftModels: string;
+ objcImplementation: string;
+ kotlinDelegate: string;
+ kotlinModule: string;
+ kotlinModels: string;
+}
+
+function getOutputPaths(
+ packageRoot: string,
+ androidJavaPackageName: string
+): NavigationOutputPaths {
+ const androidPackagePathSegments = androidJavaPackageName.split('.');
+
+ return {
+ turboModuleSpec: path.join(packageRoot, 'src', 'NativeBrownfieldNavigation.ts'),
+ navigationTs: path.join(packageRoot, 'src', 'index.ts'),
+ commonjsIndexJs: path.join(packageRoot, 'lib', 'commonjs', 'index.js'),
+ moduleIndexJs: path.join(packageRoot, 'lib', 'module', 'index.js'),
+ commonjsIndexDts: path.join(
+ packageRoot,
+ 'lib',
+ 'typescript',
+ 'commonjs',
+ 'src',
+ 'index.d.ts'
+ ),
+ moduleIndexDts: path.join(
+ packageRoot,
+ 'lib',
+ 'typescript',
+ 'module',
+ 'src',
+ 'index.d.ts'
+ ),
+ swiftDelegate: path.join(
+ packageRoot,
+ 'ios',
+ 'BrownfieldNavigationDelegate.swift'
+ ),
+ swiftModels: path.join(packageRoot, 'ios', 'BrownfieldNavigationModels.swift'),
+ objcImplementation: path.join(
+ packageRoot,
+ 'ios',
+ 'NativeBrownfieldNavigation.mm'
+ ),
+ kotlinDelegate: path.join(
+ packageRoot,
+ 'android',
+ 'src',
+ 'main',
+ 'java',
+ ...androidPackagePathSegments,
+ 'BrownfieldNavigationDelegate.kt'
+ ),
+ kotlinModule: path.join(
+ packageRoot,
+ 'android',
+ 'src',
+ 'main',
+ 'java',
+ ...androidPackagePathSegments,
+ 'NativeBrownfieldNavigationModule.kt'
+ ),
+ kotlinModels: path.join(
+ packageRoot,
+ 'android',
+ 'src',
+ 'main',
+ 'java',
+ ...androidPackagePathSegments,
+ 'BrownfieldNavigationModels.kt'
+ ),
+ };
+}
+
+function writeArtifacts(
+ paths: NavigationOutputPaths,
+ artifacts: GeneratedNavigationArtifacts
+): void {
+ fs.writeFileSync(paths.turboModuleSpec, artifacts.turboModuleSpec);
+ logger.success(`Generated ${paths.turboModuleSpec}`);
+
+ fs.writeFileSync(paths.navigationTs, artifacts.indexTs);
+ logger.success(`Generated ${paths.navigationTs}`);
+
+ fs.writeFileSync(paths.commonjsIndexJs, artifacts.indexJs);
+ logger.success(`Generated ${paths.commonjsIndexJs}`);
+
+ fs.writeFileSync(paths.moduleIndexJs, artifacts.indexJs);
+ logger.success(`Generated ${paths.moduleIndexJs}`);
+
+ fs.writeFileSync(paths.commonjsIndexDts, artifacts.indexDts);
+ logger.success(`Generated ${paths.commonjsIndexDts}`);
+
+ fs.writeFileSync(paths.moduleIndexDts, artifacts.indexDts);
+ logger.success(`Generated ${paths.moduleIndexDts}`);
+
+ fs.writeFileSync(paths.swiftDelegate, artifacts.swiftDelegate);
+ logger.success(`Generated ${paths.swiftDelegate}`);
+
+ if (artifacts.swiftModels) {
+ fs.writeFileSync(paths.swiftModels, artifacts.swiftModels);
+ logger.success(`Generated ${paths.swiftModels}`);
+ }
+
+ fs.writeFileSync(paths.objcImplementation, artifacts.objcImplementation);
+ logger.success(`Generated ${paths.objcImplementation}`);
+
+ fs.writeFileSync(paths.kotlinDelegate, artifacts.kotlinDelegate);
+ logger.success(`Generated ${paths.kotlinDelegate}`);
+
+ fs.writeFileSync(paths.kotlinModule, artifacts.kotlinModule);
+ logger.success(`Generated ${paths.kotlinModule}`);
+
+ if (artifacts.kotlinModels) {
+ fs.writeFileSync(paths.kotlinModels, artifacts.kotlinModels);
+ logger.success(`Generated ${paths.kotlinModels}`);
+ }
+}
+
+function printDryRun(
+ androidJavaPackageName: string,
+ artifacts: GeneratedNavigationArtifacts
+): void {
+ logger.info('\n--- Generated: src/NativeBrownfieldNavigation.ts ---\n');
+ logger.log(artifacts.turboModuleSpec);
+ logger.info('\n--- Generated: src/index.ts ---\n');
+ logger.log(artifacts.indexTs);
+ logger.info('\n--- Generated (Babel): lib/{commonjs,module}/index.js ---\n');
+ logger.log(artifacts.indexJs);
+ logger.info(
+ '\n--- Generated: lib/typescript/{commonjs,module}/src/index.d.ts ---\n'
+ );
+ logger.log(artifacts.indexDts);
+ logger.info('\n--- Generated: ios/BrownfieldNavigationDelegate.swift ---\n');
+ logger.log(artifacts.swiftDelegate);
+ if (artifacts.swiftModels) {
+ logger.info('\n--- Generated: ios/BrownfieldNavigationModels.swift ---\n');
+ logger.log(artifacts.swiftModels);
+ }
+ logger.info('\n--- Generated: ios/NativeBrownfieldNavigation.mm ---\n');
+ logger.log(artifacts.objcImplementation);
+ logger.info(
+ `\n--- Generated: android/src/main/java/${androidJavaPackageName.replaceAll('.', '/')}/BrownfieldNavigationDelegate.kt ---\n`
+ );
+ logger.log(artifacts.kotlinDelegate);
+ logger.info(
+ `\n--- Generated: android/src/main/java/${androidJavaPackageName.replaceAll('.', '/')}/NativeBrownfieldNavigationModule.kt ---\n`
+ );
+ logger.log(artifacts.kotlinModule);
+ if (artifacts.kotlinModels) {
+ logger.info(
+ `\n--- Generated: android/src/main/java/${androidJavaPackageName.replaceAll('.', '/')}/BrownfieldNavigationModels.kt ---\n`
+ );
+ logger.log(artifacts.kotlinModels);
+ }
+}
+
+export async function runNavigationCodegen({
+ specPath,
+ dryRun = false,
+ projectRoot = process.cwd(),
+}: RunNavigationCodegenOptions): Promise {
+ const resolvedSpecPath = resolveNavigationSpecPath(specPath, projectRoot);
+ if (!fs.existsSync(resolvedSpecPath)) {
+ throw new Error(`Spec file not found: ${resolvedSpecPath}`);
+ }
+
+ logger.info(`Parsing spec file: ${resolvedSpecPath}`);
+ const methods = parseNavigationSpec(resolvedSpecPath);
+ if (methods.length === 0) {
+ throw new Error('No methods found in spec file');
+ }
+
+ logger.info(
+ `Found ${methods.length} method(s): ${methods.map((method) => method.name).join(', ')}`
+ );
+
+ const packageRoot = getNavigationPackagePath(projectRoot);
+ const androidJavaPackageName = DEFAULT_ANDROID_JAVA_PACKAGE;
+ const indexTs = generateIndexTs(methods);
+ const models = await generateNavigationModels({
+ specPath: resolvedSpecPath,
+ methods,
+ kotlinPackageName: androidJavaPackageName,
+ });
+
+ const artifacts: GeneratedNavigationArtifacts = {
+ turboModuleSpec: generateTurboModuleSpec(methods),
+ indexTs,
+ indexJs: transpileWithConsumerBabel(indexTs, projectRoot, packageRoot),
+ indexDts: generateIndexDts(methods),
+ swiftDelegate: generateSwiftDelegate(methods),
+ objcImplementation: generateObjCImplementation(methods),
+ kotlinDelegate: generateKotlinDelegate(methods, androidJavaPackageName),
+ kotlinModule: generateKotlinModule(methods, androidJavaPackageName),
+ };
+
+ if (models.modelTypeNames.length > 0) {
+ logger.info(
+ `Generating quicktype models for types: ${models.modelTypeNames.join(', ')}`
+ );
+ artifacts.swiftModels = models.swiftModels;
+ artifacts.kotlinModels = models.kotlinModels;
+ } else {
+ logger.info('No complex model types found; skipping quicktype model generation');
+ }
+
+ if (dryRun) {
+ printDryRun(androidJavaPackageName, artifacts);
+ return;
+ }
+
+ writeArtifacts(getOutputPaths(packageRoot, androidJavaPackageName), artifacts);
+}
diff --git a/packages/cli/src/navigation/spec-discovery.ts b/packages/cli/src/navigation/spec-discovery.ts
new file mode 100644
index 00000000..4619742b
--- /dev/null
+++ b/packages/cli/src/navigation/spec-discovery.ts
@@ -0,0 +1,25 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+import { DEFAULT_SPEC_FILENAME } from './config.js';
+
+export function resolveNavigationSpecPath(
+ specPath: string | undefined,
+ projectRoot: string
+): string {
+ if (specPath) {
+ return path.isAbsolute(specPath)
+ ? specPath
+ : path.resolve(projectRoot, specPath);
+ }
+
+ return path.resolve(projectRoot, DEFAULT_SPEC_FILENAME);
+}
+
+export function isNavigationSpecPresent(
+ specPath: string | undefined,
+ projectRoot: string = process.cwd()
+): boolean {
+ const resolvedSpecPath = resolveNavigationSpecPath(specPath, projectRoot);
+ return fs.existsSync(resolvedSpecPath);
+}
diff --git a/packages/cli/src/navigation/types.ts b/packages/cli/src/navigation/types.ts
new file mode 100644
index 00000000..4be8a0cf
--- /dev/null
+++ b/packages/cli/src/navigation/types.ts
@@ -0,0 +1,25 @@
+export interface MethodParam {
+ name: string;
+ type: string;
+ optional: boolean;
+}
+
+export interface MethodSignature {
+ name: string;
+ params: MethodParam[];
+ returnType: string;
+ isAsync: boolean;
+}
+
+export interface GeneratedNavigationArtifacts {
+ turboModuleSpec: string;
+ indexTs: string;
+ indexJs: string;
+ indexDts: string;
+ swiftDelegate: string;
+ objcImplementation: string;
+ kotlinDelegate: string;
+ kotlinModule: string;
+ swiftModels?: string;
+ kotlinModels?: string;
+}
diff --git a/yarn.lock b/yarn.lock
index 1a86a49d..3f6c1763 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2255,6 +2255,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@callstack/brownfield-example-expo-app@workspace:apps/ExpoApp"
dependencies:
+ "@callstack/brownfield-navigation": "workspace:^"
"@callstack/brownie": "workspace:^"
"@callstack/react-native-brownfield": "workspace:^"
"@expo/vector-icons": "npm:^15.0.3"
@@ -2299,6 +2300,7 @@ __metadata:
"@babel/core": "npm:^7.25.2"
"@babel/preset-env": "npm:^7.25.3"
"@babel/runtime": "npm:^7.25.0"
+ "@callstack/brownfield-navigation": "workspace:^"
"@callstack/brownie": "workspace:^"
"@callstack/react-native-brownfield": "workspace:^"
"@react-native-community/cli": "npm:20.0.0"
@@ -2357,6 +2359,33 @@ __metadata:
languageName: unknown
linkType: soft
+"@callstack/brownfield-navigation@workspace:^, @callstack/brownfield-navigation@workspace:packages/brownfield-navigation":
+ version: 0.0.0-use.local
+ resolution: "@callstack/brownfield-navigation@workspace:packages/brownfield-navigation"
+ dependencies:
+ "@babel/core": "npm:^7.25.2"
+ "@babel/preset-env": "npm:^7.25.3"
+ "@babel/runtime": "npm:^7.25.0"
+ "@callstack/brownfield-cli": "workspace:^"
+ "@react-native/babel-preset": "npm:0.82.1"
+ "@types/jest": "npm:^30.0.0"
+ "@types/react": "npm:^19.1.1"
+ eslint: "npm:^9.28.0"
+ globals: "npm:^16.2.0"
+ import: "npm:^0.0.6"
+ nodemon: "npm:^3.1.11"
+ react: "npm:19.1.1"
+ react-native: "npm:0.82.1"
+ react-native-builder-bob: "npm:^0.40.17"
+ typescript: "npm:5.9.3"
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ bin:
+ brownfield: lib/commonjs/scripts/brownfield.js
+ languageName: unknown
+ linkType: soft
+
"@callstack/brownie@workspace:^, @callstack/brownie@workspace:packages/brownie":
version: 0.0.0-use.local
resolution: "@callstack/brownie@workspace:packages/brownie"