@player-lang/functional-dsl-generator 0.0.2-next.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.
Files changed (42) hide show
  1. package/dist/cjs/index.cjs +2146 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +2075 -0
  4. package/dist/index.mjs +2075 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +38 -0
  7. package/src/__tests__/__snapshots__/generator.test.ts.snap +886 -0
  8. package/src/__tests__/builder-class-generator.test.ts +627 -0
  9. package/src/__tests__/cli.test.ts +685 -0
  10. package/src/__tests__/default-value-generator.test.ts +365 -0
  11. package/src/__tests__/generator.test.ts +2860 -0
  12. package/src/__tests__/import-generator.test.ts +444 -0
  13. package/src/__tests__/path-utils.test.ts +174 -0
  14. package/src/__tests__/type-collector.test.ts +674 -0
  15. package/src/__tests__/type-transformer.test.ts +934 -0
  16. package/src/__tests__/utils.test.ts +597 -0
  17. package/src/builder-class-generator.ts +254 -0
  18. package/src/cli.ts +285 -0
  19. package/src/default-value-generator.ts +307 -0
  20. package/src/generator.ts +257 -0
  21. package/src/import-generator.ts +331 -0
  22. package/src/index.ts +38 -0
  23. package/src/path-utils.ts +155 -0
  24. package/src/ts-morph-type-finder.ts +319 -0
  25. package/src/type-categorizer.ts +131 -0
  26. package/src/type-collector.ts +296 -0
  27. package/src/type-resolver.ts +266 -0
  28. package/src/type-transformer.ts +487 -0
  29. package/src/utils.ts +762 -0
  30. package/types/builder-class-generator.d.ts +56 -0
  31. package/types/cli.d.ts +6 -0
  32. package/types/default-value-generator.d.ts +74 -0
  33. package/types/generator.d.ts +102 -0
  34. package/types/import-generator.d.ts +77 -0
  35. package/types/index.d.ts +12 -0
  36. package/types/path-utils.d.ts +65 -0
  37. package/types/ts-morph-type-finder.d.ts +73 -0
  38. package/types/type-categorizer.d.ts +46 -0
  39. package/types/type-collector.d.ts +62 -0
  40. package/types/type-resolver.d.ts +49 -0
  41. package/types/type-transformer.d.ts +74 -0
  42. package/types/utils.d.ts +205 -0
