@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.
- package/CHANGELOG.md +12 -0
- package/dist/navigation/generators/android.d.ts +6 -2
- package/dist/navigation/generators/android.d.ts.map +1 -1
- package/dist/navigation/generators/android.js +36 -17
- package/dist/navigation/generators/ios.d.ts +11 -2
- package/dist/navigation/generators/ios.d.ts.map +1 -1
- package/dist/navigation/generators/ios.js +41 -18
- package/dist/navigation/generators/models.d.ts +3 -2
- package/dist/navigation/generators/models.d.ts.map +1 -1
- package/dist/navigation/generators/models.js +223 -3
- package/dist/navigation/generators/ts.d.ts +8 -4
- package/dist/navigation/generators/ts.d.ts.map +1 -1
- package/dist/navigation/generators/ts.js +30 -7
- package/dist/navigation/parser.d.ts +2 -2
- package/dist/navigation/parser.d.ts.map +1 -1
- package/dist/navigation/parser.js +110 -1
- package/dist/navigation/runner.d.ts.map +1 -1
- package/dist/navigation/runner.js +19 -8
- package/dist/navigation/types.d.ts +18 -0
- package/dist/navigation/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/navigation/generators/android.ts +67 -17
- package/src/navigation/generators/ios.ts +75 -18
- package/src/navigation/generators/models.ts +319 -3
- package/src/navigation/generators/ts.ts +59 -8
- package/src/navigation/parser.ts +137 -3
- package/src/navigation/runner.ts +20 -8
- package/src/navigation/types.ts +22 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
168
|
+
return `${typeImportLine}declare const BrownfieldNavigation: {
|
|
118
169
|
${methodSignatures}
|
|
119
170
|
};
|
|
120
171
|
|
package/src/navigation/parser.ts
CHANGED
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { Project } from 'ts-morph';
|
|
3
3
|
|
|
4
|
-
import type {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/navigation/runner.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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) {
|
package/src/navigation/types.ts
CHANGED
|
@@ -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;
|