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