inviton-backduck 1.0.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +302 -0
  3. package/dist/apidoc/api-doc-generator.d.ts +58 -0
  4. package/dist/apidoc/api-doc-generator.d.ts.map +1 -0
  5. package/dist/apidoc/api-doc-generator.js +201 -0
  6. package/dist/apidoc/api-doc-generator.js.map +1 -0
  7. package/dist/apidoc/config.d.ts +153 -0
  8. package/dist/apidoc/config.d.ts.map +1 -0
  9. package/dist/apidoc/config.js +254 -0
  10. package/dist/apidoc/config.js.map +1 -0
  11. package/dist/apidoc/controller-parser.d.ts +208 -0
  12. package/dist/apidoc/controller-parser.d.ts.map +1 -0
  13. package/dist/apidoc/controller-parser.js +686 -0
  14. package/dist/apidoc/controller-parser.js.map +1 -0
  15. package/dist/apidoc/html-generator.d.ts +290 -0
  16. package/dist/apidoc/html-generator.d.ts.map +1 -0
  17. package/dist/apidoc/html-generator.js +2295 -0
  18. package/dist/apidoc/html-generator.js.map +1 -0
  19. package/dist/apidoc/index.d.ts +20 -0
  20. package/dist/apidoc/index.d.ts.map +1 -0
  21. package/dist/apidoc/index.js +16 -0
  22. package/dist/apidoc/index.js.map +1 -0
  23. package/dist/apidoc/openapi-builder.d.ts +169 -0
  24. package/dist/apidoc/openapi-builder.d.ts.map +1 -0
  25. package/dist/apidoc/openapi-builder.js +634 -0
  26. package/dist/apidoc/openapi-builder.js.map +1 -0
  27. package/dist/apidoc/parameterGeneratorRegistry.d.ts +20 -0
  28. package/dist/apidoc/parameterGeneratorRegistry.d.ts.map +1 -0
  29. package/dist/apidoc/parameterGeneratorRegistry.js +6 -0
  30. package/dist/apidoc/parameterGeneratorRegistry.js.map +1 -0
  31. package/dist/apidoc/test-type-resolver.d.ts +2 -0
  32. package/dist/apidoc/test-type-resolver.d.ts.map +1 -0
  33. package/dist/apidoc/test-type-resolver.js +6 -0
  34. package/dist/apidoc/test-type-resolver.js.map +1 -0
  35. package/dist/apidoc/type-resolver.d.ts +266 -0
  36. package/dist/apidoc/type-resolver.d.ts.map +1 -0
  37. package/dist/apidoc/type-resolver.js +1226 -0
  38. package/dist/apidoc/type-resolver.js.map +1 -0
  39. package/dist/apidoc/verify-type-resolution.d.ts +3 -0
  40. package/dist/apidoc/verify-type-resolution.d.ts.map +1 -0
  41. package/dist/apidoc/verify-type-resolution.js +29 -0
  42. package/dist/apidoc/verify-type-resolution.js.map +1 -0
  43. package/dist/bun/bunRouter.d.ts +70 -0
  44. package/dist/bun/bunRouter.d.ts.map +1 -0
  45. package/dist/bun/bunRouter.js +324 -0
  46. package/dist/bun/bunRouter.js.map +1 -0
  47. package/dist/bun/bunServer.d.ts +72 -0
  48. package/dist/bun/bunServer.d.ts.map +1 -0
  49. package/dist/bun/bunServer.js +218 -0
  50. package/dist/bun/bunServer.js.map +1 -0
  51. package/dist/bun/bunStaticFiles.d.ts +76 -0
  52. package/dist/bun/bunStaticFiles.d.ts.map +1 -0
  53. package/dist/bun/bunStaticFiles.js +251 -0
  54. package/dist/bun/bunStaticFiles.js.map +1 -0
  55. package/dist/bun/index.d.ts +7 -0
  56. package/dist/bun/index.d.ts.map +1 -0
  57. package/dist/bun/index.js +7 -0
  58. package/dist/bun/index.js.map +1 -0
  59. package/dist/data-contracts.d.ts +132 -0
  60. package/dist/data-contracts.d.ts.map +1 -0
  61. package/dist/data-contracts.js +2 -0
  62. package/dist/data-contracts.js.map +1 -0
  63. package/dist/decorators.d.ts +75 -0
  64. package/dist/decorators.d.ts.map +1 -0
  65. package/dist/decorators.js +101 -0
  66. package/dist/decorators.js.map +1 -0
  67. package/dist/express/expressFrontendRouter.d.ts +17 -0
  68. package/dist/express/expressFrontendRouter.d.ts.map +1 -0
  69. package/dist/express/expressFrontendRouter.js +33 -0
  70. package/dist/express/expressFrontendRouter.js.map +1 -0
  71. package/dist/express/expressRouter.d.ts +25 -0
  72. package/dist/express/expressRouter.d.ts.map +1 -0
  73. package/dist/express/expressRouter.js +150 -0
  74. package/dist/express/expressRouter.js.map +1 -0
  75. package/dist/express/index.d.ts +6 -0
  76. package/dist/express/index.d.ts.map +1 -0
  77. package/dist/express/index.js +6 -0
  78. package/dist/express/index.js.map +1 -0
  79. package/dist/index.d.ts +47 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +52 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/router.d.ts +162 -0
  84. package/dist/router.d.ts.map +1 -0
  85. package/dist/router.js +350 -0
  86. package/dist/router.js.map +1 -0
  87. package/dist/runtime-detect.d.ts +20 -0
  88. package/dist/runtime-detect.d.ts.map +1 -0
  89. package/dist/runtime-detect.js +20 -0
  90. package/dist/runtime-detect.js.map +1 -0
  91. package/dist/server.d.ts +126 -0
  92. package/dist/server.d.ts.map +1 -0
  93. package/dist/server.js +181 -0
  94. package/dist/server.js.map +1 -0
  95. package/dist/utils.d.ts +83 -0
  96. package/dist/utils.d.ts.map +1 -0
  97. package/dist/utils.js +157 -0
  98. package/dist/utils.js.map +1 -0
  99. package/package.json +65 -0
