@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,319 @@
1
+ import { Project, SourceFile, ts } from "ts-morph";
2
+ import { existsSync } from "fs";
3
+ import { dirname, resolve } from "path";
4
+
5
+ /**
6
+ * Information about an unexported type that was found
7
+ */
8
+ export interface UnexportedTypeLocation {
9
+ /** The name of the type */
10
+ typeName: string;
11
+ /** The file where the type is declared (but not exported) */
12
+ filePath: string;
13
+ }
14
+
15
+ /**
16
+ * Finds type definitions by searching through TypeScript source files using ts-morph.
17
+ * Handles interfaces, type aliases, classes, and re-exported types.
18
+ * Supports both local files and types from node_modules.
19
+ *
20
+ * Note: This is the ts-morph based implementation. For the TypeScript API based
21
+ * implementation, see TypeScriptTypeDefinitionFinder in type-resolver.ts.
22
+ */
23
+ export class TsMorphTypeDefinitionFinder {
24
+ private project: Project | undefined;
25
+ private readonly typeLocationCache = new Map<string, string | null>();
26
+ private readonly unexportedTypes = new Map<string, string>();
27
+
28
+ /**
29
+ * Finds the source file for a type by searching the codebase.
30
+ * Recursively follows imports to find where a type is actually defined.
31
+ * Supports both relative imports and node_modules packages.
32
+ *
33
+ * @param typeName - The name of the type to find
34
+ * @param startingFile - The file to start searching from
35
+ * @returns The path to the file containing the type definition, or null if not found
36
+ */
37
+ findTypeSourceFile(typeName: string, startingFile: string): string | null {
38
+ if (!typeName || !startingFile) return null;
39
+
40
+ const cacheKey = `${typeName}:${startingFile}`;
41
+ if (this.typeLocationCache.has(cacheKey)) {
42
+ return this.typeLocationCache.get(cacheKey) || null;
43
+ }
44
+
45
+ try {
46
+ if (!this.project) {
47
+ this.project = new Project({
48
+ useInMemoryFileSystem: false,
49
+ // Enable module resolution for node_modules support
50
+ skipFileDependencyResolution: false,
51
+ compilerOptions: {
52
+ moduleResolution: ts.ModuleResolutionKind.Node16,
53
+ resolveJsonModule: true,
54
+ },
55
+ });
56
+ }
57
+
58
+ const visitedFiles = new Set<string>();
59
+ const result = this.searchForType(typeName, startingFile, visitedFiles);
60
+ this.typeLocationCache.set(cacheKey, result);
61
+ return result;
62
+ } catch {
63
+ this.typeLocationCache.set(cacheKey, null);
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Recursively searches for a type definition through imports.
70
+ * Handles both relative imports and node_modules packages.
71
+ */
72
+ private searchForType(
73
+ typeName: string,
74
+ filePath: string,
75
+ visitedFiles: Set<string>,
76
+ ): string | null {
77
+ if (!existsSync(filePath) || visitedFiles.has(filePath)) return null;
78
+ visitedFiles.add(filePath);
79
+
80
+ try {
81
+ const sourceFile = this.project!.addSourceFileAtPath(filePath);
82
+
83
+ // Check if this file defines and exports the type
84
+ if (this.fileDefinesType(sourceFile, typeName)) {
85
+ return filePath;
86
+ }
87
+
88
+ // Check if the file has the type but doesn't export it
89
+ const typeCheck = this.fileHasTypeDeclaration(sourceFile, typeName);
90
+ if (typeCheck.found && !typeCheck.exported) {
91
+ // Track this unexported type for warning
92
+ this.trackUnexportedType(typeName, typeCheck.filePath);
93
+ }
94
+
95
+ // Search through imports
96
+ for (const importDecl of sourceFile.getImportDeclarations()) {
97
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
98
+
99
+ // Check if this import includes the type we're looking for
100
+ const importedType = this.getImportedTypeFromDeclaration(
101
+ importDecl,
102
+ typeName,
103
+ );
104
+
105
+ if (importedType) {
106
+ // Try to resolve the module to its source file
107
+ const resolvedSourceFile = importDecl.getModuleSpecifierSourceFile();
108
+
109
+ if (resolvedSourceFile) {
110
+ // Found it via ts-morph module resolution (works for node_modules)
111
+ const resolvedPath = resolvedSourceFile.getFilePath();
112
+
113
+ // For re-exports, we might need to search deeper
114
+ if (this.fileDefinesType(resolvedSourceFile, typeName)) {
115
+ return resolvedPath;
116
+ }
117
+
118
+ // Check if it's re-exported from somewhere else
119
+ const deeperResult = this.searchForType(
120
+ typeName,
121
+ resolvedPath,
122
+ visitedFiles,
123
+ );
124
+ if (deeperResult) return deeperResult;
125
+
126
+ // If we can't find it deeper, return the resolved path
127
+ // (the type might be defined in a way we can't detect)
128
+ return resolvedPath;
129
+ }
130
+
131
+ // Fallback: manual resolution for relative imports
132
+ if (moduleSpecifier.startsWith(".")) {
133
+ const resolvedPath = this.resolveImportPath(
134
+ filePath,
135
+ moduleSpecifier,
136
+ );
137
+ if (resolvedPath) {
138
+ const result = this.searchForType(
139
+ typeName,
140
+ resolvedPath,
141
+ visitedFiles,
142
+ );
143
+ if (result) return result;
144
+ }
145
+ }
146
+ }
147
+
148
+ // Also follow relative imports even if they don't explicitly import the type
149
+ // (the type might be re-exported)
150
+ if (moduleSpecifier.startsWith(".")) {
151
+ const resolvedPath = this.resolveImportPath(
152
+ filePath,
153
+ moduleSpecifier,
154
+ );
155
+ if (resolvedPath) {
156
+ const result = this.searchForType(
157
+ typeName,
158
+ resolvedPath,
159
+ visitedFiles,
160
+ );
161
+ if (result) return result;
162
+ }
163
+ }
164
+ }
165
+
166
+ return null;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Checks if an import declaration imports a specific type.
174
+ * Handles named imports, default imports, and namespace imports.
175
+ */
176
+ private getImportedTypeFromDeclaration(
177
+ importDecl: ReturnType<SourceFile["getImportDeclarations"]>[0],
178
+ typeName: string,
179
+ ): boolean {
180
+ // Check named imports: import { Foo } from "..."
181
+ for (const namedImport of importDecl.getNamedImports()) {
182
+ const name =
183
+ namedImport.getAliasNode()?.getText() || namedImport.getName();
184
+ if (name === typeName) return true;
185
+ }
186
+
187
+ // Check default import: import Foo from "..."
188
+ const defaultImport = importDecl.getDefaultImport();
189
+ if (defaultImport?.getText() === typeName) return true;
190
+
191
+ // Check namespace import: import * as Foo from "..."
192
+ const namespaceImport = importDecl.getNamespaceImport();
193
+ if (namespaceImport?.getText() === typeName) return true;
194
+
195
+ return false;
196
+ }
197
+
198
+ /**
199
+ * Checks if a source file defines and exports a specific type.
200
+ * Handles interfaces, type aliases, classes, and re-exported types.
201
+ * Only returns true if the type is publicly exported.
202
+ */
203
+ private fileDefinesType(sourceFile: SourceFile, typeName: string): boolean {
204
+ // Check exported interfaces, type aliases, and classes
205
+ for (const decl of [
206
+ ...sourceFile.getInterfaces(),
207
+ ...sourceFile.getTypeAliases(),
208
+ ...sourceFile.getClasses(),
209
+ ]) {
210
+ if (decl.getName() === typeName && decl.isExported()) {
211
+ return true;
212
+ }
213
+ }
214
+
215
+ // Check exported types in export declarations (re-exports)
216
+ for (const exportDecl of sourceFile.getExportDeclarations()) {
217
+ for (const namedExport of exportDecl.getNamedExports()) {
218
+ if (namedExport.getName() === typeName) return true;
219
+ }
220
+ }
221
+
222
+ return false;
223
+ }
224
+
225
+ /**
226
+ * Resolves a relative import path to an actual file path.
227
+ * Handles TypeScript extensions and index files.
228
+ */
229
+ private resolveImportPath(
230
+ fromFile: string,
231
+ importPath: string,
232
+ ): string | null {
233
+ const dir = dirname(fromFile);
234
+ const extensions = [".ts", ".tsx", ".d.ts"];
235
+
236
+ // Try direct file with TS extensions
237
+ for (const ext of extensions) {
238
+ const fullPath = resolve(dir, `${importPath}${ext}`);
239
+ if (existsSync(fullPath)) return fullPath;
240
+ }
241
+
242
+ // Try index files
243
+ for (const ext of extensions) {
244
+ const fullPath = resolve(dir, `${importPath}/index${ext}`);
245
+ if (existsSync(fullPath)) return fullPath;
246
+ }
247
+
248
+ // Handle .js → .ts mapping
249
+ if (importPath.endsWith(".js")) {
250
+ const tsPath = resolve(dir, importPath.replace(/\.js$/, ".ts"));
251
+ if (existsSync(tsPath)) return tsPath;
252
+
253
+ const dtsPath = resolve(dir, importPath.replace(/\.js$/, ".d.ts"));
254
+ if (existsSync(dtsPath)) return dtsPath;
255
+ }
256
+
257
+ return null;
258
+ }
259
+
260
+ /**
261
+ * Get all types that were found to exist but are not exported.
262
+ * Call this after searching to get the list of types that need to be exported.
263
+ */
264
+ getUnexportedTypes(): UnexportedTypeLocation[] {
265
+ return Array.from(this.unexportedTypes.entries()).map(
266
+ ([typeName, filePath]) => ({
267
+ typeName,
268
+ filePath,
269
+ }),
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Check if a file declares a type (regardless of export status).
275
+ * Used internally to track unexported types.
276
+ */
277
+ private fileHasTypeDeclaration(
278
+ sourceFile: SourceFile,
279
+ typeName: string,
280
+ ): { found: boolean; exported: boolean; filePath: string } {
281
+ const filePath = sourceFile.getFilePath();
282
+
283
+ // Check interfaces, type aliases, and classes
284
+ for (const decl of [
285
+ ...sourceFile.getInterfaces(),
286
+ ...sourceFile.getTypeAliases(),
287
+ ...sourceFile.getClasses(),
288
+ ]) {
289
+ if (decl.getName() === typeName) {
290
+ return {
291
+ found: true,
292
+ exported: decl.isExported(),
293
+ filePath,
294
+ };
295
+ }
296
+ }
297
+
298
+ return { found: false, exported: false, filePath };
299
+ }
300
+
301
+ /**
302
+ * Track an unexported type for later reporting.
303
+ */
304
+ private trackUnexportedType(typeName: string, filePath: string): void {
305
+ if (!this.unexportedTypes.has(typeName)) {
306
+ this.unexportedTypes.set(typeName, filePath);
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Disposes of internal resources and clears caches.
312
+ * Should be called when the finder is no longer needed.
313
+ */
314
+ dispose(): void {
315
+ this.typeLocationCache.clear();
316
+ this.unexportedTypes.clear();
317
+ this.project = undefined;
318
+ }
319
+ }
@@ -0,0 +1,131 @@
1
+ import { TsMorphTypeDefinitionFinder } from "./ts-morph-type-finder";
2
+ import {
3
+ isNodeModulesPath,
4
+ extractPackageNameFromPath,
5
+ createRelativeImportPath,
6
+ } from "./path-utils";
7
+
8
+ /**
9
+ * Categorized types for import generation.
10
+ * Types are split into three categories based on where they should be imported from.
11
+ */
12
+ export interface TypeCategories {
13
+ /** Types defined in the same file as the main type (import from main source) */
14
+ localTypes: Set<string>;
15
+ /** Types from other local files, grouped by relative import path */
16
+ relativeImports: Map<string, Set<string>>;
17
+ /** Types from external packages (node_modules), mapped to package name */
18
+ externalTypes: Map<string, string>;
19
+ }
20
+
21
+ /**
22
+ * Options for type categorization.
23
+ */
24
+ export interface CategorizerOptions {
25
+ /** The main source file being generated */
26
+ mainSourceFile: string;
27
+ /** Types known to be in the main source file (optional optimization) */
28
+ sameFileTypes?: Set<string>;
29
+ }
30
+
31
+ /**
32
+ * Categorizes referenced types into local, relative, and external imports.
33
+ *
34
+ * Uses TsMorphTypeDefinitionFinder to resolve where each type is defined, then
35
+ * categorizes based on the resolved path:
36
+ * - Same file as main → localTypes
37
+ * - Different local file → relativeImports
38
+ * - node_modules → externalTypes
39
+ *
40
+ * @param referencedTypes - Set of type names that need to be imported
41
+ * @param finder - TsMorphTypeDefinitionFinder instance for resolving type locations
42
+ * @param options - Categorization options including main source file
43
+ * @returns Categorized types for import generation
44
+ */
45
+ export function categorizeTypes(
46
+ referencedTypes: Set<string>,
47
+ finder: TsMorphTypeDefinitionFinder,
48
+ options: CategorizerOptions,
49
+ ): TypeCategories {
50
+ const { mainSourceFile, sameFileTypes } = options;
51
+
52
+ const result: TypeCategories = {
53
+ localTypes: new Set(),
54
+ relativeImports: new Map(),
55
+ externalTypes: new Map(),
56
+ };
57
+
58
+ for (const typeName of referencedTypes) {
59
+ // Optimization: if we know the type is in the same file, skip resolution
60
+ if (sameFileTypes?.has(typeName)) {
61
+ result.localTypes.add(typeName);
62
+ continue;
63
+ }
64
+
65
+ // Try to resolve the type's source file
66
+ const sourceFile = finder.findTypeSourceFile(typeName, mainSourceFile);
67
+
68
+ if (!sourceFile) {
69
+ // Could not resolve - assume it's in the same file
70
+ result.localTypes.add(typeName);
71
+ continue;
72
+ }
73
+
74
+ // Normalize paths for comparison
75
+ const normalizedSource = normalizePath(sourceFile);
76
+ const normalizedMain = normalizePath(mainSourceFile);
77
+
78
+ if (normalizedSource === normalizedMain) {
79
+ // Same file
80
+ result.localTypes.add(typeName);
81
+ } else if (isNodeModulesPath(sourceFile)) {
82
+ // External package
83
+ const packageName = extractPackageNameFromPath(sourceFile);
84
+ if (packageName) {
85
+ result.externalTypes.set(typeName, packageName);
86
+ } else {
87
+ // Couldn't extract package name, fallback to local
88
+ result.localTypes.add(typeName);
89
+ }
90
+ } else {
91
+ // Different local file - create relative import path
92
+ const relativePath = createRelativeImportPath(mainSourceFile, sourceFile);
93
+ if (!result.relativeImports.has(relativePath)) {
94
+ result.relativeImports.set(relativePath, new Set());
95
+ }
96
+ result.relativeImports.get(relativePath)!.add(typeName);
97
+ }
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ /**
104
+ * Groups external types by their package name for import generation.
105
+ * This allows generating single imports per package with multiple types.
106
+ *
107
+ * @param externalTypes - Map of typeName → packageName
108
+ * @returns Map of packageName → Set of typeNames
109
+ */
110
+ export function groupExternalTypesByPackage(
111
+ externalTypes: Map<string, string>,
112
+ ): Map<string, Set<string>> {
113
+ const grouped = new Map<string, Set<string>>();
114
+
115
+ for (const [typeName, packageName] of externalTypes) {
116
+ if (!grouped.has(packageName)) {
117
+ grouped.set(packageName, new Set());
118
+ }
119
+ grouped.get(packageName)!.add(typeName);
120
+ }
121
+
122
+ return grouped;
123
+ }
124
+
125
+ /**
126
+ * Normalizes a file path for comparison.
127
+ * Removes trailing slashes and normalizes separators.
128
+ */
129
+ function normalizePath(filePath: string): string {
130
+ return filePath.replace(/\\/g, "/").replace(/\/$/, "");
131
+ }