@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,331 @@
1
+ import path from "node:path";
2
+ import type { RefType } from "@xlr-lib/xlr";
3
+ import type {
4
+ TypeScriptContext,
5
+ TypeResolutionResult,
6
+ UnexportedTypeInfo,
7
+ } from "./type-resolver";
8
+ import { createTypeScriptResolver } from "./type-resolver";
9
+ import {
10
+ extractBaseName,
11
+ parseNamespacedType,
12
+ PLAYER_BUILTINS,
13
+ getAssetWrapperExtendsRefByName,
14
+ type TypeRegistry,
15
+ } from "./utils";
16
+ import type { TypeTracker } from "./type-collector";
17
+ import type { TypeTransformContext } from "./type-transformer";
18
+
19
+ /**
20
+ * Configuration for import generation.
21
+ */
22
+ export interface ImportGeneratorConfig {
23
+ /** Import path for functional utilities (default: "@player-lang/functional-dsl") */
24
+ functionalImportPath?: string;
25
+ /** Import path for player-ui types (default: "@player-ui/types") */
26
+ typesImportPath?: string;
27
+ /** TypeScript context for automatic import resolution */
28
+ tsContext?: TypeScriptContext;
29
+ /** Function to generate the type import path for a given type name */
30
+ typeImportPathGenerator?: (typeName: string) => string;
31
+ /** Types defined in the same source file as the main type */
32
+ sameFileTypes?: Set<string>;
33
+ /** External type mappings (type name -> package name) */
34
+ externalTypes?: Map<string, string>;
35
+ /** Type registry for resolving types that extend AssetWrapper */
36
+ typeRegistry?: TypeRegistry;
37
+ }
38
+
39
+ /**
40
+ * Generates import statements and tracks type references.
41
+ * Implements both TypeTracker and TypeTransformContext interfaces.
42
+ */
43
+ export class ImportGenerator implements TypeTracker, TypeTransformContext {
44
+ private readonly config: ImportGeneratorConfig;
45
+
46
+ /** Track all type references that need to be imported, grouped by source file */
47
+ private referencedTypesBySource = new Map<string, Set<string>>();
48
+
49
+ /** Track types that should be imported from the main type's source file */
50
+ private referencedTypes = new Set<string>();
51
+
52
+ /** Track whether Asset type is needed for imports */
53
+ private needsAssetImport = false;
54
+
55
+ /** Track generic parameter symbols (e.g., T, U) that should not be imported */
56
+ private genericParamSymbols = new Set<string>();
57
+
58
+ /** TypeScript resolver for automatic import path resolution */
59
+ private readonly tsResolver?: {
60
+ resolveTypePath: (typeName: string) => TypeResolutionResult;
61
+ getUnexportedTypes: () => UnexportedTypeInfo[];
62
+ };
63
+
64
+ /** Track types that couldn't be resolved - will cause errors if used */
65
+ private unresolvedTypes = new Set<string>();
66
+
67
+ /** Track namespaces that need to be imported (e.g., "Validation" from @player-ui/types) */
68
+ private namespaceImports = new Set<string>();
69
+
70
+ /** Map short type names to their full qualified names (e.g., "CrossfieldReference" -> "Validation.CrossfieldReference") */
71
+ private namespaceMemberMap = new Map<string, string>();
72
+
73
+ /** Effective sameFileTypes - computed from tsContext or provided directly */
74
+ private readonly effectiveSameFileTypes?: Set<string>;
75
+
76
+ constructor(config: ImportGeneratorConfig = {}) {
77
+ this.config = config;
78
+
79
+ // Initialize TypeScript resolver if tsContext is provided
80
+ if (config.tsContext) {
81
+ this.tsResolver = createTypeScriptResolver(config.tsContext);
82
+ }
83
+
84
+ // Use provided sameFileTypes or fall back to empty set
85
+ this.effectiveSameFileTypes = config.sameFileTypes;
86
+ }
87
+
88
+ // TypeTransformContext implementation
89
+ setNeedsAssetImport(value: boolean): void {
90
+ this.needsAssetImport = value;
91
+ }
92
+
93
+ getNeedsAssetImport(): boolean {
94
+ return this.needsAssetImport;
95
+ }
96
+
97
+ getNamespaceMemberMap(): Map<string, string> {
98
+ return this.namespaceMemberMap;
99
+ }
100
+
101
+ getGenericParamSymbols(): Set<string> {
102
+ return this.genericParamSymbols;
103
+ }
104
+
105
+ getAssetWrapperExtendsRef(typeName: string): RefType | undefined {
106
+ const registry = this.config.typeRegistry;
107
+ if (!registry) return undefined;
108
+ return getAssetWrapperExtendsRefByName(typeName, registry);
109
+ }
110
+
111
+ /**
112
+ * Get list of types that exist but need to be exported.
113
+ */
114
+ getUnexportedTypes(): UnexportedTypeInfo[] {
115
+ return this.tsResolver?.getUnexportedTypes() ?? [];
116
+ }
117
+
118
+ /**
119
+ * Get list of types that couldn't be resolved at all.
120
+ */
121
+ getUnresolvedTypes(): string[] {
122
+ return Array.from(this.unresolvedTypes);
123
+ }
124
+
125
+ /**
126
+ * Track a referenced type for import generation.
127
+ */
128
+ trackReferencedType(typeName: string): void {
129
+ const { externalTypes, typeImportPathGenerator } = this.config;
130
+
131
+ // Strip generic arguments for import purposes (import { ListItem } not { ListItem<T> })
132
+ const importName = extractBaseName(typeName);
133
+
134
+ // Never track PLAYER_BUILTINS (Asset, AssetWrapper, Binding, Expression)
135
+ // These have special handling and should not be imported as regular types.
136
+ // Note: This is intentionally redundant with isBuiltinType() filtering in
137
+ // TypeTransformer.shouldTrackTypeForImport() to provide defense in depth.
138
+ if (PLAYER_BUILTINS.has(importName)) {
139
+ return;
140
+ }
141
+
142
+ // Check if it's a namespaced type (e.g., "Validation.CrossfieldReference")
143
+ const namespaced = parseNamespacedType(importName);
144
+ if (namespaced) {
145
+ // Track the namespace for import (e.g., "Validation" from "@player-ui/types")
146
+ const namespaceName = namespaced.namespace;
147
+
148
+ // Check if we have an external types mapping for the namespace
149
+ if (externalTypes?.has(namespaceName)) {
150
+ const packageName = externalTypes.get(namespaceName)!;
151
+ if (!this.referencedTypesBySource.has(packageName)) {
152
+ this.referencedTypesBySource.set(packageName, new Set());
153
+ }
154
+ this.referencedTypesBySource.get(packageName)!.add(namespaceName);
155
+ return;
156
+ }
157
+
158
+ // Default: assume it comes from @player-ui/types for namespaced types
159
+ const typesImportPath = this.config.typesImportPath ?? "@player-ui/types";
160
+ if (!this.referencedTypesBySource.has(typesImportPath)) {
161
+ this.referencedTypesBySource.set(typesImportPath, new Set());
162
+ }
163
+ this.referencedTypesBySource.get(typesImportPath)!.add(namespaceName);
164
+ return;
165
+ }
166
+
167
+ // Check if it's an explicitly configured external type
168
+ if (externalTypes?.has(importName)) {
169
+ const packageName = externalTypes.get(importName)!;
170
+ if (!this.referencedTypesBySource.has(packageName)) {
171
+ this.referencedTypesBySource.set(packageName, new Set());
172
+ }
173
+ this.referencedTypesBySource.get(packageName)!.add(importName);
174
+ return;
175
+ }
176
+
177
+ // If TypeScript resolver is available, use it for automatic resolution
178
+ if (this.tsResolver) {
179
+ const result = this.tsResolver.resolveTypePath(importName);
180
+ if (result === "notFound") {
181
+ this.unresolvedTypes.add(importName);
182
+ return;
183
+ }
184
+ if (result === "sameFile") {
185
+ this.referencedTypes.add(importName);
186
+ } else {
187
+ if (!this.referencedTypesBySource.has(result)) {
188
+ this.referencedTypesBySource.set(result, new Set());
189
+ }
190
+ this.referencedTypesBySource.get(result)!.add(importName);
191
+ }
192
+ return;
193
+ }
194
+
195
+ // Fall back to manual configuration
196
+ const sameFileTypes = this.effectiveSameFileTypes;
197
+ if (sameFileTypes) {
198
+ if (sameFileTypes.has(importName)) {
199
+ this.referencedTypes.add(importName);
200
+ } else if (typeImportPathGenerator) {
201
+ const importPath = typeImportPathGenerator(importName);
202
+ if (importPath) {
203
+ if (!this.referencedTypesBySource.has(importPath)) {
204
+ this.referencedTypesBySource.set(importPath, new Set());
205
+ }
206
+ this.referencedTypesBySource.get(importPath)!.add(importName);
207
+ }
208
+ } else {
209
+ this.referencedTypes.add(importName);
210
+ }
211
+ } else {
212
+ this.referencedTypes.add(importName);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Track a namespace that needs to be imported.
218
+ */
219
+ trackNamespaceImport(namespaceName: string): void {
220
+ this.namespaceImports.add(namespaceName);
221
+
222
+ // Use the TypeScript resolver to find where the namespace is exported from
223
+ if (this.tsResolver) {
224
+ const result = this.tsResolver.resolveTypePath(namespaceName);
225
+ if (result === "sameFile") {
226
+ this.referencedTypes.add(namespaceName);
227
+ } else if (result !== "notFound") {
228
+ if (!this.referencedTypesBySource.has(result)) {
229
+ this.referencedTypesBySource.set(result, new Set());
230
+ }
231
+ this.referencedTypesBySource.get(result)!.add(namespaceName);
232
+ } else {
233
+ this.unresolvedTypes.add(namespaceName);
234
+ }
235
+ return;
236
+ }
237
+
238
+ // Fall back: check external types first
239
+ const { externalTypes, typeImportPathGenerator } = this.config;
240
+ if (externalTypes?.has(namespaceName)) {
241
+ const packageName = externalTypes.get(namespaceName)!;
242
+ if (!this.referencedTypesBySource.has(packageName)) {
243
+ this.referencedTypesBySource.set(packageName, new Set());
244
+ }
245
+ this.referencedTypesBySource.get(packageName)!.add(namespaceName);
246
+ return;
247
+ }
248
+
249
+ // Try typeImportPathGenerator
250
+ if (typeImportPathGenerator) {
251
+ const importPath = typeImportPathGenerator(namespaceName);
252
+ if (importPath) {
253
+ if (!this.referencedTypesBySource.has(importPath)) {
254
+ this.referencedTypesBySource.set(importPath, new Set());
255
+ }
256
+ this.referencedTypesBySource.get(importPath)!.add(namespaceName);
257
+ return;
258
+ }
259
+ }
260
+
261
+ // Last resort: assume same file
262
+ this.referencedTypes.add(namespaceName);
263
+ }
264
+
265
+ /**
266
+ * Generate import statements.
267
+ */
268
+ generateImports(mainTypeName: string): string {
269
+ // Determine the import path for the main type
270
+ let typeImportPath: string;
271
+ if (this.config.tsContext) {
272
+ const { sourceFile, outputDir } = this.config.tsContext;
273
+ let relativePath = path.relative(outputDir, sourceFile.fileName);
274
+ relativePath = relativePath.replace(/\.tsx?$/, ".js");
275
+ if (!relativePath.startsWith(".")) {
276
+ relativePath = "./" + relativePath;
277
+ }
278
+ typeImportPath = relativePath;
279
+ } else if (this.config.typeImportPathGenerator) {
280
+ typeImportPath = this.config.typeImportPathGenerator(mainTypeName);
281
+ } else {
282
+ typeImportPath = `../types/${this.getTypeFileName(mainTypeName)}`;
283
+ }
284
+
285
+ // Collect all types to import from the main source file
286
+ const typesToImport = new Set<string>([mainTypeName]);
287
+
288
+ // Add referenced types that are in the same source file
289
+ Array.from(this.referencedTypes).forEach((name) => {
290
+ typesToImport.add(name);
291
+ });
292
+
293
+ // Get import paths from config or use defaults
294
+ const typesImportPath = this.config.typesImportPath ?? "@player-ui/types";
295
+ const functionalImportPath =
296
+ this.config.functionalImportPath ?? "@player-lang/functional-dsl";
297
+
298
+ // Build import lines
299
+ const lines: string[] = [];
300
+
301
+ // Main type import
302
+ const typeImportStatement = `import type { ${Array.from(typesToImport).join(", ")} } from "${typeImportPath}";`;
303
+ lines.push(typeImportStatement);
304
+
305
+ // Generate imports for types from other source files
306
+ for (const [importPath, types] of this.referencedTypesBySource) {
307
+ const typeNames = Array.from(types).join(", ");
308
+ lines.push(`import type { ${typeNames} } from "${importPath}";`);
309
+ }
310
+
311
+ // Only import Asset if it's used
312
+ if (this.needsAssetImport) {
313
+ lines.push(`import type { Asset } from "${typesImportPath}";`);
314
+ }
315
+
316
+ lines.push(
317
+ `import { type FunctionalBuilder, type BaseBuildContext, type FunctionalPartial, FunctionalBuilderBase, createInspectMethod, type TaggedTemplateValue } from "${functionalImportPath}";`,
318
+ );
319
+
320
+ return lines.join("\n");
321
+ }
322
+
323
+ private getTypeFileName(typeName: string): string {
324
+ // Convert PascalCase to kebab-case for file name
325
+ return typeName
326
+ .replace(/([A-Z])/g, "-$1")
327
+ .toLowerCase()
328
+ .replace(/^-/, "")
329
+ .replace(/asset$/, "");
330
+ }
331
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @player-lang/functional-dsl-generator
3
+ *
4
+ * Generates functional builders from XLR types for Player-UI assets.
5
+ */
6
+
7
+ export {
8
+ generateFunctionalBuilder,
9
+ generateFunctionalBuilderWithWarnings,
10
+ type GeneratorConfig,
11
+ type BuilderInfo,
12
+ type GeneratorResult,
13
+ type UnexportedTypeInfo,
14
+ type TypeScriptContext,
15
+ } from "./generator";
16
+ export * from "./utils";
17
+ export {
18
+ TsMorphTypeDefinitionFinder,
19
+ type UnexportedTypeLocation,
20
+ } from "./ts-morph-type-finder";
21
+ export {
22
+ createTypeScriptResolver,
23
+ isBuiltInDeclarationPath,
24
+ isDeclarationExported,
25
+ type TypeResolutionResult,
26
+ } from "./type-resolver";
27
+ export {
28
+ isNodeModulesPath,
29
+ extractPackageNameFromPath,
30
+ createRelativeImportPath,
31
+ resolveRelativeImportPath,
32
+ } from "./path-utils";
33
+ export {
34
+ categorizeTypes,
35
+ groupExternalTypesByPackage,
36
+ type TypeCategories,
37
+ type CategorizerOptions,
38
+ } from "./type-categorizer";
@@ -0,0 +1,155 @@
1
+ import { normalize, sep, dirname, relative, resolve } from "path";
2
+
3
+ /**
4
+ * Normalizes a file path and splits it into parts.
5
+ */
6
+ function normalizeAndSplitPath(filePath: string): string[] {
7
+ return normalize(filePath).split(sep);
8
+ }
9
+
10
+ /**
11
+ * Checks if a path segment indicates a package directory.
12
+ */
13
+ function isPackageDirectory(part: string): boolean {
14
+ return part === "node_modules" || part.startsWith(".pnpm");
15
+ }
16
+
17
+ /**
18
+ * Determines if a file path appears to be from a package (node_modules or pnpm store).
19
+ *
20
+ * @param filePath - The file path to analyze
21
+ * @returns True if the path looks like it belongs to a package
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * isNodeModulesPath('/project/node_modules/react/index.d.ts') // true
26
+ * isNodeModulesPath('/project/src/components/Button.ts') // false
27
+ * isNodeModulesPath('/project/node_modules/.pnpm/react@18.0.0/...') // true
28
+ * ```
29
+ */
30
+ export function isNodeModulesPath(filePath: string): boolean {
31
+ const parts = normalizeAndSplitPath(filePath);
32
+ return parts.some(isPackageDirectory);
33
+ }
34
+
35
+ /**
36
+ * Extracts the package name from a node_modules path.
37
+ * Handles npm, pnpm store, scoped packages, and various structures.
38
+ *
39
+ * @param filePath - The file path to extract package name from
40
+ * @returns The package name (e.g., 'lodash', '@player-lang/types') or null
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * // Standard npm package
45
+ * extractPackageNameFromPath('/project/node_modules/lodash/index.d.ts')
46
+ * // Returns: 'lodash'
47
+ *
48
+ * // Scoped package
49
+ * extractPackageNameFromPath('/project/node_modules/@player-lang/types/index.d.ts')
50
+ * // Returns: '@player-lang/types'
51
+ *
52
+ * // pnpm store
53
+ * extractPackageNameFromPath('/project/node_modules/.pnpm/@player-lang+types@1.0.0/node_modules/@player-lang/types/index.d.ts')
54
+ * // Returns: '@player-lang/types'
55
+ * ```
56
+ */
57
+ export function extractPackageNameFromPath(filePath: string): string | null {
58
+ const parts = normalizeAndSplitPath(filePath);
59
+
60
+ // Find the last occurrence of node_modules (for pnpm which has nested node_modules)
61
+ let lastNodeModulesIndex = -1;
62
+ for (let i = parts.length - 1; i >= 0; i--) {
63
+ if (parts[i] === "node_modules") {
64
+ lastNodeModulesIndex = i;
65
+ break;
66
+ }
67
+ }
68
+
69
+ if (lastNodeModulesIndex === -1 || lastNodeModulesIndex >= parts.length - 1) {
70
+ return null;
71
+ }
72
+
73
+ const afterNodeModules = parts.slice(lastNodeModulesIndex + 1);
74
+
75
+ // Check if it's a scoped package (@scope/package)
76
+ if (afterNodeModules[0]?.startsWith("@")) {
77
+ if (afterNodeModules.length >= 2) {
78
+ return `${afterNodeModules[0]}/${afterNodeModules[1]}`;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ // Regular package
84
+ return afterNodeModules[0] || null;
85
+ }
86
+
87
+ /**
88
+ * Creates a relative import path from one file to another.
89
+ * Converts TypeScript extensions to JavaScript for runtime imports.
90
+ *
91
+ * @param fromFile - The absolute path of the importing file
92
+ * @param toFile - The absolute path of the file being imported
93
+ * @returns Relative import path with .js extension
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * createRelativeImportPath('/project/src/types/foo.ts', '/project/src/types/bar.ts')
98
+ * // Returns: './bar.js'
99
+ *
100
+ * createRelativeImportPath('/project/src/builders/foo.ts', '/project/src/types/bar.ts')
101
+ * // Returns: '../types/bar.js'
102
+ * ```
103
+ */
104
+ export function createRelativeImportPath(
105
+ fromFile: string,
106
+ toFile: string,
107
+ ): string {
108
+ const fromDir = dirname(fromFile);
109
+ let relativePath = relative(fromDir, toFile);
110
+
111
+ // Convert TypeScript extensions to JavaScript
112
+ relativePath = relativePath.replace(/\.tsx?$/, ".js");
113
+ relativePath = relativePath.replace(/\.d\.ts$/, ".js");
114
+
115
+ // Ensure the path starts with ./ or ../
116
+ if (!relativePath.startsWith(".")) {
117
+ relativePath = `./${relativePath}`;
118
+ }
119
+
120
+ return relativePath;
121
+ }
122
+
123
+ /**
124
+ * Resolves a relative import path to an absolute file path.
125
+ * Handles .js to .ts conversion for TypeScript resolution.
126
+ *
127
+ * @param fromFile - The absolute path of the file containing the import
128
+ * @param importSpecifier - The import specifier (e.g., './types', '../utils.js')
129
+ * @returns Absolute path with .ts extension
130
+ */
131
+ export function resolveRelativeImportPath(
132
+ fromFile: string,
133
+ importSpecifier: string,
134
+ ): string {
135
+ const fromDir = dirname(fromFile);
136
+
137
+ // Convert .js to .ts for TypeScript resolution
138
+ let actualSpecifier = importSpecifier;
139
+ if (importSpecifier.endsWith(".js")) {
140
+ actualSpecifier = importSpecifier.replace(/\.js$/, ".ts");
141
+ }
142
+
143
+ let resolvedPath = resolve(fromDir, actualSpecifier);
144
+
145
+ // Add .ts extension if not present
146
+ if (
147
+ !resolvedPath.endsWith(".ts") &&
148
+ !resolvedPath.endsWith(".tsx") &&
149
+ !resolvedPath.endsWith(".d.ts")
150
+ ) {
151
+ resolvedPath += ".ts";
152
+ }
153
+
154
+ return resolvedPath;
155
+ }