@@ -0,0 +1,1226 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Node, Project, } from 'ts-morph';
4
+ import { ApiDocConfig } from './config';
5
+ /**
6
+ * TypeScript type resolver for OpenAPI schema generation
7
+ * Handles inheritance, utility types (Pick, Omit), Temporal types, and JSDoc extraction
8
+ */
9
+ export class TypeResolver {
10
+ project;
11
+ serverDir;
12
+ resolvedTypes = new Map();
13
+ resolvingStack = new Set();
14
+ serviceInterfaceCache = new Map();
15
+ conversionDepth = 0;
16
+ MAX_CONVERSION_DEPTH = 20;
17
+ /** Cache for OpenAPI type conversions keyed by type text */
18
+ openApiTypeCache = new Map();
19
+ /** Cache for loaded source files */
20
+ sourceFileCache = new Map();
21
+ /** Cache for directory file listings */
22
+ directoryFilesCache = new Map();
23
+ constructor(serverDir) {
24
+ this.serverDir = path.resolve(serverDir);
25
+ this.project = new Project({
26
+ tsConfigFilePath: path.join(this.serverDir, 'tsconfig.json'),
27
+ skipAddingFilesFromTsConfig: true,
28
+ });
29
+ }
30
+ /**
31
+ * Resolve a type by name and return its OpenAPI schema
32
+ */
33
+ resolveType(typeName, sourceFile) {
34
+ // Check cache first
35
+ const cached = this.resolvedTypes.get(typeName);
36
+ if (cached) {
37
+ return cached;
38
+ }
39
+ // Check for circular reference
40
+ if (this.resolvingStack.has(typeName)) {
41
+ return {
42
+ name: typeName,
43
+ description: null,
44
+ properties: [],
45
+ isEnum: false,
46
+ sourcePath: null,
47
+ };
48
+ }
49
+ // Mark as resolving to detect circular references
50
+ this.resolvingStack.add(typeName);
51
+ try {
52
+ const resolved = this.resolveTypeInternal(typeName, sourceFile);
53
+ if (resolved) {
54
+ this.resolvedTypes.set(typeName, resolved);
55
+ }
56
+ return resolved;
57
+ }
58
+ finally {
59
+ this.resolvingStack.delete(typeName);
60
+ }
61
+ }
62
+ /**
63
+ * Convert a resolved type to OpenAPI schema
64
+ */
65
+ typeToOpenApiSchema(resolvedType) {
66
+ if (resolvedType.isEnum && resolvedType.enumValues) {
67
+ return {
68
+ type: typeof resolvedType.enumValues[0] === 'string' ? 'string' : 'integer',
69
+ enum: resolvedType.enumValues,
70
+ description: resolvedType.description || undefined,
71
+ };
72
+ }
73
+ const properties = {};
74
+ const required = [];
75
+ for (const prop of resolvedType.properties) {
76
+ properties[prop.name] = {
77
+ ...prop.type,
78
+ description: prop.description || prop.type.description,
79
+ };
80
+ if (!prop.optional) {
81
+ required.push(prop.name);
82
+ }
83
+ }
84
+ return {
85
+ type: 'object',
86
+ properties,
87
+ required: required.length > 0 ? required : undefined,
88
+ description: resolvedType.description || undefined,
89
+ };
90
+ }
91
+ /**
92
+ * Convert a TypeScript type directly to OpenAPI type
93
+ */
94
+ tsTypeToOpenApi(type, propertyName) {
95
+ // Guard against infinite recursion
96
+ if (this.conversionDepth >= this.MAX_CONVERSION_DEPTH) {
97
+ return { type: 'object', additionalProperties: true };
98
+ }
99
+ // Check cache first (use type text as key)
100
+ const typeText = type.getText();
101
+ const cached = this.openApiTypeCache.get(typeText);
102
+ if (cached) {
103
+ return cached;
104
+ }
105
+ this.conversionDepth++;
106
+ try {
107
+ const result = this.tsTypeToOpenApiInternal(type, propertyName);
108
+ // Cache the result (only for non-trivial types)
109
+ if (typeText.length > 10) {
110
+ this.openApiTypeCache.set(typeText, result);
111
+ }
112
+ return result;
113
+ }
114
+ finally {
115
+ this.conversionDepth--;
116
+ }
117
+ }
118
+ /**
119
+ * Internal implementation of tsTypeToOpenApi
120
+ */
121
+ tsTypeToOpenApiInternal(type, _propertyName) {
122
+ const typeText = type.getText();
123
+ // Check for special types (Temporal, Date, etc.)
124
+ const specialSchema = ApiDocConfig.getSpecialTypeSchema(typeText);
125
+ if (specialSchema) {
126
+ return specialSchema;
127
+ }
128
+ // Handle Temporal namespace types
129
+ if (typeText.includes('Temporal.')) {
130
+ const temporalMatch = typeText.match(/Temporal\.(PlainDateTime|PlainDate|PlainTime|Instant)/);
131
+ if (temporalMatch) {
132
+ const schema = ApiDocConfig.getSpecialTypeSchema(`Temporal.${temporalMatch[1]}`);
133
+ if (schema) {
134
+ return schema;
135
+ }
136
+ }
137
+ }
138
+ // Handle enum types - check if we can resolve to get member info
139
+ if (type.isEnum() || type.isEnumLiteral()) {
140
+ const symbol = type.getSymbol() || type.getAliasSymbol();
141
+ if (symbol) {
142
+ const enumName = symbol.getName();
143
+ const resolved = this.resolveType(enumName);
144
+ if (resolved && resolved.isEnum && resolved.enumMembers) {
145
+ return {
146
+ 'type': typeof resolved.enumValues?.[0] === 'string' ? 'string' : 'integer',
147
+ 'enum': resolved.enumValues,
148
+ 'x-enum-members': resolved.enumMembers,
149
+ 'description': resolved.description || undefined,
150
+ };
151
+ }
152
+ }
153
+ }
154
+ // Handle primitives
155
+ if (type.isString() || type.isStringLiteral()) {
156
+ return { type: 'string' };
157
+ }
158
+ if (type.isNumber() || type.isNumberLiteral()) {
159
+ return { type: 'number' };
160
+ }
161
+ if (type.isBoolean() || type.isBooleanLiteral()) {
162
+ return { type: 'boolean' };
163
+ }
164
+ // Handle null/undefined
165
+ if (typeText === 'null' || typeText === 'undefined') {
166
+ return { type: 'string', nullable: true };
167
+ }
168
+ // Handle 'any' type
169
+ if (typeText === 'any') {
170
+ return { type: 'object', additionalProperties: true };
171
+ }
172
+ // Handle arrays
173
+ if (type.isArray()) {
174
+ const elementType = type.getArrayElementType();
175
+ if (elementType) {
176
+ return {
177
+ type: 'array',
178
+ items: this.tsTypeToOpenApi(elementType),
179
+ };
180
+ }
181
+ return { type: 'array', items: { type: 'object' } };
182
+ }
183
+ // Handle union types
184
+ if (type.isUnion()) {
185
+ const unionTypes = type.getUnionTypes();
186
+ const nonNullTypes = unionTypes.filter(t => t.getText() !== 'undefined' && t.getText() !== 'null');
187
+ const hasNull = unionTypes.length !== nonNullTypes.length;
188
+ if (nonNullTypes.length === 1) {
189
+ const result = this.tsTypeToOpenApi(nonNullTypes[0]);
190
+ if (hasNull) {
191
+ result.nullable = true;
192
+ }
193
+ return result;
194
+ }
195
+ // Check if all union members are literals (enum-like)
196
+ const literals = nonNullTypes.filter(t => t.isStringLiteral() || t.isNumberLiteral());
197
+ if (literals.length === nonNullTypes.length && literals.length > 0) {
198
+ const values = literals.map((t) => {
199
+ if (t.isStringLiteral()) {
200
+ return t.getLiteralValue();
201
+ }
202
+ return t.getLiteralValue();
203
+ });
204
+ // Try to find enum member info by checking if all literals are from the same enum
205
+ const firstLiteral = nonNullTypes[0];
206
+ const symbol = firstLiteral.getSymbol();
207
+ if (symbol) {
208
+ // Try to get the parent enum declaration
209
+ const declarations = symbol.getDeclarations();
210
+ if (declarations.length > 0) {
211
+ const firstDecl = declarations[0];
212
+ const parent = firstDecl.getParent();
213
+ if (parent && Node.isEnumDeclaration(parent)) {
214
+ const enumName = parent.getName();
215
+ const resolved = this.resolveType(enumName);
216
+ if (resolved && resolved.isEnum && resolved.enumMembers) {
217
+ return {
218
+ 'type': typeof values[0] === 'string' ? 'string' : 'integer',
219
+ 'enum': values,
220
+ 'x-enum-members': resolved.enumMembers,
221
+ 'nullable': hasNull || undefined,
222
+ };
223
+ }
224
+ }
225
+ }
226
+ }
227
+ return {
228
+ type: typeof values[0] === 'string' ? 'string' : 'integer',
229
+ enum: values,
230
+ nullable: hasNull || undefined,
231
+ };
232
+ }
233
+ // Multiple different types - use oneOf
234
+ return {
235
+ oneOf: nonNullTypes.map(t => this.tsTypeToOpenApi(t)),
236
+ nullable: hasNull || undefined,
237
+ };
238
+ }
239
+ // Handle intersection types (merge properties)
240
+ if (type.isIntersection()) {
241
+ const intersectionTypes = type.getIntersectionTypes();
242
+ const allProperties = {};
243
+ const allRequired = [];
244
+ for (const intersectType of intersectionTypes) {
245
+ const resolved = this.tsTypeToOpenApi(intersectType);
246
+ if (resolved.properties) {
247
+ Object.assign(allProperties, resolved.properties);
248
+ }
249
+ if (resolved.required) {
250
+ allRequired.push(...resolved.required.filter(r => !allRequired.includes(r)));
251
+ }
252
+ }
253
+ return {
254
+ type: 'object',
255
+ properties: Object.keys(allProperties).length > 0 ? allProperties : undefined,
256
+ required: allRequired.length > 0 ? allRequired : undefined,
257
+ };
258
+ }
259
+ // Handle object types with properties
260
+ if (type.isObject()) {
261
+ const symbol = type.getSymbol() || type.getAliasSymbol();
262
+ if (symbol) {
263
+ const name = symbol.getName();
264
+ // Skip built-in types
265
+ if ([
266
+ 'Array',
267
+ 'Object',
268
+ 'Promise',
269
+ 'Map',
270
+ 'Set',
271
+ ].includes(name)) {
272
+ return { type: 'object', additionalProperties: true };
273
+ }
274
+ // Try to resolve as a known type
275
+ const resolved = this.resolveTypeFromSymbol(symbol);
276
+ if (resolved) {
277
+ // Return reference for complex types
278
+ return {
279
+ $ref: `#/components/schemas/${resolved.name}`,
280
+ };
281
+ }
282
+ }
283
+ // Handle inline object types
284
+ const properties = type.getProperties();
285
+ if (properties.length > 0) {
286
+ const props = {};
287
+ const required = [];
288
+ for (const prop of properties) {
289
+ const propName = prop.getName();
290
+ const declarations = prop.getDeclarations();
291
+ if (declarations.length > 0) {
292
+ const decl = declarations[0];
293
+ if (Node.isPropertySignature(decl)) {
294
+ const propType = decl.getType();
295
+ props[propName] = this.tsTypeToOpenApi(propType, propName);
296
+ if (!decl.hasQuestionToken()) {
297
+ required.push(propName);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ return {
303
+ type: 'object',
304
+ properties: props,
305
+ required: required.length > 0 ? required : undefined,
306
+ };
307
+ }
308
+ }
309
+ // Handle Record<K, V> type
310
+ if (typeText.startsWith('Record<')) {
311
+ return {
312
+ type: 'object',
313
+ additionalProperties: true,
314
+ };
315
+ }
316
+ // Default to object
317
+ return { type: 'object' };
318
+ }
319
+ /**
320
+ * Extract JSDoc description from a property
321
+ */
322
+ extractPropertyJsDoc(property) {
323
+ const jsDocs = property.getJsDocs();
324
+ if (jsDocs.length > 0) {
325
+ const description = jsDocs[0].getDescription();
326
+ return this.cleanJsDocDescription(description?.trim() || null, true);
327
+ }
328
+ return null;
329
+ }
330
+ /**
331
+ * Check if JSDoc contains @privateField tag (case insensitive)
332
+ * Fields with this tag should be omitted from API documentation
333
+ */
334
+ hasPrivateFieldTag(jsdocText) {
335
+ if (!jsdocText) {
336
+ return false;
337
+ }
338
+ return /@privatefield\b/i.test(jsdocText);
339
+ }
340
+ /**
341
+ * Check if JSDoc contains @enterpriseOnly tag (case insensitive)
342
+ * Fields with this tag are only available in enterprise mode
343
+ */
344
+ hasEnterpriseOnlyTag(jsdocText) {
345
+ if (!jsdocText) {
346
+ return false;
347
+ }
348
+ return /@enterpriseonly\b/i.test(jsdocText);
349
+ }
350
+ /**
351
+ * Clean JSDoc description by removing other JSDoc tags but preserving {@link} tags
352
+ * The {@link} tags will be converted to HTML links later by the HTML generator
353
+ * @param description The raw JSDoc description
354
+ * @param preserveNewlines Whether to preserve newlines (default: false for backward compatibility)
355
+ */
356
+ cleanJsDocDescription(description, preserveNewlines = false) {
357
+ if (!description) {
358
+ return null;
359
+ }
360
+ // Preserve {@link TypeName} and {@link TypeName|text} for later HTML conversion
361
+ // Only remove other JSDoc inline tags like {@see ...}, {@code ...}, etc.
362
+ // Use a negative lookahead to exclude @link from the removal pattern
363
+ let cleaned = description.replace(/\{@(?!link\s)(\w+)\s[^}]*\}/g, '');
364
+ if (!preserveNewlines) {
365
+ // Normalize whitespace - replace multiple whitespace with single space
366
+ cleaned = cleaned.replace(/\s+/g, ' ').trim();
367
+ }
368
+ else {
369
+ // Preserve newlines AND indentation (for code blocks in descriptions)
370
+ cleaned = cleaned.trim();
371
+ }
372
+ return cleaned || null;
373
+ }
374
+ /**
375
+ * Get all resolved types (for component schemas)
376
+ */
377
+ getAllResolvedTypes() {
378
+ return this.resolvedTypes;
379
+ }
380
+ /**
381
+ * Clear the resolved types cache
382
+ */
383
+ clearCache() {
384
+ this.resolvedTypes.clear();
385
+ this.resolvingStack.clear();
386
+ this.serviceInterfaceCache.clear();
387
+ }
388
+ /**
389
+ * Add a source file to the project for type resolution (with caching)
390
+ */
391
+ addSourceFile(filePath) {
392
+ // Check cache first
393
+ const cached = this.sourceFileCache.get(filePath);
394
+ if (cached) {
395
+ return cached;
396
+ }
397
+ try {
398
+ const sourceFile = this.project.addSourceFileAtPath(filePath);
399
+ this.sourceFileCache.set(filePath, sourceFile);
400
+ return sourceFile;
401
+ }
402
+ catch {
403
+ return undefined;
404
+ }
405
+ }
406
+ /**
407
+ * Extract extra parameters generator info from @extraParameters JSDoc tag in service interface method.
408
+ * @param interfaceName The name of the service interface (e.g., "IOfferService")
409
+ * @param methodName The name of the method to extract extra parameters from
410
+ * @returns Extra parameter generator info or null if not found
411
+ */
412
+ extractServiceMethodExtraParameters(interfaceName, methodName) {
413
+ // Check cache first
414
+ let interfaceDecl = this.serviceInterfaceCache.get(interfaceName);
415
+ if (interfaceDecl === undefined) {
416
+ // Not in cache, try to find it
417
+ const typeDecl = this.findTypeDeclaration(interfaceName);
418
+ if (typeDecl && Node.isInterfaceDeclaration(typeDecl)) {
419
+ interfaceDecl = typeDecl;
420
+ }
421
+ else {
422
+ interfaceDecl = null;
423
+ }
424
+ // Cache the result (even if null to avoid repeated lookups)
425
+ this.serviceInterfaceCache.set(interfaceName, interfaceDecl);
426
+ }
427
+ if (!interfaceDecl) {
428
+ return null;
429
+ }
430
+ // Find the method property in the interface
431
+ const methodProperty = interfaceDecl.getProperty(methodName);
432
+ if (methodProperty && Node.isPropertySignature(methodProperty)) {
433
+ return this.extractExtraParametersFromPropertyJsDoc(methodProperty);
434
+ }
435
+ // Also try getMethod for method signatures (e.g., methodName(): ReturnType)
436
+ const methodSignature = interfaceDecl.getMethod(methodName);
437
+ if (methodSignature) {
438
+ const jsDocs = methodSignature.getJsDocs();
439
+ if (jsDocs.length > 0) {
440
+ return this.extractExtraParametersFromJsDoc(jsDocs[0]);
441
+ }
442
+ }
443
+ return null;
444
+ }
445
+ /**
446
+ * Extract extra parameters generator info from JSDoc
447
+ */
448
+ extractExtraParametersFromJsDoc(jsDoc) {
449
+ const tags = jsDoc.getTags();
450
+ const extraParamsTag = tags.find((tag) => tag.getTagName() === 'extraParameters');
451
+ if (!extraParamsTag) {
452
+ return null;
453
+ }
454
+ const comment = 'getComment' in extraParamsTag && typeof extraParamsTag.getComment === 'function'
455
+ ? extraParamsTag.getComment()
456
+ : undefined;
457
+ if (typeof comment !== 'string') {
458
+ return null;
459
+ }
460
+ const trimmed = comment.trim();
461
+ if (!trimmed) {
462
+ return null;
463
+ }
464
+ const parts = trimmed.split(/\s+/);
465
+ if (parts.length === 0) {
466
+ return null;
467
+ }
468
+ const generatorName = parts[0];
469
+ const rawArgs = parts.slice(1);
470
+ // Convert args: try to parse as number, otherwise keep as string
471
+ const args = rawArgs.map((arg) => {
472
+ const num = parseFloat(arg);
473
+ return Number.isNaN(num) ? arg : num;
474
+ });
475
+ return { name: generatorName, args };
476
+ }
477
+ /**
478
+ * Extract extra parameters generator info from property JSDoc
479
+ */
480
+ extractExtraParametersFromPropertyJsDoc(property) {
481
+ const jsDocs = property.getJsDocs();
482
+ if (jsDocs.length === 0) {
483
+ return null;
484
+ }
485
+ return this.extractExtraParametersFromJsDoc(jsDocs[0]);
486
+ }
487
+ /**
488
+ * Extract JSDoc description from a service interface method
489
+ * @param interfaceName The name of the service interface (e.g., "ICartService")
490
+ * @param methodName The name of the method to extract JSDoc from (e.g., "validateCart")
491
+ * @returns The JSDoc description or null if not found
492
+ */
493
+ extractServiceMethodJsDoc(interfaceName, methodName) {
494
+ // Check cache first
495
+ let interfaceDecl = this.serviceInterfaceCache.get(interfaceName);
496
+ if (interfaceDecl === undefined) {
497
+ // Not in cache, try to find it
498
+ const typeDecl = this.findTypeDeclaration(interfaceName);
499
+ if (typeDecl && Node.isInterfaceDeclaration(typeDecl)) {
500
+ interfaceDecl = typeDecl;
501
+ }
502
+ else {
503
+ interfaceDecl = null;
504
+ }
505
+ // Cache the result (even if null to avoid repeated lookups)
506
+ this.serviceInterfaceCache.set(interfaceName, interfaceDecl);
507
+ }
508
+ if (!interfaceDecl) {
509
+ return null;
510
+ }
511
+ // Find the method property in the interface
512
+ const methodProperty = interfaceDecl.getProperty(methodName);
513
+ if (methodProperty && Node.isPropertySignature(methodProperty)) {
514
+ return this.extractPropertyJsDoc(methodProperty);
515
+ }
516
+ // Also try getMethod for method signatures (e.g., methodName(): ReturnType)
517
+ const methodSignature = interfaceDecl.getMethod(methodName);
518
+ if (methodSignature) {
519
+ const jsDocs = methodSignature.getJsDocs();
520
+ if (jsDocs.length > 0) {
521
+ const description = jsDocs[0].getDescription();
522
+ return this.cleanJsDocDescription(description?.trim() || null, true);
523
+ }
524
+ }
525
+ return null;
526
+ }
527
+ /**
528
+ * Internal type resolution logic
529
+ */
530
+ resolveTypeInternal(typeName, sourceFile) {
531
+ // Clean the type name
532
+ const cleanName = this.cleanTypeName(typeName);
533
+ // Try to find the type declaration
534
+ const typeDecl = this.findTypeDeclaration(cleanName, sourceFile);
535
+ if (!typeDecl) {
536
+ return null;
537
+ }
538
+ // Handle interface declarations
539
+ if (Node.isInterfaceDeclaration(typeDecl)) {
540
+ return this.resolveInterface(typeDecl);
541
+ }
542
+ // Handle class declarations
543
+ if (Node.isClassDeclaration(typeDecl)) {
544
+ return this.resolveClass(typeDecl);
545
+ }
546
+ // Handle type alias declarations
547
+ if (Node.isTypeAliasDeclaration(typeDecl)) {
548
+ return this.resolveTypeAlias(typeDecl);
549
+ }
550
+ // Handle enum declarations
551
+ if (Node.isEnumDeclaration(typeDecl)) {
552
+ const members = typeDecl.getMembers();
553
+ const enumMembers = members.map((m) => {
554
+ const memberName = m.getName();
555
+ const value = m.getValue();
556
+ const memberValue = value !== undefined ? value : memberName;
557
+ // Extract JSDoc from member
558
+ const jsDocs = m.getJsDocs();
559
+ let description = null;
560
+ if (jsDocs.length > 0) {
561
+ description = this.cleanJsDocDescription(jsDocs[0].getDescription()?.trim() || null);
562
+ }
563
+ return {
564
+ name: memberName,
565
+ value: memberValue,
566
+ description,
567
+ };
568
+ });
569
+ return {
570
+ name: cleanName,
571
+ description: this.extractNodeJsDoc(typeDecl),
572
+ properties: [],
573
+ isEnum: true,
574
+ enumValues: enumMembers.map(m => m.value),
575
+ enumMembers,
576
+ sourcePath: typeDecl.getSourceFile().getFilePath(),
577
+ };
578
+ }
579
+ return null;
580
+ }
581
+ /**
582
+ * Resolve an interface declaration including inherited properties
583
+ */
584
+ resolveInterface(interfaceDecl) {
585
+ const properties = [];
586
+ // Get properties from extended interfaces/classes first
587
+ const extendedTypes = interfaceDecl.getExtends();
588
+ for (const extendedType of extendedTypes) {
589
+ const extendedTypeName = extendedType.getText();
590
+ const extendedResolved = this.resolveType(this.cleanTypeName(extendedTypeName));
591
+ if (extendedResolved) {
592
+ properties.push(...extendedResolved.properties);
593
+ }
594
+ }
595
+ // Get own properties (override inherited ones with same name)
596
+ const ownProperties = interfaceDecl.getProperties();
597
+ for (const prop of ownProperties) {
598
+ const propName = prop.getName();
599
+ const existingIndex = properties.findIndex(p => p.name === propName);
600
+ const resolvedProp = this.resolveProperty(prop);
601
+ // Skip properties with @privateField tag
602
+ if (resolvedProp === null) {
603
+ // If this property overrides an inherited one, remove it
604
+ if (existingIndex >= 0) {
605
+ properties.splice(existingIndex, 1);
606
+ }
607
+ continue;
608
+ }
609
+ if (existingIndex >= 0) {
610
+ properties[existingIndex] = resolvedProp;
611
+ }
612
+ else {
613
+ properties.push(resolvedProp);
614
+ }
615
+ }
616
+ return {
617
+ name: interfaceDecl.getName(),
618
+ description: this.extractNodeJsDoc(interfaceDecl),
619
+ properties,
620
+ isEnum: false,
621
+ sourcePath: interfaceDecl.getSourceFile().getFilePath(),
622
+ };
623
+ }
624
+ /**
625
+ * Resolve a class declaration including inherited properties
626
+ */
627
+ resolveClass(classDecl) {
628
+ const properties = [];
629
+ // Get properties from extended class first
630
+ const extendedClass = classDecl.getExtends();
631
+ if (extendedClass) {
632
+ const extendedTypeName = extendedClass.getText();
633
+ const extendedResolved = this.resolveType(this.cleanTypeName(extendedTypeName));
634
+ if (extendedResolved) {
635
+ properties.push(...extendedResolved.properties);
636
+ }
637
+ }
638
+ // Get own properties (override inherited ones with same name)
639
+ const ownProperties = classDecl.getProperties();
640
+ for (const prop of ownProperties) {
641
+ const propName = prop.getName();
642
+ const existingIndex = properties.findIndex(p => p.name === propName);
643
+ const resolvedProp = this.resolveClassProperty(prop);
644
+ // Skip properties with @privateField tag
645
+ if (resolvedProp === null) {
646
+ // If this property overrides an inherited one, remove it
647
+ if (existingIndex >= 0) {
648
+ properties.splice(existingIndex, 1);
649
+ }
650
+ continue;
651
+ }
652
+ if (existingIndex >= 0) {
653
+ properties[existingIndex] = resolvedProp;
654
+ }
655
+ else {
656
+ properties.push(resolvedProp);
657
+ }
658
+ }
659
+ return {
660
+ name: classDecl.getName() || 'UnknownClass',
661
+ description: this.extractNodeJsDoc(classDecl),
662
+ properties,
663
+ isEnum: false,
664
+ sourcePath: classDecl.getSourceFile().getFilePath(),
665
+ };
666
+ }
667
+ /**
668
+ * Resolve a class property declaration to ResolvedProperty
669
+ * Returns null if the property has @privateField tag
670
+ */
671
+ resolveClassProperty(property) {
672
+ // Check for @privateField tag in JSDoc - skip this property if found
673
+ const jsDocs = property.getJsDocs();
674
+ let isEnterpriseOnly = false;
675
+ if (jsDocs.length > 0) {
676
+ const fullText = jsDocs[0].getText();
677
+ if (this.hasPrivateFieldTag(fullText)) {
678
+ return null;
679
+ }
680
+ // Check for @enterpriseOnly tag
681
+ isEnterpriseOnly = this.hasEnterpriseOnlyTag(fullText);
682
+ }
683
+ const name = property.getName();
684
+ const optional = property.hasQuestionToken();
685
+ const description = this.extractClassPropertyJsDoc(property);
686
+ const type = property.getType();
687
+ // Try to get enum member info from the type annotation
688
+ const typeNode = property.getTypeNode();
689
+ let openApiType = this.tsTypeToOpenApi(type, name);
690
+ if (typeNode) {
691
+ const typeAnnotation = typeNode.getText();
692
+ const resolvedEnum = this.resolveType(typeAnnotation);
693
+ if (resolvedEnum && resolvedEnum.isEnum && resolvedEnum.enumMembers) {
694
+ openApiType = {
695
+ ...openApiType,
696
+ 'x-enum-members': resolvedEnum.enumMembers,
697
+ };
698
+ }
699
+ }
700
+ // Add enterprise-only marker if tagged
701
+ if (isEnterpriseOnly) {
702
+ openApiType = {
703
+ ...openApiType,
704
+ 'x-enterprise-only': true,
705
+ };
706
+ }
707
+ return {
708
+ name,
709
+ type: openApiType,
710
+ optional,
711
+ description,
712
+ };
713
+ }
714
+ /**
715
+ * Extract JSDoc description from a class property
716
+ */
717
+ extractClassPropertyJsDoc(property) {
718
+ const jsDocs = property.getJsDocs();
719
+ if (jsDocs.length > 0) {
720
+ const description = jsDocs[0].getDescription();
721
+ const cleaned = this.cleanJsDocDescription(description?.trim() || null);
722
+ // Debug logging (uncomment for troubleshooting)
723
+ // console.log(`[DEBUG] Class property ${property.getName()}: JSDoc="${cleaned}"`);
724
+ return cleaned;
725
+ }
726
+ // Try getting leading comment trivia as fallback
727
+ const leadingCommentRanges = property.getLeadingCommentRanges();
728
+ for (const range of leadingCommentRanges) {
729
+ const text = range.getText();
730
+ if (text.startsWith('/**')) {
731
+ // Extract content between /** and */
732
+ const match = text.match(/\/\*\*\s*\*?\s*(.+?)\s*\*?\s*\*\//s);
733
+ if (match) {
734
+ return this.cleanJsDocDescription(match[1].trim());
735
+ }
736
+ }
737
+ }
738
+ return null;
739
+ }
740
+ /**
741
+ * Resolve a type alias declaration (handles Pick, Omit, etc.)
742
+ */
743
+ resolveTypeAlias(typeAlias) {
744
+ const name = typeAlias.getName();
745
+ const typeNode = typeAlias.getTypeNode();
746
+ if (!typeNode) {
747
+ return {
748
+ name,
749
+ description: this.extractNodeJsDoc(typeAlias),
750
+ properties: [],
751
+ isEnum: false,
752
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
753
+ };
754
+ }
755
+ const typeText = typeNode.getText();
756
+ // Handle Pick<T, K>
757
+ if (typeText.startsWith('Pick<')) {
758
+ return this.resolvePickType(typeAlias, typeText);
759
+ }
760
+ // Handle Omit<T, K>
761
+ if (typeText.startsWith('Omit<')) {
762
+ return this.resolveOmitType(typeAlias, typeText);
763
+ }
764
+ // Handle Partial<T>
765
+ if (typeText.startsWith('Partial<')) {
766
+ return this.resolvePartialType(typeAlias, typeText);
767
+ }
768
+ // Handle Required<T>
769
+ if (typeText.startsWith('Required<')) {
770
+ return this.resolveRequiredType(typeAlias, typeText);
771
+ }
772
+ // Handle intersection types (Type1 & Type2 & { extra: string })
773
+ if (typeText.includes(' & ')) {
774
+ return this.resolveIntersectionTypeAlias(typeAlias);
775
+ }
776
+ // Handle direct type references
777
+ const type = typeAlias.getType();
778
+ if (type.isObject()) {
779
+ const properties = this.resolveTypeProperties(type);
780
+ return {
781
+ name,
782
+ description: this.extractNodeJsDoc(typeAlias),
783
+ properties,
784
+ isEnum: false,
785
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
786
+ };
787
+ }
788
+ // Handle enum-like union types
789
+ if (type.isUnion()) {
790
+ const unionTypes = type.getUnionTypes();
791
+ const literals = unionTypes.filter(t => t.isStringLiteral() || t.isNumberLiteral());
792
+ if (literals.length === unionTypes.length) {
793
+ const values = literals.map(t => t.getLiteralValue());
794
+ return {
795
+ name,
796
+ description: this.extractNodeJsDoc(typeAlias),
797
+ properties: [],
798
+ isEnum: true,
799
+ enumValues: values,
800
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
801
+ };
802
+ }
803
+ }
804
+ return {
805
+ name,
806
+ description: this.extractNodeJsDoc(typeAlias),
807
+ properties: [],
808
+ isEnum: false,
809
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
810
+ };
811
+ }
812
+ /**
813
+ * Resolve Pick<T, K> utility type
814
+ */
815
+ resolvePickType(typeAlias, typeText) {
816
+ const match = typeText.match(/Pick<([^,]+),\s*(.+)>/);
817
+ if (!match) {
818
+ return this.createEmptyResolvedType(typeAlias);
819
+ }
820
+ const baseTypeName = match[1].trim();
821
+ const pickedKeys = this.parseUnionKeys(match[2]);
822
+ const baseResolved = this.resolveType(this.cleanTypeName(baseTypeName));
823
+ if (!baseResolved) {
824
+ return this.createEmptyResolvedType(typeAlias);
825
+ }
826
+ const properties = baseResolved.properties.filter(p => pickedKeys.includes(p.name));
827
+ return {
828
+ name: typeAlias.getName(),
829
+ description: this.extractNodeJsDoc(typeAlias),
830
+ properties,
831
+ isEnum: false,
832
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
833
+ };
834
+ }
835
+ /**
836
+ * Resolve Omit<T, K> utility type
837
+ */
838
+ resolveOmitType(typeAlias, typeText) {
839
+ const match = typeText.match(/Omit<([^,]+),\s*(.+)>/);
840
+ if (!match) {
841
+ return this.createEmptyResolvedType(typeAlias);
842
+ }
843
+ const baseTypeName = match[1].trim();
844
+ const omittedKeys = this.parseUnionKeys(match[2]);
845
+ const baseResolved = this.resolveType(this.cleanTypeName(baseTypeName));
846
+ if (!baseResolved) {
847
+ return this.createEmptyResolvedType(typeAlias);
848
+ }
849
+ const properties = baseResolved.properties.filter(p => !omittedKeys.includes(p.name));
850
+ return {
851
+ name: typeAlias.getName(),
852
+ description: this.extractNodeJsDoc(typeAlias),
853
+ properties,
854
+ isEnum: false,
855
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
856
+ };
857
+ }
858
+ /**
859
+ * Resolve Partial<T> utility type
860
+ */
861
+ resolvePartialType(typeAlias, typeText) {
862
+ const match = typeText.match(/Partial<(.+)>/);
863
+ if (!match) {
864
+ return this.createEmptyResolvedType(typeAlias);
865
+ }
866
+ const baseTypeName = match[1].trim();
867
+ const baseResolved = this.resolveType(this.cleanTypeName(baseTypeName));
868
+ if (!baseResolved) {
869
+ return this.createEmptyResolvedType(typeAlias);
870
+ }
871
+ // Make all properties optional
872
+ const properties = baseResolved.properties.map(p => ({
873
+ ...p,
874
+ optional: true,
875
+ }));
876
+ return {
877
+ name: typeAlias.getName(),
878
+ description: this.extractNodeJsDoc(typeAlias),
879
+ properties,
880
+ isEnum: false,
881
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
882
+ };
883
+ }
884
+ /**
885
+ * Resolve Required<T> utility type
886
+ */
887
+ resolveRequiredType(typeAlias, typeText) {
888
+ const match = typeText.match(/Required<(.+)>/);
889
+ if (!match) {
890
+ return this.createEmptyResolvedType(typeAlias);
891
+ }
892
+ const baseTypeName = match[1].trim();
893
+ const baseResolved = this.resolveType(this.cleanTypeName(baseTypeName));
894
+ if (!baseResolved) {
895
+ return this.createEmptyResolvedType(typeAlias);
896
+ }
897
+ // Make all properties required
898
+ const properties = baseResolved.properties.map(p => ({
899
+ ...p,
900
+ optional: false,
901
+ }));
902
+ return {
903
+ name: typeAlias.getName(),
904
+ description: this.extractNodeJsDoc(typeAlias),
905
+ properties,
906
+ isEnum: false,
907
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
908
+ };
909
+ }
910
+ /**
911
+ * Resolve intersection type alias (Type1 & Type2 & { extra })
912
+ */
913
+ resolveIntersectionTypeAlias(typeAlias) {
914
+ const type = typeAlias.getType();
915
+ const properties = this.resolveTypeProperties(type);
916
+ return {
917
+ name: typeAlias.getName(),
918
+ description: this.extractNodeJsDoc(typeAlias),
919
+ properties,
920
+ isEnum: false,
921
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
922
+ };
923
+ }
924
+ /**
925
+ * Resolve properties from a Type object
926
+ */
927
+ resolveTypeProperties(type) {
928
+ const properties = [];
929
+ const typeProperties = type.getProperties();
930
+ for (const prop of typeProperties) {
931
+ const propName = prop.getName();
932
+ const declarations = prop.getDeclarations();
933
+ let optional = false;
934
+ let description = null;
935
+ let propType = null;
936
+ if (declarations.length > 0) {
937
+ const decl = declarations[0];
938
+ if (Node.isPropertySignature(decl)) {
939
+ optional = decl.hasQuestionToken();
940
+ description = this.extractPropertyJsDoc(decl);
941
+ propType = decl.getType();
942
+ }
943
+ }
944
+ if (!propType) {
945
+ propType = prop.getValueDeclaration()?.getType() || null;
946
+ }
947
+ if (propType) {
948
+ properties.push({
949
+ name: propName,
950
+ type: this.tsTypeToOpenApi(propType, propName),
951
+ optional,
952
+ description,
953
+ });
954
+ }
955
+ }
956
+ return properties;
957
+ }
958
+ /**
959
+ * Resolve a property signature to ResolvedProperty
960
+ * Returns null if the property has @privateField tag
961
+ */
962
+ resolveProperty(property) {
963
+ // Check for @privateField tag in JSDoc - skip this property if found
964
+ const jsDocs = property.getJsDocs();
965
+ let isEnterpriseOnly = false;
966
+ if (jsDocs.length > 0) {
967
+ const fullText = jsDocs[0].getText();
968
+ if (this.hasPrivateFieldTag(fullText)) {
969
+ return null;
970
+ }
971
+ // Check for @enterpriseOnly tag
972
+ isEnterpriseOnly = this.hasEnterpriseOnlyTag(fullText);
973
+ }
974
+ const name = property.getName();
975
+ const optional = property.hasQuestionToken();
976
+ const description = this.extractPropertyJsDoc(property);
977
+ const type = property.getType();
978
+ // Try to get enum member info from the type annotation
979
+ const typeNode = property.getTypeNode();
980
+ let openApiType = this.tsTypeToOpenApi(type, name);
981
+ if (typeNode) {
982
+ const typeAnnotation = typeNode.getText();
983
+ const resolvedEnum = this.resolveType(typeAnnotation);
984
+ if (resolvedEnum && resolvedEnum.isEnum && resolvedEnum.enumMembers) {
985
+ openApiType = {
986
+ ...openApiType,
987
+ 'x-enum-members': resolvedEnum.enumMembers,
988
+ };
989
+ }
990
+ }
991
+ // Add enterprise-only marker if tagged
992
+ if (isEnterpriseOnly) {
993
+ openApiType = {
994
+ ...openApiType,
995
+ 'x-enterprise-only': true,
996
+ };
997
+ }
998
+ return {
999
+ name,
1000
+ type: openApiType,
1001
+ optional,
1002
+ description,
1003
+ };
1004
+ }
1005
+ /**
1006
+ * Resolve type from a Symbol
1007
+ */
1008
+ resolveTypeFromSymbol(symbol) {
1009
+ const name = symbol.getName();
1010
+ const declarations = symbol.getDeclarations();
1011
+ if (declarations.length === 0) {
1012
+ return null;
1013
+ }
1014
+ const decl = declarations[0];
1015
+ const sourceFile = decl.getSourceFile();
1016
+ return this.resolveType(name, sourceFile);
1017
+ }
1018
+ /**
1019
+ * Find type declaration by name
1020
+ */
1021
+ findTypeDeclaration(typeName, sourceFile) {
1022
+ // First search in the provided source file
1023
+ if (sourceFile) {
1024
+ const found = this.findTypeInSourceFile(typeName, sourceFile);
1025
+ if (found) {
1026
+ return found;
1027
+ }
1028
+ }
1029
+ // Search in all loaded source files
1030
+ for (const sf of this.project.getSourceFiles()) {
1031
+ const found = this.findTypeInSourceFile(typeName, sf);
1032
+ if (found) {
1033
+ return found;
1034
+ }
1035
+ }
1036
+ // Try to find in services directory
1037
+ const servicesDir = path.join(this.serverDir, 'src', 'services');
1038
+ const argsFiles = this.findFilesRecursive(servicesDir, '**/*.ts');
1039
+ for (const filePath of argsFiles) {
1040
+ try {
1041
+ const sf = this.project.addSourceFileAtPath(filePath);
1042
+ const found = this.findTypeInSourceFile(typeName, sf);
1043
+ if (found) {
1044
+ return found;
1045
+ }
1046
+ }
1047
+ catch {
1048
+ // File already added or invalid
1049
+ }
1050
+ }
1051
+ // Try to find in data directory
1052
+ const dataDir = path.join(this.serverDir, 'src', 'data');
1053
+ const dataFiles = this.findFilesRecursive(dataDir, '**/*.ts');
1054
+ for (const filePath of dataFiles) {
1055
+ try {
1056
+ const sf = this.project.addSourceFileAtPath(filePath);
1057
+ const found = this.findTypeInSourceFile(typeName, sf);
1058
+ if (found) {
1059
+ return found;
1060
+ }
1061
+ }
1062
+ catch {
1063
+ // File already added or invalid
1064
+ }
1065
+ }
1066
+ return null;
1067
+ }
1068
+ /**
1069
+ * Find type in a specific source file
1070
+ */
1071
+ findTypeInSourceFile(typeName, sourceFile) {
1072
+ // Check interfaces
1073
+ const interfaceDecl = sourceFile.getInterface(typeName);
1074
+ if (interfaceDecl) {
1075
+ return interfaceDecl;
1076
+ }
1077
+ // Check classes by name directly
1078
+ const classDecl = sourceFile.getClass(typeName);
1079
+ if (classDecl) {
1080
+ return classDecl;
1081
+ }
1082
+ // Check all classes in the file (including default exported ones)
1083
+ const allClasses = sourceFile.getClasses();
1084
+ for (const cls of allClasses) {
1085
+ const className = cls.getName();
1086
+ if (className === typeName) {
1087
+ return cls;
1088
+ }
1089
+ }
1090
+ // Check type aliases
1091
+ const typeAlias = sourceFile.getTypeAlias(typeName);
1092
+ if (typeAlias) {
1093
+ return typeAlias;
1094
+ }
1095
+ // Check enums
1096
+ const enumDecl = sourceFile.getEnum(typeName);
1097
+ if (enumDecl) {
1098
+ return enumDecl;
1099
+ }
1100
+ return null;
1101
+ }
1102
+ /**
1103
+ * Extract JSDoc description from a node
1104
+ */
1105
+ extractNodeJsDoc(node) {
1106
+ if (Node.isJSDocable(node)) {
1107
+ const jsDocs = node.getJsDocs();
1108
+ if (jsDocs.length > 0) {
1109
+ const description = jsDocs[0].getDescription();
1110
+ return this.cleanJsDocDescription(description?.trim() || null);
1111
+ }
1112
+ }
1113
+ return null;
1114
+ }
1115
+ /**
1116
+ * Parse union keys from a string like "'id' | 'name' | 'email'"
1117
+ */
1118
+ parseUnionKeys(keysString) {
1119
+ // Remove trailing > if present
1120
+ const cleaned = keysString.replace(/>\s*$/, '').trim();
1121
+ // Handle cases like "keyof SomeType" - can't resolve these
1122
+ if (cleaned.startsWith('keyof')) {
1123
+ return [];
1124
+ }
1125
+ // Split by | and extract string literals
1126
+ return cleaned
1127
+ .split('|')
1128
+ .map(k => k.trim().replace(/^['"]|['"]$/g, ''))
1129
+ .filter(k => k.length > 0);
1130
+ }
1131
+ /**
1132
+ * Clean type name by removing import() paths and namespaces
1133
+ */
1134
+ cleanTypeName(typeName) {
1135
+ // Remove import() syntax
1136
+ let cleaned = typeName.replace(/import\([^)]+\)\./g, '');
1137
+ // Remove namespace prefixes except Temporal
1138
+ cleaned = cleaned.replace(/(\w+)\./g, (match, namespace) => {
1139
+ if (namespace === 'Temporal') {
1140
+ return match;
1141
+ }
1142
+ return '';
1143
+ });
1144
+ // Remove generic parameters for simple lookup
1145
+ const genericIndex = cleaned.indexOf('<');
1146
+ if (genericIndex > 0) {
1147
+ cleaned = cleaned.substring(0, genericIndex);
1148
+ }
1149
+ return cleaned.trim();
1150
+ }
1151
+ /**
1152
+ * Create an empty resolved type for error cases
1153
+ */
1154
+ createEmptyResolvedType(typeAlias) {
1155
+ return {
1156
+ name: typeAlias.getName(),
1157
+ description: this.extractNodeJsDoc(typeAlias),
1158
+ properties: [],
1159
+ isEnum: false,
1160
+ sourcePath: typeAlias.getSourceFile().getFilePath(),
1161
+ };
1162
+ }
1163
+ /**
1164
+ * Find files recursively matching a pattern
1165
+ */
1166
+ findFilesRecursive(dir, pattern) {
1167
+ // Check cache first
1168
+ const cacheKey = `${dir}:${pattern}`;
1169
+ const cached = this.directoryFilesCache.get(cacheKey);
1170
+ if (cached) {
1171
+ return cached;
1172
+ }
1173
+ const results = [];
1174
+ if (!fs.existsSync(dir)) {
1175
+ return results;
1176
+ }
1177
+ const patternRegex = this.globToRegex(pattern);
1178
+ this.walkDirectory(fs, dir, '', patternRegex, results);
1179
+ // Cache the results
1180
+ this.directoryFilesCache.set(cacheKey, results);
1181
+ return results;
1182
+ }
1183
+ /**
1184
+ * Walk directory recursively
1185
+ */
1186
+ walkDirectory(fs, baseDir, relativePath, pattern, results) {
1187
+ const currentDir = path.join(baseDir, relativePath);
1188
+ try {
1189
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
1190
+ for (const entry of entries) {
1191
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1192
+ if (entry.isDirectory()) {
1193
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
1194
+ this.walkDirectory(fs, baseDir, entryRelativePath, pattern, results);
1195
+ }
1196
+ }
1197
+ else if (entry.isFile()) {
1198
+ if (pattern.test(entryRelativePath)) {
1199
+ results.push(path.join(baseDir, entryRelativePath));
1200
+ }
1201
+ }
1202
+ }
1203
+ }
1204
+ catch {
1205
+ // Directory access error - skip
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Convert glob pattern to regex
1210
+ */
1211
+ globToRegex(glob) {
1212
+ // Handle **/*.ts pattern specially - it should match both root files and nested files
1213
+ if (glob === '**/*.ts') {
1214
+ // Match: file.ts OR dir/file.ts OR dir/subdir/file.ts etc.
1215
+ return /^(.+\/)?[^/]+\.ts$/;
1216
+ }
1217
+ const escaped = glob
1218
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
1219
+ .replace(/\*\*/g, '{{DOUBLE_STAR}}')
1220
+ .replace(/\*/g, '[^/]*')
1221
+ .replace(/\{\{DOUBLE_STAR\}\}/g, '.*')
1222
+ .replace(/\?/g, '.');
1223
+ return new RegExp(`^${escaped}$`);
1224
+ }
1225
+ }
1226
+ //# sourceMappingURL=type-resolver.js.map