@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,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
+ }
@@ -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
+ }