@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,296 @@
1
+ import type { NodeType, ObjectType, NamedType } from "@xlr-lib/xlr";
2
+ import { isGenericNamedType } from "@xlr-lib/xlr-utils";
3
+ import {
4
+ isObjectType,
5
+ isArrayType,
6
+ isRefType,
7
+ isOrType,
8
+ isAndType,
9
+ isNamedType,
10
+ isBuiltinType,
11
+ extractBaseName,
12
+ parseNamespacedType,
13
+ type TypeRegistry,
14
+ } from "./utils";
15
+
16
+ /**
17
+ * Interface for tracking type references.
18
+ * Implemented by the import generator to track types that need to be imported.
19
+ */
20
+ export interface TypeTracker {
21
+ trackReferencedType(typeName: string): void;
22
+ trackNamespaceImport(namespaceName: string): void;
23
+ }
24
+
25
+ /**
26
+ * Collects type references from XLR types for import generation.
27
+ */
28
+ export class TypeCollector {
29
+ private readonly typeTracker: TypeTracker;
30
+ private readonly genericParamSymbols: Set<string>;
31
+ private readonly mainTypeName: string;
32
+ private readonly namespaceMemberMap: Map<string, string>;
33
+ private readonly typeRegistry?: TypeRegistry;
34
+
35
+ constructor(
36
+ typeTracker: TypeTracker,
37
+ genericParamSymbols: Set<string>,
38
+ mainTypeName: string,
39
+ namespaceMemberMap: Map<string, string>,
40
+ typeRegistry?: TypeRegistry,
41
+ ) {
42
+ this.typeTracker = typeTracker;
43
+ this.genericParamSymbols = genericParamSymbols;
44
+ this.mainTypeName = mainTypeName;
45
+ this.namespaceMemberMap = namespaceMemberMap;
46
+ this.typeRegistry = typeRegistry;
47
+ }
48
+
49
+ /**
50
+ * Collect generic parameter symbols (e.g., T, U) from the type definition.
51
+ * These should not be imported as they are type parameters, not concrete types.
52
+ *
53
+ * Also handles the case where a non-generic type extends a generic base without
54
+ * passing type arguments. In that scenario, XLR copies properties from the base
55
+ * (including references to the base's generic params like `AnyAsset`) but does
56
+ * NOT propagate `genericTokens` to the child type. We scan the type registry for
57
+ * generic parameter symbols that should be excluded from imports.
58
+ */
59
+ collectGenericParamSymbols(namedType: NamedType<ObjectType>): void {
60
+ if (isGenericNamedType(namedType)) {
61
+ for (const token of namedType.genericTokens) {
62
+ this.genericParamSymbols.add(token.symbol);
63
+ }
64
+ }
65
+
66
+ // Scan the type registry for generic parameter symbols from other types.
67
+ // When XLR copies properties from a generic base without resolving generics,
68
+ // the property types still reference the base's generic parameter names
69
+ // (e.g., `AnyAsset` from `FileInputAssetBase<AnyAsset>`). These names are
70
+ // not concrete types and should not be imported. We collect them from all
71
+ // registry types, excluding any that are themselves registered as concrete types.
72
+ if (this.typeRegistry) {
73
+ for (const registeredType of this.typeRegistry.values()) {
74
+ if (isGenericNamedType(registeredType)) {
75
+ for (const token of registeredType.genericTokens) {
76
+ if (!this.typeRegistry.has(token.symbol)) {
77
+ this.genericParamSymbols.add(token.symbol);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Collect type references from generic parameter constraints and defaults.
87
+ * This ensures types used in generics like "T extends Foo = Bar<X>" have
88
+ * Foo, Bar, and X added to referencedTypes for import generation.
89
+ */
90
+ collectTypesFromGenericTokens(namedType: NamedType<ObjectType>): void {
91
+ if (!isGenericNamedType(namedType)) {
92
+ return;
93
+ }
94
+
95
+ for (const token of namedType.genericTokens) {
96
+ if (token.constraints) {
97
+ this.collectTypeReferencesFromNode(token.constraints);
98
+ }
99
+
100
+ if (token.default) {
101
+ this.collectTypeReferencesFromNode(token.default);
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Collect all referenced types from an object type for imports.
108
+ */
109
+ collectReferencedTypes(objType: ObjectType): void {
110
+ for (const prop of Object.values(objType.properties)) {
111
+ this.collectReferencedTypesFromNode(prop.node);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Collect referenced types from a node type.
117
+ */
118
+ collectReferencedTypesFromNode(node: NodeType): void {
119
+ if (isObjectType(node)) {
120
+ if (isNamedType(node)) {
121
+ // Named types are defined elsewhere - track for import
122
+ // Skip built-in types and the type being generated
123
+ if (
124
+ node.name !== this.mainTypeName &&
125
+ !isBuiltinType(node.name) &&
126
+ !this.genericParamSymbols.has(node.name)
127
+ ) {
128
+ this.typeTracker.trackReferencedType(node.name);
129
+ }
130
+ } else {
131
+ // Anonymous object - recurse into properties to collect type references
132
+ for (const prop of Object.values(node.properties)) {
133
+ this.collectReferencedTypesFromNode(prop.node);
134
+ }
135
+ }
136
+ } else if (isArrayType(node)) {
137
+ this.collectReferencedTypesFromNode(node.elementType);
138
+ } else if (isOrType(node)) {
139
+ for (const variant of node.or) {
140
+ this.collectReferencedTypesFromNode(variant);
141
+ }
142
+ } else if (isAndType(node)) {
143
+ for (const part of node.and) {
144
+ this.collectReferencedTypesFromNode(part);
145
+ }
146
+ } else if (isRefType(node)) {
147
+ const baseName = extractBaseName(node.ref);
148
+
149
+ // Check if this is a namespaced type (e.g., "Validation.CrossfieldReference")
150
+ const namespaced = parseNamespacedType(baseName);
151
+ if (namespaced) {
152
+ // Track the namespace for import and the member mapping
153
+ this.typeTracker.trackNamespaceImport(namespaced.namespace);
154
+ this.namespaceMemberMap.set(namespaced.member, baseName);
155
+ } else {
156
+ // Track reference types that aren't built-in or generic params
157
+ if (
158
+ !isBuiltinType(baseName) &&
159
+ !this.genericParamSymbols.has(baseName)
160
+ ) {
161
+ this.typeTracker.trackReferencedType(baseName);
162
+ }
163
+ }
164
+
165
+ // Also process generic arguments, but skip type parameters of the referenced type
166
+ if (node.genericArguments) {
167
+ for (const arg of node.genericArguments) {
168
+ // Skip if this argument appears to be a type parameter of the referenced type
169
+ // e.g., in ref="Bar<AnyAsset>", skip "AnyAsset" since it's Bar's type param
170
+ if (isRefType(arg)) {
171
+ const argName = extractBaseName(arg.ref);
172
+ if (this.isTypeParamOfRef(argName, node.ref)) {
173
+ continue;
174
+ }
175
+ }
176
+ this.collectReferencedTypesFromNode(arg);
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Recursively collect type references from any NodeType.
184
+ * This handles refs, arrays, unions, intersections, and objects.
185
+ */
186
+ private collectTypeReferencesFromNode(node: NodeType): void {
187
+ if (isRefType(node)) {
188
+ const baseName = extractBaseName(node.ref);
189
+
190
+ // Check if this is a namespaced type (e.g., "Validation.CrossfieldReference")
191
+ const namespaced = parseNamespacedType(baseName);
192
+ if (namespaced) {
193
+ // Track the namespace for import and the member mapping
194
+ this.typeTracker.trackNamespaceImport(namespaced.namespace);
195
+ this.namespaceMemberMap.set(namespaced.member, baseName);
196
+ } else if (
197
+ !isBuiltinType(baseName) &&
198
+ !this.genericParamSymbols.has(baseName)
199
+ ) {
200
+ // Skip built-in types and generic param symbols
201
+ this.typeTracker.trackReferencedType(baseName);
202
+ }
203
+
204
+ // Also process generic arguments, but skip type parameters of the referenced type
205
+ if (node.genericArguments) {
206
+ for (const arg of node.genericArguments) {
207
+ // Skip if this argument appears to be a type parameter of the referenced type
208
+ // e.g., in ref="Bar<AnyAsset>", skip "AnyAsset" since it's Bar's type param
209
+ if (isRefType(arg)) {
210
+ const argName = extractBaseName(arg.ref);
211
+ if (this.isTypeParamOfRef(argName, node.ref)) {
212
+ continue;
213
+ }
214
+ }
215
+ this.collectTypeReferencesFromNode(arg);
216
+ }
217
+ }
218
+ } else if (isArrayType(node)) {
219
+ this.collectTypeReferencesFromNode(node.elementType);
220
+ } else if (isOrType(node)) {
221
+ for (const variant of node.or) {
222
+ this.collectTypeReferencesFromNode(variant);
223
+ }
224
+ } else if (isAndType(node)) {
225
+ for (const part of node.and) {
226
+ this.collectTypeReferencesFromNode(part);
227
+ }
228
+ } else if (isObjectType(node)) {
229
+ if (isNamedType(node)) {
230
+ // Skip generic param symbols and built-in types in named types
231
+ // Strip generic arguments for import purposes
232
+ const importName = extractBaseName(node.name);
233
+ if (
234
+ !this.genericParamSymbols.has(importName) &&
235
+ !isBuiltinType(importName)
236
+ ) {
237
+ // Use trackReferencedType to properly resolve import path
238
+ this.typeTracker.trackReferencedType(importName);
239
+ }
240
+ }
241
+
242
+ for (const prop of Object.values(node.properties)) {
243
+ this.collectTypeReferencesFromNode(prop.node);
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Check if a type name appears to be a generic type parameter of the referenced type.
250
+ * This detects cases like ref="Bar<AnyAsset>" where "AnyAsset" is Bar's type parameter,
251
+ * not a concrete type to import.
252
+ *
253
+ * @param argName - The name of the type argument being checked
254
+ * @param parentRef - The parent ref string that contains the generic usage
255
+ * @returns true if argName appears to be a type parameter in parentRef
256
+ */
257
+ private isTypeParamOfRef(argName: string, parentRef: string): boolean {
258
+ // Extract the generic parameters portion from the ref string
259
+ // e.g., "Bar<AnyAsset>" -> "AnyAsset", "Map<K, V>" -> "K, V"
260
+ const genericMatch = parentRef.match(/<(.+)>/);
261
+ if (!genericMatch) {
262
+ return false;
263
+ }
264
+
265
+ const genericPart = genericMatch[1];
266
+
267
+ // Split by comma while respecting nested generics
268
+ // and check if argName matches any parameter
269
+ let depth = 0;
270
+ let current = "";
271
+ const params: string[] = [];
272
+
273
+ for (const char of genericPart) {
274
+ if (char === "<") {
275
+ depth++;
276
+ current += char;
277
+ } else if (char === ">") {
278
+ depth--;
279
+ current += char;
280
+ } else if (char === "," && depth === 0) {
281
+ params.push(current.trim());
282
+ current = "";
283
+ } else {
284
+ current += char;
285
+ }
286
+ }
287
+ if (current.trim()) {
288
+ params.push(current.trim());
289
+ }
290
+
291
+ // Check if argName matches any parameter exactly or is the base of a constrained param
292
+ return params.some(
293
+ (param) => param === argName || param.startsWith(`${argName} `),
294
+ );
295
+ }
296
+ }
@@ -0,0 +1,266 @@
1
+ import ts from "typescript";
2
+ import path from "node:path";
3
+ import { TsMorphTypeDefinitionFinder } from "./ts-morph-type-finder";
4
+
5
+ /**
6
+ * TypeScript context for automatic import resolution.
7
+ * When provided, the generator uses TypeScript's module resolution to determine
8
+ * where types should be imported from.
9
+ */
10
+ export interface TypeScriptContext {
11
+ /** The TypeScript program */
12
+ program: ts.Program;
13
+ /** The source file containing the type being generated */
14
+ sourceFile: ts.SourceFile;
15
+ /** The output directory where generated files will be written */
16
+ outputDir: string;
17
+ }
18
+
19
+ /**
20
+ * Information about an unexported type that needs to be exported
21
+ */
22
+ export interface UnexportedTypeInfo {
23
+ /** The type name that needs to be exported */
24
+ typeName: string;
25
+ /** The file path where the type is declared */
26
+ filePath: string;
27
+ }
28
+
29
+ /**
30
+ * Result from type resolution.
31
+ * - sameFile: type is defined in the same file as the main type
32
+ * - notFound: type couldn't be resolved anywhere
33
+ * - string: import path for the type (package name or relative path)
34
+ */
35
+ export type TypeResolutionResult = "sameFile" | "notFound" | string;
36
+
37
+ /**
38
+ * Check if a file path is a TypeScript lib/built-in declaration file.
39
+ * These are files like lib.dom.d.ts that contain built-in type definitions.
40
+ */
41
+ export function isBuiltInDeclarationPath(filePath: string): boolean {
42
+ // TypeScript lib files
43
+ if (filePath.includes("/typescript/lib/lib.")) return true;
44
+ // Node.js built-in types
45
+ if (filePath.includes("/@types/node/")) return true;
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * Check if a declaration node is exported.
51
+ */
52
+ export function isDeclarationExported(
53
+ node: ts.Declaration,
54
+ typescript: typeof ts,
55
+ ): boolean {
56
+ // Check for export modifier on the declaration itself
57
+ const modifiers = typescript.canHaveModifiers(node)
58
+ ? typescript.getModifiers(node)
59
+ : undefined;
60
+ if (modifiers) {
61
+ for (const modifier of modifiers) {
62
+ if (modifier.kind === typescript.SyntaxKind.ExportKeyword) {
63
+ return true;
64
+ }
65
+ }
66
+ }
67
+
68
+ // Check if this declaration is part of an export statement
69
+ const parent = node.parent;
70
+ if (parent && typescript.isExportDeclaration(parent)) {
71
+ return true;
72
+ }
73
+
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * Creates an import resolver using TypeScript's type checker and
79
+ * TsMorphTypeDefinitionFinder for tracing types through imports.
80
+ * This handles cases where types come through extends, Pick, re-exports, etc.
81
+ */
82
+ export function createTypeScriptResolver(tsContext: TypeScriptContext): {
83
+ resolveTypePath: (typeName: string) => TypeResolutionResult;
84
+ getUnexportedTypes: () => UnexportedTypeInfo[];
85
+ } {
86
+ const { program, sourceFile, outputDir } = tsContext;
87
+ const typeChecker = program.getTypeChecker();
88
+
89
+ // Create the type definition finder for recursive search
90
+ const finder = new TsMorphTypeDefinitionFinder();
91
+
92
+ // Cache resolved paths
93
+ const resolvedCache = new Map<string, TypeResolutionResult>();
94
+
95
+ // Track types that exist but aren't exported
96
+ const unexportedTypes: UnexportedTypeInfo[] = [];
97
+
98
+ /**
99
+ * Resolve a type name to its import path.
100
+ * Returns:
101
+ * - "sameFile": type is in the same file as the source
102
+ * - "notFound": type couldn't be resolved anywhere
103
+ * - string (import path): type should be imported from this path
104
+ */
105
+ function resolveTypePath(typeName: string): TypeResolutionResult {
106
+ if (resolvedCache.has(typeName)) {
107
+ return resolvedCache.get(typeName)!;
108
+ }
109
+
110
+ // First, try to find the type using TsMorphTypeDefinitionFinder
111
+ // This recursively searches through imports
112
+ const typeFilePath = finder.findTypeSourceFile(
113
+ typeName,
114
+ sourceFile.fileName,
115
+ );
116
+
117
+ if (typeFilePath) {
118
+ // Check if it's from the same file
119
+ if (typeFilePath === sourceFile.fileName) {
120
+ resolvedCache.set(typeName, "sameFile");
121
+ return "sameFile";
122
+ }
123
+
124
+ // Check if it's a built-in declaration
125
+ if (isBuiltInDeclarationPath(typeFilePath)) {
126
+ resolvedCache.set(typeName, "sameFile");
127
+ return "sameFile";
128
+ }
129
+
130
+ // Check if it's from node_modules (external package)
131
+ if (typeFilePath.includes("node_modules")) {
132
+ const nodeModulesIdx = typeFilePath.lastIndexOf("node_modules/");
133
+ const afterNodeModules = typeFilePath.slice(
134
+ nodeModulesIdx + "node_modules/".length,
135
+ );
136
+
137
+ // Handle scoped packages (@scope/package)
138
+ let packageName: string;
139
+ if (afterNodeModules.startsWith("@")) {
140
+ const parts = afterNodeModules.split("/");
141
+ packageName = `${parts[0]}/${parts[1]}`;
142
+ } else {
143
+ packageName = afterNodeModules.split("/")[0];
144
+ }
145
+
146
+ resolvedCache.set(typeName, packageName);
147
+ return packageName;
148
+ }
149
+
150
+ // It's a local file - compute relative path from outputDir
151
+ let relativePath = path.relative(outputDir, typeFilePath);
152
+ relativePath = relativePath.replace(/\.tsx?$/, ".js");
153
+ if (!relativePath.startsWith(".")) {
154
+ relativePath = "./" + relativePath;
155
+ }
156
+
157
+ resolvedCache.set(typeName, relativePath);
158
+ return relativePath;
159
+ }
160
+
161
+ // If TsMorphTypeDefinitionFinder didn't find it, fall back to symbol resolution
162
+ // This handles cases where types are in scope but not through imports
163
+ const symbols = typeChecker.getSymbolsInScope(
164
+ sourceFile,
165
+ ts.SymbolFlags.Type | ts.SymbolFlags.Interface | ts.SymbolFlags.TypeAlias,
166
+ );
167
+
168
+ const matchingSymbols = symbols.filter((s) => s.getName() === typeName);
169
+
170
+ let symbol: ts.Symbol | undefined;
171
+ let validDeclaration: ts.Declaration | undefined;
172
+ let unexportedDeclarationFile: string | undefined;
173
+
174
+ for (const s of matchingSymbols) {
175
+ const declarations = s.getDeclarations();
176
+ if (declarations && declarations.length > 0) {
177
+ const decl = declarations[0];
178
+ const declFile = decl.getSourceFile().fileName;
179
+
180
+ // Skip built-in declarations
181
+ if (isBuiltInDeclarationPath(declFile)) {
182
+ continue;
183
+ }
184
+
185
+ // Check if the declaration is exported (has export modifier or is in node_modules)
186
+ const isFromNodeModules = declFile.includes("node_modules");
187
+ const isExported = isFromNodeModules || isDeclarationExported(decl, ts);
188
+
189
+ if (isExported) {
190
+ symbol = s;
191
+ validDeclaration = decl;
192
+ break;
193
+ } else {
194
+ // Track unexported declaration for warning
195
+ unexportedDeclarationFile = declFile;
196
+ }
197
+ }
198
+ }
199
+
200
+ if (!symbol || !validDeclaration) {
201
+ // Type exists but is not exported - track for warning
202
+ if (unexportedDeclarationFile) {
203
+ // Check if we already tracked this type
204
+ const alreadyTracked = unexportedTypes.some(
205
+ (t) =>
206
+ t.typeName === typeName && t.filePath === unexportedDeclarationFile,
207
+ );
208
+ if (!alreadyTracked) {
209
+ unexportedTypes.push({
210
+ typeName,
211
+ filePath: unexportedDeclarationFile,
212
+ });
213
+ }
214
+ // Type exists but isn't exported - treat as "not found" for import purposes
215
+ // It will be added to the same-file import, which will cause a type error,
216
+ // but the warning system will tell users what to export
217
+ resolvedCache.set(typeName, "sameFile");
218
+ return "sameFile";
219
+ }
220
+ // Type truly not found
221
+ resolvedCache.set(typeName, "notFound");
222
+ return "notFound";
223
+ }
224
+
225
+ const declSourceFile = validDeclaration.getSourceFile();
226
+ const declFilePath = declSourceFile.fileName;
227
+
228
+ if (declFilePath === sourceFile.fileName) {
229
+ resolvedCache.set(typeName, "sameFile");
230
+ return "sameFile";
231
+ }
232
+
233
+ if (declFilePath.includes("node_modules")) {
234
+ const nodeModulesIdx = declFilePath.lastIndexOf("node_modules/");
235
+ const afterNodeModules = declFilePath.slice(
236
+ nodeModulesIdx + "node_modules/".length,
237
+ );
238
+
239
+ let packageName: string;
240
+ if (afterNodeModules.startsWith("@")) {
241
+ const parts = afterNodeModules.split("/");
242
+ packageName = `${parts[0]}/${parts[1]}`;
243
+ } else {
244
+ packageName = afterNodeModules.split("/")[0];
245
+ }
246
+
247
+ resolvedCache.set(typeName, packageName);
248
+ return packageName;
249
+ }
250
+
251
+ let relativePath = path.relative(outputDir, declFilePath);
252
+ relativePath = relativePath.replace(/\.tsx?$/, ".js");
253
+ if (!relativePath.startsWith(".")) {
254
+ relativePath = "./" + relativePath;
255
+ }
256
+
257
+ resolvedCache.set(typeName, relativePath);
258
+ return relativePath;
259
+ }
260
+
261
+ function getUnexportedTypes(): UnexportedTypeInfo[] {
262
+ return [...unexportedTypes];
263
+ }
264
+
265
+ return { resolveTypePath, getUnexportedTypes };
266
+ }