@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,487 @@
1
+ import type { NodeType, ObjectType, RefType } from "@xlr-lib/xlr";
2
+ import {
3
+ isStringType,
4
+ isNumberType,
5
+ isBooleanType,
6
+ isObjectType,
7
+ isArrayType,
8
+ isRefType,
9
+ isOrType,
10
+ isAndType,
11
+ isRecordType,
12
+ isNamedType,
13
+ isTupleType,
14
+ isPrimitiveConst,
15
+ isBuiltinType,
16
+ extractBaseName,
17
+ parseNamespacedType,
18
+ } from "./utils";
19
+
20
+ /**
21
+ * Interface for tracking Asset import needs and namespace mappings.
22
+ */
23
+ export interface TypeTransformContext {
24
+ /** Set to true when Asset type needs to be imported */
25
+ setNeedsAssetImport(value: boolean): void;
26
+ /** Get the current Asset import need state */
27
+ getNeedsAssetImport(): boolean;
28
+ /** Track a referenced type for import */
29
+ trackReferencedType(typeName: string): void;
30
+ /** Track a namespace import */
31
+ trackNamespaceImport(namespaceName: string): void;
32
+ /** Get the namespace member map for type resolution */
33
+ getNamespaceMemberMap(): Map<string, string>;
34
+ /** Get the generic parameter symbols */
35
+ getGenericParamSymbols(): Set<string>;
36
+ /** Get the AssetWrapper ancestor's RefType for a type extending AssetWrapper (via registry) */
37
+ getAssetWrapperExtendsRef(typeName: string): RefType | undefined;
38
+ }
39
+
40
+ /**
41
+ * Transforms XLR types to TypeScript type strings.
42
+ */
43
+ export class TypeTransformer {
44
+ private readonly context: TypeTransformContext;
45
+
46
+ constructor(context: TypeTransformContext) {
47
+ this.context = context;
48
+ }
49
+
50
+ /**
51
+ * Determines if a type name should be tracked for import.
52
+ * A type should be tracked if it's not a generic parameter and not a builtin type.
53
+ * Note: PLAYER_BUILTINS (Asset, AssetWrapper, Binding, Expression) are filtered
54
+ * by isBuiltinType(), so no explicit Asset check is needed here.
55
+ */
56
+ private shouldTrackTypeForImport(typeName: string): boolean {
57
+ return (
58
+ !this.context.getGenericParamSymbols().has(typeName) &&
59
+ !isBuiltinType(typeName)
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Transform an XLR type to a TypeScript type string.
65
+ * This is the core recursive transformation that adds TaggedTemplateValue support.
66
+ */
67
+ transformType(node: NodeType, forParameter = false): string {
68
+ // Primitive types get TaggedTemplateValue support
69
+ if (isStringType(node)) {
70
+ if (isPrimitiveConst(node)) {
71
+ return `"${node.const}"`;
72
+ }
73
+ return forParameter ? "string | TaggedTemplateValue<string>" : "string";
74
+ }
75
+
76
+ if (isNumberType(node)) {
77
+ if (isPrimitiveConst(node)) {
78
+ return `${node.const}`;
79
+ }
80
+ return forParameter ? "number | TaggedTemplateValue<number>" : "number";
81
+ }
82
+
83
+ if (isBooleanType(node)) {
84
+ if (isPrimitiveConst(node)) {
85
+ return `${node.const}`;
86
+ }
87
+ return forParameter
88
+ ? "boolean | TaggedTemplateValue<boolean>"
89
+ : "boolean";
90
+ }
91
+
92
+ // Reference types
93
+ if (isRefType(node)) {
94
+ return this.transformRefType(node, forParameter);
95
+ }
96
+
97
+ // Array types
98
+ if (isArrayType(node)) {
99
+ const elementType = this.transformType(node.elementType, forParameter);
100
+ return `Array<${elementType}>`;
101
+ }
102
+
103
+ // Union types
104
+ if (isOrType(node)) {
105
+ const variants = node.or.map((v) => this.transformType(v, forParameter));
106
+ return variants.join(" | ");
107
+ }
108
+
109
+ // Intersection types
110
+ if (isAndType(node)) {
111
+ const parts = node.and.map((p) => this.transformType(p, forParameter));
112
+ return parts.join(" & ");
113
+ }
114
+
115
+ // Record types - key type should NOT have TaggedTemplateValue since
116
+ // TypeScript Record keys can only be string | number | symbol
117
+ if (isRecordType(node)) {
118
+ const keyType = this.transformType(node.keyType, false);
119
+ const valueType = this.transformType(node.valueType, forParameter);
120
+ return `Record<${keyType}, ${valueType}>`;
121
+ }
122
+
123
+ // Object types - transform properties recursively
124
+ // Any nested object can accept either a raw object OR a FunctionalBuilder that produces it
125
+ if (isObjectType(node)) {
126
+ if (isNamedType(node)) {
127
+ // Resolve to full qualified name if it's a namespace member
128
+ const typeName = this.resolveTypeName(node.name);
129
+
130
+ // Check if this named type extends AssetWrapper:
131
+ // 1. Inline ObjectType with extends field directly pointing to AssetWrapper
132
+ // 2. Transitive extension via registry (e.g., ListItem → ListItemBase → AssetWrapper)
133
+ const inlineExtendsRef = node.extends?.ref.startsWith("AssetWrapper")
134
+ ? node.extends
135
+ : null;
136
+ const extendsRef =
137
+ inlineExtendsRef ?? this.context.getAssetWrapperExtendsRef(node.name);
138
+
139
+ if (extendsRef) {
140
+ return this.transformAssetWrapperExtension(
141
+ typeName,
142
+ node.name,
143
+ extendsRef,
144
+ );
145
+ }
146
+
147
+ // Named type - accept raw type, a builder that produces it, or a partial with nested builders
148
+ return `${typeName} | FunctionalBuilder<${typeName}, BaseBuildContext> | FunctionalPartial<${typeName}, BaseBuildContext>`;
149
+ }
150
+
151
+ // Anonymous object - accept inline type, a builder that produces it, or a partial with nested builders
152
+ const inlineType = this.generateInlineObjectType(node, forParameter);
153
+ return `${inlineType} | FunctionalBuilder<${inlineType}, BaseBuildContext> | FunctionalPartial<${inlineType}, BaseBuildContext>`;
154
+ }
155
+
156
+ // Tuple types - transform to TypeScript tuple syntax [T1, T2, ...]
157
+ if (isTupleType(node)) {
158
+ const elements = node.elementTypes.map((member) => {
159
+ const elementType = this.transformType(member.type, forParameter);
160
+ return member.optional ? `${elementType}?` : elementType;
161
+ });
162
+
163
+ // Handle rest elements (additionalItems)
164
+ // additionalItems is either false or a NodeType; truthy check suffices
165
+ if (node.additionalItems) {
166
+ const restType = this.transformType(node.additionalItems, forParameter);
167
+ elements.push(`...${restType}[]`);
168
+ }
169
+
170
+ return `[${elements.join(", ")}]`;
171
+ }
172
+
173
+ // Handle other primitive types
174
+ if (node.type === "null") return "null";
175
+ if (node.type === "undefined") return "undefined";
176
+ if (node.type === "any") return "any";
177
+ if (node.type === "unknown") return "unknown";
178
+ if (node.type === "never") return "never";
179
+ if (node.type === "void") return "void";
180
+
181
+ // Default fallback
182
+ return "unknown";
183
+ }
184
+
185
+ /**
186
+ * Transform a type for use in generic constraints and defaults.
187
+ * Unlike transformType(), this returns raw type names without FunctionalBuilder unions,
188
+ * since constraints define type bounds, not parameter types that accept builders.
189
+ *
190
+ * @param node - The type node to transform
191
+ * @returns The raw TypeScript type string
192
+ */
193
+ transformTypeForConstraint(node: NodeType): string {
194
+ if (isRefType(node)) {
195
+ const baseName = extractBaseName(node.ref);
196
+
197
+ // Check if this is a namespaced type (e.g., "Validation.CrossfieldReference")
198
+ const namespaced = parseNamespacedType(baseName);
199
+ if (namespaced) {
200
+ // Track the namespace for import and the member mapping
201
+ this.context.trackNamespaceImport(namespaced.namespace);
202
+ this.context.getNamespaceMemberMap().set(namespaced.member, baseName);
203
+ } else if (baseName === "Asset" || node.ref.startsWith("Asset<")) {
204
+ // Track Asset import when used in generic constraints
205
+ this.context.setNeedsAssetImport(true);
206
+ } else if (this.shouldTrackTypeForImport(baseName)) {
207
+ this.context.trackReferencedType(baseName);
208
+ }
209
+
210
+ // Resolve to full qualified name if it's a namespace member
211
+ const resolvedName = this.resolveTypeName(baseName);
212
+
213
+ // Handle generic arguments
214
+ if (node.genericArguments && node.genericArguments.length > 0) {
215
+ const args = node.genericArguments.map((a) =>
216
+ this.transformTypeForConstraint(a),
217
+ );
218
+ return `${resolvedName}<${args.join(", ")}>`;
219
+ }
220
+
221
+ // Preserve embedded generics if present in the ref string
222
+ if (node.ref.includes("<")) {
223
+ // Also resolve the base name in case it's a namespace member
224
+ return (
225
+ this.resolveTypeName(extractBaseName(node.ref)) +
226
+ node.ref.substring(node.ref.indexOf("<"))
227
+ );
228
+ }
229
+
230
+ return resolvedName;
231
+ }
232
+
233
+ if (isObjectType(node) && isNamedType(node)) {
234
+ // Track Asset import if used in constraint
235
+ if (node.name === "Asset") {
236
+ this.context.setNeedsAssetImport(true);
237
+ } else if (this.shouldTrackTypeForImport(node.name)) {
238
+ this.context.trackReferencedType(node.name);
239
+ }
240
+ // Just the type name, no FunctionalBuilder union
241
+ // Resolve to full qualified name if it's a namespace member
242
+ return this.resolveTypeName(node.name);
243
+ }
244
+
245
+ if (isArrayType(node)) {
246
+ const elementType = this.transformTypeForConstraint(node.elementType);
247
+ return `Array<${elementType}>`;
248
+ }
249
+
250
+ if (isOrType(node)) {
251
+ const variants = node.or.map((v) => this.transformTypeForConstraint(v));
252
+ return variants.join(" | ");
253
+ }
254
+
255
+ if (isAndType(node)) {
256
+ const parts = node.and.map((p) => this.transformTypeForConstraint(p));
257
+ return parts.join(" & ");
258
+ }
259
+
260
+ // Tuple types - transform to TypeScript tuple syntax for constraints
261
+ if (isTupleType(node)) {
262
+ const elements = node.elementTypes.map((member) => {
263
+ const elementType = this.transformTypeForConstraint(member.type);
264
+ return member.optional ? `${elementType}?` : elementType;
265
+ });
266
+
267
+ // Handle rest elements (additionalItems)
268
+ // additionalItems is either false or a NodeType; truthy check suffices
269
+ if (node.additionalItems) {
270
+ const restType = this.transformTypeForConstraint(node.additionalItems);
271
+ elements.push(`...${restType}[]`);
272
+ }
273
+
274
+ return `[${elements.join(", ")}]`;
275
+ }
276
+
277
+ // For primitives, use standard transformation (no FunctionalBuilder needed anyway)
278
+ return this.transformType(node, false);
279
+ }
280
+
281
+ /**
282
+ * Transform a reference type to TypeScript.
283
+ */
284
+ private transformRefType(node: RefType, forParameter: boolean): string {
285
+ const ref = node.ref;
286
+
287
+ // AssetWrapper - transform to accept Asset or FunctionalBuilder
288
+ // Preserves the generic type argument when present for better type safety
289
+ if (ref.startsWith("AssetWrapper")) {
290
+ this.context.setNeedsAssetImport(true);
291
+
292
+ let innerType = "Asset";
293
+
294
+ // Track whether we handled an intersection type (parts tracked separately)
295
+ let isIntersectionType = false;
296
+
297
+ // Check for structured generic arguments first
298
+ if (node.genericArguments && node.genericArguments.length > 0) {
299
+ const genericArg = node.genericArguments[0];
300
+ // transformTypeForConstraint recursively tracks each part of intersection types
301
+ const argType = this.transformTypeForConstraint(genericArg);
302
+
303
+ // If it's a generic param (like AnyAsset), fall back to Asset
304
+ innerType = this.context.getGenericParamSymbols().has(argType)
305
+ ? "Asset"
306
+ : argType;
307
+
308
+ // Mark intersection types so we don't double-track the combined string
309
+ isIntersectionType = isAndType(genericArg);
310
+ } else if (ref.includes("<")) {
311
+ // Handle embedded generics like "AssetWrapper<ImageAsset>" or "AssetWrapper<ImageAsset & Trackable>"
312
+ const match = ref.match(/AssetWrapper<(.+)>/);
313
+ if (match) {
314
+ const extractedType = match[1].trim();
315
+
316
+ // Check if the extracted type is an intersection (contains " & ")
317
+ if (extractedType.includes(" & ")) {
318
+ // Parse intersection parts and track each separately
319
+ isIntersectionType = true;
320
+ innerType = extractedType;
321
+
322
+ const parts = extractedType.split(" & ").map((p) => p.trim());
323
+ for (const part of parts) {
324
+ const partName = extractBaseName(part);
325
+ if (this.shouldTrackTypeForImport(partName)) {
326
+ this.context.trackReferencedType(partName);
327
+ }
328
+ }
329
+ } else {
330
+ const baseName = extractBaseName(extractedType);
331
+ innerType = this.context.getGenericParamSymbols().has(baseName)
332
+ ? "Asset"
333
+ : baseName;
334
+ }
335
+ }
336
+ }
337
+
338
+ // Track inner type for import if it's concrete and not Asset
339
+ // Skip if it was an intersection type (parts already tracked above)
340
+ if (!isIntersectionType && this.shouldTrackTypeForImport(innerType)) {
341
+ this.context.trackReferencedType(innerType);
342
+ }
343
+
344
+ return `${innerType} | FunctionalBuilder<${innerType}, BaseBuildContext>`;
345
+ }
346
+
347
+ // Expression - allow TaggedTemplateValue
348
+ if (ref === "Expression") {
349
+ return forParameter ? "string | TaggedTemplateValue<string>" : "string";
350
+ }
351
+
352
+ // Binding - allow TaggedTemplateValue
353
+ if (ref === "Binding") {
354
+ return forParameter ? "string | TaggedTemplateValue<string>" : "string";
355
+ }
356
+
357
+ // Asset reference
358
+ if (ref === "Asset" || ref.startsWith("Asset<")) {
359
+ this.context.setNeedsAssetImport(true);
360
+ return "Asset";
361
+ }
362
+
363
+ // Type that extends AssetWrapper (e.g., Header extends AssetWrapper<ImageAsset>)
364
+ // Detected via registry-based transitive lookup
365
+ {
366
+ const refBaseName = extractBaseName(ref);
367
+ const extendsRef = this.context.getAssetWrapperExtendsRef(refBaseName);
368
+ if (extendsRef) {
369
+ return this.transformAssetWrapperExtension(
370
+ this.resolveTypeName(refBaseName),
371
+ refBaseName,
372
+ extendsRef,
373
+ );
374
+ }
375
+ }
376
+
377
+ // Other references - user-defined types that may be objects
378
+ // Accept both raw type or FunctionalBuilder that produces it
379
+ const baseName = extractBaseName(ref);
380
+ // Resolve to full qualified name if it's a namespace member (e.g., "CrossfieldReference" -> "Validation.CrossfieldReference")
381
+ const resolvedName = this.resolveTypeName(baseName);
382
+
383
+ // Handle structured generic arguments
384
+ if (node.genericArguments && node.genericArguments.length > 0) {
385
+ const args = node.genericArguments.map((a) =>
386
+ this.transformType(a, forParameter),
387
+ );
388
+ const fullType = `${resolvedName}<${args.join(", ")}>`;
389
+ return `${fullType} | FunctionalBuilder<${fullType}, BaseBuildContext> | FunctionalPartial<${fullType}, BaseBuildContext>`;
390
+ }
391
+
392
+ // If ref contains embedded generics but genericArguments is empty, preserve them
393
+ // This handles cases like "SimpleModifier<'format'>" where the type argument
394
+ // is encoded in the ref string rather than in genericArguments array
395
+ if (ref.includes("<")) {
396
+ // Also resolve the base name in case it's a namespace member
397
+ const resolvedRef =
398
+ this.resolveTypeName(extractBaseName(ref)) +
399
+ ref.substring(ref.indexOf("<"));
400
+ return `${resolvedRef} | FunctionalBuilder<${resolvedRef}, BaseBuildContext> | FunctionalPartial<${resolvedRef}, BaseBuildContext>`;
401
+ }
402
+
403
+ return `${resolvedName} | FunctionalBuilder<${resolvedName}, BaseBuildContext> | FunctionalPartial<${resolvedName}, BaseBuildContext>`;
404
+ }
405
+
406
+ /**
407
+ * Transform a type that extends AssetWrapper into a combined union.
408
+ * Produces: InnerType | FunctionalBuilder<InnerType> | TypeName | FunctionalBuilder<TypeName> | FunctionalPartial<TypeName>
409
+ */
410
+ private transformAssetWrapperExtension(
411
+ resolvedTypeName: string,
412
+ rawTypeName: string,
413
+ extendsRef: RefType,
414
+ ): string {
415
+ this.context.setNeedsAssetImport(true);
416
+
417
+ // Determine the inner asset type from the AssetWrapper ancestor
418
+ let innerType = "Asset";
419
+ if (extendsRef.genericArguments && extendsRef.genericArguments.length > 0) {
420
+ const genericArg = extendsRef.genericArguments[0];
421
+ const argType = this.transformTypeForConstraint(genericArg);
422
+ innerType = this.context.getGenericParamSymbols().has(argType)
423
+ ? "Asset"
424
+ : argType;
425
+ } else if (extendsRef.ref.includes("<")) {
426
+ const match = extendsRef.ref.match(/AssetWrapper<(.+)>/);
427
+ if (match) {
428
+ const extracted = extractBaseName(match[1].trim());
429
+ innerType = this.context.getGenericParamSymbols().has(extracted)
430
+ ? "Asset"
431
+ : extracted;
432
+ }
433
+ }
434
+
435
+ // Track inner type for import if it's concrete and not Asset
436
+ if (innerType !== "Asset" && this.shouldTrackTypeForImport(innerType)) {
437
+ this.context.trackReferencedType(innerType);
438
+ }
439
+
440
+ // Track the extending type itself for import
441
+ if (this.shouldTrackTypeForImport(rawTypeName)) {
442
+ this.context.trackReferencedType(rawTypeName);
443
+ }
444
+
445
+ return `${innerType} | FunctionalBuilder<${innerType}, BaseBuildContext> | ${resolvedTypeName} | FunctionalBuilder<${resolvedTypeName}, BaseBuildContext> | FunctionalPartial<${resolvedTypeName}, BaseBuildContext>`;
446
+ }
447
+
448
+ /**
449
+ * Generate an inline object type for anonymous objects.
450
+ */
451
+ generateInlineObjectType(node: ObjectType, forParameter: boolean): string {
452
+ const props = Object.entries(node.properties)
453
+ .map(([propName, prop]) => {
454
+ const propType = this.transformType(prop.node, forParameter);
455
+ const optional = prop.required ? "" : "?";
456
+ // Quote property names that contain special characters (like hyphens)
457
+ const quotedName = this.needsQuoting(propName)
458
+ ? `"${propName}"`
459
+ : propName;
460
+ return `${quotedName}${optional}: ${propType}`;
461
+ })
462
+ .join("; ");
463
+
464
+ return `{ ${props} }`;
465
+ }
466
+
467
+ /**
468
+ * Check if a property name needs to be quoted in TypeScript.
469
+ * Property names with special characters like hyphens must be quoted.
470
+ */
471
+ private needsQuoting(name: string): boolean {
472
+ // Valid unquoted property names match JavaScript identifier rules
473
+ // Must start with letter, underscore, or dollar sign
474
+ // Can contain letters, digits, underscores, or dollar signs
475
+ return !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
476
+ }
477
+
478
+ /**
479
+ * Get the full qualified name for a type if it's a namespace member.
480
+ * For example, "CrossfieldReference" -> "Validation.CrossfieldReference"
481
+ * if we've seen "Validation.CrossfieldReference" in the source.
482
+ * Returns the original name if no namespace mapping exists.
483
+ */
484
+ private resolveTypeName(typeName: string): string {
485
+ return this.context.getNamespaceMemberMap().get(typeName) ?? typeName;
486
+ }
487
+ }