Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-insects-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@callstack/brownfield-cli': patch
---

fix: Object params correctly reflect the generated native code instead of Any
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.callstack.brownfield.android.example.components.PostMessageCard
import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme
import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationDelegate
import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationManager
import com.callstack.nativebrownfieldnavigation.UserType
import com.callstack.reactnativebrownfield.ReactNativeFragment
import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames

Expand Down Expand Up @@ -81,7 +82,7 @@ class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
}
}

override fun navigateToSettings() {
override fun navigateToSettings(user: UserType) {
startActivity(Intent(this, SettingsActivity::class.java))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}

public class RNNavigationDelegate: BrownfieldNavigationDelegate {
public func navigateToSettings() {
public func navigateToSettings(_ user: BrownfieldNavigation.UserType) {
present(SettingsScreen())
}

Expand Down
8 changes: 7 additions & 1 deletion apps/ExpoApp54/brownfield.navigation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
type UserType = {
id: string;
name: string;
email: string;
};

export interface BrownfieldNavigationSpec {
/**
* Navigate to the native settings screen
*/
navigateToSettings(): void;
navigateToSettings(user: UserType): void;

/**
* Navigate to the native referrals screen
Expand Down
8 changes: 7 additions & 1 deletion apps/ExpoApp55/brownfield.navigation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
type UserType = {
id: string;
name: string;
email: string;
};

export interface BrownfieldNavigationSpec {
/**
* Navigate to the native settings screen
*/
navigateToSettings(): void;
navigateToSettings(user: UserType): void;

/**
* Navigate to the native referrals screen
Expand Down
8 changes: 7 additions & 1 deletion apps/RNApp/brownfield.navigation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
type UserType = {
id: string;
name: string;
email: string;
};

export interface BrownfieldNavigationSpec {
/**
* Navigate to the native settings screen
*/
navigateToSettings(): void;
navigateToSettings(user: UserType): void;

/**
* Navigate to the native referrals screen
Expand Down
12 changes: 6 additions & 6 deletions apps/RNApp/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- boost (1.84.0)
- BrownfieldNavigation (3.6.0):
- BrownfieldNavigation (3.7.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -28,7 +28,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- Brownie (3.6.0):
- Brownie (3.7.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2461,7 +2461,7 @@ PODS:
- SocketRocket
- ReactAppDependencyProvider (0.85.0):
- ReactCodegen
- ReactBrownfield (3.6.0):
- ReactBrownfield (3.7.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2898,8 +2898,8 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
BrownfieldNavigation: 0a4abcd0295639640d0222ac5c47ab63d94983c8
Brownie: c75e781646955724c3b385e1a53704cc06491bf0
BrownfieldNavigation: 2a110b2734c33e3a695e28117ff4515c9bb0a035
Brownie: b30acefef59a97b9d84353b4e010af58f09dc900
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
FBLazyVector: dfb9ab6ee2eac316f7869edf6ec27b9e872329f0
Expand Down Expand Up @@ -2974,7 +2974,7 @@ SPEC CHECKSUMS:
React-utils: f2dc3878565c3cc54bdf7f65a106efaf93f189a6
React-webperformancenativemodule: 214e42892a044b865f73ad4f88cac6979c27aa76
ReactAppDependencyProvider: 5787b37b8e2e51dfeab697ec031cc7c4080dcea2
ReactBrownfield: 9e36bd174c53254c7a283a6305a4b26589e75f97
ReactBrownfield: 0420c061dccf3a41c495fd2fecc22a6faed5d7fd
ReactCodegen: 6ddd8f44847646a047320a22f5ddb10b27a515c9
ReactCommon: 6a42764f1136fb9ac210e05e88a0733a00ee23d3
RNScreens: e902eba58a27d3ad399a495d578e8aba3ea0f490
Expand Down
8 changes: 7 additions & 1 deletion apps/RNApp/src/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,13 @@ export function HomeScreen({
</View>

<Button
onPress={() => BrownfieldNavigation.navigateToSettings()}
onPress={() =>
BrownfieldNavigation.navigateToSettings({
id: '123',
name: 'John Doe',
email: 'john.doe@example.com',
})
}
color={colors.secondary}
title="Open native settings"
/>
Expand Down
153 changes: 152 additions & 1 deletion packages/cli/src/navigation/__tests__/generators.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

import { generateKotlinDelegate, generateKotlinModule } from '../generators/android.js';
import { generateObjCImplementation, generateSwiftDelegate } from '../generators/ios.js';
import {
generateObjCImplementation,
generateSwiftDelegate,
} from '../generators/ios.js';
import {
generateIndexDts,
generateIndexTs,
Expand All @@ -21,6 +24,21 @@ const methods: MethodSignature[] = [
},
];

const modelMethods: MethodSignature[] = [
{
name: 'openSettings',
params: [{ name: 'payload', type: 'DummyType', optional: false }],
returnType: 'void',
isAsync: false,
},
{
name: 'openSettingsOptional',
params: [{ name: 'payload', type: 'DummyType', optional: true }],
returnType: 'void',
isAsync: false,
},
];

describe('navigation code generators', () => {
it('generates TurboModule spec and index files', () => {
const turboModuleSpec = generateTurboModuleSpec(methods);
Expand All @@ -36,6 +54,70 @@ describe('navigation code generators', () => {
expect(indexDts).toContain('openScreen: (route: string, params?: Object) => void;');
});

it('includes referenced custom type declarations in TurboModule spec', () => {
const turboModuleSpec = generateTurboModuleSpec(
[
{
name: 'navigateToSettings',
params: [{ name: 'user', type: 'UserType', optional: false }],
returnType: 'void',
isAsync: false,
},
],
[
{
name: 'UserType',
declaration: 'export type UserType = { id: string; };',
},
],
{
modelTypeNames: ['UserType'],
}
);

expect(turboModuleSpec).toContain('export type UserType = { id: string; };');
expect(turboModuleSpec).toContain(
'navigateToSettings(user: Object): void;'
);
});

it('imports referenced custom types into generated index files', () => {
const customTypeMethods: MethodSignature[] = [
{
name: 'navigateToSettings',
params: [{ name: 'user', type: 'UserType', optional: false }],
returnType: 'void',
isAsync: false,
},
];
const referencedTypeDeclarations = [
{
name: 'UserType',
declaration: 'export type UserType = { id: string; };',
},
];

const indexTs = generateIndexTs(
customTypeMethods,
referencedTypeDeclarations
);
const indexDts = generateIndexDts(
customTypeMethods,
referencedTypeDeclarations
);

expect(indexTs).toContain(
"import type { UserType } from './NativeBrownfieldNavigation';"
);
expect(indexTs).toContain('navigateToSettings: (user: UserType)');
expect(indexDts).toContain(
"import type { UserType } from './NativeBrownfieldNavigation';"
);
expect(indexDts).toContain(
'navigateToSettings: (user: UserType) => void;'
);
});

it('generates iOS bindings for sync methods', () => {
const swiftDelegate = generateSwiftDelegate(methods);
const objcImplementation = generateObjCImplementation(methods);
Expand Down Expand Up @@ -65,6 +147,75 @@ describe('navigation code generators', () => {
'BrownfieldNavigationManager.getDelegate().openScreen(route, params)'
);
});

it('maps known model types for Swift delegate signatures', () => {
const swiftDelegate = generateSwiftDelegate(modelMethods, {
modelTypeNames: ['DummyType'],
});

expect(swiftDelegate).toContain(
'@objc func openSettings(_ payload: DummyType)'
);
expect(swiftDelegate).toContain(
'@objc func openSettingsOptional(_ payload: DummyType?)'
);
});

it('maps known model types for Kotlin delegate/module signatures', () => {
const kotlinPackageName = 'com.callstack.nativebrownfieldnavigation';
const options = {
modelTypeNames: ['DummyType'],
};
const kotlinDelegate = generateKotlinDelegate(
modelMethods,
kotlinPackageName,
options
);
const kotlinModule = generateKotlinModule(
modelMethods,
kotlinPackageName,
options
);

expect(kotlinDelegate).toContain('fun openSettings(payload: DummyType)');
expect(kotlinDelegate).toContain('fun openSettingsOptional(payload: DummyType?)');
expect(kotlinModule).toContain(
'override fun openSettings(payload: ReadableMap)'
);
expect(kotlinModule).toContain(
'override fun openSettingsOptional(payload: ReadableMap?)'
);
expect(kotlinModule).toContain(
'val payloadModel = payload.let(::toDummyType)'
);
expect(kotlinModule).toContain(
'BrownfieldNavigationManager.getDelegate().openSettings(payloadModel)'
);
expect(kotlinModule).toContain(
'val payloadModel = payload?.let(::toDummyType)'
);
expect(kotlinModule).not.toContain('payload: Any');
expect(kotlinModule).not.toContain('payload: Any?');
});

it('uses NSDictionary model args and converts before delegate calls', () => {
const objcImplementation = generateObjCImplementation(modelMethods, {
modelTypeNames: ['DummyType'],
});

expect(objcImplementation).toContain(
'- (void)openSettings:(NSDictionary *)payload'
);
expect(objcImplementation).toContain(
'- (void)openSettingsOptional:(NSDictionary * _Nullable)payload'
);
expect(objcImplementation).toContain(
'DummyType *payloadModel = payload == nil ? nil : [DummyType fromDictionary:payload];'
);
expect(objcImplementation).toContain(
'openSettings:payloadModel'
);
});
});

describe('transpileWithConsumerBabel dependency errors', () => {
Expand Down
30 changes: 28 additions & 2 deletions packages/cli/src/navigation/__tests__/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ const { addSourceMock, quicktypeMock } = vi.hoisted(() => ({
quicktypeMock: vi.fn(async ({ lang }: { lang: 'swift' | 'kotlin' }) => ({
lines:
lang === 'swift'
? ['public struct UserProfile {}', 'public struct SessionResult {}']
? [
'@objcMembers public class UserProfile: NSObject, Codable {}',
'@objcMembers public class SessionResult: NSObject, Codable {}',
]
: ['data class UserProfile()', 'data class SessionResult()'],
})),
}));
Expand Down Expand Up @@ -86,11 +89,34 @@ describe('generateNavigationModels', () => {
specPath,
methods,
kotlinPackageName: 'com.callstack.nativebrownfieldnavigation',
modelDefinitions: [
{
name: 'UserProfile',
fields: [
{ name: 'id', type: 'string', optional: false },
{ name: 'name', type: 'string', optional: false },
],
},
],
});

expect(models.modelTypeNames).toEqual(['UserProfile']);
expect(models.swiftModels).toContain('public struct UserProfile');
expect(models.swiftModels).toContain(
'@objcMembers public class UserProfile: NSObject, Codable'
);
expect(models.swiftModels).toContain(
'@objc public extension UserProfile'
);
expect(models.swiftModels).toContain(
'static func fromDictionary(_ value: NSDictionary) -> UserProfile'
);
expect(models.kotlinModels).toContain('data class UserProfile');
expect(models.kotlinModels).toContain(
'import com.facebook.react.bridge.ReadableMap'
);
expect(models.kotlinModels).toContain(
'fun toUserProfile(value: ReadableMap): UserProfile'
);
expect(addSourceMock).toHaveBeenCalled();
expect(quicktypeMock).toHaveBeenCalledTimes(2);
});
Expand Down
Loading
Loading