package/src/utils.ts ADDED
@@ -0,0 +1,762 @@
1
+ import type {
2
+ NodeType,
3
+ ObjectType,
4
+ ArrayType,
5
+ StringType,
6
+ NumberType,
7
+ BooleanType,
8
+ TupleType,
9
+ RefType,
10
+ } from "@xlr-lib/xlr";
11
+
12
+ import {
13
+ isStringType,
14
+ isNumberType,
15
+ isBooleanType,
16
+ isObjectType,
17
+ isArrayType,
18
+ isRefType,
19
+ isOrType,
20
+ isAndType,
21
+ isRecordType,
22
+ isNamedType,
23
+ } from "@xlr-lib/xlr-utils";
24
+
25
+ // Re-export type guards from xlr-utils for consumers
26
+ export {
27
+ isStringType,
28
+ isNumberType,
29
+ isBooleanType,
30
+ isObjectType,
31
+ isArrayType,
32
+ isRefType,
33
+ isOrType,
34
+ isAndType,
35
+ isRecordType,
36
+ isNamedType,
37
+ };
38
+
39
+ /**
40
+ * Type guard for tuple type nodes
41
+ */
42
+ export function isTupleType(node: NodeType): node is TupleType {
43
+ return node.type === "tuple";
44
+ }
45
+
46
+ /**
47
+ * Check if a primitive type has a const value (literal type)
48
+ */
49
+ export function isPrimitiveConst(
50
+ node: NodeType,
51
+ ): node is (StringType | NumberType | BooleanType) & { const: unknown } {
52
+ return (
53
+ (isStringType(node) || isNumberType(node) || isBooleanType(node)) &&
54
+ "const" in node &&
55
+ node.const !== undefined
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Check if a ref type is an AssetWrapper
61
+ */
62
+ export function isAssetWrapperRef(node: NodeType): boolean {
63
+ return isRefType(node) && node.ref.startsWith("AssetWrapper");
64
+ }
65
+
66
+ /**
67
+ * Check if a NodeType resolves to a type that extends AssetWrapper.
68
+ * Handles:
69
+ * - RefType nodes resolved via type registry
70
+ * - Inline ObjectType nodes with an `extends` field
71
+ * - Transitive chains: e.g., ListItem → ListItemBase → AssetWrapper
72
+ * Uses cycle detection to prevent infinite recursion on circular type hierarchies.
73
+ *
74
+ * @param node - The node to check
75
+ * @param typeRegistry - Map of type names to their ObjectType definitions
76
+ * @param visited - Set of already-visited type names for cycle detection
77
+ * @returns true if the node resolves to a type extending AssetWrapper
78
+ */
79
+ export function extendsAssetWrapper(
80
+ node: NodeType,
81
+ typeRegistry: TypeRegistry,
82
+ visited: Set<string> = new Set(),
83
+ ): boolean {
84
+ // Inline ObjectType with extends field (XLR inlines types in many positions)
85
+ if (isObjectType(node) && node.extends) {
86
+ if (node.extends.ref.startsWith("AssetWrapper")) return true;
87
+ return extendsAssetWrapper(node.extends, typeRegistry, visited);
88
+ }
89
+
90
+ // RefType - look up in registry
91
+ if (isRefType(node)) {
92
+ const typeName = extractBaseName(node.ref);
93
+ if (visited.has(typeName)) return false;
94
+ visited.add(typeName);
95
+
96
+ const resolved = typeRegistry.get(typeName);
97
+ if (!resolved?.extends) return false;
98
+
99
+ if (resolved.extends.ref.startsWith("AssetWrapper")) return true;
100
+ return extendsAssetWrapper(resolved.extends, typeRegistry, visited);
101
+ }
102
+
103
+ return false;
104
+ }
105
+
106
+ /**
107
+ * Get the AssetWrapper ancestor's RefType for a node that extends AssetWrapper.
108
+ * Handles both inline ObjectTypes and RefType lookups via registry.
109
+ * This allows extracting the generic argument (e.g., ImageAsset from AssetWrapper<ImageAsset>).
110
+ *
111
+ * @param node - The node to inspect
112
+ * @param typeRegistry - Map of type names to their ObjectType definitions
113
+ * @param visited - Set of already-visited type names for cycle detection
114
+ * @returns The RefType of the AssetWrapper ancestor, or undefined if not found
115
+ */
116
+ export function getAssetWrapperExtendsRef(
117
+ node: NodeType,
118
+ typeRegistry: TypeRegistry,
119
+ visited: Set<string> = new Set(),
120
+ ): RefType | undefined {
121
+ // Inline ObjectType with extends field
122
+ if (isObjectType(node) && node.extends) {
123
+ if (node.extends.ref.startsWith("AssetWrapper")) return node.extends;
124
+ return getAssetWrapperExtendsRef(node.extends, typeRegistry, visited);
125
+ }
126
+
127
+ // RefType - look up in registry
128
+ if (isRefType(node)) {
129
+ const typeName = extractBaseName(node.ref);
130
+ if (visited.has(typeName)) return undefined;
131
+ visited.add(typeName);
132
+
133
+ const resolved = typeRegistry.get(typeName);
134
+ if (!resolved?.extends) return undefined;
135
+
136
+ if (resolved.extends.ref.startsWith("AssetWrapper"))
137
+ return resolved.extends;
138
+ return getAssetWrapperExtendsRef(resolved.extends, typeRegistry, visited);
139
+ }
140
+
141
+ return undefined;
142
+ }
143
+
144
+ /**
145
+ * Look up the AssetWrapper ancestor's RefType by type name via the registry.
146
+ * Convenience wrapper around getAssetWrapperExtendsRef for name-based lookups.
147
+ */
148
+ export function getAssetWrapperExtendsRefByName(
149
+ typeName: string,
150
+ typeRegistry: TypeRegistry,
151
+ ): RefType | undefined {
152
+ return getAssetWrapperExtendsRef(
153
+ { type: "ref", ref: typeName } as RefType,
154
+ typeRegistry,
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Check if a ref type is an Expression
160
+ */
161
+ export function isExpressionRef(node: NodeType): boolean {
162
+ return isRefType(node) && node.ref === "Expression";
163
+ }
164
+
165
+ /**
166
+ * Check if a ref type is a Binding
167
+ */
168
+ export function isBindingRef(node: NodeType): boolean {
169
+ return isRefType(node) && node.ref === "Binding";
170
+ }
171
+
172
+ /**
173
+ * Sanitize a property name by removing surrounding quotes.
174
+ * TypeScript allows quoted property names like "mime-type" which may
175
+ * end up in XLR with quotes preserved.
176
+ *
177
+ * @example
178
+ * sanitizePropertyName("'mime-type'") // "mime-type"
179
+ * sanitizePropertyName('"content-type"') // "content-type"
180
+ * sanitizePropertyName("normalProp") // "normalProp"
181
+ */
182
+ export function sanitizePropertyName(name: string): string {
183
+ return name.replace(/^['"]|['"]$/g, "");
184
+ }
185
+
186
+ /**
187
+ * Convert a property name to PascalCase for method names.
188
+ * Handles camelCase, kebab-case, snake_case inputs, and quoted property names.
189
+ *
190
+ * @example
191
+ * toPascalCase("myProperty") // "MyProperty"
192
+ * toPascalCase("my-property") // "MyProperty"
193
+ * toPascalCase("my_property") // "MyProperty"
194
+ * toPascalCase("'mime-type'") // "MimeType"
195
+ */
196
+ export function toPascalCase(str: string): string {
197
+ // First sanitize any quotes that may have been preserved from TypeScript source
198
+ const sanitized = sanitizePropertyName(str);
199
+
200
+ return sanitized
201
+ .split(/[-_]/)
202
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
203
+ .join("");
204
+ }
205
+
206
+ /**
207
+ * Convert a type name to a factory function name (camelCase)
208
+ */
209
+ export function toFactoryName(typeName: string): string {
210
+ // Remove "Asset" suffix if present
211
+ const name = typeName.replace(/Asset$/, "");
212
+ return name.charAt(0).toLowerCase() + name.slice(1);
213
+ }
214
+
215
+ /**
216
+ * Convert a type name to a builder class name
217
+ */
218
+ export function toBuilderClassName(typeName: string): string {
219
+ return `${typeName}Builder`;
220
+ }
221
+
222
+ /**
223
+ * Check if an object type is complex enough to warrant its own builder class
224
+ */
225
+ export function isComplexObjectType(obj: ObjectType): boolean {
226
+ const props = Object.values(obj.properties);
227
+
228
+ // Has AssetWrapper properties
229
+ const hasSlots = props.some((p) => isAssetWrapperRef(p.node));
230
+ if (hasSlots) return true;
231
+
232
+ // Has many properties
233
+ if (props.length > 3) return true;
234
+
235
+ // Has nested objects
236
+ const hasNestedObjects = props.some(
237
+ (p) => isObjectType(p.node) && !isPrimitiveConst(p.node),
238
+ );
239
+ if (hasNestedObjects) return true;
240
+
241
+ return false;
242
+ }
243
+
244
+ /**
245
+ * Get the asset type string from extends ref
246
+ */
247
+ export function getAssetTypeFromExtends(obj: ObjectType): string | undefined {
248
+ if (!obj.extends) return undefined;
249
+
250
+ const ref = obj.extends;
251
+ if (ref.genericArguments && ref.genericArguments.length > 0) {
252
+ const typeArg = ref.genericArguments[0];
253
+ if (isStringType(typeArg) && typeArg.const) {
254
+ return typeArg.const;
255
+ }
256
+ }
257
+ return undefined;
258
+ }
259
+
260
+ /**
261
+ * Information about a property for code generation
262
+ */
263
+ export interface PropertyInfo {
264
+ name: string;
265
+ node: NodeType;
266
+ required: boolean;
267
+ isSlot: boolean;
268
+ isArraySlot: boolean;
269
+ isArray: boolean;
270
+ }
271
+
272
+ /**
273
+ * Extract property information from an ObjectType
274
+ */
275
+ export function getPropertiesInfo(obj: ObjectType): PropertyInfo[] {
276
+ return Object.entries(obj.properties).map(([name, prop]) => {
277
+ const isSlot = isAssetWrapperRef(prop.node);
278
+ const isArray = isArrayType(prop.node);
279
+ const isArraySlot =
280
+ isArray && isAssetWrapperRef((prop.node as ArrayType).elementType);
281
+
282
+ return {
283
+ name,
284
+ node: prop.node,
285
+ required: prop.required,
286
+ isSlot,
287
+ isArraySlot,
288
+ isArray,
289
+ };
290
+ });
291
+ }
292
+
293
+ /**
294
+ * Check if a type contains an array type (directly or within a union/intersection)
295
+ * This handles cases like `Array<T> | T` where the property can be either
296
+ */
297
+ export function containsArrayType(node: NodeType): boolean {
298
+ if (isArrayType(node)) {
299
+ return true;
300
+ }
301
+
302
+ if (isOrType(node)) {
303
+ return node.or.some(containsArrayType);
304
+ }
305
+
306
+ if (isAndType(node)) {
307
+ return node.and.some(containsArrayType);
308
+ }
309
+
310
+ return false;
311
+ }
312
+
313
+ /**
314
+ * Split a string by commas, but only at the top level (ignoring commas inside angle brackets).
315
+ * This is needed for parsing generic parameter lists like "T extends Foo<A, B>, U = Bar<C, D>"
316
+ *
317
+ * @example
318
+ * splitAtTopLevelCommas("T extends Foo, U = Bar") // ["T extends Foo", "U = Bar"]
319
+ * splitAtTopLevelCommas("T extends Foo<A, B>, U") // ["T extends Foo<A, B>", "U"]
320
+ */
321
+ export function splitAtTopLevelCommas(str: string): string[] {
322
+ const result: string[] = [];
323
+ let current = "";
324
+ let depth = 0;
325
+
326
+ for (const char of str) {
327
+ if (char === "<") {
328
+ depth++;
329
+ current += char;
330
+ } else if (char === ">") {
331
+ depth--;
332
+ current += char;
333
+ } else if (char === "," && depth === 0) {
334
+ result.push(current.trim());
335
+ current = "";
336
+ } else {
337
+ current += char;
338
+ }
339
+ }
340
+
341
+ if (current.trim()) {
342
+ result.push(current.trim());
343
+ }
344
+
345
+ return result;
346
+ }
347
+
348
+ /**
349
+ * Extract generic usage string from generic params declaration
350
+ * Converts "T extends Foo, U = Bar" to "<T, U>"
351
+ * Handles nested generics like "T extends Foo<A, B>, U = Bar<C, D>" correctly
352
+ */
353
+ export function extractGenericUsage(genericParams: string | undefined): string {
354
+ if (!genericParams) {
355
+ return "";
356
+ }
357
+
358
+ const params = splitAtTopLevelCommas(genericParams)
359
+ .map((p) => p.trim().split(" ")[0])
360
+ .join(", ");
361
+
362
+ return `<${params}>`;
363
+ }
364
+
365
+ /**
366
+ * Set of TypeScript built-in types that should never be imported.
367
+ * These are either global types or utility types provided by TypeScript.
368
+ */
369
+ export const TYPESCRIPT_BUILTINS = new Set([
370
+ // Primitive wrappers
371
+ "String",
372
+ "Number",
373
+ "Boolean",
374
+ "Symbol",
375
+ "BigInt",
376
+
377
+ // Collections
378
+ "Array",
379
+ "Map",
380
+ "Set",
381
+ "WeakMap",
382
+ "WeakSet",
383
+ "ReadonlyArray",
384
+ "ReadonlyMap",
385
+ "ReadonlySet",
386
+
387
+ // Object types
388
+ "Object",
389
+ "Function",
390
+ "Date",
391
+ "RegExp",
392
+ "Error",
393
+ "Promise",
394
+ "PromiseLike",
395
+
396
+ // Utility types
397
+ "Partial",
398
+ "Required",
399
+ "Readonly",
400
+ "Pick",
401
+ "Omit",
402
+ "Exclude",
403
+ "Extract",
404
+ "NonNullable",
405
+ "Parameters",
406
+ "ConstructorParameters",
407
+ "ReturnType",
408
+ "InstanceType",
409
+ "ThisParameterType",
410
+ "OmitThisParameter",
411
+ "ThisType",
412
+ "Awaited",
413
+ "Record",
414
+
415
+ // Iterable types
416
+ "Iterable",
417
+ "Iterator",
418
+ "IterableIterator",
419
+ "Generator",
420
+ "AsyncIterator",
421
+ "AsyncIterable",
422
+ "AsyncIterableIterator",
423
+ "AsyncGenerator",
424
+ "GeneratorFunction",
425
+ "AsyncGeneratorFunction",
426
+
427
+ // Array-like types
428
+ "ArrayLike",
429
+ "ArrayBuffer",
430
+ "SharedArrayBuffer",
431
+ "DataView",
432
+ "TypedArray",
433
+ "Int8Array",
434
+ "Uint8Array",
435
+ "Uint8ClampedArray",
436
+ "Int16Array",
437
+ "Uint16Array",
438
+ "Int32Array",
439
+ "Uint32Array",
440
+ "Float32Array",
441
+ "Float64Array",
442
+ "BigInt64Array",
443
+ "BigUint64Array",
444
+
445
+ // Other built-ins
446
+ "JSON",
447
+ "Math",
448
+ "Console",
449
+ "Proxy",
450
+ "Reflect",
451
+ "WeakRef",
452
+ "FinalizationRegistry",
453
+ ]);
454
+
455
+ /**
456
+ * Set of Player-specific built-in types that have special handling
457
+ * and should not be imported as regular types.
458
+ */
459
+ export const PLAYER_BUILTINS = new Set([
460
+ "Asset",
461
+ "AssetWrapper",
462
+ "Binding",
463
+ "Expression",
464
+ ]);
465
+
466
+ /**
467
+ * Check if a type name is a built-in type (TypeScript or Player-specific)
468
+ * that should not be imported.
469
+ */
470
+ export function isBuiltinType(typeName: string): boolean {
471
+ return TYPESCRIPT_BUILTINS.has(typeName) || PLAYER_BUILTINS.has(typeName);
472
+ }
473
+
474
+ /**
475
+ * Extracts the base type name from a ref string, handling nested generics.
476
+ * @example
477
+ * extractBaseName("MyType") // "MyType"
478
+ * extractBaseName("MyType<T>") // "MyType"
479
+ * extractBaseName("Map<string, Array<T>>") // "Map"
480
+ */
481
+ export function extractBaseName(ref: string): string {
482
+ const bracketIndex = ref.indexOf("<");
483
+ return bracketIndex === -1 ? ref : ref.substring(0, bracketIndex);
484
+ }
485
+
486
+ /**
487
+ * Checks if a type name is a namespaced type (e.g., "Validation.CrossfieldReference").
488
+ * Returns the namespace and member name if it is, null otherwise.
489
+ */
490
+ export function parseNamespacedType(
491
+ typeName: string,
492
+ ): { namespace: string; member: string } | null {
493
+ const dotIndex = typeName.indexOf(".");
494
+ if (dotIndex === -1) return null;
495
+ return {
496
+ namespace: typeName.substring(0, dotIndex),
497
+ member: typeName.substring(dotIndex + 1),
498
+ };
499
+ }
500
+
501
+ /**
502
+ * Type registry for resolving named type references.
503
+ *
504
+ * This map stores XLR ObjectType definitions keyed by their type name.
505
+ * It's used by `findAssetWrapperPaths` to resolve references to named
506
+ * interface types when searching for nested AssetWrapper properties.
507
+ *
508
+ * Example usage:
509
+ * ```typescript
510
+ * const registry: TypeRegistry = new Map([
511
+ * ["ContentCardHeader", headerObjectType],
512
+ * ["SlotConfig", slotConfigObjectType],
513
+ * ]);
514
+ *
515
+ * // Now findAssetWrapperPaths can resolve ContentCardHeader references
516
+ * const paths = findAssetWrapperPaths(contentCardType, registry);
517
+ * ```
518
+ *
519
+ * Types should be registered when:
520
+ * - They are referenced by other types in the codebase
521
+ * - They contain AssetWrapper properties that need to be discovered
522
+ * - They are part of a nested type hierarchy
523
+ */
524
+ export type TypeRegistry = Map<string, ObjectType>;
525
+
526
+ /**
527
+ * Context for AssetWrapper path finding
528
+ */
529
+ interface PathFindingContext {
530
+ typeRegistry: TypeRegistry;
531
+ visited: Set<string>;
532
+ currentPath: string[];
533
+ }
534
+
535
+ /**
536
+ * Finds all paths to AssetWrapper properties within a type, including nested interfaces.
537
+ *
538
+ * This function recursively traverses the type tree to find all property paths
539
+ * that lead to AssetWrapper fields. It supports:
540
+ * - Direct AssetWrapper properties
541
+ * - Named interface references resolved via type registry
542
+ * - Arbitrary nesting depth
543
+ * - Cycle detection for recursive types
544
+ *
545
+ * @param node - The type node to search
546
+ * @param typeRegistry - Map of type names to their ObjectType definitions
547
+ * @returns Array of paths, where each path is an array of property names
548
+ *
549
+ * @example
550
+ * // For a type like:
551
+ * // interface ContentCard { header: ContentCardHeader }
552
+ * // interface ContentCardHeader { left: AssetWrapper }
553
+ * // Returns: [["header", "left"]]
554
+ */
555
+ export function findAssetWrapperPaths(
556
+ node: NodeType,
557
+ typeRegistry: TypeRegistry,
558
+ ): string[][] {
559
+ const context: PathFindingContext = {
560
+ typeRegistry,
561
+ visited: new Set(),
562
+ currentPath: [],
563
+ };
564
+
565
+ return findPathsRecursive(node, context);
566
+ }
567
+
568
+ /**
569
+ * Recursively finds AssetWrapper paths within a node
570
+ */
571
+ function findPathsRecursive(
572
+ node: NodeType,
573
+ context: PathFindingContext,
574
+ ): string[][] {
575
+ const paths: string[][] = [];
576
+
577
+ // Handle object types with properties
578
+ if (isObjectType(node)) {
579
+ // Check if this is a named type we need to track for cycle detection
580
+ if (isNamedType(node)) {
581
+ if (context.visited.has(node.name)) {
582
+ return [];
583
+ }
584
+ context.visited.add(node.name);
585
+ }
586
+
587
+ // Process each property
588
+ for (const [propName, prop] of Object.entries(node.properties)) {
589
+ const propPaths = findPathsForProperty(propName, prop.node, context);
590
+ paths.push(...propPaths);
591
+ }
592
+
593
+ // Clean up visited for named types
594
+ if (isNamedType(node)) {
595
+ context.visited.delete(node.name);
596
+ }
597
+ }
598
+
599
+ return paths;
600
+ }
601
+
602
+ /**
603
+ * Recurse into a type node to find nested AssetWrapper paths.
604
+ * Handles both inline ObjectTypes (recurse directly) and RefTypes (resolve from registry).
605
+ * Used for AssetWrapper-extending types and array element types.
606
+ */
607
+ function recurseIntoExtendingType(
608
+ targetNode: NodeType,
609
+ context: PathFindingContext,
610
+ propName: string,
611
+ ): string[][] {
612
+ const newContext = {
613
+ ...context,
614
+ currentPath: [...context.currentPath, propName],
615
+ };
616
+ if (isObjectType(targetNode)) {
617
+ return findPathsRecursive(targetNode, newContext);
618
+ }
619
+ if (isRefType(targetNode)) {
620
+ const typeName = extractBaseName(targetNode.ref);
621
+ const resolvedType = context.typeRegistry.get(typeName);
622
+ if (resolvedType && !context.visited.has(typeName)) {
623
+ context.visited.add(typeName);
624
+ const nestedPaths = findPathsRecursive(resolvedType, newContext);
625
+ context.visited.delete(typeName);
626
+ return nestedPaths;
627
+ }
628
+ }
629
+ return [];
630
+ }
631
+
632
+ /**
633
+ * Finds AssetWrapper paths for a specific property.
634
+ *
635
+ * Design decision: The `visited` set is intentionally shared across recursive calls
636
+ * for cycle detection. When processing union/intersection types, we spread the context
637
+ * but keep the same `visited` reference. This ensures that if TypeA -> TypeB -> TypeA,
638
+ * the cycle is detected regardless of which branch we came from. This prevents
639
+ * infinite recursion in complex type hierarchies with circular references.
640
+ */
641
+ function findPathsForProperty(
642
+ propName: string,
643
+ node: NodeType,
644
+ context: PathFindingContext,
645
+ ): string[][] {
646
+ const paths: string[][] = [];
647
+
648
+ // Direct AssetWrapper property
649
+ if (isAssetWrapperRef(node)) {
650
+ paths.push([...context.currentPath, propName]);
651
+ return paths;
652
+ }
653
+
654
+ // Array of AssetWrappers
655
+ if (isArrayType(node) && isAssetWrapperRef(node.elementType)) {
656
+ paths.push([...context.currentPath, propName]);
657
+ return paths;
658
+ }
659
+
660
+ // Type (ref or inline object) that extends AssetWrapper
661
+ // e.g., Header extends AssetWrapper<AnyAsset>
662
+ if (
663
+ extendsAssetWrapper(node, context.typeRegistry, new Set(context.visited))
664
+ ) {
665
+ paths.push([...context.currentPath, propName]);
666
+ paths.push(...recurseIntoExtendingType(node, context, propName));
667
+ return paths;
668
+ }
669
+
670
+ // Array where element type extends AssetWrapper
671
+ if (
672
+ isArrayType(node) &&
673
+ extendsAssetWrapper(
674
+ node.elementType,
675
+ context.typeRegistry,
676
+ new Set(context.visited),
677
+ )
678
+ ) {
679
+ paths.push([...context.currentPath, propName]);
680
+ paths.push(
681
+ ...recurseIntoExtendingType(node.elementType, context, propName),
682
+ );
683
+ return paths;
684
+ }
685
+
686
+ // Array with complex element type — recurse into element type to find nested
687
+ // AssetWrapper paths (e.g., Array<StaticFilter> where StaticFilter contains
688
+ // label: AssetWrapper and value: AssetWrapper)
689
+ if (isArrayType(node)) {
690
+ paths.push(
691
+ ...recurseIntoExtendingType(node.elementType, context, propName),
692
+ );
693
+ return paths;
694
+ }
695
+
696
+ // Named reference - look up in type registry
697
+ if (isRefType(node)) {
698
+ const typeName = extractBaseName(node.ref);
699
+ const resolvedType = context.typeRegistry.get(typeName);
700
+
701
+ if (resolvedType && !context.visited.has(typeName)) {
702
+ context.visited.add(typeName);
703
+ const newContext = {
704
+ ...context,
705
+ currentPath: [...context.currentPath, propName],
706
+ };
707
+ const nestedPaths = findPathsRecursive(resolvedType, newContext);
708
+ paths.push(...nestedPaths);
709
+ context.visited.delete(typeName);
710
+ }
711
+ return paths;
712
+ }
713
+
714
+ // Nested object type (inline)
715
+ if (isObjectType(node)) {
716
+ const newContext = {
717
+ ...context,
718
+ currentPath: [...context.currentPath, propName],
719
+ };
720
+ const nestedPaths = findPathsRecursive(node, newContext);
721
+ paths.push(...nestedPaths);
722
+ return paths;
723
+ }
724
+
725
+ // Union types - check all variants
726
+ if (isOrType(node)) {
727
+ for (const variant of node.or) {
728
+ const variantPaths = findPathsForProperty(propName, variant, {
729
+ ...context,
730
+ currentPath: context.currentPath,
731
+ });
732
+ // Only add unique paths
733
+ for (const path of variantPaths) {
734
+ const pathStr = path.join(".");
735
+ if (!paths.some((p) => p.join(".") === pathStr)) {
736
+ paths.push(path);
737
+ }
738
+ }
739
+ }
740
+ return paths;
741
+ }
742
+
743
+ // Intersection types - check all parts
744
+ if (isAndType(node)) {
745
+ for (const part of node.and) {
746
+ const partPaths = findPathsForProperty(propName, part, {
747
+ ...context,
748
+ currentPath: context.currentPath,
749
+ });
750
+ // Only add unique paths
751
+ for (const path of partPaths) {
752
+ const pathStr = path.join(".");
753
+ if (!paths.some((p) => p.join(".") === pathStr)) {
754
+ paths.push(path);
755
+ }
756
+ }
757
+ }
758
+ return paths;
759
+ }
760
+
761
+ return paths;
762
+ }