@callstack/brownfield-cli 3.7.0 → 3.8.1

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.
@@ -8,12 +8,13 @@ import {
8
8
  } from 'quicktype-core';
9
9
  import { schemaForTypeScriptSources } from 'quicktype-typescript-input';
10
10
 
11
- import type { MethodSignature } from '../types.js';
11
+ import type { MethodSignature, ModelDefinition } from '../types.js';
12
12
 
13
13
  export interface NavigationModelsOptions {
14
14
  specPath: string;
15
15
  methods: MethodSignature[];
16
16
  kotlinPackageName: string;
17
+ modelDefinitions?: ModelDefinition[];
17
18
  }
18
19
 
19
20
  export interface GeneratedNavigationModels {
@@ -102,6 +103,8 @@ async function generateModelsForLanguage({
102
103
  'access-level': 'public',
103
104
  'mutable-properties': 'true',
104
105
  initializers: 'false',
106
+ 'struct-or-class': 'class',
107
+ 'objective-c-support': 'true',
105
108
  'swift-5-support': 'true',
106
109
  }
107
110
  : {
@@ -122,6 +125,7 @@ export async function generateNavigationModels({
122
125
  specPath,
123
126
  methods,
124
127
  kotlinPackageName,
128
+ modelDefinitions = [],
125
129
  }: NavigationModelsOptions): Promise<GeneratedNavigationModels> {
126
130
  const absoluteSpecPath = path.resolve(process.cwd(), specPath);
127
131
 
@@ -162,9 +166,321 @@ export async function generateNavigationModels({
162
166
  }),
163
167
  ]);
164
168
 
169
+ const swiftModelConversions = generateSwiftModelConversionExtensions(
170
+ modelDefinitions,
171
+ modelTypeNames
172
+ );
173
+ const kotlinModelConversions = generateKotlinModelConversionExtensions(
174
+ modelDefinitions,
175
+ modelTypeNames
176
+ );
177
+
165
178
  return {
166
- swiftModels,
167
- kotlinModels,
179
+ swiftModels: swiftModelConversions
180
+ ? `${swiftModels}\n\n${swiftModelConversions}`
181
+ : swiftModels,
182
+ kotlinModels: kotlinModelConversions
183
+ ? `${ensureKotlinReadableMapImport(kotlinModels)}\n\n${kotlinModelConversions}`
184
+ : kotlinModels,
168
185
  modelTypeNames,
169
186
  };
170
187
  }
188
+
189
+ function ensureKotlinReadableMapImport(kotlinModels: string): string {
190
+ const importLine = 'import com.facebook.react.bridge.ReadableMap';
191
+ if (kotlinModels.includes(importLine)) {
192
+ return kotlinModels;
193
+ }
194
+
195
+ const packageMatch = kotlinModels.match(/^package[^\n]*\n/);
196
+ if (!packageMatch) {
197
+ return `${importLine}\n${kotlinModels}`;
198
+ }
199
+
200
+ const packageLine = packageMatch[0];
201
+ const rest = kotlinModels.slice(packageLine.length);
202
+ return `${packageLine}\n${importLine}\n${rest}`;
203
+ }
204
+
205
+ function generateSwiftModelConversionExtensions(
206
+ modelDefinitions: ModelDefinition[],
207
+ modelTypeNames: string[]
208
+ ): string {
209
+ const modelDefinitionsByLookupName = buildModelDefinitionsByLookupName(
210
+ modelDefinitions
211
+ );
212
+ const conversionModelNames = collectConversionModelNames(
213
+ modelDefinitionsByLookupName,
214
+ modelTypeNames
215
+ );
216
+
217
+ return conversionModelNames
218
+ .map((modelName) => {
219
+ const definition = modelDefinitionsByLookupName.get(modelName);
220
+ if (!definition) {
221
+ return '';
222
+ }
223
+
224
+ const sortedFields = [...definition.fields].sort((a, b) =>
225
+ a.name.localeCompare(b.name)
226
+ );
227
+ if (sortedFields.length === 0) {
228
+ return '';
229
+ }
230
+
231
+ const args = sortedFields
232
+ .map(
233
+ (field) =>
234
+ `${field.name}: ${mapDictionaryValueToSwiftExpression(
235
+ field.name,
236
+ field.type,
237
+ field.optional
238
+ )}`
239
+ )
240
+ .join(', ');
241
+
242
+ return `@objc public extension ${modelName} {
243
+ static func fromDictionary(_ value: NSDictionary) -> ${modelName} {
244
+ return ${modelName}(${args})
245
+ }
246
+ }`;
247
+ })
248
+ .filter(Boolean)
249
+ .join('\n\n');
250
+ }
251
+
252
+ function mapDictionaryValueToSwiftExpression(
253
+ fieldName: string,
254
+ fieldType: string,
255
+ optional: boolean
256
+ ): string {
257
+ const parsedType = parseFieldType(fieldType);
258
+ const normalizedFieldType = parsedType.normalizedType;
259
+ const valueAccess = `value["${fieldName}"]`;
260
+ const allowsNil = optional || parsedType.nullable;
261
+ if (normalizedFieldType === 'string') {
262
+ return allowsNil ? `${valueAccess} as? String` : `${valueAccess} as! String`;
263
+ }
264
+ if (normalizedFieldType === 'number') {
265
+ return allowsNil
266
+ ? `(${valueAccess} as? NSNumber)?.doubleValue`
267
+ : `(${valueAccess} as! NSNumber).doubleValue`;
268
+ }
269
+ if (normalizedFieldType === 'boolean') {
270
+ return allowsNil
271
+ ? `(${valueAccess} as? NSNumber)?.boolValue`
272
+ : `(${valueAccess} as! NSNumber).boolValue`;
273
+ }
274
+ if (normalizedFieldType === 'string[]') {
275
+ return allowsNil ? `${valueAccess} as? [String]` : `${valueAccess} as! [String]`;
276
+ }
277
+
278
+ if (isSimpleTypeReference(normalizedFieldType)) {
279
+ const nestedModelType = resolveNestedModelTypeName(normalizedFieldType);
280
+ return allowsNil
281
+ ? `(${valueAccess} as? NSDictionary).map(${nestedModelType}.fromDictionary)`
282
+ : `${nestedModelType}.fromDictionary(${valueAccess} as! NSDictionary)`;
283
+ }
284
+
285
+ return `fatalError("Unsupported TypeScript field type '${escapeSwiftStringLiteral(normalizedFieldType)}' for field '${escapeSwiftStringLiteral(fieldName)}' in Swift conversion. Consider using 'Any' or a custom converter.")`;
286
+ }
287
+
288
+ function generateKotlinModelConversionExtensions(
289
+ modelDefinitions: ModelDefinition[],
290
+ modelTypeNames: string[]
291
+ ): string {
292
+ const modelDefinitionsByLookupName = buildModelDefinitionsByLookupName(
293
+ modelDefinitions
294
+ );
295
+ const conversionModelNames = collectConversionModelNames(
296
+ modelDefinitionsByLookupName,
297
+ modelTypeNames
298
+ );
299
+ let usesStringArrayHelper = false;
300
+
301
+ const conversionFunctions = conversionModelNames
302
+ .map((modelName) => {
303
+ const definition = modelDefinitionsByLookupName.get(modelName);
304
+ if (!definition) {
305
+ return '';
306
+ }
307
+
308
+ const sortedFields = [...definition.fields].sort((a, b) =>
309
+ a.name.localeCompare(b.name)
310
+ );
311
+ if (sortedFields.length === 0) {
312
+ return '';
313
+ }
314
+
315
+ const args = sortedFields
316
+ .map((field) => {
317
+ const fieldExpression = mapReadableMapFieldExpression(
318
+ 'value',
319
+ field.name,
320
+ field.type,
321
+ field.optional
322
+ );
323
+ if (fieldExpression.includes('readStringArray(')) {
324
+ usesStringArrayHelper = true;
325
+ }
326
+ return `${field.name} = ${fieldExpression}`;
327
+ })
328
+ .join(', ');
329
+
330
+ return `fun to${modelName}(value: ReadableMap): ${modelName} {
331
+ return ${modelName}(${args})
332
+ }`;
333
+ })
334
+ .filter(Boolean)
335
+ .join('\n\n');
336
+
337
+ if (!usesStringArrayHelper) {
338
+ return conversionFunctions;
339
+ }
340
+
341
+ const stringArrayHelper = `private fun readStringArray(value: ReadableMap, key: String, required: Boolean): List<String>? {
342
+ if (!value.hasKey(key) || value.isNull(key)) {
343
+ if (required) error("Missing required array field '$key'")
344
+ return null
345
+ }
346
+ val array = value.getArray(key) ?: return null
347
+ return array.toArrayList().map {
348
+ it as? String ?: error("Expected string elements for array field '$key'")
349
+ }
350
+ }`;
351
+
352
+ return conversionFunctions
353
+ ? `${conversionFunctions}\n\n${stringArrayHelper}`
354
+ : stringArrayHelper;
355
+ }
356
+
357
+ function mapReadableMapFieldExpression(
358
+ mapName: string,
359
+ fieldName: string,
360
+ fieldType: string,
361
+ optional: boolean
362
+ ): string {
363
+ const parsedType = parseFieldType(fieldType);
364
+ const normalizedFieldType = parsedType.normalizedType;
365
+ const allowsNull = optional || parsedType.nullable;
366
+ if (normalizedFieldType === 'string') {
367
+ return allowsNull
368
+ ? `${mapName}.getString("${fieldName}")`
369
+ : `${mapName}.getString("${fieldName}")!!`;
370
+ }
371
+ if (normalizedFieldType === 'number') {
372
+ return allowsNull
373
+ ? `if (${mapName}.hasKey("${fieldName}") && !${mapName}.isNull("${fieldName}")) ${mapName}.getDouble("${fieldName}") else null`
374
+ : `${mapName}.getDouble("${fieldName}")`;
375
+ }
376
+ if (normalizedFieldType === 'boolean') {
377
+ return allowsNull
378
+ ? `if (${mapName}.hasKey("${fieldName}") && !${mapName}.isNull("${fieldName}")) ${mapName}.getBoolean("${fieldName}") else null`
379
+ : `${mapName}.getBoolean("${fieldName}")`;
380
+ }
381
+ if (normalizedFieldType === 'string[]') {
382
+ return allowsNull
383
+ ? `readStringArray(${mapName}, "${fieldName}", false)`
384
+ : `readStringArray(${mapName}, "${fieldName}", true)!!`;
385
+ }
386
+ if (isSimpleTypeReference(normalizedFieldType)) {
387
+ const nestedModelType = resolveNestedModelTypeName(normalizedFieldType);
388
+ return allowsNull
389
+ ? `${mapName}.getMap("${fieldName}")?.let { to${nestedModelType}(it) }`
390
+ : `to${nestedModelType}(${mapName}.getMap("${fieldName}")!!)`;
391
+ }
392
+ return `error("Unsupported TypeScript field type '${escapeKotlinStringLiteral(normalizedFieldType)}' for field '${escapeKotlinStringLiteral(fieldName)}' in Kotlin conversion. Consider using 'Any' or a custom converter.")`;
393
+ }
394
+
395
+ function isSimpleTypeReference(typeName: string): boolean {
396
+ return /^[A-Za-z_]\w*$/.test(typeName);
397
+ }
398
+
399
+ function escapeSwiftStringLiteral(value: string): string {
400
+ return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
401
+ }
402
+
403
+ function escapeKotlinStringLiteral(value: string): string {
404
+ return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
405
+ }
406
+
407
+ function parseFieldType(fieldType: string): {
408
+ normalizedType: string;
409
+ nullable: boolean;
410
+ } {
411
+ const unionTypes = fieldType
412
+ .split('|')
413
+ .map((part) => part.trim())
414
+ .filter(Boolean);
415
+ const nullable = unionTypes.includes('null') || unionTypes.includes('undefined');
416
+ const nonNullableTypes = unionTypes.filter(
417
+ (part) => part !== 'null' && part !== 'undefined'
418
+ );
419
+ const normalizedType =
420
+ nonNullableTypes.length === 1 ? nonNullableTypes[0] : fieldType.trim();
421
+ return { normalizedType, nullable };
422
+ }
423
+
424
+ function resolveNestedModelTypeName(typeName: string): string {
425
+ if (typeName.endsWith('Type') && typeName.length > 'Type'.length) {
426
+ return typeName.slice(0, -'Type'.length);
427
+ }
428
+ return typeName;
429
+ }
430
+
431
+ function buildModelDefinitionsByLookupName(
432
+ modelDefinitions: ModelDefinition[]
433
+ ): Map<string, ModelDefinition> {
434
+ const definitionsByLookupName = new Map<string, ModelDefinition>();
435
+ for (const definition of modelDefinitions) {
436
+ if (!definitionsByLookupName.has(definition.name)) {
437
+ definitionsByLookupName.set(definition.name, definition);
438
+ }
439
+ const nestedName = resolveNestedModelTypeName(definition.name);
440
+ if (
441
+ nestedName !== definition.name &&
442
+ !definitionsByLookupName.has(nestedName)
443
+ ) {
444
+ definitionsByLookupName.set(nestedName, definition);
445
+ }
446
+ }
447
+ return definitionsByLookupName;
448
+ }
449
+
450
+ function collectConversionModelNames(
451
+ modelDefinitionsByLookupName: Map<string, ModelDefinition>,
452
+ modelTypeNames: string[]
453
+ ): string[] {
454
+ const queue = [...modelTypeNames];
455
+ const visited = new Set<string>();
456
+ const orderedModelNames: string[] = [];
457
+
458
+ while (queue.length > 0) {
459
+ const modelName = queue.shift();
460
+ if (!modelName || visited.has(modelName)) {
461
+ continue;
462
+ }
463
+ visited.add(modelName);
464
+
465
+ const definition = modelDefinitionsByLookupName.get(modelName);
466
+ if (!definition) {
467
+ continue;
468
+ }
469
+ orderedModelNames.push(modelName);
470
+
471
+ for (const field of definition.fields) {
472
+ const parsedFieldType = parseFieldType(field.type);
473
+ if (!isSimpleTypeReference(parsedFieldType.normalizedType)) {
474
+ continue;
475
+ }
476
+ const nestedModelName = resolveNestedModelTypeName(
477
+ parsedFieldType.normalizedType
478
+ );
479
+ if (!visited.has(nestedModelName)) {
480
+ queue.push(nestedModelName);
481
+ }
482
+ }
483
+ }
484
+
485
+ return orderedModelNames;
486
+ }
@@ -1,21 +1,52 @@
1
1
  import path from 'node:path';
2
2
  import { createRequire } from 'node:module';
3
3
 
4
- import type { MethodSignature } from '../types.js';
4
+ import type { MethodSignature, TypeDeclaration } from '../types.js';
5
5
 
6
- export function generateTurboModuleSpec(methods: MethodSignature[]): string {
6
+ interface TurboSpecGenerationOptions {
7
+ modelTypeNames?: string[];
8
+ }
9
+
10
+ function mapTypeForTurboSpec(
11
+ typeText: string,
12
+ options: TurboSpecGenerationOptions
13
+ ): string {
14
+ if (typeText.startsWith('Promise<')) {
15
+ const inner = typeText.slice(8, -1);
16
+ return `Promise<${mapTypeForTurboSpec(inner, options)}>`;
17
+ }
18
+
19
+ if (options.modelTypeNames?.includes(typeText)) {
20
+ return 'Object';
21
+ }
22
+
23
+ return typeText;
24
+ }
25
+
26
+ export function generateTurboModuleSpec(
27
+ methods: MethodSignature[],
28
+ referencedTypeDeclarations: TypeDeclaration[] = [],
29
+ options: TurboSpecGenerationOptions = {}
30
+ ): string {
7
31
  const methodSignatures = methods
8
32
  .map((method) => {
9
33
  const params = method.params
10
- .map((param) => `${param.name}${param.optional ? '?' : ''}: ${param.type}`)
34
+ .map(
35
+ (param) =>
36
+ `${param.name}${param.optional ? '?' : ''}: ${mapTypeForTurboSpec(param.type, options)}`
37
+ )
11
38
  .join(', ');
12
- return ` ${method.name}(${params}): ${method.returnType};`;
39
+ return ` ${method.name}(${params}): ${mapTypeForTurboSpec(method.returnType, options)};`;
13
40
  })
14
41
  .join('\n');
42
+ const typeDeclarationsBlock =
43
+ referencedTypeDeclarations.length === 0
44
+ ? ''
45
+ : `${referencedTypeDeclarations.map((entry) => entry.declaration).join('\n\n')}\n\n`;
15
46
 
16
47
  return `import { TurboModuleRegistry, type TurboModule } from 'react-native';
17
48
 
18
- export interface Spec extends TurboModule {
49
+ ${typeDeclarationsBlock}export interface Spec extends TurboModule {
19
50
  ${methodSignatures}
20
51
  }
21
52
 
@@ -25,7 +56,22 @@ export default TurboModuleRegistry.getEnforcing<Spec>(
25
56
  `;
26
57
  }
27
58
 
28
- export function generateIndexTs(methods: MethodSignature[]): string {
59
+ function buildTypeImportLine(
60
+ referencedTypeDeclarations: TypeDeclaration[]
61
+ ): string {
62
+ if (referencedTypeDeclarations.length === 0) {
63
+ return '';
64
+ }
65
+
66
+ const names = referencedTypeDeclarations.map((entry) => entry.name).join(', ');
67
+ return `import type { ${names} } from './NativeBrownfieldNavigation';\n`;
68
+ }
69
+
70
+ export function generateIndexTs(
71
+ methods: MethodSignature[],
72
+ referencedTypeDeclarations: TypeDeclaration[] = []
73
+ ): string {
74
+ const typeImportLine = buildTypeImportLine(referencedTypeDeclarations);
29
75
  const functionImplementations = methods
30
76
  .map((method) => {
31
77
  const params = method.params
@@ -47,6 +93,7 @@ export function generateIndexTs(methods: MethodSignature[]): string {
47
93
  .join(',\n');
48
94
 
49
95
  return `import NativeBrownfieldNavigation from './NativeBrownfieldNavigation';
96
+ ${typeImportLine}
50
97
 
51
98
  const BrownfieldNavigation = {
52
99
  ${functionImplementations},
@@ -104,7 +151,11 @@ export function transpileWithConsumerBabel(
104
151
  return transformed.code;
105
152
  }
106
153
 
107
- export function generateIndexDts(methods: MethodSignature[]): string {
154
+ export function generateIndexDts(
155
+ methods: MethodSignature[],
156
+ referencedTypeDeclarations: TypeDeclaration[] = []
157
+ ): string {
158
+ const typeImportLine = buildTypeImportLine(referencedTypeDeclarations);
108
159
  const methodSignatures = methods
109
160
  .map((method) => {
110
161
  const params = method.params
@@ -114,7 +165,7 @@ export function generateIndexDts(methods: MethodSignature[]): string {
114
165
  })
115
166
  .join('\n');
116
167
 
117
- return `declare const BrownfieldNavigation: {
168
+ return `${typeImportLine}declare const BrownfieldNavigation: {
118
169
  ${methodSignatures}
119
170
  };
120
171
 
@@ -1,9 +1,48 @@
1
1
  import fs from 'node:fs';
2
2
  import { Project } from 'ts-morph';
3
3
 
4
- import type { MethodParam, MethodSignature } from './types.js';
4
+ import type {
5
+ ModelDefinition,
6
+ ModelFieldDefinition,
7
+ MethodParam,
8
+ MethodSignature,
9
+ ParsedNavigationSpec,
10
+ TypeDeclaration,
11
+ } from './types.js';
12
+ import { Node } from 'ts-morph';
5
13
 
6
- export function parseNavigationSpec(specPath: string): MethodSignature[] {
14
+ const SKIP_TYPE_TOKENS = new Set([
15
+ 'Array',
16
+ 'Date',
17
+ 'Map',
18
+ 'Object',
19
+ 'Promise',
20
+ 'ReadonlyArray',
21
+ 'Record',
22
+ 'Set',
23
+ 'any',
24
+ 'boolean',
25
+ 'false',
26
+ 'null',
27
+ 'number',
28
+ 'object',
29
+ 'string',
30
+ 'true',
31
+ 'undefined',
32
+ 'unknown',
33
+ 'void',
34
+ ]);
35
+
36
+ function collectReferencedTypesFromText(typeText: string): string[] {
37
+ const matches = typeText.match(/\b[A-Za-z_]\w*\b/g);
38
+ if (!matches) {
39
+ return [];
40
+ }
41
+
42
+ return matches.filter((match) => !SKIP_TYPE_TOKENS.has(match));
43
+ }
44
+
45
+ export function parseNavigationSpec(specPath: string): ParsedNavigationSpec {
7
46
  if (!fs.existsSync(specPath)) {
8
47
  throw new Error(`Spec file not found: ${specPath}`);
9
48
  }
@@ -20,7 +59,7 @@ export function parseNavigationSpec(specPath: string): MethodSignature[] {
20
59
  );
21
60
  }
22
61
 
23
- return specInterface.getMethods().map((method): MethodSignature => {
62
+ const methods = specInterface.getMethods().map((method): MethodSignature => {
24
63
  const name = method.getName();
25
64
  const params: MethodParam[] = method.getParameters().map((param) => {
26
65
  const typeNode = param.getTypeNode();
@@ -40,4 +79,99 @@ export function parseNavigationSpec(specPath: string): MethodSignature[] {
40
79
  isAsync: returnType.startsWith('Promise<'),
41
80
  };
42
81
  });
82
+
83
+ const declarationNodes = [
84
+ ...sourceFile.getTypeAliases(),
85
+ ...sourceFile.getInterfaces(),
86
+ ].filter((node) => {
87
+ const nodeName = node.getName();
88
+ return nodeName !== 'BrownfieldNavigationSpec' && nodeName !== 'Spec';
89
+ });
90
+
91
+ const declarationByName = new Map<string, string>(
92
+ declarationNodes.map((node) => {
93
+ const declarationText = node.getText();
94
+ const normalizedDeclaration = node.isExported()
95
+ ? declarationText
96
+ : `export ${declarationText}`;
97
+ return [node.getName(), normalizedDeclaration];
98
+ })
99
+ );
100
+ const referencedTypeNames = new Set<string>();
101
+ for (const method of methods) {
102
+ for (const typeText of [
103
+ method.returnType,
104
+ ...method.params.map((param) => param.type),
105
+ ]) {
106
+ for (const typeName of collectReferencedTypesFromText(typeText)) {
107
+ referencedTypeNames.add(typeName);
108
+ }
109
+ }
110
+ }
111
+
112
+ const referencedTypeDeclarations: TypeDeclaration[] = [];
113
+ const modelDefinitions: ModelDefinition[] = [];
114
+ const addedTypeNames = new Set<string>();
115
+ const queue = [...referencedTypeNames];
116
+
117
+ while (queue.length > 0) {
118
+ const typeName = queue.shift();
119
+ if (!typeName || addedTypeNames.has(typeName)) {
120
+ continue;
121
+ }
122
+
123
+ const declaration = declarationByName.get(typeName);
124
+ if (!declaration) {
125
+ continue;
126
+ }
127
+
128
+ addedTypeNames.add(typeName);
129
+ referencedTypeDeclarations.push({ name: typeName, declaration });
130
+ const declarationNode = declarationNodes.find((node) => node.getName() === typeName);
131
+ if (declarationNode) {
132
+ const fields = extractModelFields(declarationNode);
133
+ if (fields.length > 0) {
134
+ modelDefinitions.push({ name: typeName, fields });
135
+ }
136
+ }
137
+
138
+ for (const nestedTypeName of collectReferencedTypesFromText(declaration)) {
139
+ if (
140
+ !addedTypeNames.has(nestedTypeName) &&
141
+ declarationByName.has(nestedTypeName)
142
+ ) {
143
+ queue.push(nestedTypeName);
144
+ }
145
+ }
146
+ }
147
+
148
+ return {
149
+ methods,
150
+ referencedTypeDeclarations,
151
+ modelDefinitions,
152
+ };
153
+ }
154
+
155
+ function extractModelFields(node: Node): ModelFieldDefinition[] {
156
+ if (Node.isInterfaceDeclaration(node)) {
157
+ return node.getProperties().map((property) => ({
158
+ name: property.getName(),
159
+ type: property.getTypeNode()?.getText() ?? 'unknown',
160
+ optional: property.hasQuestionToken(),
161
+ }));
162
+ }
163
+
164
+ if (Node.isTypeAliasDeclaration(node)) {
165
+ const typeNode = node.getTypeNode();
166
+ if (!typeNode || !Node.isTypeLiteral(typeNode)) {
167
+ return [];
168
+ }
169
+ return typeNode.getProperties().map((property) => ({
170
+ name: property.getName(),
171
+ type: property.getTypeNode()?.getText() ?? 'unknown',
172
+ optional: property.hasQuestionToken(),
173
+ }));
174
+ }
175
+
176
+ return [];
43
177
  }
@@ -218,7 +218,8 @@ export async function runNavigationCodegen({
218
218
  intro(`Running Brownfield Navigation codegen`);
219
219
 
220
220
  logger.info(`Parsing spec file: ${resolvedSpecPath}`);
221
- const methods = parseNavigationSpec(resolvedSpecPath);
221
+ const { methods, referencedTypeDeclarations, modelDefinitions } =
222
+ parseNavigationSpec(resolvedSpecPath);
222
223
  if (methods.length === 0) {
223
224
  throw new Error('No methods found in spec file');
224
225
  }
@@ -229,22 +230,33 @@ export async function runNavigationCodegen({
229
230
 
230
231
  const packageRoot = getNavigationPackagePath(projectRoot);
231
232
  const androidJavaPackageName = DEFAULT_ANDROID_JAVA_PACKAGE;
232
- const indexTs = generateIndexTs(methods);
233
+ const indexTs = generateIndexTs(methods, referencedTypeDeclarations);
233
234
  const models = await generateNavigationModels({
234
235
  specPath: resolvedSpecPath,
235
236
  methods,
236
237
  kotlinPackageName: androidJavaPackageName,
238
+ modelDefinitions,
237
239
  });
238
240
 
239
241
  const artifacts: GeneratedNavigationArtifacts = {
240
- turboModuleSpec: generateTurboModuleSpec(methods),
242
+ turboModuleSpec: generateTurboModuleSpec(methods, referencedTypeDeclarations, {
243
+ modelTypeNames: models.modelTypeNames,
244
+ }),
241
245
  indexTs,
242
246
  indexJs: transpileWithConsumerBabel(indexTs, projectRoot, packageRoot),
243
- indexDts: generateIndexDts(methods),
244
- swiftDelegate: generateSwiftDelegate(methods),
245
- objcImplementation: generateObjCImplementation(methods),
246
- kotlinDelegate: generateKotlinDelegate(methods, androidJavaPackageName),
247
- kotlinModule: generateKotlinModule(methods, androidJavaPackageName),
247
+ indexDts: generateIndexDts(methods, referencedTypeDeclarations),
248
+ swiftDelegate: generateSwiftDelegate(methods, {
249
+ modelTypeNames: models.modelTypeNames,
250
+ }),
251
+ objcImplementation: generateObjCImplementation(methods, {
252
+ modelTypeNames: models.modelTypeNames,
253
+ }),
254
+ kotlinDelegate: generateKotlinDelegate(methods, androidJavaPackageName, {
255
+ modelTypeNames: models.modelTypeNames,
256
+ }),
257
+ kotlinModule: generateKotlinModule(methods, androidJavaPackageName, {
258
+ modelTypeNames: models.modelTypeNames,
259
+ }),
248
260
  };
249
261
 
250
262
  if (models.modelTypeNames.length > 0) {
@@ -11,6 +11,28 @@ export interface MethodSignature {
11
11
  isAsync: boolean;
12
12
  }
13
13
 
14
+ export interface TypeDeclaration {
15
+ name: string;
16
+ declaration: string;
17
+ }
18
+
19
+ export interface ModelFieldDefinition {
20
+ name: string;
21
+ type: string;
22
+ optional: boolean;
23
+ }
24
+
25
+ export interface ModelDefinition {
26
+ name: string;
27
+ fields: ModelFieldDefinition[];
28
+ }
29
+
30
+ export interface ParsedNavigationSpec {
31
+ methods: MethodSignature[];
32
+ referencedTypeDeclarations: TypeDeclaration[];
33
+ modelDefinitions: ModelDefinition[];
34
+ }
35
+
14
36
  export interface GeneratedNavigationArtifacts {
15
37
  turboModuleSpec: string;
16
38
  indexTs: string;