@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,934 @@
1
+ import { describe, test, expect, beforeEach } from "vitest";
2
+ import type { NodeType, RefType } from "@xlr-lib/xlr";
3
+ import {
4
+ TypeTransformer,
5
+ type TypeTransformContext,
6
+ } from "../type-transformer";
7
+
8
+ /**
9
+ * Mock implementation of TypeTransformContext for testing
10
+ */
11
+ class MockTypeTransformContext implements TypeTransformContext {
12
+ private needsAssetImport = false;
13
+ private readonly namespaceMemberMap = new Map<string, string>();
14
+ private readonly genericParamSymbols = new Set<string>();
15
+ readonly trackedTypes: string[] = [];
16
+ readonly trackedNamespaces: string[] = [];
17
+
18
+ setNeedsAssetImport(value: boolean): void {
19
+ this.needsAssetImport = value;
20
+ }
21
+
22
+ getNeedsAssetImport(): boolean {
23
+ return this.needsAssetImport;
24
+ }
25
+
26
+ trackReferencedType(typeName: string): void {
27
+ this.trackedTypes.push(typeName);
28
+ }
29
+
30
+ trackNamespaceImport(namespaceName: string): void {
31
+ this.trackedNamespaces.push(namespaceName);
32
+ }
33
+
34
+ getNamespaceMemberMap(): Map<string, string> {
35
+ return this.namespaceMemberMap;
36
+ }
37
+
38
+ getGenericParamSymbols(): Set<string> {
39
+ return this.genericParamSymbols;
40
+ }
41
+
42
+ getAssetWrapperExtendsRef(_typeName: string): RefType | undefined {
43
+ return undefined;
44
+ }
45
+
46
+ addGenericParam(symbol: string): void {
47
+ this.genericParamSymbols.add(symbol);
48
+ }
49
+
50
+ addNamespaceMapping(member: string, fullName: string): void {
51
+ this.namespaceMemberMap.set(member, fullName);
52
+ }
53
+ }
54
+
55
+ describe("TypeTransformer", () => {
56
+ let context: MockTypeTransformContext;
57
+ let transformer: TypeTransformer;
58
+
59
+ beforeEach(() => {
60
+ context = new MockTypeTransformContext();
61
+ transformer = new TypeTransformer(context);
62
+ });
63
+
64
+ describe("Primitive Types", () => {
65
+ test("transforms string type", () => {
66
+ const node: NodeType = { type: "string" };
67
+ expect(transformer.transformType(node, false)).toBe("string");
68
+ expect(transformer.transformType(node, true)).toBe(
69
+ "string | TaggedTemplateValue<string>",
70
+ );
71
+ });
72
+
73
+ test("transforms number type", () => {
74
+ const node: NodeType = { type: "number" };
75
+ expect(transformer.transformType(node, false)).toBe("number");
76
+ expect(transformer.transformType(node, true)).toBe(
77
+ "number | TaggedTemplateValue<number>",
78
+ );
79
+ });
80
+
81
+ test("transforms boolean type", () => {
82
+ const node: NodeType = { type: "boolean" };
83
+ expect(transformer.transformType(node, false)).toBe("boolean");
84
+ expect(transformer.transformType(node, true)).toBe(
85
+ "boolean | TaggedTemplateValue<boolean>",
86
+ );
87
+ });
88
+
89
+ test("transforms string const type", () => {
90
+ const node: NodeType = { type: "string", const: "hello" };
91
+ expect(transformer.transformType(node, false)).toBe('"hello"');
92
+ expect(transformer.transformType(node, true)).toBe('"hello"');
93
+ });
94
+
95
+ test("transforms number const type", () => {
96
+ const node: NodeType = { type: "number", const: 42 };
97
+ expect(transformer.transformType(node, false)).toBe("42");
98
+ expect(transformer.transformType(node, true)).toBe("42");
99
+ });
100
+
101
+ test("transforms boolean const type", () => {
102
+ const node: NodeType = { type: "boolean", const: true };
103
+ expect(transformer.transformType(node, false)).toBe("true");
104
+ expect(transformer.transformType(node, true)).toBe("true");
105
+ });
106
+ });
107
+
108
+ describe("Special Types", () => {
109
+ test("transforms null type", () => {
110
+ const node: NodeType = { type: "null" };
111
+ expect(transformer.transformType(node, false)).toBe("null");
112
+ });
113
+
114
+ test("transforms undefined type", () => {
115
+ const node: NodeType = { type: "undefined" };
116
+ expect(transformer.transformType(node, false)).toBe("undefined");
117
+ });
118
+
119
+ test("transforms any type", () => {
120
+ const node: NodeType = { type: "any" };
121
+ expect(transformer.transformType(node, false)).toBe("any");
122
+ });
123
+
124
+ test("transforms unknown type", () => {
125
+ const node: NodeType = { type: "unknown" };
126
+ expect(transformer.transformType(node, false)).toBe("unknown");
127
+ });
128
+
129
+ test("transforms never type", () => {
130
+ const node: NodeType = { type: "never" };
131
+ expect(transformer.transformType(node, false)).toBe("never");
132
+ });
133
+
134
+ test("transforms void type", () => {
135
+ const node: NodeType = { type: "void" };
136
+ expect(transformer.transformType(node, false)).toBe("void");
137
+ });
138
+ });
139
+
140
+ describe("Ref Types", () => {
141
+ test("transforms AssetWrapper ref to Asset | FunctionalBuilder", () => {
142
+ const node: NodeType = { type: "ref", ref: "AssetWrapper" };
143
+ const result = transformer.transformType(node, false);
144
+ expect(result).toBe("Asset | FunctionalBuilder<Asset, BaseBuildContext>");
145
+ expect(context.getNeedsAssetImport()).toBe(true);
146
+ });
147
+
148
+ test("transforms AssetWrapper with generic arguments preserving the type", () => {
149
+ const node: NodeType = {
150
+ type: "ref",
151
+ ref: "AssetWrapper",
152
+ genericArguments: [{ type: "ref", ref: "ImageAsset" }],
153
+ };
154
+ const result = transformer.transformType(node, false);
155
+ expect(result).toBe(
156
+ "ImageAsset | FunctionalBuilder<ImageAsset, BaseBuildContext>",
157
+ );
158
+ expect(context.getNeedsAssetImport()).toBe(true);
159
+ expect(context.trackedTypes).toContain("ImageAsset");
160
+ });
161
+
162
+ test("transforms AssetWrapper with embedded generic in ref string", () => {
163
+ const node: NodeType = { type: "ref", ref: "AssetWrapper<TextAsset>" };
164
+ const result = transformer.transformType(node, false);
165
+ expect(result).toBe(
166
+ "TextAsset | FunctionalBuilder<TextAsset, BaseBuildContext>",
167
+ );
168
+ expect(context.getNeedsAssetImport()).toBe(true);
169
+ expect(context.trackedTypes).toContain("TextAsset");
170
+ });
171
+
172
+ test("transforms AssetWrapper with embedded intersection in ref string", () => {
173
+ const node: NodeType = {
174
+ type: "ref",
175
+ ref: "AssetWrapper<ImageAsset & Trackable>",
176
+ };
177
+ const result = transformer.transformType(node, false);
178
+ expect(result).toBe(
179
+ "ImageAsset & Trackable | FunctionalBuilder<ImageAsset & Trackable, BaseBuildContext>",
180
+ );
181
+ expect(context.getNeedsAssetImport()).toBe(true);
182
+ // Each part of the intersection should be tracked separately for imports
183
+ expect(context.trackedTypes).toContain("ImageAsset");
184
+ expect(context.trackedTypes).toContain("Trackable");
185
+ // The combined string should NOT be tracked
186
+ expect(context.trackedTypes).not.toContain("ImageAsset & Trackable");
187
+ });
188
+
189
+ test("transforms AssetWrapper with generic param falls back to Asset", () => {
190
+ context.addGenericParam("AnyAsset");
191
+ const node: NodeType = {
192
+ type: "ref",
193
+ ref: "AssetWrapper",
194
+ genericArguments: [{ type: "ref", ref: "AnyAsset" }],
195
+ };
196
+ const result = transformer.transformType(node, false);
197
+ // Should fall back to Asset because AnyAsset is a generic param
198
+ expect(result).toBe("Asset | FunctionalBuilder<Asset, BaseBuildContext>");
199
+ expect(context.getNeedsAssetImport()).toBe(true);
200
+ // Should NOT track AnyAsset as a type to import
201
+ expect(context.trackedTypes).not.toContain("AnyAsset");
202
+ });
203
+
204
+ test("transforms AssetWrapper with intersection generic argument and tracks parts separately", () => {
205
+ const node: NodeType = {
206
+ type: "ref",
207
+ ref: "AssetWrapper",
208
+ genericArguments: [
209
+ {
210
+ type: "and",
211
+ and: [
212
+ { type: "ref", ref: "ImageAsset" },
213
+ { type: "ref", ref: "Trackable" },
214
+ ],
215
+ },
216
+ ],
217
+ };
218
+ const result = transformer.transformType(node, false);
219
+ expect(result).toBe(
220
+ "ImageAsset & Trackable | FunctionalBuilder<ImageAsset & Trackable, BaseBuildContext>",
221
+ );
222
+ expect(context.getNeedsAssetImport()).toBe(true);
223
+ // Each part of the intersection should be tracked separately
224
+ expect(context.trackedTypes).toContain("ImageAsset");
225
+ expect(context.trackedTypes).toContain("Trackable");
226
+ // The intersection string itself should NOT be tracked
227
+ expect(context.trackedTypes).not.toContain("ImageAsset & Trackable");
228
+ });
229
+
230
+ test("does not double-track intersection type parts", () => {
231
+ const node: NodeType = {
232
+ type: "ref",
233
+ ref: "AssetWrapper",
234
+ genericArguments: [
235
+ {
236
+ type: "and",
237
+ and: [
238
+ { type: "ref", ref: "ImageAsset" },
239
+ { type: "ref", ref: "Trackable" },
240
+ ],
241
+ },
242
+ ],
243
+ };
244
+ transformer.transformType(node, false);
245
+ // Each type should be tracked exactly once, not twice
246
+ const imageAssetCount = context.trackedTypes.filter(
247
+ (t) => t === "ImageAsset",
248
+ ).length;
249
+ const trackableCount = context.trackedTypes.filter(
250
+ (t) => t === "Trackable",
251
+ ).length;
252
+ expect(imageAssetCount).toBe(1);
253
+ expect(trackableCount).toBe(1);
254
+ });
255
+
256
+ test("handles intersection with Asset type (skips Asset in tracking)", () => {
257
+ const node: NodeType = {
258
+ type: "ref",
259
+ ref: "AssetWrapper",
260
+ genericArguments: [
261
+ {
262
+ type: "and",
263
+ and: [
264
+ { type: "ref", ref: "AnyAsset" },
265
+ { type: "ref", ref: "Asset" },
266
+ ],
267
+ },
268
+ ],
269
+ };
270
+ context.addGenericParam("AnyAsset");
271
+ transformer.transformType(node, false);
272
+ // Asset should not be tracked (it's special-cased)
273
+ // AnyAsset should not be tracked (it's a generic param)
274
+ expect(context.trackedTypes).not.toContain("Asset");
275
+ expect(context.trackedTypes).not.toContain("AnyAsset");
276
+ });
277
+
278
+ test("handles intersection with generic param (skips generic params in tracking)", () => {
279
+ context.addGenericParam("T");
280
+ const node: NodeType = {
281
+ type: "ref",
282
+ ref: "AssetWrapper",
283
+ genericArguments: [
284
+ {
285
+ type: "and",
286
+ and: [
287
+ { type: "ref", ref: "T" },
288
+ { type: "ref", ref: "Mixin" },
289
+ ],
290
+ },
291
+ ],
292
+ };
293
+ transformer.transformType(node, false);
294
+ // T should not be tracked (it's a generic param)
295
+ expect(context.trackedTypes).not.toContain("T");
296
+ // Mixin should be tracked
297
+ expect(context.trackedTypes).toContain("Mixin");
298
+ });
299
+
300
+ test("handles three-way intersection in genericArguments", () => {
301
+ const node: NodeType = {
302
+ type: "ref",
303
+ ref: "AssetWrapper",
304
+ genericArguments: [
305
+ {
306
+ type: "and",
307
+ and: [
308
+ { type: "ref", ref: "TypeA" },
309
+ { type: "ref", ref: "TypeB" },
310
+ { type: "ref", ref: "TypeC" },
311
+ ],
312
+ },
313
+ ],
314
+ };
315
+ const result = transformer.transformType(node, false);
316
+ expect(result).toBe(
317
+ "TypeA & TypeB & TypeC | FunctionalBuilder<TypeA & TypeB & TypeC, BaseBuildContext>",
318
+ );
319
+ expect(context.trackedTypes).toContain("TypeA");
320
+ expect(context.trackedTypes).toContain("TypeB");
321
+ expect(context.trackedTypes).toContain("TypeC");
322
+ });
323
+
324
+ test("handles three-way intersection in embedded ref string", () => {
325
+ const node: NodeType = {
326
+ type: "ref",
327
+ ref: "AssetWrapper<TypeA & TypeB & TypeC>",
328
+ };
329
+ const result = transformer.transformType(node, false);
330
+ expect(result).toBe(
331
+ "TypeA & TypeB & TypeC | FunctionalBuilder<TypeA & TypeB & TypeC, BaseBuildContext>",
332
+ );
333
+ expect(context.trackedTypes).toContain("TypeA");
334
+ expect(context.trackedTypes).toContain("TypeB");
335
+ expect(context.trackedTypes).toContain("TypeC");
336
+ expect(context.trackedTypes).not.toContain("TypeA & TypeB & TypeC");
337
+ });
338
+
339
+ test("transforms Expression ref to string with TaggedTemplateValue", () => {
340
+ const node: NodeType = { type: "ref", ref: "Expression" };
341
+ expect(transformer.transformType(node, false)).toBe("string");
342
+ expect(transformer.transformType(node, true)).toBe(
343
+ "string | TaggedTemplateValue<string>",
344
+ );
345
+ });
346
+
347
+ test("transforms Binding ref to string with TaggedTemplateValue", () => {
348
+ const node: NodeType = { type: "ref", ref: "Binding" };
349
+ expect(transformer.transformType(node, false)).toBe("string");
350
+ expect(transformer.transformType(node, true)).toBe(
351
+ "string | TaggedTemplateValue<string>",
352
+ );
353
+ });
354
+
355
+ test("transforms Asset ref", () => {
356
+ const node: NodeType = { type: "ref", ref: "Asset" };
357
+ const result = transformer.transformType(node, false);
358
+ expect(result).toBe("Asset");
359
+ expect(context.getNeedsAssetImport()).toBe(true);
360
+ });
361
+
362
+ test("transforms custom ref type with FunctionalBuilder", () => {
363
+ const node: NodeType = { type: "ref", ref: "CustomType" };
364
+ const result = transformer.transformType(node, false);
365
+ expect(result).toBe(
366
+ "CustomType | FunctionalBuilder<CustomType, BaseBuildContext> | FunctionalPartial<CustomType, BaseBuildContext>",
367
+ );
368
+ });
369
+
370
+ test("transforms ref with generic arguments", () => {
371
+ const node: NodeType = {
372
+ type: "ref",
373
+ ref: "Container",
374
+ genericArguments: [{ type: "string" }],
375
+ };
376
+ const result = transformer.transformType(node, true);
377
+ expect(result).toBe(
378
+ "Container<string | TaggedTemplateValue<string>> | FunctionalBuilder<Container<string | TaggedTemplateValue<string>>, BaseBuildContext> | FunctionalPartial<Container<string | TaggedTemplateValue<string>>, BaseBuildContext>",
379
+ );
380
+ });
381
+
382
+ test("transforms ref with embedded generics", () => {
383
+ const node: NodeType = { type: "ref", ref: "SimpleModifier<'format'>" };
384
+ const result = transformer.transformType(node, false);
385
+ expect(result).toBe(
386
+ "SimpleModifier<'format'> | FunctionalBuilder<SimpleModifier<'format'>, BaseBuildContext> | FunctionalPartial<SimpleModifier<'format'>, BaseBuildContext>",
387
+ );
388
+ });
389
+ });
390
+
391
+ describe("Array Types", () => {
392
+ test("transforms array of primitives", () => {
393
+ const node: NodeType = {
394
+ type: "array",
395
+ elementType: { type: "string" },
396
+ };
397
+ const result = transformer.transformType(node, true);
398
+ expect(result).toBe("Array<string | TaggedTemplateValue<string>>");
399
+ });
400
+
401
+ test("transforms array of refs", () => {
402
+ const node: NodeType = {
403
+ type: "array",
404
+ elementType: { type: "ref", ref: "Asset" },
405
+ };
406
+ const result = transformer.transformType(node, false);
407
+ expect(result).toBe("Array<Asset>");
408
+ });
409
+ });
410
+
411
+ describe("Record Types", () => {
412
+ test("transforms Record type", () => {
413
+ const node: NodeType = {
414
+ type: "record",
415
+ keyType: { type: "string" },
416
+ valueType: { type: "number" },
417
+ };
418
+ const result = transformer.transformType(node, true);
419
+ // Key type should NOT have TaggedTemplateValue
420
+ expect(result).toBe(
421
+ "Record<string, number | TaggedTemplateValue<number>>",
422
+ );
423
+ });
424
+ });
425
+
426
+ describe("Union Types (Or)", () => {
427
+ test("transforms union of primitives", () => {
428
+ const node: NodeType = {
429
+ type: "or",
430
+ or: [{ type: "string" }, { type: "number" }],
431
+ };
432
+ const result = transformer.transformType(node, true);
433
+ expect(result).toBe(
434
+ "string | TaggedTemplateValue<string> | number | TaggedTemplateValue<number>",
435
+ );
436
+ });
437
+
438
+ test("transforms union of string literals", () => {
439
+ const node: NodeType = {
440
+ type: "or",
441
+ or: [
442
+ { type: "string", const: "small" },
443
+ { type: "string", const: "medium" },
444
+ { type: "string", const: "large" },
445
+ ],
446
+ };
447
+ const result = transformer.transformType(node, false);
448
+ expect(result).toBe('"small" | "medium" | "large"');
449
+ });
450
+ });
451
+
452
+ describe("Intersection Types (And)", () => {
453
+ test("transforms intersection of types", () => {
454
+ const node: NodeType = {
455
+ type: "and",
456
+ and: [
457
+ { type: "ref", ref: "BaseProps" },
458
+ { type: "ref", ref: "ExtendedProps" },
459
+ ],
460
+ };
461
+ const result = transformer.transformType(node, false);
462
+ expect(result).toContain("BaseProps");
463
+ expect(result).toContain("ExtendedProps");
464
+ expect(result).toContain("&");
465
+ });
466
+ });
467
+
468
+ describe("Object Types", () => {
469
+ test("transforms anonymous object type with properties", () => {
470
+ const node: NodeType = {
471
+ type: "object",
472
+ properties: {
473
+ name: { required: true, node: { type: "string" } },
474
+ },
475
+ };
476
+ const result = transformer.transformType(node, false);
477
+ expect(result).toContain("name: string");
478
+ expect(result).toContain("FunctionalBuilder");
479
+ });
480
+
481
+ test("transforms anonymous object type", () => {
482
+ const node: NodeType = {
483
+ type: "object",
484
+ properties: {
485
+ name: { required: true, node: { type: "string" } },
486
+ count: { required: false, node: { type: "number" } },
487
+ },
488
+ };
489
+ const result = transformer.transformType(node, true);
490
+ expect(result).toContain("name: string | TaggedTemplateValue<string>");
491
+ expect(result).toContain("count?: number | TaggedTemplateValue<number>");
492
+ expect(result).toContain("| FunctionalBuilder<{");
493
+ });
494
+ });
495
+
496
+ describe("transformTypeForConstraint", () => {
497
+ test("returns raw type without FunctionalBuilder for constraints", () => {
498
+ const node: NodeType = { type: "ref", ref: "Bar" };
499
+ const result = transformer.transformTypeForConstraint(node);
500
+ expect(result).toBe("Bar");
501
+ expect(result).not.toContain("FunctionalBuilder");
502
+ });
503
+
504
+ test("handles generic ref in constraints", () => {
505
+ const node: NodeType = {
506
+ type: "ref",
507
+ ref: "ListItemNoHelp",
508
+ genericArguments: [{ type: "ref", ref: "AnyAsset" }],
509
+ };
510
+ const result = transformer.transformTypeForConstraint(node);
511
+ expect(result).toBe("ListItemNoHelp<AnyAsset>");
512
+ expect(result).not.toContain("FunctionalBuilder");
513
+ });
514
+
515
+ test("handles Asset type in constraints", () => {
516
+ const node: NodeType = { type: "ref", ref: "Asset" };
517
+ const result = transformer.transformTypeForConstraint(node);
518
+ expect(result).toBe("Asset");
519
+ expect(context.getNeedsAssetImport()).toBe(true);
520
+ });
521
+
522
+ test("handles object type in constraints", () => {
523
+ const node: NodeType = {
524
+ type: "object",
525
+ properties: {
526
+ value: { required: true, node: { type: "number" } },
527
+ },
528
+ };
529
+ const result = transformer.transformTypeForConstraint(node);
530
+ // Anonymous objects in constraints still get inline treatment
531
+ expect(result).toContain("value: number");
532
+ });
533
+
534
+ test("handles array type in constraints", () => {
535
+ const node: NodeType = {
536
+ type: "array",
537
+ elementType: { type: "ref", ref: "Item" },
538
+ };
539
+ const result = transformer.transformTypeForConstraint(node);
540
+ expect(result).toBe("Array<Item>");
541
+ });
542
+
543
+ test("handles union type in constraints", () => {
544
+ const node: NodeType = {
545
+ type: "or",
546
+ or: [
547
+ { type: "ref", ref: "TypeA" },
548
+ { type: "ref", ref: "TypeB" },
549
+ ],
550
+ };
551
+ const result = transformer.transformTypeForConstraint(node);
552
+ expect(result).toBe("TypeA | TypeB");
553
+ });
554
+ });
555
+
556
+ describe("FunctionalPartial Support for Nested Builders", () => {
557
+ test("includes FunctionalPartial for named object types", () => {
558
+ // Named types use ref, not object with name property
559
+ const node: NodeType = { type: "ref", ref: "Foo" };
560
+ const result = transformer.transformType(node, false);
561
+ expect(result).toBe(
562
+ "Foo | FunctionalBuilder<Foo, BaseBuildContext> | FunctionalPartial<Foo, BaseBuildContext>",
563
+ );
564
+ });
565
+
566
+ test("includes FunctionalPartial for anonymous object types", () => {
567
+ const node: NodeType = {
568
+ type: "object",
569
+ properties: {
570
+ value: { required: true, node: { type: "string" } },
571
+ },
572
+ };
573
+ const result = transformer.transformType(node, false);
574
+ expect(result).toContain(
575
+ "| FunctionalPartial<{ value: string }, BaseBuildContext>",
576
+ );
577
+ });
578
+
579
+ test("includes FunctionalPartial for ref types", () => {
580
+ const node: NodeType = { type: "ref", ref: "CustomType" };
581
+ const result = transformer.transformType(node, false);
582
+ expect(result).toBe(
583
+ "CustomType | FunctionalBuilder<CustomType, BaseBuildContext> | FunctionalPartial<CustomType, BaseBuildContext>",
584
+ );
585
+ });
586
+
587
+ test("includes FunctionalPartial for ref types with generic arguments", () => {
588
+ const node: NodeType = {
589
+ type: "ref",
590
+ ref: "Container",
591
+ genericArguments: [{ type: "string" }],
592
+ };
593
+ const result = transformer.transformType(node, true);
594
+ expect(result).toContain(
595
+ "| FunctionalPartial<Container<string | TaggedTemplateValue<string>>, BaseBuildContext>",
596
+ );
597
+ });
598
+
599
+ test("includes FunctionalPartial for ref types with embedded generics", () => {
600
+ const node: NodeType = { type: "ref", ref: "SimpleModifier<'format'>" };
601
+ const result = transformer.transformType(node, false);
602
+ expect(result).toBe(
603
+ "SimpleModifier<'format'> | FunctionalBuilder<SimpleModifier<'format'>, BaseBuildContext> | FunctionalPartial<SimpleModifier<'format'>, BaseBuildContext>",
604
+ );
605
+ });
606
+
607
+ test("does not include FunctionalPartial for AssetWrapper (unwrapped to Asset)", () => {
608
+ const node: NodeType = { type: "ref", ref: "AssetWrapper" };
609
+ const result = transformer.transformType(node, false);
610
+ // AssetWrapper is unwrapped to Asset | FunctionalBuilder<Asset>, no FunctionalPartial
611
+ expect(result).toBe("Asset | FunctionalBuilder<Asset, BaseBuildContext>");
612
+ expect(result).not.toContain("FunctionalPartial");
613
+ });
614
+ });
615
+
616
+ describe("Namespace Type Resolution", () => {
617
+ test("resolves namespaced type member to full qualified name", () => {
618
+ context.addNamespaceMapping(
619
+ "CrossfieldReference",
620
+ "Validation.CrossfieldReference",
621
+ );
622
+
623
+ const node: NodeType = { type: "ref", ref: "CrossfieldReference" };
624
+ const result = transformer.transformType(node, false);
625
+ expect(result).toContain("Validation.CrossfieldReference");
626
+ });
627
+ });
628
+
629
+ describe("Generic Parameter Tracking", () => {
630
+ test("does not track generic parameters as types to import", () => {
631
+ context.addGenericParam("T");
632
+ context.addGenericParam("AnyAsset");
633
+
634
+ const node: NodeType = { type: "ref", ref: "AnyAsset" };
635
+ transformer.transformTypeForConstraint(node);
636
+
637
+ // AnyAsset should not be tracked since it's a generic param
638
+ expect(context.trackedTypes).not.toContain("AnyAsset");
639
+ });
640
+
641
+ test("tracks non-generic-param types for import", () => {
642
+ const node: NodeType = { type: "ref", ref: "CustomType" };
643
+ transformer.transformTypeForConstraint(node);
644
+
645
+ expect(context.trackedTypes).toContain("CustomType");
646
+ });
647
+ });
648
+
649
+ describe("Property Name Quoting", () => {
650
+ test("quotes property names with special characters", () => {
651
+ const node: NodeType = {
652
+ type: "object",
653
+ properties: {
654
+ "mime-type": { required: true, node: { type: "string" } },
655
+ normalProp: { required: true, node: { type: "string" } },
656
+ },
657
+ };
658
+ const result = transformer.generateInlineObjectType(node, false);
659
+ expect(result).toContain('"mime-type"');
660
+ expect(result).not.toContain('"normalProp"');
661
+ expect(result).toContain("normalProp:");
662
+ });
663
+
664
+ test("quotes property names with dots", () => {
665
+ const node: NodeType = {
666
+ type: "object",
667
+ properties: {
668
+ "data.path": { required: true, node: { type: "string" } },
669
+ },
670
+ };
671
+ const result = transformer.generateInlineObjectType(node, false);
672
+ expect(result).toContain('"data.path"');
673
+ });
674
+
675
+ test("quotes property names with spaces", () => {
676
+ const node: NodeType = {
677
+ type: "object",
678
+ properties: {
679
+ "my property": { required: true, node: { type: "string" } },
680
+ },
681
+ };
682
+ const result = transformer.generateInlineObjectType(node, false);
683
+ expect(result).toContain('"my property"');
684
+ });
685
+
686
+ test("does not quote reserved words (valid in TypeScript)", () => {
687
+ const node: NodeType = {
688
+ type: "object",
689
+ properties: {
690
+ class: { required: true, node: { type: "string" } },
691
+ default: { required: false, node: { type: "number" } },
692
+ },
693
+ };
694
+ const result = transformer.generateInlineObjectType(node, false);
695
+ // Reserved words are valid property names in TypeScript without quotes
696
+ expect(result).toContain("class:");
697
+ expect(result).toContain("default?:");
698
+ expect(result).not.toContain('"class"');
699
+ expect(result).not.toContain('"default"');
700
+ });
701
+ });
702
+
703
+ describe("Generic Parameter Handling (Edge Cases)", () => {
704
+ test("handles generic with extends constraint", () => {
705
+ context.addGenericParam("T");
706
+
707
+ const node: NodeType = { type: "ref", ref: "T" };
708
+ const result = transformer.transformTypeForConstraint(node);
709
+
710
+ // T is a generic param, should return as-is
711
+ expect(result).toBe("T");
712
+ expect(context.trackedTypes).not.toContain("T");
713
+ });
714
+
715
+ test("handles generic with extends and default", () => {
716
+ context.addGenericParam("T");
717
+ context.addGenericParam("U");
718
+
719
+ const node: NodeType = {
720
+ type: "ref",
721
+ ref: "Container",
722
+ genericArguments: [
723
+ { type: "ref", ref: "T" },
724
+ { type: "ref", ref: "U" },
725
+ ],
726
+ };
727
+ const result = transformer.transformTypeForConstraint(node);
728
+
729
+ expect(result).toBe("Container<T, U>");
730
+ expect(context.trackedTypes).not.toContain("T");
731
+ expect(context.trackedTypes).not.toContain("U");
732
+ expect(context.trackedTypes).toContain("Container");
733
+ });
734
+
735
+ test("handles Record with generic param value type in constraints", () => {
736
+ context.addGenericParam("T");
737
+ context.addGenericParam("U");
738
+
739
+ const node: NodeType = {
740
+ type: "record",
741
+ keyType: { type: "string" },
742
+ valueType: { type: "ref", ref: "U" },
743
+ };
744
+ const result = transformer.transformTypeForConstraint(node);
745
+
746
+ // Record falls through to transformType which adds FunctionalBuilder and FunctionalPartial union for ref types
747
+ expect(result).toBe(
748
+ "Record<string, U | FunctionalBuilder<U, BaseBuildContext> | FunctionalPartial<U, BaseBuildContext>>",
749
+ );
750
+ });
751
+
752
+ test("deduplicates identical generic parameters", () => {
753
+ context.addGenericParam("T");
754
+
755
+ // Using T in multiple places
756
+ const node: NodeType = {
757
+ type: "or",
758
+ or: [
759
+ { type: "ref", ref: "T" },
760
+ { type: "array", elementType: { type: "ref", ref: "T" } },
761
+ ],
762
+ };
763
+ const result = transformer.transformTypeForConstraint(node);
764
+
765
+ expect(result).toContain("T");
766
+ });
767
+ });
768
+
769
+ describe("Deeply Nested Object Types", () => {
770
+ test("handles deeply nested object types (5+ levels)", () => {
771
+ const node: NodeType = {
772
+ type: "object",
773
+ properties: {
774
+ level1: {
775
+ required: true,
776
+ node: {
777
+ type: "object",
778
+ properties: {
779
+ level2: {
780
+ required: true,
781
+ node: {
782
+ type: "object",
783
+ properties: {
784
+ level3: {
785
+ required: true,
786
+ node: {
787
+ type: "object",
788
+ properties: {
789
+ level4: {
790
+ required: true,
791
+ node: {
792
+ type: "object",
793
+ properties: {
794
+ level5: {
795
+ required: true,
796
+ node: { type: "string" },
797
+ },
798
+ },
799
+ },
800
+ },
801
+ },
802
+ },
803
+ },
804
+ },
805
+ },
806
+ },
807
+ },
808
+ },
809
+ },
810
+ },
811
+ };
812
+
813
+ const result = transformer.generateInlineObjectType(node, false);
814
+ expect(result).toContain("level1:");
815
+ expect(result).toContain("level2:");
816
+ expect(result).toContain("level3:");
817
+ expect(result).toContain("level4:");
818
+ expect(result).toContain("level5:");
819
+ expect(result).toContain("string");
820
+ });
821
+ });
822
+
823
+ describe("Tuple Types", () => {
824
+ test("transforms basic tuple type", () => {
825
+ const node: NodeType = {
826
+ type: "tuple",
827
+ elementTypes: [
828
+ { type: { type: "string" } },
829
+ { type: { type: "number" } },
830
+ { type: { type: "boolean" } },
831
+ ],
832
+ minItems: 3,
833
+ additionalItems: false,
834
+ };
835
+ const result = transformer.transformType(node, false);
836
+ expect(result).toBe("[string, number, boolean]");
837
+ });
838
+
839
+ test("transforms tuple type with TaggedTemplateValue", () => {
840
+ const node: NodeType = {
841
+ type: "tuple",
842
+ elementTypes: [
843
+ { type: { type: "string" } },
844
+ { type: { type: "number" } },
845
+ ],
846
+ minItems: 2,
847
+ additionalItems: false,
848
+ };
849
+ const result = transformer.transformType(node, true);
850
+ expect(result).toBe(
851
+ "[string | TaggedTemplateValue<string>, number | TaggedTemplateValue<number>]",
852
+ );
853
+ });
854
+
855
+ test("transforms tuple type with optional elements", () => {
856
+ const node: NodeType = {
857
+ type: "tuple",
858
+ elementTypes: [
859
+ { type: { type: "string" } },
860
+ { type: { type: "number" }, optional: true },
861
+ { type: { type: "boolean" }, optional: true },
862
+ ],
863
+ minItems: 1,
864
+ additionalItems: false,
865
+ };
866
+ const result = transformer.transformType(node, false);
867
+ expect(result).toBe("[string, number?, boolean?]");
868
+ });
869
+
870
+ test("transforms tuple type with rest element", () => {
871
+ const node: NodeType = {
872
+ type: "tuple",
873
+ elementTypes: [
874
+ { type: { type: "string" } },
875
+ { type: { type: "number" } },
876
+ ],
877
+ minItems: 2,
878
+ additionalItems: { type: "boolean" },
879
+ };
880
+ const result = transformer.transformType(node, false);
881
+ expect(result).toBe("[string, number, ...boolean[]]");
882
+ });
883
+
884
+ test("transforms tuple type with ref elements", () => {
885
+ const node: NodeType = {
886
+ type: "tuple",
887
+ elementTypes: [
888
+ { type: { type: "ref", ref: "Asset" } },
889
+ { type: { type: "ref", ref: "CustomType" } },
890
+ ],
891
+ minItems: 2,
892
+ additionalItems: false,
893
+ };
894
+ const result = transformer.transformType(node, false);
895
+ expect(result).toBe(
896
+ "[Asset, CustomType | FunctionalBuilder<CustomType, BaseBuildContext> | FunctionalPartial<CustomType, BaseBuildContext>]",
897
+ );
898
+ expect(context.getNeedsAssetImport()).toBe(true);
899
+ });
900
+
901
+ test("transformTypeForConstraint handles tuple types", () => {
902
+ const node: NodeType = {
903
+ type: "tuple",
904
+ elementTypes: [
905
+ { type: { type: "string" } },
906
+ { type: { type: "ref", ref: "Item" } },
907
+ ],
908
+ minItems: 2,
909
+ additionalItems: false,
910
+ };
911
+ const result = transformer.transformTypeForConstraint(node);
912
+ // Constraints should not have FunctionalBuilder unions
913
+ expect(result).toBe("[string, Item]");
914
+ });
915
+ });
916
+
917
+ describe("Object additionalProperties Handling", () => {
918
+ test("ignores additionalProperties in object types", () => {
919
+ // additionalProperties (index signatures) are not supported in inline object generation
920
+ const node: NodeType = {
921
+ type: "object",
922
+ properties: {
923
+ name: { required: true, node: { type: "string" } },
924
+ },
925
+ additionalProperties: { type: "string" },
926
+ };
927
+
928
+ const result = transformer.generateInlineObjectType(node, false);
929
+ // Only explicit properties are included, additionalProperties is ignored
930
+ expect(result).toContain("name: string");
931
+ expect(result).not.toContain("[key:");
932
+ });
933
+ });
934
+ });