@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,307 @@
|
|
|
1
|
+
import type { NodeType, ObjectType } from "@xlr-lib/xlr";
|
|
2
|
+
import {
|
|
3
|
+
isStringType,
|
|
4
|
+
isNumberType,
|
|
5
|
+
isBooleanType,
|
|
6
|
+
isObjectType,
|
|
7
|
+
isArrayType,
|
|
8
|
+
isRefType,
|
|
9
|
+
isOrType,
|
|
10
|
+
isAndType,
|
|
11
|
+
isPrimitiveConst,
|
|
12
|
+
isAssetWrapperRef,
|
|
13
|
+
isExpressionRef,
|
|
14
|
+
isBindingRef,
|
|
15
|
+
} from "./utils";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration for default value generation
|
|
19
|
+
*/
|
|
20
|
+
export interface DefaultValueConfig {
|
|
21
|
+
/**
|
|
22
|
+
* Maximum depth for recursive object defaults.
|
|
23
|
+
* Prevents infinite recursion for deeply nested or circular types.
|
|
24
|
+
*
|
|
25
|
+
* The depth counter starts at 0 for the root object and increments
|
|
26
|
+
* for each nested object level. When depth equals maxDepth, nested
|
|
27
|
+
* objects are returned as empty `{}` instead of recursing further.
|
|
28
|
+
*
|
|
29
|
+
* Example with maxDepth=3:
|
|
30
|
+
* - Root object (depth 0): full defaults generated
|
|
31
|
+
* - level1.nested (depth 1): full defaults generated
|
|
32
|
+
* - level1.nested.child (depth 2): full defaults generated
|
|
33
|
+
* - level1.nested.child.deep (depth 3): returns {} (depth limit reached)
|
|
34
|
+
*
|
|
35
|
+
* Default: 3
|
|
36
|
+
*/
|
|
37
|
+
maxDepth?: number;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Type names to skip (user must provide these values).
|
|
41
|
+
* Typically includes "Asset" and "AssetWrapper".
|
|
42
|
+
*/
|
|
43
|
+
skipTypes?: Set<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_CONFIG: Required<DefaultValueConfig> = {
|
|
47
|
+
maxDepth: 3,
|
|
48
|
+
skipTypes: new Set(["Asset", "AssetWrapper"]),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Context for tracking default generation state
|
|
53
|
+
*/
|
|
54
|
+
interface GenerationContext {
|
|
55
|
+
depth: number;
|
|
56
|
+
config: Required<DefaultValueConfig>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generates smart default values for builder classes.
|
|
61
|
+
*
|
|
62
|
+
* This generator creates sensible defaults for required fields:
|
|
63
|
+
* - String → ""
|
|
64
|
+
* - Number → 0
|
|
65
|
+
* - Boolean → false
|
|
66
|
+
* - Array → []
|
|
67
|
+
* - Object → {} or recursive defaults for required properties
|
|
68
|
+
* - Expression/Binding → ""
|
|
69
|
+
* - Union types → uses the first non-null/undefined variant
|
|
70
|
+
* - AssetWrapper → SKIPPED (user must provide)
|
|
71
|
+
*/
|
|
72
|
+
export class DefaultValueGenerator {
|
|
73
|
+
private readonly config: Required<DefaultValueConfig>;
|
|
74
|
+
|
|
75
|
+
constructor(config: DefaultValueConfig = {}) {
|
|
76
|
+
this.config = {
|
|
77
|
+
...DEFAULT_CONFIG,
|
|
78
|
+
...config,
|
|
79
|
+
skipTypes: config.skipTypes ?? DEFAULT_CONFIG.skipTypes,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate default values for an ObjectType.
|
|
85
|
+
*
|
|
86
|
+
* @param objectType - The XLR ObjectType to generate defaults for
|
|
87
|
+
* @param assetType - Optional asset type string for Asset types
|
|
88
|
+
* @returns Record of property names to default values
|
|
89
|
+
*/
|
|
90
|
+
generateDefaults(
|
|
91
|
+
objectType: ObjectType,
|
|
92
|
+
assetType?: string,
|
|
93
|
+
): Record<string, unknown> {
|
|
94
|
+
const defaults: Record<string, unknown> = {};
|
|
95
|
+
|
|
96
|
+
// Add asset type default if this is an asset
|
|
97
|
+
if (assetType) {
|
|
98
|
+
defaults["type"] = assetType;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Add default ID for assets (types that extend Asset)
|
|
102
|
+
if (objectType.extends?.ref.startsWith("Asset")) {
|
|
103
|
+
defaults["id"] = "";
|
|
104
|
+
}
|
|
105
|
+
// Also add default ID for non-Asset types that have an 'id' property
|
|
106
|
+
else if ("id" in objectType.properties) {
|
|
107
|
+
defaults["id"] = "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Process each property
|
|
111
|
+
const context: GenerationContext = {
|
|
112
|
+
depth: 0,
|
|
113
|
+
config: this.config,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const [propName, prop] of Object.entries(objectType.properties)) {
|
|
117
|
+
// Const values take precedence
|
|
118
|
+
if (isPrimitiveConst(prop.node)) {
|
|
119
|
+
defaults[propName] = prop.node.const;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Only generate defaults for required properties
|
|
124
|
+
if (!prop.required) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Skip if we already have a default (e.g., id)
|
|
129
|
+
if (propName in defaults) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const defaultValue = this.getDefaultForType(prop.node, context);
|
|
134
|
+
if (defaultValue !== undefined) {
|
|
135
|
+
defaults[propName] = defaultValue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return defaults;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the default value for a specific type node.
|
|
144
|
+
*
|
|
145
|
+
* @param node - The type node
|
|
146
|
+
* @param context - Generation context for tracking depth
|
|
147
|
+
* @returns The default value, or undefined if the type should be skipped
|
|
148
|
+
*/
|
|
149
|
+
private getDefaultForType(
|
|
150
|
+
node: NodeType,
|
|
151
|
+
context: GenerationContext,
|
|
152
|
+
): unknown {
|
|
153
|
+
// Skip AssetWrapper - user must provide
|
|
154
|
+
if (isAssetWrapperRef(node)) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check for other skip types
|
|
159
|
+
if (isRefType(node)) {
|
|
160
|
+
const baseName = node.ref.split("<")[0];
|
|
161
|
+
if (context.config.skipTypes.has(baseName)) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle primitive types
|
|
167
|
+
if (isStringType(node)) {
|
|
168
|
+
return "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (isNumberType(node)) {
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (isBooleanType(node)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Handle Expression and Binding refs
|
|
180
|
+
if (isExpressionRef(node) || isBindingRef(node)) {
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Handle arrays
|
|
185
|
+
if (isArrayType(node)) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle union types - pick first non-null/undefined variant
|
|
190
|
+
if (isOrType(node)) {
|
|
191
|
+
return this.getDefaultForUnion(node.or, context);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Handle intersection types - try to merge defaults
|
|
195
|
+
if (isAndType(node)) {
|
|
196
|
+
return this.getDefaultForIntersection(node.and, context);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Handle object types with depth limit
|
|
200
|
+
if (isObjectType(node)) {
|
|
201
|
+
if (context.depth >= context.config.maxDepth) {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return this.getDefaultForObject(node, {
|
|
206
|
+
...context,
|
|
207
|
+
depth: context.depth + 1,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Handle null/undefined types
|
|
212
|
+
if (node.type === "null") {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (node.type === "undefined") {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Refs to other types - return empty object as a safe default
|
|
221
|
+
if (isRefType(node)) {
|
|
222
|
+
return {};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get default for a union type by picking the first non-null/undefined variant.
|
|
230
|
+
*/
|
|
231
|
+
private getDefaultForUnion(
|
|
232
|
+
variants: NodeType[],
|
|
233
|
+
context: GenerationContext,
|
|
234
|
+
): unknown {
|
|
235
|
+
for (const variant of variants) {
|
|
236
|
+
// Skip null and undefined
|
|
237
|
+
if (variant.type === "null" || variant.type === "undefined") {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const defaultValue = this.getDefaultForType(variant, context);
|
|
242
|
+
if (defaultValue !== undefined) {
|
|
243
|
+
return defaultValue;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// If all variants are null/undefined, return undefined
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get default for an intersection type by attempting to merge.
|
|
253
|
+
*/
|
|
254
|
+
private getDefaultForIntersection(
|
|
255
|
+
parts: NodeType[],
|
|
256
|
+
context: GenerationContext,
|
|
257
|
+
): unknown {
|
|
258
|
+
// For intersections, we need to satisfy all parts
|
|
259
|
+
// Start with an empty object and merge
|
|
260
|
+
const merged: Record<string, unknown> = {};
|
|
261
|
+
|
|
262
|
+
for (const part of parts) {
|
|
263
|
+
const partDefault = this.getDefaultForType(part, context);
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
partDefault !== undefined &&
|
|
267
|
+
typeof partDefault === "object" &&
|
|
268
|
+
partDefault !== null &&
|
|
269
|
+
!Array.isArray(partDefault)
|
|
270
|
+
) {
|
|
271
|
+
Object.assign(merged, partDefault);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return Object.keys(merged).length > 0 ? merged : {};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get default for an object type by processing required properties.
|
|
280
|
+
*/
|
|
281
|
+
private getDefaultForObject(
|
|
282
|
+
node: ObjectType,
|
|
283
|
+
context: GenerationContext,
|
|
284
|
+
): Record<string, unknown> {
|
|
285
|
+
const result: Record<string, unknown> = {};
|
|
286
|
+
|
|
287
|
+
for (const [propName, prop] of Object.entries(node.properties)) {
|
|
288
|
+
// Const values take precedence
|
|
289
|
+
if (isPrimitiveConst(prop.node)) {
|
|
290
|
+
result[propName] = prop.node.const;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Only generate defaults for required properties
|
|
295
|
+
if (!prop.required) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const defaultValue = this.getDefaultForType(prop.node, context);
|
|
300
|
+
if (defaultValue !== undefined) {
|
|
301
|
+
result[propName] = defaultValue;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
}
|
package/src/generator.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { ObjectType, NamedType } from "@xlr-lib/xlr";
|
|
2
|
+
import { isGenericNamedType } from "@xlr-lib/xlr-utils";
|
|
3
|
+
import {
|
|
4
|
+
toFactoryName,
|
|
5
|
+
toBuilderClassName,
|
|
6
|
+
getAssetTypeFromExtends,
|
|
7
|
+
type TypeRegistry,
|
|
8
|
+
} from "./utils";
|
|
9
|
+
import {
|
|
10
|
+
type TypeScriptContext,
|
|
11
|
+
type UnexportedTypeInfo,
|
|
12
|
+
} from "./type-resolver";
|
|
13
|
+
import { TypeCollector } from "./type-collector";
|
|
14
|
+
import { TypeTransformer } from "./type-transformer";
|
|
15
|
+
import {
|
|
16
|
+
ImportGenerator,
|
|
17
|
+
type ImportGeneratorConfig,
|
|
18
|
+
} from "./import-generator";
|
|
19
|
+
import {
|
|
20
|
+
BuilderClassGenerator,
|
|
21
|
+
type BuilderInfo,
|
|
22
|
+
} from "./builder-class-generator";
|
|
23
|
+
|
|
24
|
+
// Re-export types for public API
|
|
25
|
+
export type { TypeScriptContext, UnexportedTypeInfo } from "./type-resolver";
|
|
26
|
+
export type { BuilderInfo } from "./builder-class-generator";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for the generator
|
|
30
|
+
*/
|
|
31
|
+
export interface GeneratorConfig {
|
|
32
|
+
/** Import path for functional utilities (default: "@player-lang/functional-dsl") */
|
|
33
|
+
functionalImportPath?: string;
|
|
34
|
+
/** Import path for player-ui types (default: "@player-ui/types") */
|
|
35
|
+
typesImportPath?: string;
|
|
36
|
+
/**
|
|
37
|
+
* TypeScript context for automatic import resolution.
|
|
38
|
+
* When provided, the generator will automatically resolve import paths
|
|
39
|
+
* using TypeScript's module resolution.
|
|
40
|
+
*/
|
|
41
|
+
tsContext?: TypeScriptContext;
|
|
42
|
+
/** Function to generate the type import path for a given type name */
|
|
43
|
+
typeImportPathGenerator?: (typeName: string) => string;
|
|
44
|
+
/**
|
|
45
|
+
* Set of type names that are defined in the same source file as the main type.
|
|
46
|
+
* Types not in this set will be imported from their own source files using typeImportPathGenerator.
|
|
47
|
+
* When tsContext is provided, this is computed automatically from the source file.
|
|
48
|
+
*/
|
|
49
|
+
sameFileTypes?: Set<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Explicitly maps type names to their package names for external imports.
|
|
52
|
+
* Types in this map will be imported from the specified package (e.g., "@player-lang/types").
|
|
53
|
+
* This takes precedence over typeImportPathGenerator for the specified types.
|
|
54
|
+
*/
|
|
55
|
+
externalTypes?: Map<string, string>;
|
|
56
|
+
/**
|
|
57
|
+
* Type registry for resolving nested interface references.
|
|
58
|
+
* Maps type names to their XLR ObjectType definitions.
|
|
59
|
+
* Used to find AssetWrapper paths in nested interfaces.
|
|
60
|
+
*/
|
|
61
|
+
typeRegistry?: TypeRegistry;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result of builder generation including warnings
|
|
66
|
+
*/
|
|
67
|
+
export interface GeneratorResult {
|
|
68
|
+
/** Generated TypeScript code */
|
|
69
|
+
code: string;
|
|
70
|
+
/** Types that need to be exported in their source files */
|
|
71
|
+
unexportedTypes: UnexportedTypeInfo[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generates functional builder TypeScript code from XLR types.
|
|
76
|
+
* This class orchestrates the type collection, transformation, and code generation.
|
|
77
|
+
*/
|
|
78
|
+
export class FunctionalBuilderGenerator {
|
|
79
|
+
private readonly namedType: NamedType<ObjectType>;
|
|
80
|
+
private readonly config: GeneratorConfig;
|
|
81
|
+
|
|
82
|
+
/** Import generator handles import tracking and generation */
|
|
83
|
+
private readonly importGenerator: ImportGenerator;
|
|
84
|
+
|
|
85
|
+
/** Type transformer handles XLR to TypeScript type conversion */
|
|
86
|
+
private readonly typeTransformer: TypeTransformer;
|
|
87
|
+
|
|
88
|
+
/** Type collector handles collecting type references */
|
|
89
|
+
private readonly typeCollector: TypeCollector;
|
|
90
|
+
|
|
91
|
+
/** Builder class generator handles class code generation */
|
|
92
|
+
private readonly builderClassGenerator: BuilderClassGenerator;
|
|
93
|
+
|
|
94
|
+
/** Map short type names to their full qualified names */
|
|
95
|
+
private readonly namespaceMemberMap = new Map<string, string>();
|
|
96
|
+
|
|
97
|
+
constructor(namedType: NamedType<ObjectType>, config: GeneratorConfig = {}) {
|
|
98
|
+
this.namedType = namedType;
|
|
99
|
+
this.config = config;
|
|
100
|
+
|
|
101
|
+
// Create import generator config
|
|
102
|
+
const importConfig: ImportGeneratorConfig = {
|
|
103
|
+
functionalImportPath: config.functionalImportPath,
|
|
104
|
+
typesImportPath: config.typesImportPath,
|
|
105
|
+
tsContext: config.tsContext,
|
|
106
|
+
typeImportPathGenerator: config.typeImportPathGenerator,
|
|
107
|
+
sameFileTypes: config.sameFileTypes,
|
|
108
|
+
externalTypes: config.externalTypes,
|
|
109
|
+
typeRegistry: config.typeRegistry,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Initialize the import generator
|
|
113
|
+
this.importGenerator = new ImportGenerator(importConfig);
|
|
114
|
+
|
|
115
|
+
// Initialize the type transformer with the import generator as context
|
|
116
|
+
this.typeTransformer = new TypeTransformer(this.importGenerator);
|
|
117
|
+
|
|
118
|
+
// Initialize the type collector with the import generator as tracker
|
|
119
|
+
this.typeCollector = new TypeCollector(
|
|
120
|
+
this.importGenerator,
|
|
121
|
+
this.importGenerator.getGenericParamSymbols(),
|
|
122
|
+
namedType.name,
|
|
123
|
+
this.importGenerator.getNamespaceMemberMap(),
|
|
124
|
+
config.typeRegistry,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Initialize the builder class generator with the type transformer and type registry
|
|
128
|
+
this.builderClassGenerator = new BuilderClassGenerator(
|
|
129
|
+
this.typeTransformer,
|
|
130
|
+
config.typeRegistry,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get list of types that exist but need to be exported.
|
|
136
|
+
* Call this after generate() to get warnings for the user.
|
|
137
|
+
*/
|
|
138
|
+
getUnexportedTypes(): UnexportedTypeInfo[] {
|
|
139
|
+
return this.importGenerator.getUnexportedTypes();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get list of types that couldn't be resolved at all.
|
|
144
|
+
* These types are used in the generated code but won't be imported,
|
|
145
|
+
* causing type errors. Often these are namespaced types (e.g., Validation.CrossfieldReference).
|
|
146
|
+
*/
|
|
147
|
+
getUnresolvedTypes(): string[] {
|
|
148
|
+
return this.importGenerator.getUnresolvedTypes();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate the builder code
|
|
153
|
+
*/
|
|
154
|
+
generate(): string {
|
|
155
|
+
// Collect generic parameter symbols first so we can exclude them from imports
|
|
156
|
+
// This MUST happen before createBuilderInfo since transformTypeForConstraint needs it
|
|
157
|
+
this.typeCollector.collectGenericParamSymbols(this.namedType);
|
|
158
|
+
|
|
159
|
+
const mainBuilder = this.createBuilderInfo(this.namedType);
|
|
160
|
+
|
|
161
|
+
// Collect types from generic constraints/defaults for import generation
|
|
162
|
+
this.typeCollector.collectTypesFromGenericTokens(this.namedType);
|
|
163
|
+
|
|
164
|
+
// Collect all referenced types for imports (no nested builders are generated)
|
|
165
|
+
this.typeCollector.collectReferencedTypes(this.namedType);
|
|
166
|
+
|
|
167
|
+
// Generate main builder class (this also sets needsAssetImport flag)
|
|
168
|
+
const mainBuilderCode =
|
|
169
|
+
this.builderClassGenerator.generateBuilderClass(mainBuilder);
|
|
170
|
+
|
|
171
|
+
// Generate imports after builder code so we know what imports are needed
|
|
172
|
+
const imports = this.importGenerator.generateImports(mainBuilder.name);
|
|
173
|
+
|
|
174
|
+
return [imports, mainBuilderCode].filter(Boolean).join("\n\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private createBuilderInfo(namedType: NamedType<ObjectType>): BuilderInfo {
|
|
178
|
+
const assetType = getAssetTypeFromExtends(namedType);
|
|
179
|
+
const isAsset = !!assetType;
|
|
180
|
+
|
|
181
|
+
let genericParams: string | undefined;
|
|
182
|
+
if (isGenericNamedType(namedType)) {
|
|
183
|
+
// Deduplicate generic parameters by symbol name
|
|
184
|
+
// This handles cases where a type extends another generic type without
|
|
185
|
+
// passing type arguments, causing XLR to collect parameters from both
|
|
186
|
+
const seenParams = new Set<string>();
|
|
187
|
+
genericParams = namedType.genericTokens
|
|
188
|
+
.filter((t) => {
|
|
189
|
+
if (seenParams.has(t.symbol)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
seenParams.add(t.symbol);
|
|
193
|
+
return true;
|
|
194
|
+
})
|
|
195
|
+
.map((t) => {
|
|
196
|
+
let param = t.symbol;
|
|
197
|
+
if (t.constraints) {
|
|
198
|
+
const constraintType =
|
|
199
|
+
this.typeTransformer.transformTypeForConstraint(t.constraints);
|
|
200
|
+
// Skip 'any' constraints - these represent unconstrained generics in TypeScript
|
|
201
|
+
// Adding "extends any" is redundant and reduces type safety
|
|
202
|
+
if (constraintType !== "any") {
|
|
203
|
+
param += ` extends ${constraintType}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (t.default) {
|
|
207
|
+
param += ` = ${this.typeTransformer.transformTypeForConstraint(t.default)}`;
|
|
208
|
+
}
|
|
209
|
+
return param;
|
|
210
|
+
})
|
|
211
|
+
.join(", ");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
name: namedType.name,
|
|
216
|
+
className: toBuilderClassName(namedType.name),
|
|
217
|
+
factoryName: toFactoryName(namedType.name),
|
|
218
|
+
objectType: namedType,
|
|
219
|
+
assetType,
|
|
220
|
+
genericParams,
|
|
221
|
+
isAsset,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generate functional builder code from a NamedType<ObjectType>
|
|
228
|
+
* @param namedType - The XLR NamedType to generate a builder for
|
|
229
|
+
* @param config - Optional generator configuration
|
|
230
|
+
* @returns Generated TypeScript code for the functional builder
|
|
231
|
+
*/
|
|
232
|
+
export function generateFunctionalBuilder(
|
|
233
|
+
namedType: NamedType<ObjectType>,
|
|
234
|
+
config: GeneratorConfig = {},
|
|
235
|
+
): string {
|
|
236
|
+
const generator = new FunctionalBuilderGenerator(namedType, config);
|
|
237
|
+
return generator.generate();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Generate functional builder code with warnings about unexported types.
|
|
242
|
+
* Use this when you want to get detailed information about types that need
|
|
243
|
+
* to be exported in their source files.
|
|
244
|
+
*
|
|
245
|
+
* @param namedType - The XLR NamedType to generate a builder for
|
|
246
|
+
* @param config - Optional generator configuration
|
|
247
|
+
* @returns Generated code and list of types that need to be exported
|
|
248
|
+
*/
|
|
249
|
+
export function generateFunctionalBuilderWithWarnings(
|
|
250
|
+
namedType: NamedType<ObjectType>,
|
|
251
|
+
config: GeneratorConfig = {},
|
|
252
|
+
): GeneratorResult {
|
|
253
|
+
const generator = new FunctionalBuilderGenerator(namedType, config);
|
|
254
|
+
const code = generator.generate();
|
|
255
|
+
const unexportedTypes = generator.getUnexportedTypes();
|
|
256
|
+
return { code, unexportedTypes };
|
|
257
|
+
}
|