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