@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
@@ -0,0 +1,2860 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { setupTestEnv } from "@player-lang/test-utils";
3
+ import { TsConverter } from "@xlr-lib/xlr-converters";
4
+ import type { NamedType, ObjectType } from "@xlr-lib/xlr";
5
+ import { generateFunctionalBuilder, type GeneratorConfig } from "../generator";
6
+ import type { TypeRegistry } from "../utils";
7
+ import { FunctionalBuilderBase } from "@player-lang/functional-dsl";
8
+
9
+ /** Custom primitives that should be treated as refs rather than resolved */
10
+ const CUSTOM_PRIMITIVES = ["Asset", "AssetWrapper", "Binding", "Expression"];
11
+
12
+ vi.setConfig({
13
+ testTimeout: 2 * 60 * 1000,
14
+ });
15
+
16
+ /**
17
+ * Converts TypeScript source code to XLR types
18
+ */
19
+ function convertTsToXLR(
20
+ sourceCode: string,
21
+ customPrimitives = CUSTOM_PRIMITIVES,
22
+ ) {
23
+ const { sf, tc } = setupTestEnv(sourceCode);
24
+ const converter = new TsConverter(tc, customPrimitives);
25
+ return converter.convertSourceFile(sf).data.types;
26
+ }
27
+
28
+ describe("FunctionalBuilderGenerator", () => {
29
+ describe("Basic Types", () => {
30
+ test("generates builder for simple asset with string property", () => {
31
+ const source = `
32
+ interface Asset<T extends string> {
33
+ id: string;
34
+ type: T;
35
+ }
36
+
37
+ export interface TextAsset extends Asset<"text"> {
38
+ value: string;
39
+ }
40
+ `;
41
+
42
+ const types = convertTsToXLR(source);
43
+ const textAsset = types.find(
44
+ (t) => t.name === "TextAsset",
45
+ ) as NamedType<ObjectType>;
46
+ expect(textAsset).toBeDefined();
47
+
48
+ const code = generateFunctionalBuilder(textAsset);
49
+ expect(code).toMatchSnapshot();
50
+ });
51
+
52
+ test("generates builder for asset with optional properties", () => {
53
+ const source = `
54
+ interface Asset<T extends string> {
55
+ id: string;
56
+ type: T;
57
+ }
58
+
59
+ export interface InputAsset extends Asset<"input"> {
60
+ binding: string;
61
+ label?: string;
62
+ placeholder?: string;
63
+ }
64
+ `;
65
+
66
+ const types = convertTsToXLR(source);
67
+ const inputAsset = types.find(
68
+ (t) => t.name === "InputAsset",
69
+ ) as NamedType<ObjectType>;
70
+ expect(inputAsset).toBeDefined();
71
+
72
+ const code = generateFunctionalBuilder(inputAsset);
73
+ expect(code).toMatchSnapshot();
74
+ });
75
+
76
+ test("generates builder for asset with number and boolean properties", () => {
77
+ const source = `
78
+ interface Asset<T extends string> {
79
+ id: string;
80
+ type: T;
81
+ }
82
+
83
+ export interface CounterAsset extends Asset<"counter"> {
84
+ value: number;
85
+ min?: number;
86
+ max?: number;
87
+ enabled?: boolean;
88
+ }
89
+ `;
90
+
91
+ const types = convertTsToXLR(source);
92
+ const counterAsset = types.find(
93
+ (t) => t.name === "CounterAsset",
94
+ ) as NamedType<ObjectType>;
95
+ expect(counterAsset).toBeDefined();
96
+
97
+ const code = generateFunctionalBuilder(counterAsset);
98
+ expect(code).toMatchSnapshot();
99
+ });
100
+ });
101
+
102
+ describe("Asset Wrapper (Slot) Types", () => {
103
+ test("generates builder for asset with AssetWrapper slots", () => {
104
+ const source = `
105
+ interface Asset<T extends string = string> {
106
+ id: string;
107
+ type: T;
108
+ }
109
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
110
+
111
+ export interface InfoAsset extends Asset<"info"> {
112
+ title?: AssetWrapper<Asset>;
113
+ subtitle?: AssetWrapper<Asset>;
114
+ primaryInfo: Array<AssetWrapper<Asset>>;
115
+ }
116
+ `;
117
+
118
+ const types = convertTsToXLR(source);
119
+ const infoAsset = types.find(
120
+ (t) => t.name === "InfoAsset",
121
+ ) as NamedType<ObjectType>;
122
+ expect(infoAsset).toBeDefined();
123
+
124
+ const code = generateFunctionalBuilder(infoAsset);
125
+ expect(code).toMatchSnapshot();
126
+ });
127
+
128
+ test("generates builder for asset with array of AssetWrapper slots", () => {
129
+ const source = `
130
+ interface Asset<T extends string = string> {
131
+ id: string;
132
+ type: T;
133
+ }
134
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
135
+
136
+ export interface CollectionAsset extends Asset<"collection"> {
137
+ label?: AssetWrapper<Asset>;
138
+ values?: Array<AssetWrapper<Asset>>;
139
+ }
140
+ `;
141
+
142
+ const types = convertTsToXLR(source);
143
+ const collectionAsset = types.find(
144
+ (t) => t.name === "CollectionAsset",
145
+ ) as NamedType<ObjectType>;
146
+ expect(collectionAsset).toBeDefined();
147
+
148
+ const code = generateFunctionalBuilder(collectionAsset);
149
+ expect(code).toMatchSnapshot();
150
+ });
151
+ });
152
+
153
+ describe("Binding and Expression Types", () => {
154
+ test("generates builder for asset with Binding property", () => {
155
+ const source = `
156
+ interface Asset<T extends string = string> {
157
+ id: string;
158
+ type: T;
159
+ }
160
+ type Binding = string;
161
+
162
+ export interface InputAsset extends Asset<"input"> {
163
+ binding: Binding;
164
+ label?: string;
165
+ }
166
+ `;
167
+
168
+ const types = convertTsToXLR(source);
169
+ const inputAsset = types.find(
170
+ (t) => t.name === "InputAsset",
171
+ ) as NamedType<ObjectType>;
172
+ expect(inputAsset).toBeDefined();
173
+
174
+ const code = generateFunctionalBuilder(inputAsset);
175
+ expect(code).toMatchSnapshot();
176
+ });
177
+
178
+ test("generates builder for asset with Expression property", () => {
179
+ const source = `
180
+ interface Asset<T extends string = string> {
181
+ id: string;
182
+ type: T;
183
+ }
184
+ type Expression = string;
185
+
186
+ export interface ActionAsset extends Asset<"action"> {
187
+ value?: string;
188
+ exp?: Expression;
189
+ }
190
+ `;
191
+
192
+ const types = convertTsToXLR(source);
193
+ const actionAsset = types.find(
194
+ (t) => t.name === "ActionAsset",
195
+ ) as NamedType<ObjectType>;
196
+ expect(actionAsset).toBeDefined();
197
+
198
+ const code = generateFunctionalBuilder(actionAsset);
199
+ expect(code).toMatchSnapshot();
200
+ });
201
+ });
202
+
203
+ describe("Nested Object Types", () => {
204
+ test("generates builder for asset with nested object property", () => {
205
+ const source = `
206
+ interface Asset<T extends string = string> {
207
+ id: string;
208
+ type: T;
209
+ }
210
+
211
+ export interface ActionAsset extends Asset<"action"> {
212
+ value?: string;
213
+ confirmation?: {
214
+ message: string;
215
+ affirmativeLabel: string;
216
+ negativeLabel?: string;
217
+ };
218
+ }
219
+ `;
220
+
221
+ const types = convertTsToXLR(source);
222
+ const actionAsset = types.find(
223
+ (t) => t.name === "ActionAsset",
224
+ ) as NamedType<ObjectType>;
225
+ expect(actionAsset).toBeDefined();
226
+
227
+ const code = generateFunctionalBuilder(actionAsset);
228
+ expect(code).toMatchSnapshot();
229
+ });
230
+
231
+ test("generates builder for asset with named nested type", () => {
232
+ const source = `
233
+ interface Asset<T extends string = string> {
234
+ id: string;
235
+ type: T;
236
+ }
237
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
238
+
239
+ export interface ActionMetaData {
240
+ beacon?: string | Record<string, unknown>;
241
+ role?: "primary" | "secondary" | "link";
242
+ skipValidation?: boolean;
243
+ }
244
+
245
+ export interface ActionAsset extends Asset<"action"> {
246
+ value?: string;
247
+ label?: AssetWrapper<Asset>;
248
+ metaData?: ActionMetaData;
249
+ }
250
+ `;
251
+
252
+ const types = convertTsToXLR(source);
253
+ const actionAsset = types.find(
254
+ (t) => t.name === "ActionAsset",
255
+ ) as NamedType<ObjectType>;
256
+ expect(actionAsset).toBeDefined();
257
+
258
+ const code = generateFunctionalBuilder(actionAsset);
259
+ expect(code).toMatchSnapshot();
260
+ });
261
+ });
262
+
263
+ describe("Union Types", () => {
264
+ test("generates builder for asset with union property", () => {
265
+ const source = `
266
+ interface Asset<T extends string = string> {
267
+ id: string;
268
+ type: T;
269
+ }
270
+
271
+ export interface ActionAsset extends Asset<"action"> {
272
+ size?: "small" | "medium" | "large";
273
+ }
274
+ `;
275
+
276
+ const types = convertTsToXLR(source);
277
+ const actionAsset = types.find(
278
+ (t) => t.name === "ActionAsset",
279
+ ) as NamedType<ObjectType>;
280
+ expect(actionAsset).toBeDefined();
281
+
282
+ const code = generateFunctionalBuilder(actionAsset);
283
+ expect(code).toMatchSnapshot();
284
+ });
285
+
286
+ test("generates builder for asset with discriminated union modifiers", () => {
287
+ const source = `
288
+ interface Asset<T extends string = string> {
289
+ id: string;
290
+ type: T;
291
+ }
292
+
293
+ interface CalloutModifier {
294
+ type: "callout";
295
+ value: "support" | "legal";
296
+ }
297
+
298
+ interface TagModifier {
299
+ type: "tag";
300
+ value: "block";
301
+ }
302
+
303
+ export interface CollectionAsset extends Asset<"collection"> {
304
+ modifiers?: Array<CalloutModifier | TagModifier>;
305
+ }
306
+ `;
307
+
308
+ const types = convertTsToXLR(source);
309
+ const collectionAsset = types.find(
310
+ (t) => t.name === "CollectionAsset",
311
+ ) as NamedType<ObjectType>;
312
+ expect(collectionAsset).toBeDefined();
313
+
314
+ const code = generateFunctionalBuilder(collectionAsset);
315
+ expect(code).toMatchSnapshot();
316
+ });
317
+ });
318
+
319
+ describe("Array Types", () => {
320
+ test("generates builder for asset with array of primitives", () => {
321
+ const source = `
322
+ interface Asset<T extends string = string> {
323
+ id: string;
324
+ type: T;
325
+ }
326
+ type Binding = string;
327
+
328
+ export interface ActionAsset extends Asset<"action"> {
329
+ validate?: Array<Binding> | Binding;
330
+ }
331
+ `;
332
+
333
+ const types = convertTsToXLR(source);
334
+ const actionAsset = types.find(
335
+ (t) => t.name === "ActionAsset",
336
+ ) as NamedType<ObjectType>;
337
+ expect(actionAsset).toBeDefined();
338
+
339
+ const code = generateFunctionalBuilder(actionAsset);
340
+ expect(code).toMatchSnapshot();
341
+ });
342
+
343
+ test("generates builder for asset with array of complex objects", () => {
344
+ const source = `
345
+ interface Asset<T extends string = string> {
346
+ id: string;
347
+ type: T;
348
+ }
349
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
350
+
351
+ export interface ChoiceItem {
352
+ id: string;
353
+ label?: AssetWrapper<Asset>;
354
+ value?: string | number | boolean | null;
355
+ }
356
+
357
+ export interface ChoiceAsset extends Asset<"choice"> {
358
+ binding: string;
359
+ choices?: Array<ChoiceItem>;
360
+ }
361
+ `;
362
+
363
+ const types = convertTsToXLR(source);
364
+ const choiceAsset = types.find(
365
+ (t) => t.name === "ChoiceAsset",
366
+ ) as NamedType<ObjectType>;
367
+ expect(choiceAsset).toBeDefined();
368
+
369
+ const code = generateFunctionalBuilder(choiceAsset);
370
+ expect(code).toMatchSnapshot();
371
+ });
372
+
373
+ test("generates builder for non-Asset type with complex properties", () => {
374
+ const source = `
375
+ interface Asset<T extends string = string> {
376
+ id: string;
377
+ type: T;
378
+ }
379
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
380
+
381
+ export interface ChoiceItem {
382
+ id: string;
383
+ label?: AssetWrapper<Asset>;
384
+ value?: string | number | boolean | null;
385
+ }
386
+ `;
387
+
388
+ const types = convertTsToXLR(source);
389
+ const choiceItem = types.find(
390
+ (t) => t.name === "ChoiceItem",
391
+ ) as NamedType<ObjectType>;
392
+ expect(choiceItem).toBeDefined();
393
+
394
+ const code = generateFunctionalBuilder(choiceItem);
395
+ expect(code).toMatchSnapshot();
396
+ });
397
+ });
398
+
399
+ describe("Generic Types", () => {
400
+ test("generates builder for asset with generic parameter", () => {
401
+ const source = `
402
+ interface Asset<T extends string = string> {
403
+ id: string;
404
+ type: T;
405
+ }
406
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
407
+
408
+ export interface InputAsset<AnyTextAsset extends Asset = Asset> extends Asset<"input"> {
409
+ binding: string;
410
+ label?: AssetWrapper<AnyTextAsset>;
411
+ note?: AssetWrapper<AnyTextAsset>;
412
+ }
413
+ `;
414
+
415
+ const types = convertTsToXLR(source);
416
+ const inputAsset = types.find(
417
+ (t) => t.name === "InputAsset",
418
+ ) as NamedType<ObjectType>;
419
+ expect(inputAsset).toBeDefined();
420
+
421
+ const code = generateFunctionalBuilder(inputAsset);
422
+ expect(code).toMatchSnapshot();
423
+ });
424
+ });
425
+
426
+ test("string properties accept TaggedTemplateValue for binding support", () => {
427
+ const source = `
428
+ interface Asset<T extends string = string> {
429
+ id: string;
430
+ type: T;
431
+ }
432
+
433
+ export interface TextAsset extends Asset<"text"> {
434
+ value: string;
435
+ }
436
+ `;
437
+
438
+ const types = convertTsToXLR(source);
439
+ const textAsset = types.find(
440
+ (t) => t.name === "TextAsset",
441
+ ) as NamedType<ObjectType>;
442
+ const code = generateFunctionalBuilder(textAsset);
443
+
444
+ // New generator adds TaggedTemplateValue support - OLD generator did NOT
445
+ expect(code).toContain(
446
+ "withValue(value: string | TaggedTemplateValue<string>)",
447
+ );
448
+ // Verify we don't just have plain string
449
+ expect(code).not.toMatch(/withValue\(value: string\):/);
450
+ });
451
+
452
+ test("AssetWrapper slots accept Asset | FunctionalBuilder instead of AssetWrapper", () => {
453
+ const source = `
454
+ interface Asset<T extends string = string> {
455
+ id: string;
456
+ type: T;
457
+ }
458
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
459
+
460
+ export interface ActionAsset extends Asset<"action"> {
461
+ label?: AssetWrapper<Asset>;
462
+ }
463
+ `;
464
+
465
+ const types = convertTsToXLR(source);
466
+ const actionAsset = types.find(
467
+ (t) => t.name === "ActionAsset",
468
+ ) as NamedType<ObjectType>;
469
+ const code = generateFunctionalBuilder(actionAsset);
470
+
471
+ // New generator uses Asset | FunctionalBuilder<Asset> - OLD generator incorrectly used AssetWrapper
472
+ expect(code).toContain(
473
+ "Asset | FunctionalBuilder<Asset, BaseBuildContext>",
474
+ );
475
+ // Verify we don't use AssetWrapper (which was the bug)
476
+ expect(code).not.toContain("FunctionalBuilder<AssetWrapper");
477
+ });
478
+
479
+ test("Binding properties accept TaggedTemplateValue", () => {
480
+ const source = `
481
+ interface Asset<T extends string = string> {
482
+ id: string;
483
+ type: T;
484
+ }
485
+ type Binding = string;
486
+
487
+ export interface InputAsset extends Asset<"input"> {
488
+ binding: Binding;
489
+ }
490
+ `;
491
+
492
+ const types = convertTsToXLR(source);
493
+ const inputAsset = types.find(
494
+ (t) => t.name === "InputAsset",
495
+ ) as NamedType<ObjectType>;
496
+ const code = generateFunctionalBuilder(inputAsset);
497
+
498
+ // Bindings should accept TaggedTemplateValue
499
+ expect(code).toContain(
500
+ "withBinding(value: string | TaggedTemplateValue<string>)",
501
+ );
502
+ });
503
+
504
+ test("Expression properties accept TaggedTemplateValue", () => {
505
+ const source = `
506
+ interface Asset<T extends string = string> {
507
+ id: string;
508
+ type: T;
509
+ }
510
+ type Expression = string;
511
+
512
+ export interface ActionAsset extends Asset<"action"> {
513
+ exp?: Expression;
514
+ }
515
+ `;
516
+
517
+ const types = convertTsToXLR(source);
518
+ const actionAsset = types.find(
519
+ (t) => t.name === "ActionAsset",
520
+ ) as NamedType<ObjectType>;
521
+ const code = generateFunctionalBuilder(actionAsset);
522
+
523
+ // Expressions should accept TaggedTemplateValue
524
+ expect(code).toContain(
525
+ "withExp(value: string | TaggedTemplateValue<string>)",
526
+ );
527
+ });
528
+ });
529
+
530
+ describe("Integration with Complex Type Patterns", () => {
531
+ test("generates builder for TextAsset", () => {
532
+ const source = `
533
+ interface Asset<T extends string = string> {
534
+ id: string;
535
+ type: T;
536
+ }
537
+
538
+ export interface TextAsset extends Asset<"text"> {
539
+ value: string;
540
+ }
541
+ `;
542
+
543
+ const types = convertTsToXLR(source);
544
+ const textAsset = types.find(
545
+ (t) => t.name === "TextAsset",
546
+ ) as NamedType<ObjectType>;
547
+
548
+ const code = generateFunctionalBuilder(textAsset);
549
+
550
+ // Verify it has the expected parts
551
+ expect(code).toContain("TextAssetBuilder");
552
+ expect(code).toContain("withValue");
553
+ expect(code).toContain("string | TaggedTemplateValue<string>");
554
+ // Smart defaults now include required primitive fields
555
+ expect(code).toContain(
556
+ 'defaults: Record<string, unknown> = {"type":"text","id":"","value":""}',
557
+ );
558
+ });
559
+
560
+ test("generates builder for InfoAsset", () => {
561
+ const source = `
562
+ interface Asset<T extends string = string> {
563
+ id: string;
564
+ type: T;
565
+ }
566
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
567
+
568
+ export interface InfoAsset extends Asset<"info"> {
569
+ primaryInfo: Array<AssetWrapper<Asset>>;
570
+ title?: AssetWrapper<Asset>;
571
+ subtitle?: AssetWrapper<Asset>;
572
+ }
573
+ `;
574
+
575
+ const types = convertTsToXLR(source);
576
+ const infoAsset = types.find(
577
+ (t) => t.name === "InfoAsset",
578
+ ) as NamedType<ObjectType>;
579
+
580
+ const code = generateFunctionalBuilder(infoAsset);
581
+
582
+ // Verify it has the expected parts
583
+ expect(code).toContain("InfoAssetBuilder");
584
+ expect(code).toContain("withPrimaryInfo");
585
+ expect(code).toContain("withTitle");
586
+ expect(code).toContain("withSubtitle");
587
+ expect(code).toContain(
588
+ "Asset | FunctionalBuilder<Asset, BaseBuildContext>",
589
+ );
590
+ expect(code).toContain("__arrayProperties__");
591
+ expect(code).toContain('"primaryInfo"');
592
+ });
593
+
594
+ test("generates builder for InputAsset", () => {
595
+ const source = `
596
+ interface Asset<T extends string = string> {
597
+ id: string;
598
+ type: T;
599
+ }
600
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
601
+
602
+ export interface InputAsset extends Asset<"input"> {
603
+ binding: string;
604
+ label: AssetWrapper<Asset>;
605
+ placeholder?: string;
606
+ }
607
+ `;
608
+
609
+ const types = convertTsToXLR(source);
610
+ const inputAsset = types.find(
611
+ (t) => t.name === "InputAsset",
612
+ ) as NamedType<ObjectType>;
613
+
614
+ const code = generateFunctionalBuilder(inputAsset);
615
+
616
+ // Verify it has the expected parts
617
+ expect(code).toContain("InputAssetBuilder");
618
+ expect(code).toContain("withBinding");
619
+ expect(code).toContain("withLabel");
620
+ expect(code).toContain("withPlaceholder");
621
+ expect(code).toContain(
622
+ "Asset | FunctionalBuilder<Asset, BaseBuildContext>",
623
+ );
624
+ });
625
+ });
626
+
627
+ describe("__arrayProperties__ generation", () => {
628
+ test("generates __arrayProperties__ for types with array properties", () => {
629
+ const source = `
630
+ interface Asset<T extends string = string> {
631
+ id: string;
632
+ type: T;
633
+ }
634
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
635
+
636
+ export interface CollectionAsset extends Asset<"collection"> {
637
+ values: Array<AssetWrapper<Asset>>;
638
+ actions?: Array<AssetWrapper<Asset>>;
639
+ label?: AssetWrapper<Asset>;
640
+ modifiers?: Array<{ type: string }>;
641
+ }
642
+ `;
643
+
644
+ const types = convertTsToXLR(source);
645
+ const collectionAsset = types.find(
646
+ (t) => t.name === "CollectionAsset",
647
+ ) as NamedType<ObjectType>;
648
+
649
+ const code = generateFunctionalBuilder(collectionAsset);
650
+
651
+ // Must have __arrayProperties__ static property
652
+ expect(code).toContain("__arrayProperties__");
653
+ expect(code).toContain("ReadonlySet<string>");
654
+
655
+ // Must include the array properties
656
+ expect(code).toContain('"values"');
657
+ expect(code).toContain('"actions"');
658
+ expect(code).toContain('"modifiers"');
659
+
660
+ // Non-array properties should NOT be in __arrayProperties__
661
+ expect(code).not.toMatch(/__arrayProperties__.*"label"/);
662
+ });
663
+
664
+ test("does not generate __arrayProperties__ when no array properties exist", () => {
665
+ const source = `
666
+ interface Asset<T extends string = string> {
667
+ id: string;
668
+ type: T;
669
+ }
670
+
671
+ export interface TextAsset extends Asset<"text"> {
672
+ value: string;
673
+ optional?: string;
674
+ }
675
+ `;
676
+
677
+ const types = convertTsToXLR(source);
678
+ const textAsset = types.find(
679
+ (t) => t.name === "TextAsset",
680
+ ) as NamedType<ObjectType>;
681
+
682
+ const code = generateFunctionalBuilder(textAsset);
683
+
684
+ // Should NOT have __arrayProperties__ since there are no arrays
685
+ expect(code).not.toContain("__arrayProperties__");
686
+ });
687
+
688
+ test("array detection works with union types containing arrays", () => {
689
+ const source = `
690
+ interface Asset<T extends string = string> {
691
+ id: string;
692
+ type: T;
693
+ }
694
+ type Binding = string;
695
+
696
+ export interface ActionAsset extends Asset<"action"> {
697
+ value?: string;
698
+ validate?: Array<Binding> | Binding;
699
+ }
700
+ `;
701
+
702
+ const types = convertTsToXLR(source);
703
+ const actionAsset = types.find(
704
+ (t) => t.name === "ActionAsset",
705
+ ) as NamedType<ObjectType>;
706
+
707
+ const code = generateFunctionalBuilder(actionAsset);
708
+
709
+ // validate is Array | string, the Array variant should still be detected
710
+ expect(code).toContain("__arrayProperties__");
711
+ expect(code).toContain('"validate"');
712
+ });
713
+ });
714
+
715
+ describe("Generator configuration", () => {
716
+ test("supports custom functional import path", () => {
717
+ const source = `
718
+ interface Asset<T extends string = string> {
719
+ id: string;
720
+ type: T;
721
+ }
722
+
723
+ export interface TextAsset extends Asset<"text"> {
724
+ value: string;
725
+ }
726
+ `;
727
+
728
+ const types = convertTsToXLR(source);
729
+ const textAsset = types.find(
730
+ (t) => t.name === "TextAsset",
731
+ ) as NamedType<ObjectType>;
732
+
733
+ const config: GeneratorConfig = {
734
+ functionalImportPath: "../../../gen/common.js",
735
+ };
736
+
737
+ const code = generateFunctionalBuilder(textAsset, config);
738
+
739
+ expect(code).toContain('from "../../../gen/common.js"');
740
+ expect(code).not.toContain('from "@player-lang/functional-dsl"');
741
+ });
742
+
743
+ test("supports custom types import path", () => {
744
+ const source = `
745
+ interface Asset<T extends string = string> {
746
+ id: string;
747
+ type: T;
748
+ }
749
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
750
+
751
+ export interface ActionAsset extends Asset<"action"> {
752
+ label?: AssetWrapper<Asset>;
753
+ }
754
+ `;
755
+
756
+ const types = convertTsToXLR(source);
757
+ const actionAsset = types.find(
758
+ (t) => t.name === "ActionAsset",
759
+ ) as NamedType<ObjectType>;
760
+
761
+ const config: GeneratorConfig = {
762
+ typesImportPath: "../custom-types.js",
763
+ };
764
+
765
+ const code = generateFunctionalBuilder(actionAsset, config);
766
+
767
+ expect(code).toContain('from "../custom-types.js"');
768
+ expect(code).not.toContain('from "@player-ui/types"');
769
+ });
770
+
771
+ test("supports custom type import path generator", () => {
772
+ const source = `
773
+ interface Asset<T extends string = string> {
774
+ id: string;
775
+ type: T;
776
+ }
777
+
778
+ export interface TextAsset extends Asset<"text"> {
779
+ value: string;
780
+ }
781
+ `;
782
+
783
+ const types = convertTsToXLR(source);
784
+ const textAsset = types.find(
785
+ (t) => t.name === "TextAsset",
786
+ ) as NamedType<ObjectType>;
787
+
788
+ const config: GeneratorConfig = {
789
+ typeImportPathGenerator: (typeName) =>
790
+ `../types/${typeName.toLowerCase()}.js`,
791
+ };
792
+
793
+ const code = generateFunctionalBuilder(textAsset, config);
794
+
795
+ expect(code).toContain('from "../types/textasset.js"');
796
+ });
797
+ });
798
+
799
+ describe("End-to-end: TypeScript → XLR → Builder → Built Object", () => {
800
+ /**
801
+ * This e2e test validates the entire pipeline:
802
+ * 1. TypeScript interface definition
803
+ * 2. Conversion to XLR via TsConverter
804
+ * 3. Builder generation via FunctionalBuilderGenerator
805
+ * 4. Builder execution to produce an object
806
+ * 5. Verification that the object matches the original interface structure
807
+ */
808
+ test("generates working builder for simple asset", () => {
809
+ // Step 1: Define TypeScript interface
810
+ const source = `
811
+ interface Asset<T extends string = string> {
812
+ id: string;
813
+ type: T;
814
+ }
815
+
816
+ export interface TextAsset extends Asset<"text"> {
817
+ value: string;
818
+ }
819
+ `;
820
+
821
+ // Step 2: Convert to XLR
822
+ const types = convertTsToXLR(source);
823
+ const textAsset = types.find(
824
+ (t) => t.name === "TextAsset",
825
+ ) as NamedType<ObjectType>;
826
+ expect(textAsset).toBeDefined();
827
+
828
+ // Step 3: Generate builder code
829
+ const code = generateFunctionalBuilder(textAsset);
830
+
831
+ // Step 4: Verify the generated code structure
832
+ // The generated builder should have defaults for id, type, and required primitive fields
833
+ expect(code).toContain(
834
+ 'defaults: Record<string, unknown> = {"type":"text","id":"","value":""}',
835
+ );
836
+ // Should have withValue method that accepts TaggedTemplateValue
837
+ expect(code).toContain(
838
+ "withValue(value: string | TaggedTemplateValue<string>)",
839
+ );
840
+ // Should have proper build method
841
+ expect(code).toContain("build(context?: BaseBuildContext): TextAsset");
842
+
843
+ // Step 5: Verify the built object structure (by parsing the generated code)
844
+ // The defaults object shows what fields the builder produces
845
+ const defaultsMatch = code.match(
846
+ /defaults: Record<string, unknown> = ({[^}]+})/,
847
+ );
848
+ expect(defaultsMatch).toBeTruthy();
849
+ const defaults = JSON.parse(defaultsMatch![1]);
850
+ // Smart defaults now include required primitive fields
851
+ expect(defaults).toEqual({ type: "text", id: "", value: "" });
852
+ });
853
+
854
+ test("generates working builder for asset with slots", () => {
855
+ // Step 1: Define TypeScript interface with AssetWrapper slots
856
+ const source = `
857
+ interface Asset<T extends string = string> {
858
+ id: string;
859
+ type: T;
860
+ }
861
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
862
+
863
+ export interface ActionAsset extends Asset<"action"> {
864
+ value?: string;
865
+ label?: AssetWrapper<Asset>;
866
+ }
867
+ `;
868
+
869
+ // Step 2: Convert to XLR
870
+ const types = convertTsToXLR(source);
871
+ const actionAsset = types.find(
872
+ (t) => t.name === "ActionAsset",
873
+ ) as NamedType<ObjectType>;
874
+ expect(actionAsset).toBeDefined();
875
+
876
+ // Step 3: Generate builder code
877
+ const code = generateFunctionalBuilder(actionAsset);
878
+
879
+ // Step 4: Verify slot handling
880
+ // The label slot should accept Asset | FunctionalBuilder, not AssetWrapper
881
+ expect(code).toContain(
882
+ "withLabel(value: Asset | FunctionalBuilder<Asset, BaseBuildContext>)",
883
+ );
884
+ // Should NOT have AssetWrapper in the parameter type
885
+ expect(code).not.toContain("FunctionalBuilder<AssetWrapper");
886
+ });
887
+
888
+ test("generates working builder for asset with array properties", () => {
889
+ // Step 1: Define TypeScript interface with array properties
890
+ const source = `
891
+ interface Asset<T extends string = string> {
892
+ id: string;
893
+ type: T;
894
+ }
895
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
896
+
897
+ export interface CollectionAsset extends Asset<"collection"> {
898
+ values?: Array<AssetWrapper<Asset>>;
899
+ actions?: Array<AssetWrapper<Asset>>;
900
+ label?: AssetWrapper<Asset>;
901
+ }
902
+ `;
903
+
904
+ // Step 2: Convert to XLR
905
+ const types = convertTsToXLR(source);
906
+ const collectionAsset = types.find(
907
+ (t) => t.name === "CollectionAsset",
908
+ ) as NamedType<ObjectType>;
909
+ expect(collectionAsset).toBeDefined();
910
+
911
+ // Step 3: Generate builder code
912
+ const code = generateFunctionalBuilder(collectionAsset);
913
+
914
+ // Step 4: Verify __arrayProperties__ is generated correctly
915
+ expect(code).toContain("__arrayProperties__");
916
+ expect(code).toContain('"values"');
917
+ expect(code).toContain('"actions"');
918
+
919
+ // Step 5: Verify array properties are correctly identified
920
+ // The __arrayProperties__ line should include values and actions, but not label
921
+ const arrayPropsMatch = code.match(
922
+ /__arrayProperties__.*new Set\(\[([^\]]+)\]\)/,
923
+ );
924
+ expect(arrayPropsMatch).toBeTruthy();
925
+ const arrayPropsContent = arrayPropsMatch![1];
926
+ expect(arrayPropsContent).toContain('"values"');
927
+ expect(arrayPropsContent).toContain('"actions"');
928
+ expect(arrayPropsContent).not.toContain('"label"');
929
+ });
930
+
931
+ test("generates working builder for asset with nested objects", () => {
932
+ // Step 1: Define TypeScript interface with nested object
933
+ const source = `
934
+ interface Asset<T extends string = string> {
935
+ id: string;
936
+ type: T;
937
+ }
938
+
939
+ export interface ActionAsset extends Asset<"action"> {
940
+ value?: string;
941
+ confirmation?: {
942
+ message: string;
943
+ affirmativeLabel: string;
944
+ negativeLabel?: string;
945
+ };
946
+ }
947
+ `;
948
+
949
+ // Step 2: Convert to XLR
950
+ const types = convertTsToXLR(source);
951
+ const actionAsset = types.find(
952
+ (t) => t.name === "ActionAsset",
953
+ ) as NamedType<ObjectType>;
954
+ expect(actionAsset).toBeDefined();
955
+
956
+ // Step 3: Generate builder code
957
+ const code = generateFunctionalBuilder(actionAsset);
958
+
959
+ // Step 4: Verify nested object handling
960
+ // The confirmation property should have inline type with TaggedTemplateValue support
961
+ expect(code).toContain("withConfirmation");
962
+ expect(code).toContain("message: string | TaggedTemplateValue<string>");
963
+ expect(code).toContain(
964
+ "affirmativeLabel: string | TaggedTemplateValue<string>",
965
+ );
966
+ expect(code).toContain(
967
+ "negativeLabel?: string | TaggedTemplateValue<string>",
968
+ );
969
+
970
+ // Step 5: Verify complex nested objects accept both raw objects AND FunctionalBuilder
971
+ // Complex nested objects (3+ properties) should accept FunctionalBuilder alternative
972
+ expect(code).toContain("FunctionalBuilder<{");
973
+ expect(code).toContain("}, BaseBuildContext>");
974
+ });
975
+
976
+ test("generates working builder for generic asset", () => {
977
+ // Step 1: Define TypeScript interface with generic parameter
978
+ const source = `
979
+ interface Asset<T extends string = string> {
980
+ id: string;
981
+ type: T;
982
+ }
983
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
984
+
985
+ export interface InputAsset<AnyTextAsset extends Asset = Asset> extends Asset<"input"> {
986
+ binding: string;
987
+ label?: AssetWrapper<AnyTextAsset>;
988
+ }
989
+ `;
990
+
991
+ // Step 2: Convert to XLR
992
+ const types = convertTsToXLR(source);
993
+ const inputAsset = types.find(
994
+ (t) => t.name === "InputAsset",
995
+ ) as NamedType<ObjectType>;
996
+ expect(inputAsset).toBeDefined();
997
+
998
+ // Step 3: Generate builder code
999
+ const code = generateFunctionalBuilder(inputAsset);
1000
+
1001
+ // Step 4: Verify generic parameter handling
1002
+ expect(code).toContain("InputAssetBuilder<AnyTextAsset extends Asset");
1003
+ expect(code).toContain("export function input<AnyTextAsset extends Asset");
1004
+ });
1005
+ });
1006
+
1007
+ describe("Edge Cases", () => {
1008
+ test("handles special types: null, undefined, any, unknown, never, void", () => {
1009
+ const source = `
1010
+ interface Asset<T extends string = string> {
1011
+ id: string;
1012
+ type: T;
1013
+ }
1014
+
1015
+ export interface SpecialTypesAsset extends Asset<"special"> {
1016
+ nullValue: null;
1017
+ undefinedValue: undefined;
1018
+ anyValue: any;
1019
+ unknownValue: unknown;
1020
+ neverValue: never;
1021
+ voidValue: void;
1022
+ }
1023
+ `;
1024
+
1025
+ const types = convertTsToXLR(source);
1026
+ const specialAsset = types.find(
1027
+ (t) => t.name === "SpecialTypesAsset",
1028
+ ) as NamedType<ObjectType>;
1029
+ expect(specialAsset).toBeDefined();
1030
+
1031
+ const code = generateFunctionalBuilder(specialAsset);
1032
+
1033
+ expect(code).toContain("withNullValue(value: null)");
1034
+ expect(code).toContain("withUndefinedValue(value: undefined)");
1035
+ expect(code).toContain("withAnyValue(value: any)");
1036
+ expect(code).toContain("withUnknownValue(value: unknown)");
1037
+ expect(code).toContain("withNeverValue(value: never)");
1038
+ expect(code).toContain("withVoidValue(value: void)");
1039
+ });
1040
+
1041
+ test("handles intersection types (AndType)", () => {
1042
+ const source = `
1043
+ interface Asset<T extends string = string> {
1044
+ id: string;
1045
+ type: T;
1046
+ }
1047
+
1048
+ interface BaseProps {
1049
+ name: string;
1050
+ }
1051
+
1052
+ interface ExtendedProps {
1053
+ description: string;
1054
+ }
1055
+
1056
+ export interface IntersectionAsset extends Asset<"intersection"> {
1057
+ combined: BaseProps & ExtendedProps;
1058
+ }
1059
+ `;
1060
+
1061
+ const types = convertTsToXLR(source);
1062
+ const asset = types.find(
1063
+ (t) => t.name === "IntersectionAsset",
1064
+ ) as NamedType<ObjectType>;
1065
+ expect(asset).toBeDefined();
1066
+
1067
+ const code = generateFunctionalBuilder(asset);
1068
+
1069
+ // Should handle intersection type
1070
+ expect(code).toContain("withCombined");
1071
+ });
1072
+
1073
+ test("handles union types containing arrays for __arrayProperties__", () => {
1074
+ const source = `
1075
+ interface Asset<T extends string = string> {
1076
+ id: string;
1077
+ type: T;
1078
+ }
1079
+ type Binding = string;
1080
+
1081
+ export interface ActionAsset extends Asset<"action"> {
1082
+ value?: string;
1083
+ /** Can be single binding or array of bindings */
1084
+ validate?: Array<Binding> | Binding;
1085
+ }
1086
+ `;
1087
+
1088
+ const types = convertTsToXLR(source);
1089
+ const actionAsset = types.find(
1090
+ (t) => t.name === "ActionAsset",
1091
+ ) as NamedType<ObjectType>;
1092
+ expect(actionAsset).toBeDefined();
1093
+
1094
+ const code = generateFunctionalBuilder(actionAsset);
1095
+
1096
+ // The validate property is Array<Binding> | Binding - should be in __arrayProperties__
1097
+ expect(code).toContain("__arrayProperties__");
1098
+ expect(code).toContain('"validate"');
1099
+ });
1100
+
1101
+ test("handles Record types", () => {
1102
+ const source = `
1103
+ interface Asset<T extends string = string> {
1104
+ id: string;
1105
+ type: T;
1106
+ }
1107
+
1108
+ export interface DataAsset extends Asset<"data"> {
1109
+ metadata: Record<string, unknown>;
1110
+ counts: Record<string, number>;
1111
+ }
1112
+ `;
1113
+
1114
+ const types = convertTsToXLR(source);
1115
+ const dataAsset = types.find(
1116
+ (t) => t.name === "DataAsset",
1117
+ ) as NamedType<ObjectType>;
1118
+ expect(dataAsset).toBeDefined();
1119
+
1120
+ const code = generateFunctionalBuilder(dataAsset);
1121
+
1122
+ expect(code).toContain("withMetadata(value: Record<string, unknown>)");
1123
+ expect(code).toContain("withCounts(value: Record<string, number");
1124
+ });
1125
+
1126
+ test("handles deeply nested union types", () => {
1127
+ const source = `
1128
+ interface Asset<T extends string = string> {
1129
+ id: string;
1130
+ type: T;
1131
+ }
1132
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
1133
+
1134
+ export interface ComplexAsset extends Asset<"complex"> {
1135
+ /** Single asset, array of assets, or nothing */
1136
+ content?: AssetWrapper<Asset> | Array<AssetWrapper<Asset>>;
1137
+ }
1138
+ `;
1139
+
1140
+ const types = convertTsToXLR(source);
1141
+ const complexAsset = types.find(
1142
+ (t) => t.name === "ComplexAsset",
1143
+ ) as NamedType<ObjectType>;
1144
+ expect(complexAsset).toBeDefined();
1145
+
1146
+ const code = generateFunctionalBuilder(complexAsset);
1147
+
1148
+ // Should handle the union and include in __arrayProperties__
1149
+ expect(code).toContain("withContent");
1150
+ expect(code).toContain("__arrayProperties__");
1151
+ expect(code).toContain('"content"');
1152
+ });
1153
+
1154
+ test("handles literal union types", () => {
1155
+ const source = `
1156
+ interface Asset<T extends string = string> {
1157
+ id: string;
1158
+ type: T;
1159
+ }
1160
+
1161
+ export interface SizeAsset extends Asset<"sized"> {
1162
+ size: "small" | "medium" | "large";
1163
+ alignment?: "left" | "center" | "right";
1164
+ }
1165
+ `;
1166
+
1167
+ const types = convertTsToXLR(source);
1168
+ const sizeAsset = types.find(
1169
+ (t) => t.name === "SizeAsset",
1170
+ ) as NamedType<ObjectType>;
1171
+ expect(sizeAsset).toBeDefined();
1172
+
1173
+ const code = generateFunctionalBuilder(sizeAsset);
1174
+
1175
+ // Should preserve literal union types
1176
+ expect(code).toContain('"small"');
1177
+ expect(code).toContain('"medium"');
1178
+ expect(code).toContain('"large"');
1179
+ });
1180
+ });
1181
+
1182
+ describe("Nested Objects Accept Raw or Builder", () => {
1183
+ test("complex nested objects accept raw object OR FunctionalBuilder", () => {
1184
+ const source = `
1185
+ interface Asset<T extends string = string> {
1186
+ id: string;
1187
+ type: T;
1188
+ }
1189
+
1190
+ export interface ActionAsset extends Asset<"action"> {
1191
+ confirmation?: {
1192
+ message: string;
1193
+ affirmativeLabel: string;
1194
+ negativeLabel?: string;
1195
+ extra?: string;
1196
+ };
1197
+ }
1198
+ `;
1199
+
1200
+ const types = convertTsToXLR(source);
1201
+ const actionAsset = types.find(
1202
+ (t) => t.name === "ActionAsset",
1203
+ ) as NamedType<ObjectType>;
1204
+ const code = generateFunctionalBuilder(actionAsset);
1205
+
1206
+ // Should accept both raw inline object AND FunctionalBuilder
1207
+ expect(code).toContain("withConfirmation(value: {");
1208
+ expect(code).toContain("| FunctionalBuilder<{");
1209
+ expect(code).toContain("}, BaseBuildContext>");
1210
+ });
1211
+
1212
+ test("any nested object accepts raw object OR FunctionalBuilder", () => {
1213
+ const source = `
1214
+ interface Asset<T extends string = string> {
1215
+ id: string;
1216
+ type: T;
1217
+ }
1218
+
1219
+ export interface ActionAsset extends Asset<"action"> {
1220
+ simple?: {
1221
+ a: string;
1222
+ b: string;
1223
+ };
1224
+ }
1225
+ `;
1226
+
1227
+ const types = convertTsToXLR(source);
1228
+ const actionAsset = types.find(
1229
+ (t) => t.name === "ActionAsset",
1230
+ ) as NamedType<ObjectType>;
1231
+ const code = generateFunctionalBuilder(actionAsset);
1232
+
1233
+ // Any nested object should accept either raw object or FunctionalBuilder
1234
+ expect(code).toContain("withSimple(value: {");
1235
+ expect(code).toContain("| FunctionalBuilder<{");
1236
+ });
1237
+
1238
+ test("named nested types accept raw object OR FunctionalBuilder", () => {
1239
+ const source = `
1240
+ interface Asset<T extends string = string> {
1241
+ id: string;
1242
+ type: T;
1243
+ }
1244
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
1245
+
1246
+ export interface Metadata {
1247
+ beacon?: string;
1248
+ role?: "primary" | "secondary";
1249
+ skipValidation?: boolean;
1250
+ extra?: string;
1251
+ }
1252
+
1253
+ export interface ActionAsset extends Asset<"action"> {
1254
+ value?: string;
1255
+ label?: AssetWrapper<Asset>;
1256
+ metaData?: Metadata;
1257
+ }
1258
+ `;
1259
+
1260
+ const types = convertTsToXLR(source);
1261
+ const actionAsset = types.find(
1262
+ (t) => t.name === "ActionAsset",
1263
+ ) as NamedType<ObjectType>;
1264
+ const code = generateFunctionalBuilder(actionAsset);
1265
+
1266
+ // Named complex types should accept the type, FunctionalBuilder, or FunctionalPartial
1267
+ expect(code).toContain(
1268
+ "withMetaData(value: Metadata | FunctionalBuilder<Metadata, BaseBuildContext> | FunctionalPartial<Metadata, BaseBuildContext>)",
1269
+ );
1270
+ });
1271
+ });
1272
+
1273
+ describe("Bug Fixes", () => {
1274
+ describe("Issue #1: Malformed Generic Type Parameters", () => {
1275
+ test("handles simple generic parameters", () => {
1276
+ const source = `
1277
+ interface Asset<T extends string = string> {
1278
+ id: string;
1279
+ type: T;
1280
+ }
1281
+
1282
+ export interface SimpleGeneric<T extends string = string, U extends number = number> extends Asset<"test"> {
1283
+ prop1?: T;
1284
+ prop2?: U;
1285
+ }
1286
+ `;
1287
+
1288
+ const types = convertTsToXLR(source);
1289
+ const asset = types.find(
1290
+ (t) => t.name === "SimpleGeneric",
1291
+ ) as NamedType<ObjectType>;
1292
+ const code = generateFunctionalBuilder(asset);
1293
+
1294
+ expect(code).toContain("SimpleGenericBuilder<T extends string");
1295
+ expect(code).toContain("<T, U>");
1296
+ // Should not have malformed syntax
1297
+ expect(code).not.toContain("<T, U,");
1298
+ expect(code).not.toContain("<T, U, BaseBuildContext>");
1299
+ });
1300
+
1301
+ test("handles nested generic parameters with commas", () => {
1302
+ const source = `
1303
+ interface Asset<T extends string = string> {
1304
+ id: string;
1305
+ type: T;
1306
+ }
1307
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
1308
+
1309
+ interface ListItemNoHelp<AnyAsset extends Asset = Asset> extends AssetWrapper<AnyAsset> {}
1310
+ interface ListItem<AnyAsset extends Asset = Asset> extends ListItemNoHelp<AnyAsset> {
1311
+ help?: { id: string; };
1312
+ }
1313
+
1314
+ export interface ListAsset<
1315
+ AnyAsset extends Asset = Asset,
1316
+ ItemType extends ListItemNoHelp<AnyAsset> = ListItem<AnyAsset>
1317
+ > extends Asset<"list"> {
1318
+ values?: Array<ItemType>;
1319
+ }
1320
+ `;
1321
+
1322
+ const types = convertTsToXLR(source);
1323
+ const listAsset = types.find(
1324
+ (t) => t.name === "ListAsset",
1325
+ ) as NamedType<ObjectType>;
1326
+ const code = generateFunctionalBuilder(listAsset);
1327
+
1328
+ // Should produce <AnyAsset, ItemType>, not something malformed
1329
+ expect(code).toContain("ListAssetBuilder<AnyAsset extends Asset");
1330
+ expect(code).toContain("<AnyAsset, ItemType>");
1331
+ // Should not have extra generic parameter in the class type usage
1332
+ // The bug was producing things like <AnyAsset, ItemType, BaseBuildContext> as if
1333
+ // BaseBuildContext was a third type parameter
1334
+ expect(code).not.toMatch(
1335
+ /ListAssetBuilder<AnyAsset,\s*ItemType,\s*BaseBuildContext>/,
1336
+ );
1337
+ // The method return type should just be <AnyAsset, ItemType>, not malformed
1338
+ expect(code).toMatch(/:\s*ListAssetBuilder<AnyAsset,\s*ItemType>/);
1339
+ });
1340
+ });
1341
+
1342
+ describe("Issue #2: Quoted Property Names", () => {
1343
+ test("generates valid method names for quoted properties", () => {
1344
+ const source = `
1345
+ interface Asset<T extends string = string> {
1346
+ id: string;
1347
+ type: T;
1348
+ }
1349
+
1350
+ export interface ImageAsset extends Asset<"image"> {
1351
+ value?: string;
1352
+ "mime-type"?: string;
1353
+ 'single-quoted'?: number;
1354
+ }
1355
+ `;
1356
+
1357
+ const types = convertTsToXLR(source);
1358
+ const imageAsset = types.find(
1359
+ (t) => t.name === "ImageAsset",
1360
+ ) as NamedType<ObjectType>;
1361
+ const code = generateFunctionalBuilder(imageAsset);
1362
+
1363
+ // Should generate valid method names without quotes
1364
+ expect(code).toContain("withMimeType");
1365
+ expect(code).toContain("withSingleQuoted");
1366
+ // Should NOT have quotes in method names
1367
+ expect(code).not.toContain("with'");
1368
+ expect(code).not.toContain('with"');
1369
+ // The set() call should also have clean property names
1370
+ expect(code).toContain('this.set("mime-type"');
1371
+ expect(code).toContain('this.set("single-quoted"');
1372
+ });
1373
+ });
1374
+
1375
+ describe("Issue #3: Missing Type Imports", () => {
1376
+ test("imports types referenced in generic constraints and defaults", () => {
1377
+ const source = `
1378
+ interface Asset<T extends string = string> {
1379
+ id: string;
1380
+ type: T;
1381
+ }
1382
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
1383
+
1384
+ export interface ListItemNoHelp<AnyAsset extends Asset = Asset>
1385
+ extends AssetWrapper<AnyAsset> {}
1386
+
1387
+ export interface ListItem<AnyAsset extends Asset = Asset>
1388
+ extends ListItemNoHelp<AnyAsset> {
1389
+ help?: { id: string; };
1390
+ }
1391
+
1392
+ export interface ListAsset<
1393
+ AnyAsset extends Asset = Asset,
1394
+ ItemType extends ListItemNoHelp<AnyAsset> = ListItem<AnyAsset>
1395
+ > extends Asset<"list"> {
1396
+ values?: Array<ItemType>;
1397
+ }
1398
+ `;
1399
+
1400
+ const types = convertTsToXLR(source);
1401
+ const listAsset = types.find(
1402
+ (t) => t.name === "ListAsset",
1403
+ ) as NamedType<ObjectType>;
1404
+ const code = generateFunctionalBuilder(listAsset);
1405
+
1406
+ // Should import types from generic constraints/defaults
1407
+ // The import statement should include these types
1408
+ expect(code).toMatch(/import type \{[^}]*ListItemNoHelp[^}]*\}/);
1409
+ expect(code).toMatch(/import type \{[^}]*ListItem[^}]*\}/);
1410
+ });
1411
+
1412
+ test("imports nested generic argument types", () => {
1413
+ const source = `
1414
+ interface Asset<T extends string = string> {
1415
+ id: string;
1416
+ type: T;
1417
+ }
1418
+
1419
+ export interface Wrapper<T> {
1420
+ value: T;
1421
+ }
1422
+
1423
+ export interface Nested<T> {
1424
+ inner: T;
1425
+ }
1426
+
1427
+ export interface ComplexGeneric<
1428
+ T extends Wrapper<Nested<string>> = Wrapper<Nested<string>>
1429
+ > extends Asset<"complex"> {
1430
+ prop?: T;
1431
+ }
1432
+ `;
1433
+
1434
+ const types = convertTsToXLR(source);
1435
+ const asset = types.find(
1436
+ (t) => t.name === "ComplexGeneric",
1437
+ ) as NamedType<ObjectType>;
1438
+ const code = generateFunctionalBuilder(asset);
1439
+
1440
+ // Should import nested types from generic arguments
1441
+ expect(code).toMatch(/import type \{[^}]*Wrapper[^}]*\}/);
1442
+ expect(code).toMatch(/import type \{[^}]*Nested[^}]*\}/);
1443
+ });
1444
+ });
1445
+
1446
+ describe("Issue #4: Built-in TypeScript Types Should Not Be Imported", () => {
1447
+ test("does not import Map type from source file", () => {
1448
+ const source = `
1449
+ interface Asset<T extends string = string> {
1450
+ id: string;
1451
+ type: T;
1452
+ }
1453
+
1454
+ export interface DataAsset extends Asset<"data"> {
1455
+ errorsAndWarnings?: Map<string, Array<string>>;
1456
+ }
1457
+ `;
1458
+
1459
+ const types = convertTsToXLR(source);
1460
+ const asset = types.find(
1461
+ (t) => t.name === "DataAsset",
1462
+ ) as NamedType<ObjectType>;
1463
+ const code = generateFunctionalBuilder(asset);
1464
+
1465
+ // Map should NOT appear in imports - it's a built-in
1466
+ expect(code).not.toMatch(/import type \{[^}]*\bMap\b[^}]*\}/);
1467
+ // Should still generate the method correctly
1468
+ expect(code).toContain("withErrorsAndWarnings");
1469
+ });
1470
+
1471
+ test("does not import Set type from source file", () => {
1472
+ const source = `
1473
+ interface Asset<T extends string = string> {
1474
+ id: string;
1475
+ type: T;
1476
+ }
1477
+
1478
+ export interface CollectionAsset extends Asset<"collection"> {
1479
+ uniqueIds?: Set<string>;
1480
+ }
1481
+ `;
1482
+
1483
+ const types = convertTsToXLR(source);
1484
+ const asset = types.find(
1485
+ (t) => t.name === "CollectionAsset",
1486
+ ) as NamedType<ObjectType>;
1487
+ const code = generateFunctionalBuilder(asset);
1488
+
1489
+ // Set should NOT appear in imports - it's a built-in
1490
+ expect(code).not.toMatch(/import type \{[^}]*\bSet\b[^}]*\}/);
1491
+ // Should still generate the method correctly
1492
+ expect(code).toContain("withUniqueIds");
1493
+ });
1494
+
1495
+ test("does not import WeakMap or WeakSet types", () => {
1496
+ const source = `
1497
+ interface Asset<T extends string = string> {
1498
+ id: string;
1499
+ type: T;
1500
+ }
1501
+
1502
+ export interface CacheAsset extends Asset<"cache"> {
1503
+ weakCache?: WeakMap<object, string>;
1504
+ weakSet?: WeakSet<object>;
1505
+ }
1506
+ `;
1507
+
1508
+ const types = convertTsToXLR(source);
1509
+ const asset = types.find(
1510
+ (t) => t.name === "CacheAsset",
1511
+ ) as NamedType<ObjectType>;
1512
+ const code = generateFunctionalBuilder(asset);
1513
+
1514
+ // WeakMap and WeakSet should NOT appear in imports
1515
+ expect(code).not.toMatch(/import type \{[^}]*\bWeakMap\b[^}]*\}/);
1516
+ expect(code).not.toMatch(/import type \{[^}]*\bWeakSet\b[^}]*\}/);
1517
+ });
1518
+
1519
+ test("handles multiple built-in types in single property", () => {
1520
+ const source = `
1521
+ interface Asset<T extends string = string> {
1522
+ id: string;
1523
+ type: T;
1524
+ }
1525
+
1526
+ export interface ComplexAsset extends Asset<"complex"> {
1527
+ data?: Map<string, Set<number>>;
1528
+ metadata?: Record<string, Array<Promise<string>>>;
1529
+ }
1530
+ `;
1531
+
1532
+ const types = convertTsToXLR(source);
1533
+ const asset = types.find(
1534
+ (t) => t.name === "ComplexAsset",
1535
+ ) as NamedType<ObjectType>;
1536
+ const code = generateFunctionalBuilder(asset);
1537
+
1538
+ // None of the built-in types should appear in imports
1539
+ expect(code).not.toMatch(/import type \{[^}]*\bMap\b[^}]*\}/);
1540
+ expect(code).not.toMatch(/import type \{[^}]*\bSet\b[^}]*\}/);
1541
+ expect(code).not.toMatch(/import type \{[^}]*\bRecord\b[^}]*\}/);
1542
+ expect(code).not.toMatch(/import type \{[^}]*\bArray\b[^}]*\}/);
1543
+ expect(code).not.toMatch(/import type \{[^}]*\bPromise\b[^}]*\}/);
1544
+ // Should still generate the methods correctly
1545
+ expect(code).toContain("withData");
1546
+ expect(code).toContain("withMetadata");
1547
+ });
1548
+
1549
+ test("does not import Date, Error, or RegExp types", () => {
1550
+ const source = `
1551
+ interface Asset<T extends string = string> {
1552
+ id: string;
1553
+ type: T;
1554
+ }
1555
+
1556
+ export interface EventAsset extends Asset<"event"> {
1557
+ timestamp?: Date;
1558
+ error?: Error;
1559
+ pattern?: RegExp;
1560
+ }
1561
+ `;
1562
+
1563
+ const types = convertTsToXLR(source);
1564
+ const asset = types.find(
1565
+ (t) => t.name === "EventAsset",
1566
+ ) as NamedType<ObjectType>;
1567
+ const code = generateFunctionalBuilder(asset);
1568
+
1569
+ // Date, Error, RegExp should NOT appear in imports
1570
+ expect(code).not.toMatch(/import type \{[^}]*\bDate\b[^}]*\}/);
1571
+ expect(code).not.toMatch(/import type \{[^}]*\bError\b[^}]*\}/);
1572
+ expect(code).not.toMatch(/import type \{[^}]*\bRegExp\b[^}]*\}/);
1573
+ });
1574
+
1575
+ test("imports custom types used as generic arguments of built-in types", () => {
1576
+ const source = `
1577
+ interface Asset<T extends string = string> {
1578
+ id: string;
1579
+ type: T;
1580
+ }
1581
+
1582
+ export interface CustomData {
1583
+ name: string;
1584
+ value: number;
1585
+ }
1586
+
1587
+ export interface CustomKey {
1588
+ id: string;
1589
+ }
1590
+
1591
+ export interface DataAsset extends Asset<"data"> {
1592
+ /** Map with custom value type - should import CustomData but not Map */
1593
+ dataMap?: Map<string, CustomData>;
1594
+ /** Set with custom type - should import CustomKey but not Set */
1595
+ keySet?: Set<CustomKey>;
1596
+ /** Array with custom type - should import CustomData but not Array */
1597
+ items?: Array<CustomData>;
1598
+ }
1599
+ `;
1600
+
1601
+ const types = convertTsToXLR(source);
1602
+ const asset = types.find(
1603
+ (t) => t.name === "DataAsset",
1604
+ ) as NamedType<ObjectType>;
1605
+ const code = generateFunctionalBuilder(asset);
1606
+
1607
+ // Built-in types should NOT appear in imports
1608
+ expect(code).not.toMatch(/import type \{[^}]*\bMap\b[^}]*\}/);
1609
+ expect(code).not.toMatch(/import type \{[^}]*\bSet\b[^}]*\}/);
1610
+ expect(code).not.toMatch(/import type \{[^}]*\bArray\b[^}]*\}/);
1611
+
1612
+ // Custom types SHOULD appear in imports
1613
+ expect(code).toMatch(/import type \{[^}]*\bCustomData\b[^}]*\}/);
1614
+ expect(code).toMatch(/import type \{[^}]*\bCustomKey\b[^}]*\}/);
1615
+
1616
+ // Should still generate the methods correctly
1617
+ expect(code).toContain("withDataMap");
1618
+ expect(code).toContain("withKeySet");
1619
+ expect(code).toContain("withItems");
1620
+ });
1621
+ });
1622
+
1623
+ describe("Issue #5: External Package Imports", () => {
1624
+ test("imports types from external packages using externalTypes config", () => {
1625
+ const source = `
1626
+ interface Asset<T extends string = string> {
1627
+ id: string;
1628
+ type: T;
1629
+ }
1630
+
1631
+ export interface ExternalData {
1632
+ name: string;
1633
+ }
1634
+
1635
+ export interface MyAsset extends Asset<"my"> {
1636
+ data?: ExternalData;
1637
+ }
1638
+ `;
1639
+
1640
+ const types = convertTsToXLR(source);
1641
+ const asset = types.find(
1642
+ (t) => t.name === "MyAsset",
1643
+ ) as NamedType<ObjectType>;
1644
+
1645
+ // Configure ExternalData as coming from an external package
1646
+ const externalTypes = new Map<string, string>();
1647
+ externalTypes.set("ExternalData", "@player-lang/types");
1648
+
1649
+ const code = generateFunctionalBuilder(asset, { externalTypes });
1650
+
1651
+ // ExternalData should be imported from the package
1652
+ expect(code).toContain(
1653
+ 'import type { ExternalData } from "@player-lang/types"',
1654
+ );
1655
+ // Should still generate the method correctly
1656
+ expect(code).toContain("withData");
1657
+ });
1658
+
1659
+ test("groups multiple types from same external package", () => {
1660
+ const source = `
1661
+ interface Asset<T extends string = string> {
1662
+ id: string;
1663
+ type: T;
1664
+ }
1665
+
1666
+ export interface TypeA {
1667
+ value: string;
1668
+ }
1669
+
1670
+ export interface TypeB {
1671
+ count: number;
1672
+ }
1673
+
1674
+ export interface MyAsset extends Asset<"my"> {
1675
+ a?: TypeA;
1676
+ b?: TypeB;
1677
+ }
1678
+ `;
1679
+
1680
+ const types = convertTsToXLR(source);
1681
+ const asset = types.find(
1682
+ (t) => t.name === "MyAsset",
1683
+ ) as NamedType<ObjectType>;
1684
+
1685
+ // Configure both types as coming from the same external package
1686
+ const externalTypes = new Map<string, string>();
1687
+ externalTypes.set("TypeA", "@external/types");
1688
+ externalTypes.set("TypeB", "@external/types");
1689
+
1690
+ const code = generateFunctionalBuilder(asset, { externalTypes });
1691
+
1692
+ // Both types should be imported from the same package in one statement
1693
+ expect(code).toMatch(
1694
+ /import type \{[^}]*TypeA[^}]*TypeB[^}]*\} from "@external\/types"/,
1695
+ );
1696
+ });
1697
+
1698
+ test("external types take precedence over sameFileTypes", () => {
1699
+ const source = `
1700
+ interface Asset<T extends string = string> {
1701
+ id: string;
1702
+ type: T;
1703
+ }
1704
+
1705
+ export interface SharedType {
1706
+ value: string;
1707
+ }
1708
+
1709
+ export interface MyAsset extends Asset<"my"> {
1710
+ shared?: SharedType;
1711
+ }
1712
+ `;
1713
+
1714
+ const types = convertTsToXLR(source);
1715
+ const asset = types.find(
1716
+ (t) => t.name === "MyAsset",
1717
+ ) as NamedType<ObjectType>;
1718
+
1719
+ // Configure SharedType as external (even though it would be in sameFileTypes)
1720
+ const sameFileTypes = new Set<string>(["SharedType"]);
1721
+ const externalTypes = new Map<string, string>();
1722
+ externalTypes.set("SharedType", "@external/shared");
1723
+
1724
+ const code = generateFunctionalBuilder(asset, {
1725
+ sameFileTypes,
1726
+ externalTypes,
1727
+ });
1728
+
1729
+ // SharedType should be imported from external package, not from same file
1730
+ expect(code).toContain(
1731
+ 'import type { SharedType } from "@external/shared"',
1732
+ );
1733
+ // Should NOT be in the main type import
1734
+ expect(code).not.toMatch(
1735
+ /import type \{[^}]*MyAsset[^}]*SharedType[^}]*\} from/,
1736
+ );
1737
+ });
1738
+ });
1739
+
1740
+ describe("Issue #6: Generic Type Parameters Should Not Be Imported", () => {
1741
+ test("does not import generic parameters from referenced type constraints", () => {
1742
+ const source = `
1743
+ interface Asset<T extends string = string> {
1744
+ id: string;
1745
+ type: T;
1746
+ }
1747
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
1748
+
1749
+ export interface Bar<AnyAsset extends Asset = Asset> {
1750
+ label: AssetWrapper<AnyAsset>;
1751
+ info: AssetWrapper<AnyAsset>;
1752
+ }
1753
+
1754
+ export interface DataVisualizationAsset<
1755
+ MetadataType = DataBarsMetaData,
1756
+ BarType extends Bar = SingleBar
1757
+ > extends Asset<"dataViz"> {
1758
+ metaData: MetadataType;
1759
+ data: BarType[];
1760
+ }
1761
+
1762
+ export interface DataBarsMetaData {
1763
+ title: string;
1764
+ }
1765
+
1766
+ export interface SingleBar extends Bar {
1767
+ value: number;
1768
+ }
1769
+ `;
1770
+
1771
+ const types = convertTsToXLR(source);
1772
+ const asset = types.find(
1773
+ (t) => t.name === "DataVisualizationAsset",
1774
+ ) as NamedType<ObjectType>;
1775
+ const code = generateFunctionalBuilder(asset);
1776
+
1777
+ // Should NOT try to import AnyAsset (it's a generic param of Bar, not a concrete type)
1778
+ expect(code).not.toMatch(/import type \{[^}]*\bAnyAsset\b[^}]*\}/);
1779
+
1780
+ // Should import actual types like Bar, SingleBar, DataBarsMetaData
1781
+ expect(code).toMatch(/import type \{[^}]*\bBar\b[^}]*\}/);
1782
+ expect(code).toMatch(/import type \{[^}]*\bSingleBar\b[^}]*\}/);
1783
+ expect(code).toMatch(/import type \{[^}]*\bDataBarsMetaData\b[^}]*\}/);
1784
+ });
1785
+
1786
+ test("does not import generic parameters from nested generic constraints", () => {
1787
+ const source = `
1788
+ interface Asset<T extends string = string> {
1789
+ id: string;
1790
+ type: T;
1791
+ }
1792
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
1793
+
1794
+ export interface ListItemNoHelp<AnyAsset extends Asset = Asset>
1795
+ extends AssetWrapper<AnyAsset> {}
1796
+
1797
+ export interface ListItem<AnyAsset extends Asset = Asset>
1798
+ extends ListItemNoHelp<AnyAsset> {
1799
+ help?: { id: string; };
1800
+ }
1801
+
1802
+ export interface ListAsset<
1803
+ AnyAsset extends Asset = Asset,
1804
+ ItemType extends ListItemNoHelp = ListItem<AnyAsset>
1805
+ > extends Asset<"list"> {
1806
+ values?: Array<ItemType>;
1807
+ }
1808
+ `;
1809
+
1810
+ const types = convertTsToXLR(source);
1811
+ const listAsset = types.find(
1812
+ (t) => t.name === "ListAsset",
1813
+ ) as NamedType<ObjectType>;
1814
+ const code = generateFunctionalBuilder(listAsset);
1815
+
1816
+ // Should NOT try to import AnyAsset (it's a generic param)
1817
+ expect(code).not.toMatch(/import type \{[^}]*\bAnyAsset\b[^}]*\}/);
1818
+
1819
+ // Should import actual types
1820
+ expect(code).toMatch(/import type \{[^}]*\bListItemNoHelp\b[^}]*\}/);
1821
+ expect(code).toMatch(/import type \{[^}]*\bListItem\b[^}]*\}/);
1822
+ });
1823
+ });
1824
+
1825
+ describe("Issue #7: Generic Constraints Should Not Include FunctionalBuilder", () => {
1826
+ test("generates raw type names in generic constraints", () => {
1827
+ const source = `
1828
+ interface Asset<T extends string = string> {
1829
+ id: string;
1830
+ type: T;
1831
+ }
1832
+
1833
+ export interface Bar<AnyAsset extends Asset = Asset> {
1834
+ label: string;
1835
+ }
1836
+
1837
+ export interface DataVisualizationAsset<
1838
+ BarType extends Bar = SingleBar
1839
+ > extends Asset<"dataViz"> {
1840
+ data: BarType[];
1841
+ }
1842
+
1843
+ export interface SingleBar extends Bar {
1844
+ value: number;
1845
+ }
1846
+ `;
1847
+
1848
+ const types = convertTsToXLR(source);
1849
+ const asset = types.find(
1850
+ (t) => t.name === "DataVisualizationAsset",
1851
+ ) as NamedType<ObjectType>;
1852
+ const code = generateFunctionalBuilder(asset);
1853
+
1854
+ // Constraint should be "extends Bar", NOT "extends Bar | FunctionalBuilder<Bar>"
1855
+ expect(code).toContain("BarType extends Bar");
1856
+ expect(code).not.toContain("BarType extends Bar | FunctionalBuilder");
1857
+
1858
+ // Default should be "= SingleBar", NOT "= SingleBar | FunctionalBuilder<SingleBar>"
1859
+ expect(code).toContain("= SingleBar");
1860
+ expect(code).not.toContain("= SingleBar | FunctionalBuilder");
1861
+ });
1862
+
1863
+ test("generates multiple generic constraints without FunctionalBuilder", () => {
1864
+ const source = `
1865
+ interface Asset<T extends string = string> {
1866
+ id: string;
1867
+ type: T;
1868
+ }
1869
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
1870
+
1871
+ export interface ListItemNoHelp<AnyAsset extends Asset = Asset>
1872
+ extends AssetWrapper<AnyAsset> {}
1873
+
1874
+ export interface ListItem<AnyAsset extends Asset = Asset>
1875
+ extends ListItemNoHelp<AnyAsset> {
1876
+ help?: { id: string; };
1877
+ }
1878
+
1879
+ export interface ListAsset<
1880
+ AnyAsset extends Asset = Asset,
1881
+ ItemType extends ListItemNoHelp = ListItem
1882
+ > extends Asset<"list"> {
1883
+ values?: Array<ItemType>;
1884
+ }
1885
+ `;
1886
+
1887
+ const types = convertTsToXLR(source);
1888
+ const listAsset = types.find(
1889
+ (t) => t.name === "ListAsset",
1890
+ ) as NamedType<ObjectType>;
1891
+ const code = generateFunctionalBuilder(listAsset);
1892
+
1893
+ // Both constraints should be raw types without FunctionalBuilder
1894
+ expect(code).toContain("AnyAsset extends Asset");
1895
+ expect(code).not.toContain("AnyAsset extends Asset | FunctionalBuilder");
1896
+ expect(code).toContain("ItemType extends ListItemNoHelp");
1897
+ expect(code).not.toContain(
1898
+ "ItemType extends ListItemNoHelp | FunctionalBuilder",
1899
+ );
1900
+
1901
+ // Defaults should also be raw types
1902
+ expect(code).toContain("= Asset");
1903
+ expect(code).toContain("= ListItem");
1904
+ expect(code).not.toMatch(/=\s*Asset\s*\|\s*FunctionalBuilder/);
1905
+ expect(code).not.toMatch(/=\s*ListItem\s*\|\s*FunctionalBuilder/);
1906
+ });
1907
+ });
1908
+
1909
+ describe("Issue #8: Embedded Type Arguments Should Be Preserved When Available", () => {
1910
+ test("preserves embedded type arguments in extends ref string", () => {
1911
+ const source = `
1912
+ interface Asset<T extends string = string> {
1913
+ id: string;
1914
+ type: T;
1915
+ }
1916
+
1917
+ export interface LinkModifier extends Asset<"link"> {
1918
+ url: string;
1919
+ }
1920
+
1921
+ export interface FormattingAsset extends Asset<"format"> {
1922
+ style: string;
1923
+ }
1924
+ `;
1925
+
1926
+ const types = convertTsToXLR(source);
1927
+ const linkAsset = types.find(
1928
+ (t) => t.name === "LinkModifier",
1929
+ ) as NamedType<ObjectType>;
1930
+ const code = generateFunctionalBuilder(linkAsset);
1931
+
1932
+ // Should have the correct asset type default
1933
+ expect(code).toContain('"type":"link"');
1934
+ });
1935
+
1936
+ test("handles union types with different modifiers", () => {
1937
+ const source = `
1938
+ interface Asset<T extends string = string> {
1939
+ id: string;
1940
+ type: T;
1941
+ }
1942
+
1943
+ export interface LinkModifier {
1944
+ type: 'link';
1945
+ url: string;
1946
+ }
1947
+
1948
+ export interface FormatModifier {
1949
+ type: 'format';
1950
+ style: string;
1951
+ }
1952
+
1953
+ export interface TextAsset extends Asset<"text"> {
1954
+ value: string;
1955
+ modifiers?: Array<LinkModifier | FormatModifier>;
1956
+ }
1957
+ `;
1958
+
1959
+ const types = convertTsToXLR(source);
1960
+ const asset = types.find(
1961
+ (t) => t.name === "TextAsset",
1962
+ ) as NamedType<ObjectType>;
1963
+ const code = generateFunctionalBuilder(asset);
1964
+
1965
+ // Should generate method for modifiers
1966
+ expect(code).toContain("withModifiers");
1967
+
1968
+ // Should handle both modifier types in the union
1969
+ expect(code).toContain("LinkModifier");
1970
+ expect(code).toContain("FormatModifier");
1971
+ });
1972
+ });
1973
+ });
1974
+
1975
+ describe("Namespaced Types", () => {
1976
+ test("preserves full qualified name for namespaced types in constraints", () => {
1977
+ const source = `
1978
+ interface Asset<T extends string = string> {
1979
+ id: string;
1980
+ type: T;
1981
+ }
1982
+
1983
+ namespace Validation {
1984
+ export interface CrossfieldReference {
1985
+ field: string;
1986
+ condition: string;
1987
+ }
1988
+ }
1989
+
1990
+ export interface InputAsset extends Asset<"input"> {
1991
+ binding: string;
1992
+ validation?: Validation.CrossfieldReference;
1993
+ }
1994
+ `;
1995
+
1996
+ const types = convertTsToXLR(source);
1997
+ const inputAsset = types.find(
1998
+ (t) => t.name === "InputAsset",
1999
+ ) as NamedType<ObjectType>;
2000
+ const code = generateFunctionalBuilder(inputAsset);
2001
+
2002
+ // Should preserve the full qualified name
2003
+ expect(code).toContain("Validation.CrossfieldReference");
2004
+ });
2005
+
2006
+ test("handles namespaced types in generic arguments", () => {
2007
+ const source = `
2008
+ interface Asset<T extends string = string> {
2009
+ id: string;
2010
+ type: T;
2011
+ }
2012
+
2013
+ namespace Config {
2014
+ export interface Settings {
2015
+ enabled: boolean;
2016
+ }
2017
+ }
2018
+
2019
+ export interface AppAsset extends Asset<"app"> {
2020
+ settings?: Config.Settings;
2021
+ allSettings?: Array<Config.Settings>;
2022
+ }
2023
+ `;
2024
+
2025
+ const types = convertTsToXLR(source);
2026
+ const appAsset = types.find(
2027
+ (t) => t.name === "AppAsset",
2028
+ ) as NamedType<ObjectType>;
2029
+ const code = generateFunctionalBuilder(appAsset);
2030
+
2031
+ // Should preserve the namespace in type references
2032
+ expect(code).toContain("Config.Settings");
2033
+ });
2034
+
2035
+ test("imports namespace for namespaced types", () => {
2036
+ const source = `
2037
+ interface Asset<T extends string = string> {
2038
+ id: string;
2039
+ type: T;
2040
+ }
2041
+
2042
+ namespace Types {
2043
+ export interface Data {
2044
+ value: string;
2045
+ }
2046
+ }
2047
+
2048
+ export interface DataAsset extends Asset<"data"> {
2049
+ data?: Types.Data;
2050
+ }
2051
+ `;
2052
+
2053
+ const types = convertTsToXLR(source);
2054
+ const dataAsset = types.find(
2055
+ (t) => t.name === "DataAsset",
2056
+ ) as NamedType<ObjectType>;
2057
+
2058
+ // Configure Types namespace as external
2059
+ const externalTypes = new Map<string, string>();
2060
+ externalTypes.set("Types", "@custom/types");
2061
+
2062
+ const code = generateFunctionalBuilder(dataAsset, { externalTypes });
2063
+
2064
+ // Should import the namespace
2065
+ expect(code).toContain('import type { Types } from "@custom/types"');
2066
+ // Should use the full qualified name in the code
2067
+ expect(code).toContain("Types.Data");
2068
+ });
2069
+ });
2070
+
2071
+ describe("FunctionalPartial Support for Nested Builder Objects", () => {
2072
+ test("generates FunctionalPartial union for nested object properties with AssetWrapper", () => {
2073
+ const source = `
2074
+ interface Asset<T extends string = string> {
2075
+ id: string;
2076
+ type: T;
2077
+ }
2078
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2079
+
2080
+ export interface Foo {
2081
+ bar: AssetWrapper<Asset>;
2082
+ buz: AssetWrapper<Asset>;
2083
+ lol: string;
2084
+ ok: boolean;
2085
+ }
2086
+
2087
+ export interface ContainerAsset extends Asset<"container"> {
2088
+ foo: Foo;
2089
+ }
2090
+ `;
2091
+
2092
+ const types = convertTsToXLR(source);
2093
+ const containerAsset = types.find(
2094
+ (t) => t.name === "ContainerAsset",
2095
+ ) as NamedType<ObjectType>;
2096
+ expect(containerAsset).toBeDefined();
2097
+
2098
+ const code = generateFunctionalBuilder(containerAsset);
2099
+
2100
+ // The foo property should accept Foo, FunctionalBuilder<Foo>, or FunctionalPartial<Foo>
2101
+ expect(code).toContain("FunctionalPartial<Foo, BaseBuildContext>");
2102
+ // Verify the full signature
2103
+ expect(code).toContain(
2104
+ "withFoo(value: Foo | FunctionalBuilder<Foo, BaseBuildContext> | FunctionalPartial<Foo, BaseBuildContext>)",
2105
+ );
2106
+ });
2107
+
2108
+ test("generates FunctionalPartial for deeply nested object structures", () => {
2109
+ const source = `
2110
+ interface Asset<T extends string = string> {
2111
+ id: string;
2112
+ type: T;
2113
+ }
2114
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2115
+
2116
+ export interface DeepNested {
2117
+ level1: {
2118
+ level2: {
2119
+ content: AssetWrapper<Asset>;
2120
+ value: string;
2121
+ };
2122
+ };
2123
+ }
2124
+
2125
+ export interface DeepAsset extends Asset<"deep"> {
2126
+ nested: DeepNested;
2127
+ }
2128
+ `;
2129
+
2130
+ const types = convertTsToXLR(source);
2131
+ const deepAsset = types.find(
2132
+ (t) => t.name === "DeepAsset",
2133
+ ) as NamedType<ObjectType>;
2134
+ expect(deepAsset).toBeDefined();
2135
+
2136
+ const code = generateFunctionalBuilder(deepAsset);
2137
+
2138
+ // The nested property should accept DeepNested, FunctionalBuilder<DeepNested>, or FunctionalPartial<DeepNested>
2139
+ expect(code).toContain("FunctionalPartial<DeepNested, BaseBuildContext>");
2140
+ });
2141
+
2142
+ test("generates FunctionalPartial for array element types", () => {
2143
+ const source = `
2144
+ interface Asset<T extends string = string> {
2145
+ id: string;
2146
+ type: T;
2147
+ }
2148
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2149
+
2150
+ export interface ListItem {
2151
+ label: AssetWrapper<Asset>;
2152
+ value: string;
2153
+ }
2154
+
2155
+ export interface ListAsset extends Asset<"list"> {
2156
+ items: Array<ListItem>;
2157
+ }
2158
+ `;
2159
+
2160
+ const types = convertTsToXLR(source);
2161
+ const listAsset = types.find(
2162
+ (t) => t.name === "ListAsset",
2163
+ ) as NamedType<ObjectType>;
2164
+ expect(listAsset).toBeDefined();
2165
+
2166
+ const code = generateFunctionalBuilder(listAsset);
2167
+
2168
+ // Array elements should also support FunctionalPartial
2169
+ expect(code).toContain(
2170
+ "Array<ListItem | FunctionalBuilder<ListItem, BaseBuildContext> | FunctionalPartial<ListItem, BaseBuildContext>>",
2171
+ );
2172
+ });
2173
+
2174
+ test("generates FunctionalPartial for union types containing objects", () => {
2175
+ const source = `
2176
+ interface Asset<T extends string = string> {
2177
+ id: string;
2178
+ type: T;
2179
+ }
2180
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2181
+
2182
+ export interface TypeA {
2183
+ slot: AssetWrapper<Asset>;
2184
+ kind: "a";
2185
+ }
2186
+
2187
+ export interface TypeB {
2188
+ slot: AssetWrapper<Asset>;
2189
+ kind: "b";
2190
+ }
2191
+
2192
+ export interface UnionAsset extends Asset<"union"> {
2193
+ content: TypeA | TypeB;
2194
+ }
2195
+ `;
2196
+
2197
+ const types = convertTsToXLR(source);
2198
+ const unionAsset = types.find(
2199
+ (t) => t.name === "UnionAsset",
2200
+ ) as NamedType<ObjectType>;
2201
+ expect(unionAsset).toBeDefined();
2202
+
2203
+ const code = generateFunctionalBuilder(unionAsset);
2204
+
2205
+ // Both TypeA and TypeB should support FunctionalPartial in the union
2206
+ expect(code).toContain("FunctionalPartial<TypeA, BaseBuildContext>");
2207
+ expect(code).toContain("FunctionalPartial<TypeB, BaseBuildContext>");
2208
+ });
2209
+ });
2210
+
2211
+ describe("Generic Type Arguments Preservation", () => {
2212
+ test("preserves string literal generic arguments in type names", () => {
2213
+ const source = `
2214
+ interface Asset<T extends string = string> {
2215
+ id: string;
2216
+ type: T;
2217
+ }
2218
+
2219
+ export interface SimpleModifier<T extends string> {
2220
+ type: T;
2221
+ value: string;
2222
+ }
2223
+
2224
+ export interface TextAsset extends Asset<"text"> {
2225
+ value: string;
2226
+ modifiers?: Array<SimpleModifier<"format"> | SimpleModifier<"link">>;
2227
+ }
2228
+ `;
2229
+
2230
+ const types = convertTsToXLR(source);
2231
+ const textAsset = types.find(
2232
+ (t) => t.name === "TextAsset",
2233
+ ) as NamedType<ObjectType>;
2234
+ const code = generateFunctionalBuilder(textAsset);
2235
+
2236
+ // Should preserve the generic arguments in the type
2237
+ expect(code).toContain('SimpleModifier<"format">');
2238
+ expect(code).toContain('SimpleModifier<"link">');
2239
+ });
2240
+ });
2241
+
2242
+ describe("Import Handling for Asset", () => {
2243
+ test("Asset is imported exactly once when used in AssetWrapper", () => {
2244
+ const source = `
2245
+ interface Asset<T extends string = string> {
2246
+ id: string;
2247
+ type: T;
2248
+ }
2249
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2250
+
2251
+ export interface ActionAsset extends Asset<"action"> {
2252
+ label?: AssetWrapper<Asset>;
2253
+ icon?: AssetWrapper<Asset>;
2254
+ }
2255
+ `;
2256
+
2257
+ const types = convertTsToXLR(source);
2258
+ const actionAsset = types.find(
2259
+ (t) => t.name === "ActionAsset",
2260
+ ) as NamedType<ObjectType>;
2261
+ const code = generateFunctionalBuilder(actionAsset);
2262
+
2263
+ // Asset should be imported exactly once from @player-ui/types
2264
+ const assetImportMatches = code.match(
2265
+ /import type \{ Asset \} from "@player-ui\/types"/g,
2266
+ );
2267
+ expect(assetImportMatches?.length).toBe(1);
2268
+
2269
+ // Asset should NOT be in the main type import
2270
+ expect(code).not.toMatch(
2271
+ /import type \{[^}]*ActionAsset[^}]*Asset[^}]*\} from/,
2272
+ );
2273
+ });
2274
+
2275
+ test("generic parameters of the current type are not imported", () => {
2276
+ const source = `
2277
+ interface Asset<T extends string = string> {
2278
+ id: string;
2279
+ type: T;
2280
+ }
2281
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2282
+
2283
+ export interface InputAsset<AnyTextAsset extends Asset = Asset> extends Asset<"input"> {
2284
+ binding: string;
2285
+ label?: AssetWrapper<AnyTextAsset>;
2286
+ }
2287
+ `;
2288
+
2289
+ const types = convertTsToXLR(source);
2290
+ const inputAsset = types.find(
2291
+ (t) => t.name === "InputAsset",
2292
+ ) as NamedType<ObjectType>;
2293
+ const code = generateFunctionalBuilder(inputAsset);
2294
+
2295
+ // AnyTextAsset should NOT be imported - it's a generic parameter of InputAsset
2296
+ expect(code).not.toMatch(/import type \{[^}]*AnyTextAsset[^}]*\}/);
2297
+ });
2298
+ });
2299
+
2300
+ describe("Types Extending AssetWrapper", () => {
2301
+ test("generates correct type signature and __assetWrapperPaths__ for type extending AssetWrapper", () => {
2302
+ const source = `
2303
+ interface Asset<T extends string = string> {
2304
+ id: string;
2305
+ type: T;
2306
+ }
2307
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2308
+
2309
+ export interface Header extends AssetWrapper<Asset> {
2310
+ title: string;
2311
+ }
2312
+
2313
+ export interface TableAsset extends Asset<"table"> {
2314
+ headers: Array<Header>;
2315
+ }
2316
+ `;
2317
+
2318
+ const types = convertTsToXLR(source);
2319
+ const headerType = types.find(
2320
+ (t) => t.name === "Header",
2321
+ ) as NamedType<ObjectType>;
2322
+ const tableAsset = types.find(
2323
+ (t) => t.name === "TableAsset",
2324
+ ) as NamedType<ObjectType>;
2325
+ expect(headerType).toBeDefined();
2326
+ expect(tableAsset).toBeDefined();
2327
+
2328
+ const typeRegistry: TypeRegistry = new Map([["Header", headerType]]);
2329
+
2330
+ const code = generateFunctionalBuilder(tableAsset, { typeRegistry });
2331
+
2332
+ // Should include Asset | FunctionalBuilder<Asset> for the inner asset type
2333
+ expect(code).toContain(
2334
+ "Asset | FunctionalBuilder<Asset, BaseBuildContext>",
2335
+ );
2336
+ // Should include Header type in the union
2337
+ expect(code).toContain("Header");
2338
+ // Should have __assetWrapperPaths__ that includes "headers"
2339
+ expect(code).toContain("__assetWrapperPaths__");
2340
+ expect(code).toContain('"headers"');
2341
+ });
2342
+
2343
+ test("generates correct type for single property extending AssetWrapper", () => {
2344
+ const source = `
2345
+ interface Asset<T extends string = string> {
2346
+ id: string;
2347
+ type: T;
2348
+ }
2349
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2350
+
2351
+ export interface Header extends AssetWrapper<Asset> {
2352
+ title: string;
2353
+ }
2354
+
2355
+ export interface CardAsset extends Asset<"card"> {
2356
+ header?: Header;
2357
+ }
2358
+ `;
2359
+
2360
+ const types = convertTsToXLR(source);
2361
+ const headerType = types.find(
2362
+ (t) => t.name === "Header",
2363
+ ) as NamedType<ObjectType>;
2364
+ const cardAsset = types.find(
2365
+ (t) => t.name === "CardAsset",
2366
+ ) as NamedType<ObjectType>;
2367
+ expect(headerType).toBeDefined();
2368
+ expect(cardAsset).toBeDefined();
2369
+
2370
+ const typeRegistry: TypeRegistry = new Map([["Header", headerType]]);
2371
+
2372
+ const code = generateFunctionalBuilder(cardAsset, { typeRegistry });
2373
+
2374
+ // Should accept Asset, FunctionalBuilder<Asset>, Header, FunctionalBuilder<Header>, or FunctionalPartial<Header>
2375
+ expect(code).toContain(
2376
+ "Asset | FunctionalBuilder<Asset, BaseBuildContext>",
2377
+ );
2378
+ expect(code).toContain(
2379
+ "Header | FunctionalBuilder<Header, BaseBuildContext>",
2380
+ );
2381
+ expect(code).toContain("FunctionalPartial<Header, BaseBuildContext>");
2382
+ // Should have __assetWrapperPaths__
2383
+ expect(code).toContain("__assetWrapperPaths__");
2384
+ expect(code).toContain('"header"');
2385
+ });
2386
+
2387
+ test("handles transitive AssetWrapper extension", () => {
2388
+ const source = `
2389
+ interface Asset<T extends string = string> {
2390
+ id: string;
2391
+ type: T;
2392
+ }
2393
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2394
+
2395
+ export interface ListItemBase extends AssetWrapper<Asset> {}
2396
+
2397
+ export interface ListItem extends ListItemBase {
2398
+ help?: string;
2399
+ }
2400
+
2401
+ export interface ListAsset extends Asset<"list"> {
2402
+ items: Array<ListItem>;
2403
+ }
2404
+ `;
2405
+
2406
+ const types = convertTsToXLR(source);
2407
+ const listItemBaseType = types.find(
2408
+ (t) => t.name === "ListItemBase",
2409
+ ) as NamedType<ObjectType>;
2410
+ const listItemType = types.find(
2411
+ (t) => t.name === "ListItem",
2412
+ ) as NamedType<ObjectType>;
2413
+ const listAsset = types.find(
2414
+ (t) => t.name === "ListAsset",
2415
+ ) as NamedType<ObjectType>;
2416
+ expect(listItemBaseType).toBeDefined();
2417
+ expect(listItemType).toBeDefined();
2418
+ expect(listAsset).toBeDefined();
2419
+
2420
+ const typeRegistry: TypeRegistry = new Map([
2421
+ ["ListItemBase", listItemBaseType],
2422
+ ["ListItem", listItemType],
2423
+ ]);
2424
+
2425
+ const code = generateFunctionalBuilder(listAsset, { typeRegistry });
2426
+
2427
+ // Should detect ListItem as extending AssetWrapper (transitively)
2428
+ expect(code).toContain("__assetWrapperPaths__");
2429
+ expect(code).toContain('"items"');
2430
+ // Should include Asset in the type signature
2431
+ expect(code).toContain(
2432
+ "Asset | FunctionalBuilder<Asset, BaseBuildContext>",
2433
+ );
2434
+ });
2435
+
2436
+ test("generates correct inner type from specific AssetWrapper generic argument", () => {
2437
+ const source = `
2438
+ interface Asset<T extends string = string> {
2439
+ id: string;
2440
+ type: T;
2441
+ }
2442
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2443
+
2444
+ export interface ImageAsset extends Asset<"image"> {
2445
+ src: string;
2446
+ }
2447
+
2448
+ export interface ImageSlot extends AssetWrapper<ImageAsset> {
2449
+ alt?: string;
2450
+ }
2451
+
2452
+ export interface GalleryAsset extends Asset<"gallery"> {
2453
+ images: Array<ImageSlot>;
2454
+ }
2455
+ `;
2456
+
2457
+ const types = convertTsToXLR(source);
2458
+ const imageAssetType = types.find(
2459
+ (t) => t.name === "ImageAsset",
2460
+ ) as NamedType<ObjectType>;
2461
+ const imageSlotType = types.find(
2462
+ (t) => t.name === "ImageSlot",
2463
+ ) as NamedType<ObjectType>;
2464
+ const galleryAsset = types.find(
2465
+ (t) => t.name === "GalleryAsset",
2466
+ ) as NamedType<ObjectType>;
2467
+ expect(imageSlotType).toBeDefined();
2468
+ expect(galleryAsset).toBeDefined();
2469
+
2470
+ const typeRegistry: TypeRegistry = new Map([
2471
+ ["ImageSlot", imageSlotType],
2472
+ ["ImageAsset", imageAssetType],
2473
+ ]);
2474
+
2475
+ const code = generateFunctionalBuilder(galleryAsset, { typeRegistry });
2476
+
2477
+ // Should use ImageAsset as the inner type (not generic Asset)
2478
+ expect(code).toContain(
2479
+ "ImageAsset | FunctionalBuilder<ImageAsset, BaseBuildContext>",
2480
+ );
2481
+ // Should also include ImageSlot in the union
2482
+ expect(code).toContain("ImageSlot");
2483
+ // Should have __assetWrapperPaths__
2484
+ expect(code).toContain("__assetWrapperPaths__");
2485
+ expect(code).toContain('"images"');
2486
+ });
2487
+ });
2488
+
2489
+ describe("Runtime: Table Composition with Nested Builders", () => {
2490
+ // Inline builder classes matching generated patterns to validate
2491
+ // that the functional builder runtime correctly handles:
2492
+ // 1. AssetWrapper auto-wrapping (search, filters, actions)
2493
+ // 2. Types extending AssetWrapper (Header, TableColumn)
2494
+ // 3. Nested builders (StaticFilter inside Header inside Table)
2495
+ // 4. Array of extending types (Array<Header>, Array<TableColumn>)
2496
+
2497
+ class TextBuilder extends FunctionalBuilderBase<any> {
2498
+ static defaults = { type: "text", id: "" };
2499
+ withValue(v: any) {
2500
+ return this.set("value", v);
2501
+ }
2502
+ build(ctx?: any) {
2503
+ return this.buildWithDefaults(TextBuilder.defaults, ctx);
2504
+ }
2505
+ }
2506
+ const text = (initial?: any) => new TextBuilder(initial);
2507
+
2508
+ class ActionBuilder extends FunctionalBuilderBase<any> {
2509
+ static defaults = { type: "action", id: "" };
2510
+ withLabel(v: any) {
2511
+ return this.set("label", v);
2512
+ }
2513
+ build(ctx?: any) {
2514
+ return this.buildWithDefaults(ActionBuilder.defaults, ctx);
2515
+ }
2516
+ }
2517
+ const action = (initial?: any) => new ActionBuilder(initial);
2518
+
2519
+ class StaticFilterBuilder extends FunctionalBuilderBase<any> {
2520
+ static defaults = {};
2521
+ static __assetWrapperPaths__ = [["label"], ["value"]];
2522
+ withLabel(v: any) {
2523
+ return this.set("label", v);
2524
+ }
2525
+ withValue(v: any) {
2526
+ return this.set("value", v);
2527
+ }
2528
+ withComparator(v: any) {
2529
+ return this.set("comparator", v);
2530
+ }
2531
+ build(ctx?: any) {
2532
+ return this.buildWithDefaults(StaticFilterBuilder.defaults, ctx);
2533
+ }
2534
+ }
2535
+ const staticFilter = (initial?: any) => new StaticFilterBuilder(initial);
2536
+
2537
+ class TableColumnBuilder extends FunctionalBuilderBase<any> {
2538
+ static defaults = { id: "" };
2539
+ withComparator(v: any) {
2540
+ return this.set("comparator", v);
2541
+ }
2542
+ build(ctx?: any) {
2543
+ return this.buildWithDefaults(TableColumnBuilder.defaults, ctx);
2544
+ }
2545
+ }
2546
+ const tableColumn = (initial?: any) => new TableColumnBuilder(initial);
2547
+
2548
+ class HeaderBuilder extends FunctionalBuilderBase<any> {
2549
+ static defaults = { id: "", significance: "optional" };
2550
+ static __assetWrapperPaths__: string[][] = [
2551
+ ["staticFilters", "label"],
2552
+ ["staticFilters", "value"],
2553
+ ];
2554
+ withStaticFilters(v: any) {
2555
+ return this.set("staticFilters", v);
2556
+ }
2557
+ withSortable(v: any) {
2558
+ return this.set("sortable", v);
2559
+ }
2560
+ withSignificance(v: any) {
2561
+ return this.set("significance", v);
2562
+ }
2563
+ build(ctx?: any) {
2564
+ return this.buildWithDefaults(HeaderBuilder.defaults, ctx);
2565
+ }
2566
+ }
2567
+ const header = (initial?: any) => new HeaderBuilder(initial);
2568
+
2569
+ class RowBuilder extends FunctionalBuilderBase<any> {
2570
+ static defaults = {};
2571
+ static __assetWrapperPaths__ = [["values"], ["actions"]];
2572
+ withContext(v: any) {
2573
+ return this.set("context", v);
2574
+ }
2575
+ withValues(v: any) {
2576
+ return this.set("values", v);
2577
+ }
2578
+ withActions(v: any) {
2579
+ return this.set("actions", v);
2580
+ }
2581
+ build(ctx?: any) {
2582
+ return this.buildWithDefaults(RowBuilder.defaults, ctx);
2583
+ }
2584
+ }
2585
+ const row = (initial?: any) => new RowBuilder(initial);
2586
+
2587
+ class TableAssetBuilder extends FunctionalBuilderBase<any> {
2588
+ static defaults = { type: "table", id: "" };
2589
+ static __assetWrapperPaths__ = [
2590
+ ["search"],
2591
+ ["filters"],
2592
+ ["headers", "values"],
2593
+ ["headers", "values", "staticFilters", "label"],
2594
+ ["headers", "values", "staticFilters", "value"],
2595
+ ["actions"],
2596
+ ["values", "values"],
2597
+ ["values", "actions"],
2598
+ ];
2599
+ withSearch(v: any) {
2600
+ return this.set("search", v);
2601
+ }
2602
+ withFilters(v: any) {
2603
+ return this.set("filters", v);
2604
+ }
2605
+ withHeaders(v: any) {
2606
+ return this.set("headers", v);
2607
+ }
2608
+ withValues(v: any) {
2609
+ return this.set("values", v);
2610
+ }
2611
+ withActions(v: any) {
2612
+ return this.set("actions", v);
2613
+ }
2614
+ withPlaceholder(v: any) {
2615
+ return this.set("placeholder", v);
2616
+ }
2617
+ withAutomationId(v: any) {
2618
+ return this.set("automationId", v);
2619
+ }
2620
+ build(ctx?: any) {
2621
+ return this.buildWithDefaults(TableAssetBuilder.defaults, ctx);
2622
+ }
2623
+ }
2624
+ const tableAsset = (initial?: any) => new TableAssetBuilder(initial);
2625
+
2626
+ test("composes table with nested builders and auto-wraps AssetWrapper properties", () => {
2627
+ const result = tableAsset()
2628
+ .withSearch(text().withValue("Search..."))
2629
+ .withFilters([text().withValue("Filter 1")])
2630
+ .withHeaders({
2631
+ values: [
2632
+ header()
2633
+ .withStaticFilters([
2634
+ staticFilter()
2635
+ .withLabel(text().withValue("Active"))
2636
+ .withValue(text().withValue("Yes"))
2637
+ .withComparator("active"),
2638
+ ])
2639
+ .withSortable(true),
2640
+ header().withSignificance("optional"),
2641
+ ],
2642
+ })
2643
+ .withValues([
2644
+ row()
2645
+ .withContext({ state: "complete" })
2646
+ .withValues([
2647
+ tableColumn().withComparator("name"),
2648
+ tableColumn().withComparator("status"),
2649
+ ])
2650
+ .withActions([action().withLabel("Edit")]),
2651
+ ])
2652
+ .withActions([action().withLabel("Add Row")])
2653
+ .withPlaceholder("No data available")
2654
+ .withAutomationId("main-table")
2655
+ .build();
2656
+
2657
+ // Top-level structure
2658
+ expect(result.type).toBe("table");
2659
+ expect(result.placeholder).toBe("No data available");
2660
+ expect(result.automationId).toBe("main-table");
2661
+
2662
+ // Direct AssetWrapper: search is auto-wrapped
2663
+ expect(result.search.asset).toBeDefined();
2664
+ expect(result.search.asset.type).toBe("text");
2665
+ expect(result.search.asset.value).toBe("Search...");
2666
+
2667
+ // Array<AssetWrapper>: filters are auto-wrapped
2668
+ expect(result.filters).toHaveLength(1);
2669
+ expect(result.filters[0].asset.type).toBe("text");
2670
+ expect(result.filters[0].asset.value).toBe("Filter 1");
2671
+
2672
+ // Table-level actions are auto-wrapped
2673
+ expect(result.actions).toHaveLength(1);
2674
+ expect(result.actions[0].asset.type).toBe("action");
2675
+ expect(result.actions[0].asset.label).toBe("Add Row");
2676
+
2677
+ // Headers with nested Header builders
2678
+ expect(result.headers.values).toHaveLength(2);
2679
+ const header1 = result.headers.values[0];
2680
+ expect(header1.sortable).toBe(true);
2681
+ expect(header1.staticFilters).toHaveLength(1);
2682
+
2683
+ // StaticFilter.label/value auto-wrapped within the nested builder
2684
+ const sf = header1.staticFilters[0];
2685
+ expect(sf.comparator).toBe("active");
2686
+ expect(sf.label.asset).toBeDefined();
2687
+ expect(sf.label.asset.value).toBe("Active");
2688
+ expect(sf.value.asset).toBeDefined();
2689
+ expect(sf.value.asset.value).toBe("Yes");
2690
+
2691
+ // Rows with nested TableColumn builders
2692
+ expect(result.values).toHaveLength(1);
2693
+ const row1 = result.values[0];
2694
+ expect(row1.context).toEqual({ state: "complete" });
2695
+ expect(row1.values).toHaveLength(2);
2696
+ expect(row1.values[0].comparator).toBe("name");
2697
+ expect(row1.values[1].comparator).toBe("status");
2698
+
2699
+ // Row-level actions are auto-wrapped
2700
+ expect(row1.actions).toHaveLength(1);
2701
+ expect(row1.actions[0].asset.type).toBe("action");
2702
+ expect(row1.actions[0].asset.label).toBe("Edit");
2703
+ });
2704
+
2705
+ test("generated __assetWrapperPaths__ match for Table with extends-AssetWrapper types", () => {
2706
+ const source = `
2707
+ interface Asset<T extends string = string> {
2708
+ id: string;
2709
+ type: T;
2710
+ }
2711
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2712
+ type Binding = string;
2713
+
2714
+ export interface StaticFilter<AnyAsset extends Asset = Asset> {
2715
+ label: AssetWrapper<AnyAsset>;
2716
+ value: AssetWrapper<AnyAsset>;
2717
+ comparator: string;
2718
+ }
2719
+
2720
+ export interface Header<AnyAsset extends Asset = Asset>
2721
+ extends AssetWrapper<AnyAsset> {
2722
+ staticFilters?: Array<StaticFilter<AnyAsset>>;
2723
+ sortable?: boolean;
2724
+ }
2725
+
2726
+ export interface TableColumn<AnyAsset extends Asset = Asset>
2727
+ extends AssetWrapper<AnyAsset> {
2728
+ comparator?: string;
2729
+ }
2730
+
2731
+ export interface Row<AnyAsset extends Asset = Asset> {
2732
+ values: Array<TableColumn<AnyAsset>>;
2733
+ actions?: Array<AssetWrapper<AnyAsset>>;
2734
+ }
2735
+
2736
+ export interface TableAsset<AnyAsset extends Asset = Asset>
2737
+ extends Asset<'table'> {
2738
+ search?: AssetWrapper<AnyAsset>;
2739
+ filters?: Array<AssetWrapper<AnyAsset>>;
2740
+ headers?: { values?: Array<Header<AnyAsset>> };
2741
+ values?: Array<Row<AnyAsset>>;
2742
+ actions?: Array<AssetWrapper<AnyAsset>>;
2743
+ }
2744
+ `;
2745
+
2746
+ const types = convertTsToXLR(source);
2747
+ const findType = (name: string) =>
2748
+ types.find((t) => t.name === name) as NamedType<ObjectType>;
2749
+
2750
+ const headerType = findType("Header");
2751
+ const tableColumnType = findType("TableColumn");
2752
+ const rowType = findType("Row");
2753
+ const staticFilterType = findType("StaticFilter");
2754
+ const tableAssetType = findType("TableAsset");
2755
+
2756
+ const typeRegistry: TypeRegistry = new Map(
2757
+ [headerType, tableColumnType, rowType, staticFilterType]
2758
+ .filter(Boolean)
2759
+ .map((t) => [t.name, t]),
2760
+ );
2761
+ const config: GeneratorConfig = { typeRegistry };
2762
+
2763
+ const tableCode = generateFunctionalBuilder(tableAssetType, config);
2764
+
2765
+ // Direct AssetWrapper paths
2766
+ expect(tableCode).toContain('"search"');
2767
+ expect(tableCode).toContain('"filters"');
2768
+ expect(tableCode).toContain('"actions"');
2769
+
2770
+ // Nested paths through extends-AssetWrapper types
2771
+ // headers.values -> Header extends AssetWrapper
2772
+ expect(tableCode).toMatch(/"headers".*"values"/);
2773
+ // headers.values -> staticFilters.label/value (via array recursion)
2774
+ expect(tableCode).toMatch(/"headers".*"values".*"staticFilters".*"label"/);
2775
+ expect(tableCode).toMatch(/"headers".*"values".*"staticFilters".*"value"/);
2776
+
2777
+ // values.values -> Row.values = Array<TableColumn extends AssetWrapper>
2778
+ expect(tableCode).toMatch(/"values".*"values"/);
2779
+ // values.actions -> Row.actions = Array<AssetWrapper>
2780
+ expect(tableCode).toMatch(/"values".*"actions"/);
2781
+
2782
+ // Type signatures: Header accepted with Asset union
2783
+ expect(tableCode).toContain(
2784
+ "Asset | FunctionalBuilder<Asset, BaseBuildContext>",
2785
+ );
2786
+ expect(tableCode).toContain("Header");
2787
+ });
2788
+ });
2789
+
2790
+ describe("Issue #9: Generic Parameter Leak from Base Type", () => {
2791
+ test("non-generic type extending generic base does not import generic params", () => {
2792
+ const source = `
2793
+ interface Asset<T extends string = string> {
2794
+ id: string;
2795
+ type: T;
2796
+ }
2797
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2798
+
2799
+ export interface FileInputAssetBase<AnyAsset extends Asset = Asset> extends Asset<'fileInput'> {
2800
+ label?: AssetWrapper<AnyAsset>;
2801
+ }
2802
+
2803
+ export interface FileInputAsset extends FileInputAssetBase {
2804
+ uploadTrigger: string;
2805
+ }
2806
+ `;
2807
+
2808
+ const types = convertTsToXLR(source);
2809
+ const base = types.find(
2810
+ (t) => t.name === "FileInputAssetBase",
2811
+ ) as NamedType<ObjectType>;
2812
+ const asset = types.find(
2813
+ (t) => t.name === "FileInputAsset",
2814
+ ) as NamedType<ObjectType>;
2815
+
2816
+ const typeRegistry: TypeRegistry = new Map([["FileInputAssetBase", base]]);
2817
+ const code = generateFunctionalBuilder(asset, { typeRegistry });
2818
+
2819
+ // AnyAsset is a generic param of the base type, not a concrete type to import
2820
+ expect(code).not.toContain("AnyAsset");
2821
+ // The AssetWrapper<AnyAsset> property should resolve to Asset (the default)
2822
+ expect(code).toContain(
2823
+ "Asset | FunctionalBuilder<Asset, BaseBuildContext>",
2824
+ );
2825
+ });
2826
+
2827
+ test("non-generic type extending generic base with multiple params", () => {
2828
+ const source = `
2829
+ interface Asset<T extends string = string> {
2830
+ id: string;
2831
+ type: T;
2832
+ }
2833
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
2834
+
2835
+ export interface MultiGenericBase<AnyLabelAsset extends Asset = Asset, AnyIconAsset extends Asset = Asset> extends Asset<'multi'> {
2836
+ label?: AssetWrapper<AnyLabelAsset>;
2837
+ icon?: AssetWrapper<AnyIconAsset>;
2838
+ }
2839
+
2840
+ export interface ConcreteMulti extends MultiGenericBase {
2841
+ extra: string;
2842
+ }
2843
+ `;
2844
+
2845
+ const types = convertTsToXLR(source);
2846
+ const base = types.find(
2847
+ (t) => t.name === "MultiGenericBase",
2848
+ ) as NamedType<ObjectType>;
2849
+ const asset = types.find(
2850
+ (t) => t.name === "ConcreteMulti",
2851
+ ) as NamedType<ObjectType>;
2852
+
2853
+ const typeRegistry: TypeRegistry = new Map([["MultiGenericBase", base]]);
2854
+ const code = generateFunctionalBuilder(asset, { typeRegistry });
2855
+
2856
+ // Neither generic param should be imported as a concrete type
2857
+ expect(code).not.toContain("AnyLabelAsset");
2858
+ expect(code).not.toContain("AnyIconAsset");
2859
+ });
2860
+ });