Skip to content

Commit 32ba266

Browse files
committed
feat: add android codegen support
1 parent 0a31c7a commit 32ba266

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

packages/brownfield-navigation/src/scripts/brownfield-navigation.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const scriptDir = path.dirname(scriptFile);
3131

3232
// Package root is one level up from scripts/
3333
const PACKAGE_ROOT = path.resolve(scriptDir, '../../../');
34+
const DEFAULT_ANDROID_JAVA_PACKAGE = 'com.callstack.nativebrownfieldnavigation';
35+
const ANDROID_JAVA_PACKAGE_NAME = DEFAULT_ANDROID_JAVA_PACKAGE;
36+
const ANDROID_JAVA_PACKAGE_PATH_SEGMENTS = ANDROID_JAVA_PACKAGE_NAME.split('.');
3437

3538
// ============================================================================
3639
// Types
@@ -69,6 +72,14 @@ const TS_TO_SWIFT_TYPE: Record<string, string> = {
6972
Object: '[String: Any]',
7073
};
7174

75+
const TS_TO_KOTLIN_TYPE: Record<string, string> = {
76+
string: 'String',
77+
number: 'Double',
78+
boolean: 'Boolean',
79+
void: 'Unit',
80+
Object: 'ReadableMap',
81+
};
82+
7283
function mapTsTypeToObjC(tsType: string, nullable: boolean = false): string {
7384
if (tsType.startsWith('Promise<')) {
7485
return 'void';
@@ -99,6 +110,20 @@ function mapTsTypeToSwift(tsType: string, optional: boolean = false): string {
99110
return optional ? 'Any?' : 'Any';
100111
}
101112

113+
function mapTsTypeToKotlin(tsType: string, optional: boolean = false): string {
114+
if (tsType.startsWith('Promise<')) {
115+
const inner = tsType.slice(8, -1);
116+
return mapTsTypeToKotlin(inner, optional);
117+
}
118+
119+
const mapped = TS_TO_KOTLIN_TYPE[tsType];
120+
if (mapped) {
121+
return optional ? `${mapped}?` : mapped;
122+
}
123+
124+
return optional ? 'Any?' : 'Any';
125+
}
126+
102127
// ============================================================================
103128
// Spec Parser
104129
// ============================================================================
@@ -339,6 +364,108 @@ ${methodImpls}
339364
`;
340365
}
341366

367+
function generateKotlinDelegate(
368+
methods: MethodSignature[],
369+
kotlinPackageName: string
370+
): string {
371+
const methodSignatures = methods
372+
.map((m) => {
373+
const params = m.params
374+
.map((p) => `${p.name}: ${mapTsTypeToKotlin(p.type, p.optional)}`)
375+
.join(', ');
376+
const returnType =
377+
m.returnType === 'void'
378+
? ''
379+
: `: ${mapTsTypeToKotlin(m.returnType, false)}`;
380+
return ` fun ${m.name}(${params})${returnType}`;
381+
})
382+
.join('\n');
383+
384+
return `package ${kotlinPackageName}
385+
386+
interface BrownfieldNavigationDelegate {
387+
${methodSignatures}
388+
}
389+
`;
390+
}
391+
392+
function generateKotlinModule(
393+
methods: MethodSignature[],
394+
kotlinPackageName: string
395+
): string {
396+
const hasAsyncMethod = methods.some((m) => m.isAsync);
397+
const hasObjectType = methods.some(
398+
(m) =>
399+
m.returnType.includes('Object') ||
400+
m.params.some((p) => p.type === 'Object')
401+
);
402+
403+
const methodImpls = methods
404+
.map((m) => {
405+
if (m.isAsync) {
406+
return generateAsyncKotlinMethod(m);
407+
}
408+
return generateSyncKotlinMethod(m);
409+
})
410+
.join('\n\n');
411+
412+
return `package ${kotlinPackageName}
413+
414+
import com.facebook.react.bridge.ReactApplicationContext
415+
import com.facebook.react.bridge.ReactMethod${
416+
hasAsyncMethod ? '\nimport com.facebook.react.bridge.Promise' : ''
417+
}${hasObjectType ? '\nimport com.facebook.react.bridge.ReadableMap' : ''}
418+
419+
class NativeBrownfieldNavigationModule(
420+
reactContext: ReactApplicationContext
421+
) : NativeBrownfieldNavigationSpec(reactContext) {
422+
${methodImpls}
423+
424+
companion object {
425+
const val NAME = "NativeBrownfieldNavigation"
426+
}
427+
}
428+
`;
429+
}
430+
431+
function generateSyncKotlinMethod(method: MethodSignature): string {
432+
const params = method.params
433+
.map((p) => `${p.name}: ${mapTsTypeToKotlin(p.type, p.optional)}`)
434+
.join(', ');
435+
const args = method.params.map((p) => p.name).join(', ');
436+
437+
const signature = ` @ReactMethod\n override fun ${method.name}(${params})${
438+
method.returnType === 'void'
439+
? ''
440+
: `: ${mapTsTypeToKotlin(method.returnType, false)}`
441+
}`;
442+
443+
if (method.returnType === 'void') {
444+
return `${signature} {
445+
BrownfieldNavigationManager.getDelegate().${method.name}(${args})
446+
}`;
447+
}
448+
449+
return `${signature} {
450+
return BrownfieldNavigationManager.getDelegate().${method.name}(${args})
451+
}`;
452+
}
453+
454+
function generateAsyncKotlinMethod(method: MethodSignature): string {
455+
const paramsWithTypes = method.params
456+
.map((p) => `${p.name}: ${mapTsTypeToKotlin(p.type, p.optional)}`)
457+
.join(', ');
458+
const params =
459+
paramsWithTypes.length > 0
460+
? `${paramsWithTypes}, promise: Promise`
461+
: 'promise: Promise';
462+
463+
return ` @ReactMethod
464+
override fun ${method.name}(${params}) {
465+
promise.reject("not_implemented", "${method.name} is not implemented")
466+
}`;
467+
}
468+
342469
function generateSyncObjCMethod(method: MethodSignature): string {
343470
const { name, params, returnType } = method;
344471

@@ -477,6 +604,11 @@ function main(): void {
477604
const indexDts = generateIndexDts(methods);
478605
const swiftDelegate = generateSwiftDelegate(methods);
479606
const objcImpl = generateObjCImplementation(methods);
607+
const kotlinDelegate = generateKotlinDelegate(
608+
methods,
609+
ANDROID_JAVA_PACKAGE_NAME
610+
);
611+
const kotlinModule = generateKotlinModule(methods, ANDROID_JAVA_PACKAGE_NAME);
480612

481613
if (dryRun) {
482614
console.log('\n--- Generated: src/NativeBrownfieldNavigation.ts ---');
@@ -493,6 +625,14 @@ function main(): void {
493625
console.log(swiftDelegate);
494626
console.log('\n--- Generated: ios/NativeBrownfieldNavigation.mm ---');
495627
console.log(objcImpl);
628+
console.log(
629+
`\n--- Generated: android/src/main/java/${ANDROID_JAVA_PACKAGE_NAME.replaceAll('.', '/')}/BrownfieldNavigationDelegate.kt ---`
630+
);
631+
console.log(kotlinDelegate);
632+
console.log(
633+
`\n--- Generated: android/src/main/java/${ANDROID_JAVA_PACKAGE_NAME.replaceAll('.', '/')}/NativeBrownfieldNavigationModule.kt ---`
634+
);
635+
console.log(kotlinModule);
496636
return;
497637
}
498638

@@ -528,6 +668,24 @@ function main(): void {
528668
'BrownfieldNavigationDelegate.swift'
529669
),
530670
objcImpl: path.join(PACKAGE_ROOT, 'ios', 'NativeBrownfieldNavigation.mm'),
671+
kotlinDelegate: path.join(
672+
PACKAGE_ROOT,
673+
'android',
674+
'src',
675+
'main',
676+
'java',
677+
...ANDROID_JAVA_PACKAGE_PATH_SEGMENTS,
678+
'BrownfieldNavigationDelegate.kt'
679+
),
680+
kotlinModule: path.join(
681+
PACKAGE_ROOT,
682+
'android',
683+
'src',
684+
'main',
685+
'java',
686+
...ANDROID_JAVA_PACKAGE_PATH_SEGMENTS,
687+
'NativeBrownfieldNavigationModule.kt'
688+
),
531689
};
532690

533691
fs.writeFileSync(paths.turboModuleSpec, turboModuleSpec);
@@ -554,6 +712,12 @@ function main(): void {
554712
fs.writeFileSync(paths.objcImpl, objcImpl);
555713
console.log(`Generated: ${paths.objcImpl}`);
556714

715+
fs.writeFileSync(paths.kotlinDelegate, kotlinDelegate);
716+
console.log(`Generated: ${paths.kotlinDelegate}`);
717+
718+
fs.writeFileSync(paths.kotlinModule, kotlinModule);
719+
console.log(`Generated: ${paths.kotlinModule}`);
720+
557721
console.log('\nCodegen complete!');
558722
console.log('');
559723
console.log('Next steps:');

0 commit comments

Comments
 (0)