@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.
- package/dist/cjs/index.cjs +2146 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2075 -0
- package/dist/index.mjs +2075 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
- package/src/__tests__/__snapshots__/generator.test.ts.snap +886 -0
- package/src/__tests__/builder-class-generator.test.ts +627 -0
- package/src/__tests__/cli.test.ts +685 -0
- package/src/__tests__/default-value-generator.test.ts +365 -0
- package/src/__tests__/generator.test.ts +2860 -0
- package/src/__tests__/import-generator.test.ts +444 -0
- package/src/__tests__/path-utils.test.ts +174 -0
- package/src/__tests__/type-collector.test.ts +674 -0
- package/src/__tests__/type-transformer.test.ts +934 -0
- package/src/__tests__/utils.test.ts +597 -0
- package/src/builder-class-generator.ts +254 -0
- package/src/cli.ts +285 -0
- package/src/default-value-generator.ts +307 -0
- package/src/generator.ts +257 -0
- package/src/import-generator.ts +331 -0
- package/src/index.ts +38 -0
- package/src/path-utils.ts +155 -0
- package/src/ts-morph-type-finder.ts +319 -0
- package/src/type-categorizer.ts +131 -0
- package/src/type-collector.ts +296 -0
- package/src/type-resolver.ts +266 -0
- package/src/type-transformer.ts +487 -0
- package/src/utils.ts +762 -0
- package/types/builder-class-generator.d.ts +56 -0
- package/types/cli.d.ts +6 -0
- package/types/default-value-generator.d.ts +74 -0
- package/types/generator.d.ts +102 -0
- package/types/import-generator.d.ts +77 -0
- package/types/index.d.ts +12 -0
- package/types/path-utils.d.ts +65 -0
- package/types/ts-morph-type-finder.d.ts +73 -0
- package/types/type-categorizer.d.ts +46 -0
- package/types/type-collector.d.ts +62 -0
- package/types/type-resolver.d.ts +49 -0
- package/types/type-transformer.d.ts +74 -0
- 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
|
+
}
|