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