@callstack/brownfield-cli 3.0.0-rc.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +1 -1
- package/dist/brownfield/commands/packageAndroid.d.ts.map +1 -1
- package/dist/brownfield/commands/packageAndroid.js +2 -0
- package/dist/brownfield/commands/packageIos.d.ts.map +1 -1
- package/dist/brownfield/commands/packageIos.js +17 -1
- package/dist/brownfield/commands/publishAndroid.d.ts.map +1 -1
- package/dist/brownfield/commands/publishAndroid.js +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/navigation/commands/codegen.d.ts +9 -0
- package/dist/navigation/commands/codegen.d.ts.map +1 -0
- package/dist/navigation/commands/codegen.js +24 -0
- package/dist/navigation/commands/index.d.ts +2 -0
- package/dist/navigation/commands/index.d.ts.map +1 -0
- package/dist/navigation/commands/index.js +1 -0
- package/dist/navigation/config.d.ts +6 -0
- package/dist/navigation/config.d.ts.map +1 -0
- package/dist/navigation/config.js +25 -0
- package/dist/navigation/generators/android.d.ts +4 -0
- package/dist/navigation/generators/android.d.ts.map +1 -0
- package/dist/navigation/generators/android.js +91 -0
- package/dist/navigation/generators/ios.d.ts +4 -0
- package/dist/navigation/generators/ios.d.ts.map +1 -0
- package/dist/navigation/generators/ios.js +141 -0
- package/dist/navigation/generators/models.d.ts +13 -0
- package/dist/navigation/generators/models.d.ts.map +1 -0
- package/dist/navigation/generators/models.js +112 -0
- package/dist/navigation/generators/ts.d.ts +6 -0
- package/dist/navigation/generators/ts.d.ts.map +1 -0
- package/dist/navigation/generators/ts.js +96 -0
- package/dist/navigation/helpers/runNavigationCodegenIfApplicable.d.ts +5 -0
- package/dist/navigation/helpers/runNavigationCodegenIfApplicable.d.ts.map +1 -0
- package/dist/navigation/helpers/runNavigationCodegenIfApplicable.js +11 -0
- package/dist/navigation/index.d.ts +7 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +6 -0
- package/dist/navigation/parser.d.ts +3 -0
- package/dist/navigation/parser.d.ts.map +1 -0
- package/dist/navigation/parser.js +33 -0
- package/dist/navigation/runner.d.ts +8 -0
- package/dist/navigation/runner.d.ts.map +1 -0
- package/dist/navigation/runner.js +126 -0
- package/dist/navigation/spec-discovery.d.ts +3 -0
- package/dist/navigation/spec-discovery.d.ts.map +1 -0
- package/dist/navigation/spec-discovery.js +15 -0
- package/dist/navigation/types.d.ts +24 -0
- package/dist/navigation/types.d.ts.map +1 -0
- package/dist/navigation/types.js +1 -0
- package/package.json +7 -2
- package/src/brownfield/commands/packageAndroid.ts +2 -0
- package/src/brownfield/commands/packageIos.ts +34 -1
- package/src/brownfield/commands/publishAndroid.ts +2 -0
- package/src/index.ts +4 -0
- package/src/navigation/commands/codegen.ts +57 -0
- package/src/navigation/commands/index.ts +1 -0
- package/src/navigation/config.ts +35 -0
- package/src/navigation/generators/android.ts +127 -0
- package/src/navigation/generators/ios.ts +169 -0
- package/src/navigation/generators/models.ts +170 -0
- package/src/navigation/generators/ts.ts +123 -0
- package/src/navigation/helpers/runNavigationCodegenIfApplicable.ts +17 -0
- package/src/navigation/index.ts +10 -0
- package/src/navigation/parser.ts +43 -0
- package/src/navigation/runner.ts +256 -0
- package/src/navigation/spec-discovery.ts +25 -0
- package/src/navigation/types.ts +25 -0
- package/dist/brownfield/utils/index.d.ts +0 -4
- package/dist/brownfield/utils/index.d.ts.map +0 -1
- package/dist/brownfield/utils/index.js +0 -3
- package/dist/brownfield/utils/rn-cli.d.ts +0 -17
- package/dist/brownfield/utils/rn-cli.d.ts.map +0 -1
- package/dist/brownfield/utils/rn-cli.js +0 -31
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { MethodSignature } from '../types.js';
|
|
2
|
+
|
|
3
|
+
const TS_TO_KOTLIN_TYPE: Record<string, string> = {
|
|
4
|
+
string: 'String',
|
|
5
|
+
number: 'Double',
|
|
6
|
+
boolean: 'Boolean',
|
|
7
|
+
void: 'Unit',
|
|
8
|
+
Object: 'ReadableMap',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function mapTsTypeToKotlin(tsType: string, optional: boolean = false): string {
|
|
12
|
+
if (tsType.startsWith('Promise<')) {
|
|
13
|
+
const inner = tsType.slice(8, -1);
|
|
14
|
+
return mapTsTypeToKotlin(inner, optional);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const mapped = TS_TO_KOTLIN_TYPE[tsType];
|
|
18
|
+
if (mapped) {
|
|
19
|
+
return optional ? `${mapped}?` : mapped;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return optional ? 'Any?' : 'Any';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function generateKotlinDelegate(
|
|
26
|
+
methods: MethodSignature[],
|
|
27
|
+
kotlinPackageName: string
|
|
28
|
+
): string {
|
|
29
|
+
const methodSignatures = methods
|
|
30
|
+
.map((method) => {
|
|
31
|
+
const params = method.params
|
|
32
|
+
.map(
|
|
33
|
+
(param) =>
|
|
34
|
+
`${param.name}: ${mapTsTypeToKotlin(param.type, param.optional)}`
|
|
35
|
+
)
|
|
36
|
+
.join(', ');
|
|
37
|
+
const returnType =
|
|
38
|
+
method.returnType === 'void'
|
|
39
|
+
? ''
|
|
40
|
+
: `: ${mapTsTypeToKotlin(method.returnType, false)}`;
|
|
41
|
+
return ` fun ${method.name}(${params})${returnType}`;
|
|
42
|
+
})
|
|
43
|
+
.join('\n');
|
|
44
|
+
|
|
45
|
+
return `package ${kotlinPackageName}
|
|
46
|
+
|
|
47
|
+
interface BrownfieldNavigationDelegate {
|
|
48
|
+
${methodSignatures}
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function generateKotlinModule(
|
|
54
|
+
methods: MethodSignature[],
|
|
55
|
+
kotlinPackageName: string
|
|
56
|
+
): string {
|
|
57
|
+
const hasAsyncMethod = methods.some((method) => method.isAsync);
|
|
58
|
+
const hasObjectType = methods.some(
|
|
59
|
+
(method) =>
|
|
60
|
+
method.returnType.includes('Object') ||
|
|
61
|
+
method.params.some((param) => param.type === 'Object')
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const methodImplementations = methods
|
|
65
|
+
.map((method) =>
|
|
66
|
+
method.isAsync
|
|
67
|
+
? generateAsyncKotlinMethod(method)
|
|
68
|
+
: generateSyncKotlinMethod(method)
|
|
69
|
+
)
|
|
70
|
+
.join('\n\n');
|
|
71
|
+
|
|
72
|
+
return `package ${kotlinPackageName}
|
|
73
|
+
|
|
74
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
75
|
+
import com.facebook.react.bridge.ReactMethod${
|
|
76
|
+
hasAsyncMethod ? '\nimport com.facebook.react.bridge.Promise' : ''
|
|
77
|
+
}${hasObjectType ? '\nimport com.facebook.react.bridge.ReadableMap' : ''}
|
|
78
|
+
|
|
79
|
+
class NativeBrownfieldNavigationModule(
|
|
80
|
+
reactContext: ReactApplicationContext
|
|
81
|
+
) : NativeBrownfieldNavigationSpec(reactContext) {
|
|
82
|
+
${methodImplementations}
|
|
83
|
+
|
|
84
|
+
companion object {
|
|
85
|
+
const val NAME = "NativeBrownfieldNavigation"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function generateSyncKotlinMethod(method: MethodSignature): string {
|
|
92
|
+
const params = method.params
|
|
93
|
+
.map((param) => `${param.name}: ${mapTsTypeToKotlin(param.type, param.optional)}`)
|
|
94
|
+
.join(', ');
|
|
95
|
+
const args = method.params.map((param) => param.name).join(', ');
|
|
96
|
+
|
|
97
|
+
const signature = ` @ReactMethod\n override fun ${method.name}(${params})${
|
|
98
|
+
method.returnType === 'void'
|
|
99
|
+
? ''
|
|
100
|
+
: `: ${mapTsTypeToKotlin(method.returnType, false)}`
|
|
101
|
+
}`;
|
|
102
|
+
|
|
103
|
+
if (method.returnType === 'void') {
|
|
104
|
+
return `${signature} {
|
|
105
|
+
BrownfieldNavigationManager.getDelegate().${method.name}(${args})
|
|
106
|
+
}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return `${signature} {
|
|
110
|
+
return BrownfieldNavigationManager.getDelegate().${method.name}(${args})
|
|
111
|
+
}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function generateAsyncKotlinMethod(method: MethodSignature): string {
|
|
115
|
+
const paramsWithTypes = method.params
|
|
116
|
+
.map((param) => `${param.name}: ${mapTsTypeToKotlin(param.type, param.optional)}`)
|
|
117
|
+
.join(', ');
|
|
118
|
+
const params =
|
|
119
|
+
paramsWithTypes.length > 0
|
|
120
|
+
? `${paramsWithTypes}, promise: Promise`
|
|
121
|
+
: 'promise: Promise';
|
|
122
|
+
|
|
123
|
+
return ` @ReactMethod
|
|
124
|
+
override fun ${method.name}(${params}) {
|
|
125
|
+
promise.reject("not_implemented", "${method.name} is not implemented")
|
|
126
|
+
}`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { MethodSignature } from '../types.js';
|
|
2
|
+
|
|
3
|
+
const TS_TO_OBJC_TYPE: Record<string, string> = {
|
|
4
|
+
string: 'NSString *',
|
|
5
|
+
number: 'double',
|
|
6
|
+
boolean: 'BOOL',
|
|
7
|
+
void: 'void',
|
|
8
|
+
Object: 'NSDictionary *',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const TS_TO_SWIFT_TYPE: Record<string, string> = {
|
|
12
|
+
string: 'String',
|
|
13
|
+
number: 'Double',
|
|
14
|
+
boolean: 'Bool',
|
|
15
|
+
void: 'Void',
|
|
16
|
+
Object: '[String: Any]',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function mapTsTypeToObjC(tsType: string, nullable: boolean = false): string {
|
|
20
|
+
if (tsType.startsWith('Promise<')) {
|
|
21
|
+
return 'void';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const mapped = TS_TO_OBJC_TYPE[tsType];
|
|
25
|
+
if (mapped) {
|
|
26
|
+
if (nullable && mapped.includes('*')) {
|
|
27
|
+
return mapped.replace(' *', ' * _Nullable');
|
|
28
|
+
}
|
|
29
|
+
return mapped;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return nullable ? 'id _Nullable' : 'id';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mapTsTypeToSwift(tsType: string, optional: boolean = false): string {
|
|
36
|
+
if (tsType.startsWith('Promise<')) {
|
|
37
|
+
const inner = tsType.slice(8, -1);
|
|
38
|
+
return mapTsTypeToSwift(inner, optional);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const mapped = TS_TO_SWIFT_TYPE[tsType];
|
|
42
|
+
if (mapped) {
|
|
43
|
+
return optional ? `${mapped}?` : mapped;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return optional ? 'Any?' : 'Any';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function generateSwiftDelegate(methods: MethodSignature[]): string {
|
|
50
|
+
const protocolMethods = methods
|
|
51
|
+
.map((method) => {
|
|
52
|
+
const params = method.params
|
|
53
|
+
.map((param, index) => {
|
|
54
|
+
const swiftType = mapTsTypeToSwift(param.type, param.optional);
|
|
55
|
+
const label = index === 0 ? '_' : param.name;
|
|
56
|
+
return `${label} ${param.name}: ${swiftType}`;
|
|
57
|
+
})
|
|
58
|
+
.join(', ');
|
|
59
|
+
|
|
60
|
+
const returnType =
|
|
61
|
+
method.returnType === 'void'
|
|
62
|
+
? ''
|
|
63
|
+
: ` -> ${mapTsTypeToSwift(method.returnType, false)}`;
|
|
64
|
+
|
|
65
|
+
return ` @objc func ${method.name}(${params})${returnType}`;
|
|
66
|
+
})
|
|
67
|
+
.join('\n');
|
|
68
|
+
|
|
69
|
+
return `import Foundation
|
|
70
|
+
|
|
71
|
+
@objc public protocol BrownfieldNavigationDelegate: AnyObject {
|
|
72
|
+
${protocolMethods}
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function generateObjCImplementation(methods: MethodSignature[]): string {
|
|
78
|
+
const methodImplementations = methods
|
|
79
|
+
.map((method) =>
|
|
80
|
+
method.isAsync ? generateAsyncObjCMethod(method) : generateSyncObjCMethod(method)
|
|
81
|
+
)
|
|
82
|
+
.join('\n\n');
|
|
83
|
+
|
|
84
|
+
return `#import "NativeBrownfieldNavigation.h"
|
|
85
|
+
|
|
86
|
+
#if __has_include("BrownfieldNavigation/BrownfieldNavigation-Swift.h")
|
|
87
|
+
#import "BrownfieldNavigation/BrownfieldNavigation-Swift.h"
|
|
88
|
+
#else
|
|
89
|
+
#import "BrownfieldNavigation-Swift.h"
|
|
90
|
+
#endif
|
|
91
|
+
|
|
92
|
+
@implementation NativeBrownfieldNavigation
|
|
93
|
+
|
|
94
|
+
${methodImplementations}
|
|
95
|
+
|
|
96
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
97
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
98
|
+
{
|
|
99
|
+
return std::make_shared<facebook::react::NativeBrownfieldNavigationSpecJSI>(params);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
+ (NSString *)moduleName
|
|
103
|
+
{
|
|
104
|
+
return @"NativeBrownfieldNavigation";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@end
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function generateSyncObjCMethod(method: MethodSignature): string {
|
|
112
|
+
const { name, params, returnType } = method;
|
|
113
|
+
|
|
114
|
+
let signature = `- (${mapTsTypeToObjC(returnType)})${name}`;
|
|
115
|
+
if (params.length > 0) {
|
|
116
|
+
signature += params
|
|
117
|
+
.map((param, index) => {
|
|
118
|
+
const prefix = index === 0 ? ':' : ` ${param.name}:`;
|
|
119
|
+
return `${prefix}(${mapTsTypeToObjC(param.type, param.optional)})${param.name}`;
|
|
120
|
+
})
|
|
121
|
+
.join('');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let delegateCall = `[[[BrownfieldNavigationManager shared] getDelegate] ${name}`;
|
|
125
|
+
if (params.length > 0) {
|
|
126
|
+
delegateCall += params
|
|
127
|
+
.map((param, index) => {
|
|
128
|
+
const label = index === 0 ? '' : param.name;
|
|
129
|
+
return `${label}:${param.name}`;
|
|
130
|
+
})
|
|
131
|
+
.join(' ');
|
|
132
|
+
}
|
|
133
|
+
delegateCall += ']';
|
|
134
|
+
|
|
135
|
+
const returnPrefix = returnType === 'void' ? '' : 'return ';
|
|
136
|
+
|
|
137
|
+
return `${signature} {
|
|
138
|
+
${returnPrefix}${delegateCall};
|
|
139
|
+
}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function generateAsyncObjCMethod(method: MethodSignature): string {
|
|
143
|
+
const { name, params } = method;
|
|
144
|
+
|
|
145
|
+
let signature = `- (void)${name}`;
|
|
146
|
+
const allParams: Array<{ name: string; type: string; optional: boolean }> = [
|
|
147
|
+
...params,
|
|
148
|
+
{ name: 'resolve', type: 'RCTPromiseResolveBlock', optional: false },
|
|
149
|
+
{ name: 'reject', type: 'RCTPromiseRejectBlock', optional: false },
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
signature += ':';
|
|
153
|
+
signature += allParams
|
|
154
|
+
.map((param, index) => {
|
|
155
|
+
const prefix = index === 0 ? '' : param.name;
|
|
156
|
+
const type =
|
|
157
|
+
param.type === 'RCTPromiseResolveBlock'
|
|
158
|
+
? 'RCTPromiseResolveBlock'
|
|
159
|
+
: param.type === 'RCTPromiseRejectBlock'
|
|
160
|
+
? 'RCTPromiseRejectBlock'
|
|
161
|
+
: mapTsTypeToObjC(param.type, param.optional);
|
|
162
|
+
return `${prefix}(${type})${param.name}`;
|
|
163
|
+
})
|
|
164
|
+
.join(' ');
|
|
165
|
+
|
|
166
|
+
return `${signature} {
|
|
167
|
+
reject(@"not_implemented", @"${name} is not implemented", nil);
|
|
168
|
+
}`;
|
|
169
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
FetchingJSONSchemaStore,
|
|
5
|
+
InputData,
|
|
6
|
+
JSONSchemaInput,
|
|
7
|
+
quicktype,
|
|
8
|
+
} from 'quicktype-core';
|
|
9
|
+
import { schemaForTypeScriptSources } from 'quicktype-typescript-input';
|
|
10
|
+
|
|
11
|
+
import type { MethodSignature } from '../types.js';
|
|
12
|
+
|
|
13
|
+
export interface NavigationModelsOptions {
|
|
14
|
+
specPath: string;
|
|
15
|
+
methods: MethodSignature[];
|
|
16
|
+
kotlinPackageName: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GeneratedNavigationModels {
|
|
20
|
+
swiftModels?: string;
|
|
21
|
+
kotlinModels?: string;
|
|
22
|
+
modelTypeNames: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SKIP_TYPE_TOKENS = new Set([
|
|
26
|
+
'Array',
|
|
27
|
+
'Date',
|
|
28
|
+
'Map',
|
|
29
|
+
'Object',
|
|
30
|
+
'Promise',
|
|
31
|
+
'ReadonlyArray',
|
|
32
|
+
'Record',
|
|
33
|
+
'Set',
|
|
34
|
+
'any',
|
|
35
|
+
'boolean',
|
|
36
|
+
'false',
|
|
37
|
+
'null',
|
|
38
|
+
'number',
|
|
39
|
+
'object',
|
|
40
|
+
'string',
|
|
41
|
+
'true',
|
|
42
|
+
'undefined',
|
|
43
|
+
'unknown',
|
|
44
|
+
'void',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
function collectReferencedTypes(methods: MethodSignature[]): Set<string> {
|
|
48
|
+
const referenced = new Set<string>();
|
|
49
|
+
|
|
50
|
+
for (const method of methods) {
|
|
51
|
+
const typeTexts = [
|
|
52
|
+
method.returnType,
|
|
53
|
+
...method.params.map((param) => param.type),
|
|
54
|
+
];
|
|
55
|
+
for (const typeText of typeTexts) {
|
|
56
|
+
const matches = typeText.match(/\b[A-Za-z_]\w*\b/g);
|
|
57
|
+
if (!matches) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const match of matches) {
|
|
61
|
+
if (!SKIP_TYPE_TOKENS.has(match)) {
|
|
62
|
+
referenced.add(match);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return referenced;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function generateModelsForLanguage({
|
|
72
|
+
typeNames,
|
|
73
|
+
schema,
|
|
74
|
+
lang,
|
|
75
|
+
kotlinPackageName,
|
|
76
|
+
}: {
|
|
77
|
+
typeNames: string[];
|
|
78
|
+
schema: object;
|
|
79
|
+
lang: 'swift' | 'kotlin';
|
|
80
|
+
kotlinPackageName: string;
|
|
81
|
+
}): Promise<string> {
|
|
82
|
+
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());
|
|
83
|
+
|
|
84
|
+
for (const typeName of typeNames) {
|
|
85
|
+
const rootSchema = JSON.parse(JSON.stringify(schema)) as {
|
|
86
|
+
$ref?: string;
|
|
87
|
+
};
|
|
88
|
+
rootSchema.$ref = `#/definitions/${typeName}`;
|
|
89
|
+
|
|
90
|
+
await schemaInput.addSource({
|
|
91
|
+
name: typeName,
|
|
92
|
+
schema: JSON.stringify(rootSchema),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const inputData = new InputData();
|
|
97
|
+
inputData.addInput(schemaInput);
|
|
98
|
+
|
|
99
|
+
const rendererOptions =
|
|
100
|
+
lang === 'swift'
|
|
101
|
+
? {
|
|
102
|
+
'access-level': 'public',
|
|
103
|
+
'mutable-properties': 'true',
|
|
104
|
+
initializers: 'false',
|
|
105
|
+
'swift-5-support': 'true',
|
|
106
|
+
}
|
|
107
|
+
: {
|
|
108
|
+
framework: 'just-types',
|
|
109
|
+
package: kotlinPackageName,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const { lines } = await quicktype({
|
|
113
|
+
inputData,
|
|
114
|
+
lang,
|
|
115
|
+
rendererOptions,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return lines.join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function generateNavigationModels({
|
|
122
|
+
specPath,
|
|
123
|
+
methods,
|
|
124
|
+
kotlinPackageName,
|
|
125
|
+
}: NavigationModelsOptions): Promise<GeneratedNavigationModels> {
|
|
126
|
+
const absoluteSpecPath = path.resolve(process.cwd(), specPath);
|
|
127
|
+
|
|
128
|
+
if (!fs.existsSync(absoluteSpecPath)) {
|
|
129
|
+
throw new Error(`Spec file not found: ${absoluteSpecPath}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const schemaData = schemaForTypeScriptSources([absoluteSpecPath]);
|
|
133
|
+
if (!schemaData.schema) {
|
|
134
|
+
throw new Error('Failed to generate schema from TypeScript spec');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const parsedSchema = JSON.parse(schemaData.schema) as {
|
|
138
|
+
definitions?: Record<string, unknown>;
|
|
139
|
+
};
|
|
140
|
+
const referencedTypes = collectReferencedTypes(methods);
|
|
141
|
+
const definitions = parsedSchema.definitions ?? {};
|
|
142
|
+
const modelTypeNames = [...referencedTypes].filter((typeName) =>
|
|
143
|
+
Object.hasOwn(definitions, typeName)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (modelTypeNames.length === 0) {
|
|
147
|
+
return { modelTypeNames: [] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const [swiftModels, kotlinModels] = await Promise.all([
|
|
151
|
+
generateModelsForLanguage({
|
|
152
|
+
typeNames: modelTypeNames,
|
|
153
|
+
schema: parsedSchema,
|
|
154
|
+
lang: 'swift',
|
|
155
|
+
kotlinPackageName,
|
|
156
|
+
}),
|
|
157
|
+
generateModelsForLanguage({
|
|
158
|
+
typeNames: modelTypeNames,
|
|
159
|
+
schema: parsedSchema,
|
|
160
|
+
lang: 'kotlin',
|
|
161
|
+
kotlinPackageName,
|
|
162
|
+
}),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
swiftModels,
|
|
167
|
+
kotlinModels,
|
|
168
|
+
modelTypeNames,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
import type { MethodSignature } from '../types.js';
|
|
5
|
+
|
|
6
|
+
export function generateTurboModuleSpec(methods: MethodSignature[]): string {
|
|
7
|
+
const methodSignatures = methods
|
|
8
|
+
.map((method) => {
|
|
9
|
+
const params = method.params
|
|
10
|
+
.map((param) => `${param.name}${param.optional ? '?' : ''}: ${param.type}`)
|
|
11
|
+
.join(', ');
|
|
12
|
+
return ` ${method.name}(${params}): ${method.returnType};`;
|
|
13
|
+
})
|
|
14
|
+
.join('\n');
|
|
15
|
+
|
|
16
|
+
return `import { TurboModuleRegistry, type TurboModule } from 'react-native';
|
|
17
|
+
|
|
18
|
+
export interface Spec extends TurboModule {
|
|
19
|
+
${methodSignatures}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default TurboModuleRegistry.getEnforcing<Spec>(
|
|
23
|
+
'NativeBrownfieldNavigation'
|
|
24
|
+
);
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function generateIndexTs(methods: MethodSignature[]): string {
|
|
29
|
+
const functionImplementations = methods
|
|
30
|
+
.map((method) => {
|
|
31
|
+
const params = method.params
|
|
32
|
+
.map((param) => `${param.name}${param.optional ? '?' : ''}: ${param.type}`)
|
|
33
|
+
.join(', ');
|
|
34
|
+
const args = method.params.map((param) => param.name).join(', ');
|
|
35
|
+
const returnType = method.returnType === 'void' ? '' : `: ${method.returnType}`;
|
|
36
|
+
|
|
37
|
+
if (method.isAsync) {
|
|
38
|
+
return ` ${method.name}: async (${params})${returnType} => {
|
|
39
|
+
return NativeBrownfieldNavigation.${method.name}(${args});
|
|
40
|
+
}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ` ${method.name}: (${params})${returnType} => {
|
|
44
|
+
NativeBrownfieldNavigation.${method.name}(${args});
|
|
45
|
+
}`;
|
|
46
|
+
})
|
|
47
|
+
.join(',\n');
|
|
48
|
+
|
|
49
|
+
return `import NativeBrownfieldNavigation from './NativeBrownfieldNavigation';
|
|
50
|
+
|
|
51
|
+
const BrownfieldNavigation = {
|
|
52
|
+
${functionImplementations},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default BrownfieldNavigation;
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function transpileWithConsumerBabel(
|
|
60
|
+
tsCode: string,
|
|
61
|
+
projectRoot: string,
|
|
62
|
+
packageRoot: string
|
|
63
|
+
): string {
|
|
64
|
+
const nodeRequire = createRequire(path.join(projectRoot, 'package.json'));
|
|
65
|
+
const moduleCandidates = [projectRoot, packageRoot];
|
|
66
|
+
|
|
67
|
+
function resolveOrThrow(moduleName: string): string {
|
|
68
|
+
for (const modulePath of moduleCandidates) {
|
|
69
|
+
try {
|
|
70
|
+
return nodeRequire.resolve(moduleName, { paths: [modulePath] });
|
|
71
|
+
} catch {
|
|
72
|
+
// Continue with remaining candidates.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Could not resolve "${moduleName}". Install it in your app devDependencies.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const babelCorePath = resolveOrThrow('@babel/core');
|
|
82
|
+
const rnPresetPath = resolveOrThrow('@react-native/babel-preset');
|
|
83
|
+
const babelCore = nodeRequire(babelCorePath) as {
|
|
84
|
+
transformSync: (
|
|
85
|
+
source: string,
|
|
86
|
+
options: Record<string, unknown>
|
|
87
|
+
) => { code?: string | null } | null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const transformed = babelCore.transformSync(tsCode, {
|
|
91
|
+
filename: 'index.ts',
|
|
92
|
+
babelrc: false,
|
|
93
|
+
configFile: false,
|
|
94
|
+
comments: false,
|
|
95
|
+
compact: true,
|
|
96
|
+
minified: true,
|
|
97
|
+
presets: [[rnPresetPath, {}]],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!transformed?.code) {
|
|
101
|
+
throw new Error('Babel transpilation failed for generated index.ts');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return transformed.code;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function generateIndexDts(methods: MethodSignature[]): string {
|
|
108
|
+
const methodSignatures = methods
|
|
109
|
+
.map((method) => {
|
|
110
|
+
const params = method.params
|
|
111
|
+
.map((param) => `${param.name}${param.optional ? '?' : ''}: ${param.type}`)
|
|
112
|
+
.join(', ');
|
|
113
|
+
return ` ${method.name}: (${params}) => ${method.returnType};`;
|
|
114
|
+
})
|
|
115
|
+
.join('\n');
|
|
116
|
+
|
|
117
|
+
return `declare const BrownfieldNavigation: {
|
|
118
|
+
${methodSignatures}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export default BrownfieldNavigation;
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isNavigationInstalled } from '../config.js';
|
|
2
|
+
import { isNavigationSpecPresent } from '../spec-discovery.js';
|
|
3
|
+
import { runNavigationCodegen } from '../runner.js';
|
|
4
|
+
|
|
5
|
+
export async function runNavigationCodegenIfApplicable(
|
|
6
|
+
projectRoot: string,
|
|
7
|
+
specPath?: string
|
|
8
|
+
): Promise<{ hasNavigation: boolean; hasSpec: boolean }> {
|
|
9
|
+
const hasNavigation = isNavigationInstalled(projectRoot);
|
|
10
|
+
const hasSpec = hasNavigation && isNavigationSpecPresent(specPath, projectRoot);
|
|
11
|
+
|
|
12
|
+
if (hasSpec) {
|
|
13
|
+
await runNavigationCodegen({ specPath, projectRoot });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { hasNavigation, hasSpec };
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
|
|
3
|
+
import * as Commands from './commands/index.js';
|
|
4
|
+
|
|
5
|
+
export const groupName = `${styleText(['bold', 'blueBright'], '@callstack/brownfield-navigation')}${styleText('whiteBright', ' - native codegen utilities for Brownfield Navigation')}`;
|
|
6
|
+
|
|
7
|
+
export { Commands };
|
|
8
|
+
export type * from './types.js';
|
|
9
|
+
export * from './runner.js';
|
|
10
|
+
export default Commands;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { Project } from 'ts-morph';
|
|
3
|
+
|
|
4
|
+
import type { MethodParam, MethodSignature } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function parseNavigationSpec(specPath: string): MethodSignature[] {
|
|
7
|
+
if (!fs.existsSync(specPath)) {
|
|
8
|
+
throw new Error(`Spec file not found: ${specPath}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
12
|
+
const sourceFile = project.addSourceFileAtPath(specPath);
|
|
13
|
+
const specInterface =
|
|
14
|
+
sourceFile.getInterface('BrownfieldNavigationSpec') ??
|
|
15
|
+
sourceFile.getInterface('Spec');
|
|
16
|
+
|
|
17
|
+
if (!specInterface) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'Could not find BrownfieldNavigationSpec or Spec interface in spec file'
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return specInterface.getMethods().map((method): MethodSignature => {
|
|
24
|
+
const name = method.getName();
|
|
25
|
+
const params: MethodParam[] = method.getParameters().map((param) => {
|
|
26
|
+
const typeNode = param.getTypeNode();
|
|
27
|
+
return {
|
|
28
|
+
name: param.getName(),
|
|
29
|
+
type: typeNode?.getText() ?? 'unknown',
|
|
30
|
+
optional: param.isOptional(),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
const returnTypeNode = method.getReturnTypeNode();
|
|
34
|
+
const returnType = returnTypeNode?.getText() ?? 'void';
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
params,
|
|
39
|
+
returnType,
|
|
40
|
+
isAsync: returnType.startsWith('Promise<'),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|