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,686 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Node, Project, SyntaxKind } from 'ts-morph';
4
+ import { ApiDocConfig } from './config';
5
+ /**
6
+ * Parser for TypeScript controller files using ts-morph
7
+ */
8
+ export class ControllerParser {
9
+ project;
10
+ serverDir;
11
+ routerCache = new Map();
12
+ controllerPathCache = new Map();
13
+ typeResolver = null;
14
+ constructor(serverDir) {
15
+ this.serverDir = path.resolve(serverDir);
16
+ this.project = new Project({
17
+ tsConfigFilePath: path.join(this.serverDir, 'tsconfig.json'),
18
+ skipAddingFilesFromTsConfig: true,
19
+ });
20
+ }
21
+ /**
22
+ * Add a source file to the project by file path
23
+ * @param filePath The path to the TS source file
24
+ * @returns The added SourceFile object
25
+ */
26
+ addSourceFileAtPath(filePath) {
27
+ return this.project.addSourceFileAtPath(filePath);
28
+ }
29
+ /**
30
+ * Set the TypeResolver instance for extracting service method JSDoc
31
+ */
32
+ setTypeResolver(typeResolver) {
33
+ this.typeResolver = typeResolver;
34
+ }
35
+ /**
36
+ * Parse endpoint info by controller class name
37
+ */
38
+ parseEndpoint(controllerClassName, apiType = 'shopApi') {
39
+ // Build controller path cache if not already built
40
+ if (this.controllerPathCache.size === 0) {
41
+ this.buildControllerPathCache(apiType);
42
+ }
43
+ const cached = this.controllerPathCache.get(controllerClassName);
44
+ if (!cached) {
45
+ return null;
46
+ }
47
+ // Parse the controller to get route path
48
+ const controllerInfo = this.parseController(cached.filePath);
49
+ if (!controllerInfo) {
50
+ return null;
51
+ }
52
+ return {
53
+ className: controllerClassName,
54
+ routePath: controllerInfo.routePath,
55
+ fullPath: cached.fullPath,
56
+ filePath: cached.filePath,
57
+ tag: cached.tag,
58
+ };
59
+ }
60
+ /**
61
+ * Build cache of controller -> full path mappings by traversing router hierarchy
62
+ */
63
+ buildControllerPathCache(apiType) {
64
+ const sourceDir = ApiDocConfig.getSourcePath(this.serverDir, apiType);
65
+ const basePath = apiType === 'shopApi' ? ApiDocConfig.basePaths.shop : ApiDocConfig.basePaths.admin;
66
+ const mainRouterPath = path.join(sourceDir, '_router.ts');
67
+ if (!fs.existsSync(mainRouterPath)) {
68
+ return;
69
+ }
70
+ this.traverseRouterHierarchy(mainRouterPath, basePath, '');
71
+ }
72
+ /**
73
+ * Recursively traverse router hierarchy and build controller path mappings
74
+ */
75
+ traverseRouterHierarchy(routerFilePath, pathPrefix, tag) {
76
+ const routerInfo = this.parseRouterFile(routerFilePath);
77
+ if (!routerInfo) {
78
+ return;
79
+ }
80
+ // Process registered controllers
81
+ for (const controller of routerInfo.controllers) {
82
+ const controllerFilePath = this.resolveImportPath(routerFilePath, controller.importPath);
83
+ if (!controllerFilePath || !fs.existsSync(controllerFilePath)) {
84
+ continue;
85
+ }
86
+ // Parse controller to get @Route path
87
+ const controllerInfo = this.parseController(controllerFilePath);
88
+ if (controllerInfo) {
89
+ const fullPath = this.normalizePath(pathPrefix + controllerInfo.routePath);
90
+ this.controllerPathCache.set(controller.controllerClassName, {
91
+ fullPath,
92
+ filePath: controllerFilePath,
93
+ tag: tag || controllerInfo.tag,
94
+ });
95
+ }
96
+ }
97
+ // Process sub-routers
98
+ for (const subRouter of routerInfo.subRouters) {
99
+ const subRouterFilePath = this.resolveImportPath(routerFilePath, subRouter.importPath);
100
+ if (!subRouterFilePath || !fs.existsSync(subRouterFilePath)) {
101
+ continue;
102
+ }
103
+ const newPathPrefix = pathPrefix + subRouter.path;
104
+ const newTag = tag || subRouter.path.replace(/^\//, '');
105
+ this.traverseRouterHierarchy(subRouterFilePath, newPathPrefix, newTag);
106
+ }
107
+ }
108
+ /**
109
+ * Parse a _router.ts file to extract router.use() and router.registerController() calls
110
+ */
111
+ parseRouterFile(filePath) {
112
+ const cached = this.routerCache.get(filePath);
113
+ if (cached) {
114
+ return cached;
115
+ }
116
+ if (!fs.existsSync(filePath)) {
117
+ return null;
118
+ }
119
+ const sourceFile = this.project.addSourceFileAtPath(filePath);
120
+ const imports = this.parseImports(sourceFile);
121
+ const subRouters = [];
122
+ const controllers = [];
123
+ // Find all call expressions
124
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
125
+ for (const callExpr of callExpressions) {
126
+ const expression = callExpr.getExpression();
127
+ if (!Node.isPropertyAccessExpression(expression)) {
128
+ continue;
129
+ }
130
+ const methodName = expression.getName();
131
+ const args = callExpr.getArguments();
132
+ if (methodName === 'use' && args.length >= 2) {
133
+ // router.use('/path', subRouter)
134
+ const pathArg = args[0];
135
+ const routerArg = args[1];
136
+ if (Node.isStringLiteral(pathArg)) {
137
+ const routePath = pathArg.getLiteralValue();
138
+ const routerVarName = routerArg.getText();
139
+ const importPath = imports.get(routerVarName);
140
+ if (importPath) {
141
+ subRouters.push({
142
+ path: routePath,
143
+ routerVariableName: routerVarName,
144
+ importPath,
145
+ });
146
+ }
147
+ }
148
+ }
149
+ else if (methodName === 'registerController' && args.length >= 1) {
150
+ // router.registerController(SomeController)
151
+ const controllerArg = args[0];
152
+ const controllerName = controllerArg.getText();
153
+ const importPath = imports.get(controllerName);
154
+ if (importPath) {
155
+ controllers.push({
156
+ controllerClassName: controllerName,
157
+ importPath,
158
+ });
159
+ }
160
+ }
161
+ }
162
+ const relativePath = path.relative(this.serverDir, filePath).replace(/\\/g, '/');
163
+ const directory = path.dirname(filePath);
164
+ const routerInfo = {
165
+ filePath,
166
+ relativePath,
167
+ directory,
168
+ subRouters,
169
+ controllers,
170
+ };
171
+ this.routerCache.set(filePath, routerInfo);
172
+ return routerInfo;
173
+ }
174
+ /**
175
+ * Parse import statements from a source file
176
+ */
177
+ parseImports(sourceFile) {
178
+ const imports = new Map();
179
+ const importDeclarations = sourceFile.getImportDeclarations();
180
+ for (const importDecl of importDeclarations) {
181
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
182
+ const defaultImport = importDecl.getDefaultImport();
183
+ if (defaultImport) {
184
+ imports.set(defaultImport.getText(), moduleSpecifier);
185
+ }
186
+ const namedImports = importDecl.getNamedImports();
187
+ for (const namedImport of namedImports) {
188
+ const name = namedImport.getAliasNode()?.getText() || namedImport.getName();
189
+ imports.set(name, moduleSpecifier);
190
+ }
191
+ }
192
+ return imports;
193
+ }
194
+ /**
195
+ * Resolve a relative import path to an absolute file path
196
+ */
197
+ resolveImportPath(fromFile, importPath) {
198
+ if (!importPath.startsWith('.')) {
199
+ return null;
200
+ }
201
+ const fromDir = path.dirname(fromFile);
202
+ const resolvedPath = path.resolve(fromDir, importPath);
203
+ // Try with .ts extension
204
+ if (fs.existsSync(`${resolvedPath}.ts`)) {
205
+ return `${resolvedPath}.ts`;
206
+ }
207
+ // Try as directory with index.ts
208
+ if (fs.existsSync(path.join(resolvedPath, 'index.ts'))) {
209
+ return path.join(resolvedPath, 'index.ts');
210
+ }
211
+ // Try as directory with _router.ts (for sub-router imports)
212
+ if (fs.existsSync(path.join(resolvedPath, '_router.ts'))) {
213
+ return path.join(resolvedPath, '_router.ts');
214
+ }
215
+ // Maybe it's already a complete path
216
+ if (fs.existsSync(resolvedPath)) {
217
+ return resolvedPath;
218
+ }
219
+ return null;
220
+ }
221
+ /**
222
+ * Normalize path by removing duplicate slashes and ensuring single leading slash
223
+ */
224
+ normalizePath(p) {
225
+ return (`/${p}`).replace(/\/+/g, '/').replace(/\/$/, '') || '/';
226
+ }
227
+ /**
228
+ * Discover all controller files in the shop API directory
229
+ */
230
+ discoverControllers(apiType = 'shopApi') {
231
+ const sourceDir = ApiDocConfig.getSourcePath(this.serverDir, apiType);
232
+ const pattern = ApiDocConfig.patterns.controllerFiles;
233
+ return this.findFilesRecursive(sourceDir, pattern);
234
+ }
235
+ /**
236
+ * Parse a single controller file
237
+ */
238
+ parseController(filePath) {
239
+ // Try to get already-loaded source file first, otherwise add it
240
+ let sourceFile = this.project.getSourceFile(filePath);
241
+ if (!sourceFile) {
242
+ sourceFile = this.project.addSourceFileAtPath(filePath);
243
+ }
244
+ const classes = sourceFile.getClasses();
245
+ const controllerClass = classes.find(cls => cls.getExtends()?.getText()?.includes('ControllerBase')
246
+ || cls.getName()?.includes('Controller'));
247
+ if (!controllerClass) {
248
+ return null;
249
+ }
250
+ const className = controllerClass.getName() || 'UnknownController';
251
+ const routePath = this.extractRouteDecorator(controllerClass);
252
+ const methods = this.extractMethods(controllerClass);
253
+ const relativePath = path.relative(this.serverDir, filePath).replace(/\\/g, '/');
254
+ const tag = this.extractTagFromPath(relativePath);
255
+ return {
256
+ className,
257
+ routePath: routePath || this.inferRouteFromClassName(className),
258
+ filePath,
259
+ relativePath,
260
+ methods,
261
+ tag,
262
+ };
263
+ }
264
+ /**
265
+ * Parse all controllers in the shop API
266
+ */
267
+ parseAllControllers(apiType = 'shopApi') {
268
+ /* eslint-disable no-console */
269
+ console.time('🔍 Discovering controller files');
270
+ const files = this.discoverControllers(apiType);
271
+ console.timeEnd('🔍 Discovering controller files');
272
+ console.log(`📁 Found ${files.length} controller files`);
273
+ console.time('📦 Adding all source files to ts-morph project');
274
+ // Pre-load all source files into the project for faster parsing
275
+ for (const file of files) {
276
+ this.project.addSourceFileAtPath(file);
277
+ }
278
+ console.timeEnd('📦 Adding all source files to ts-morph project');
279
+ console.time('🔨 Parsing all controllers');
280
+ const controllers = [];
281
+ for (let i = 0; i < files.length; i++) {
282
+ const file = files[i];
283
+ const fileName = path.basename(file);
284
+ const startTime = performance.now();
285
+ const info = this.parseController(file);
286
+ const duration = performance.now() - startTime;
287
+ if (duration > 100) {
288
+ console.log(` ⏱️ ${fileName} took ${duration.toFixed(0)}ms`);
289
+ }
290
+ if (info && info.methods.length > 0) {
291
+ controllers.push(info);
292
+ }
293
+ }
294
+ console.timeEnd('🔨 Parsing all controllers');
295
+ console.log(`✅ Parsed ${controllers.length} controllers with endpoints`);
296
+ /* eslint-enable no-console */
297
+ return controllers;
298
+ }
299
+ /**
300
+ * Extract the route path from @Route decorator
301
+ */
302
+ extractRouteDecorator(classDecl) {
303
+ const decorators = classDecl.getDecorators();
304
+ for (const decorator of decorators) {
305
+ const name = decorator.getName();
306
+ if (name === ApiDocConfig.decorators.route) {
307
+ return this.extractDecoratorStringArg(decorator);
308
+ }
309
+ }
310
+ return null;
311
+ }
312
+ /**
313
+ * Extract methods from a controller class
314
+ */
315
+ extractMethods(classDecl) {
316
+ const methods = [];
317
+ const classMethods = classDecl.getMethods();
318
+ const sourceFile = classDecl.getSourceFile();
319
+ for (const method of classMethods) {
320
+ const methodName = method.getName();
321
+ const httpMethod = ApiDocConfig.getHttpMethod(methodName);
322
+ if (!httpMethod) {
323
+ continue;
324
+ }
325
+ const auth = this.extractAuthDecorator(method);
326
+ const { requestType, responseType } = this.extractMethodTypes(method);
327
+ const description = this.extractJsDocDescription(method);
328
+ const paramDescription = this.extractJsDocParams(method);
329
+ // Extract service description and extra parameters from service interface if available
330
+ let serviceDescription = null;
331
+ let extraParameterGenerator = null;
332
+ if (this.typeResolver) {
333
+ const serviceMapping = this.extractServiceMethodMapping(method, sourceFile);
334
+ if (serviceMapping) {
335
+ serviceDescription = this.typeResolver.extractServiceMethodJsDoc(serviceMapping.interfaceType, serviceMapping.methodName);
336
+ extraParameterGenerator = this.typeResolver.extractServiceMethodExtraParameters(serviceMapping.interfaceType, serviceMapping.methodName);
337
+ }
338
+ }
339
+ methods.push({
340
+ name: methodName,
341
+ httpMethod,
342
+ auth,
343
+ requestType,
344
+ responseType,
345
+ description,
346
+ serviceDescription,
347
+ paramDescription,
348
+ extraParameterGenerator,
349
+ });
350
+ }
351
+ return methods;
352
+ }
353
+ /**
354
+ * Extract authentication decorator information
355
+ */
356
+ extractAuthDecorator(method) {
357
+ const decorators = method.getDecorators();
358
+ const result = {
359
+ hasAuth: false,
360
+ requiresLogin: false,
361
+ requiresVerifiedAccount: false,
362
+ isEnterpriseOnly: false,
363
+ };
364
+ for (const decorator of decorators) {
365
+ const name = decorator.getName();
366
+ // Check for enterprise auth decorator
367
+ if (name === ApiDocConfig.decorators.shopEnterpriseAuth
368
+ || name === 'ShopApiEnterpriseAuthRequired'
369
+ || name === 'ShopApiEnterpriseAuthRequiresVerifiedAccount') {
370
+ result.hasAuth = true;
371
+ result.isEnterpriseOnly = true;
372
+ result.requiresLogin = true; // Enterprise auth always requires login (via API key)
373
+ if (name === 'ShopApiEnterpriseAuthRequiresVerifiedAccount') {
374
+ result.requiresVerifiedAccount = true;
375
+ }
376
+ else {
377
+ const args = this.extractDecoratorObjectArg(decorator);
378
+ if (args) {
379
+ result.requiresVerifiedAccount = args.requiresVerifiedAccount === true;
380
+ }
381
+ }
382
+ break;
383
+ }
384
+ // Check for regular shop auth decorator
385
+ if (name === ApiDocConfig.decorators.shopAuth || name === 'ShopApiAuthRequiresLogin' || name === 'ShopApiAuthRequiresVerifiedAccount') {
386
+ result.hasAuth = true;
387
+ if (name === 'ShopApiAuthRequiresLogin') {
388
+ result.requiresLogin = true;
389
+ }
390
+ else if (name === 'ShopApiAuthRequiresVerifiedAccount') {
391
+ result.requiresLogin = true;
392
+ result.requiresVerifiedAccount = true;
393
+ }
394
+ else {
395
+ const args = this.extractDecoratorObjectArg(decorator);
396
+ if (args) {
397
+ result.requiresLogin = args.requiresLogin === true;
398
+ result.requiresVerifiedAccount = args.requiresVerifiedAccount === true;
399
+ }
400
+ }
401
+ break;
402
+ }
403
+ }
404
+ return result;
405
+ }
406
+ /**
407
+ * Extract request and response types from method signature
408
+ */
409
+ extractMethodTypes(method) {
410
+ let requestType = null;
411
+ let responseType = null;
412
+ // Get request type from first parameter
413
+ const params = method.getParameters();
414
+ if (params.length > 0) {
415
+ const firstParam = params[0];
416
+ const paramType = firstParam.getType();
417
+ requestType = this.cleanTypeName(paramType.getText());
418
+ }
419
+ // Get response type from return type
420
+ const returnType = method.getReturnType();
421
+ const returnTypeText = returnType.getText();
422
+ // Handle Promise<T>
423
+ const promiseMatch = returnTypeText.match(/Promise<(.+)>/);
424
+ if (promiseMatch) {
425
+ responseType = this.cleanTypeName(promiseMatch[1]);
426
+ }
427
+ else {
428
+ responseType = this.cleanTypeName(returnTypeText);
429
+ }
430
+ return { requestType, responseType };
431
+ }
432
+ /**
433
+ * Extract JSDoc description from a method
434
+ */
435
+ extractJsDocDescription(method) {
436
+ const jsDocs = method.getJsDocs();
437
+ if (jsDocs.length > 0) {
438
+ // Preserve newlines and indentation for complex descriptions with code blocks
439
+ const description = jsDocs[0].getDescription();
440
+ if (description) {
441
+ return description.trim() || null;
442
+ }
443
+ }
444
+ return null;
445
+ }
446
+ /**
447
+ * Extract @param description from a method's JSDoc
448
+ * Returns the description of the first parameter (typically 'args')
449
+ */
450
+ extractJsDocParams(method) {
451
+ const jsDocs = method.getJsDocs();
452
+ if (jsDocs.length === 0) {
453
+ return null;
454
+ }
455
+ const tags = jsDocs[0].getTags();
456
+ const paramTags = tags.filter(tag => tag.getTagName() === 'param');
457
+ if (paramTags.length === 0) {
458
+ return null;
459
+ }
460
+ // Get the first @param tag (typically for the 'args' parameter)
461
+ const firstParamTag = paramTags[0];
462
+ // Extract the comment text from the param tag
463
+ // The tag format is: @param {type} paramName - description
464
+ // or: @param paramName - description
465
+ // or: @param paramName description
466
+ const tagText = firstParamTag.getText();
467
+ // Remove the @param prefix and extract description
468
+ // Pattern: @param {type}? paramName [-:]? description
469
+ const paramMatch = tagText.match(/@param\s+(?:\{[^}]+\}\s+)?(\w+)\s*[-:]?\s*(.*)/);
470
+ if (paramMatch && paramMatch[2]) {
471
+ const description = paramMatch[2].trim();
472
+ return description || null;
473
+ }
474
+ // Alternative: try to get the comment using getComment() if available
475
+ if ('getComment' in firstParamTag && typeof firstParamTag.getComment === 'function') {
476
+ const comment = firstParamTag.getComment();
477
+ if (typeof comment === 'string') {
478
+ return comment.trim() || null;
479
+ }
480
+ }
481
+ return null;
482
+ }
483
+ /**
484
+ * Extract service method mapping from a controller method
485
+ * Parses the method body to find container.get<InterfaceType>() and service.methodName() calls
486
+ */
487
+ extractServiceMethodMapping(method, sourceFile) {
488
+ const imports = this.parseImports(sourceFile);
489
+ // Find variable declarations with container.get<InterfaceType>()
490
+ const variableDeclarations = method.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
491
+ let serviceVariableName = null;
492
+ let interfaceType = null;
493
+ for (const varDecl of variableDeclarations) {
494
+ const initializer = varDecl.getInitializer();
495
+ if (!initializer || !Node.isCallExpression(initializer)) {
496
+ continue;
497
+ }
498
+ // Check if it's container.get() call
499
+ const callExpr = initializer;
500
+ const expression = callExpr.getExpression();
501
+ if (!Node.isPropertyAccessExpression(expression)) {
502
+ continue;
503
+ }
504
+ const objectName = expression.getExpression().getText();
505
+ const methodName = expression.getName();
506
+ if (objectName === 'container' && methodName === 'get') {
507
+ // Extract type argument (e.g., ICartService from container.get<ICartService>())
508
+ const typeArgs = callExpr.getTypeArguments();
509
+ if (typeArgs.length > 0) {
510
+ interfaceType = typeArgs[0].getText();
511
+ serviceVariableName = varDecl.getName();
512
+ break;
513
+ }
514
+ }
515
+ }
516
+ if (!serviceVariableName || !interfaceType) {
517
+ return null;
518
+ }
519
+ // Find the import path for the interface type
520
+ const interfaceImportPath = imports.get(interfaceType);
521
+ if (!interfaceImportPath) {
522
+ return null;
523
+ }
524
+ // Find method call on the service variable (e.g., service.validateCart())
525
+ const callExpressions = method.getDescendantsOfKind(SyntaxKind.CallExpression);
526
+ for (const callExpr of callExpressions) {
527
+ const expression = callExpr.getExpression();
528
+ if (!Node.isPropertyAccessExpression(expression)) {
529
+ continue;
530
+ }
531
+ const objectExpr = expression.getExpression();
532
+ // Check if we're calling a method on our service variable
533
+ // Handle both `service.method()` and `await service.method()`
534
+ if (Node.isIdentifier(objectExpr) && objectExpr.getText() === serviceVariableName) {
535
+ const serviceMethodName = expression.getName();
536
+ return {
537
+ interfaceType,
538
+ interfaceImportPath,
539
+ methodName: serviceMethodName,
540
+ };
541
+ }
542
+ }
543
+ return null;
544
+ }
545
+ /**
546
+ * Extract string argument from decorator
547
+ */
548
+ extractDecoratorStringArg(decorator) {
549
+ const args = decorator.getArguments();
550
+ if (args.length > 0) {
551
+ const arg = args[0];
552
+ if (Node.isStringLiteral(arg)) {
553
+ return arg.getLiteralValue();
554
+ }
555
+ // Handle template literals or other string expressions
556
+ const text = arg.getText();
557
+ if (text.startsWith('\'') || text.startsWith('"') || text.startsWith('`')) {
558
+ return text.slice(1, -1);
559
+ }
560
+ }
561
+ return null;
562
+ }
563
+ /**
564
+ * Extract object argument from decorator
565
+ */
566
+ extractDecoratorObjectArg(decorator) {
567
+ const args = decorator.getArguments();
568
+ if (args.length > 0) {
569
+ const arg = args[0];
570
+ if (Node.isObjectLiteralExpression(arg)) {
571
+ const result = {};
572
+ for (const prop of arg.getProperties()) {
573
+ if (Node.isPropertyAssignment(prop)) {
574
+ const name = prop.getName();
575
+ const init = prop.getInitializer();
576
+ if (init) {
577
+ if (Node.isTrueLiteral(init)) {
578
+ result[name] = true;
579
+ }
580
+ else if (Node.isFalseLiteral(init)) {
581
+ result[name] = false;
582
+ }
583
+ else if (Node.isStringLiteral(init)) {
584
+ result[name] = init.getLiteralValue();
585
+ }
586
+ else if (Node.isNumericLiteral(init)) {
587
+ result[name] = Number(init.getText());
588
+ }
589
+ }
590
+ }
591
+ }
592
+ return result;
593
+ }
594
+ }
595
+ return null;
596
+ }
597
+ /**
598
+ * Find files recursively matching a pattern
599
+ */
600
+ findFilesRecursive(dir, pattern) {
601
+ const results = [];
602
+ if (!fs.existsSync(dir)) {
603
+ return results;
604
+ }
605
+ const patternRegex = this.globToRegex(pattern);
606
+ this.walkDirectory(dir, '', patternRegex, results);
607
+ return results;
608
+ }
609
+ /**
610
+ * Walk directory recursively
611
+ */
612
+ walkDirectory(baseDir, relativePath, pattern, results) {
613
+ const currentDir = path.join(baseDir, relativePath);
614
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
615
+ for (const entry of entries) {
616
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
617
+ if (entry.isDirectory()) {
618
+ // Skip node_modules and hidden directories
619
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
620
+ this.walkDirectory(baseDir, entryRelativePath, pattern, results);
621
+ }
622
+ }
623
+ else if (entry.isFile()) {
624
+ if (pattern.test(entryRelativePath)) {
625
+ results.push(path.join(baseDir, entryRelativePath));
626
+ }
627
+ }
628
+ }
629
+ }
630
+ /**
631
+ * Convert glob pattern to regex
632
+ */
633
+ globToRegex(glob) {
634
+ const escaped = glob
635
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
636
+ .replace(/\*\*/g, '{{DOUBLE_STAR}}')
637
+ .replace(/\*/g, '[^/]*')
638
+ .replace(/\{\{DOUBLE_STAR\}\}/g, '.*')
639
+ .replace(/\?/g, '.');
640
+ return new RegExp(`^${escaped}$`);
641
+ }
642
+ /**
643
+ * Infer route from controller class name
644
+ */
645
+ inferRouteFromClassName(className) {
646
+ // Remove 'Controller' suffix and convert to kebab-case
647
+ let name = className.replace(/Controller$/, '');
648
+ // Handle common prefixes
649
+ name = name.replace(/^Shop/, '');
650
+ // Convert PascalCase to kebab-case
651
+ const kebab = name
652
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
653
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
654
+ .toLowerCase();
655
+ return `/${kebab}`;
656
+ }
657
+ /**
658
+ * Extract tag from file path
659
+ */
660
+ extractTagFromPath(relativePath) {
661
+ // Example: src/api/shop/cart/cartAddItemsController.ts -> cart
662
+ const parts = relativePath.split('/');
663
+ const apiIndex = parts.indexOf('api');
664
+ if (apiIndex >= 0 && apiIndex + 2 < parts.length) {
665
+ return parts[apiIndex + 2];
666
+ }
667
+ return 'general';
668
+ }
669
+ /**
670
+ * Clean type name by removing import paths and generics from common wrappers
671
+ */
672
+ cleanTypeName(typeName) {
673
+ // Remove import() syntax
674
+ let cleaned = typeName.replace(/import\([^)]+\)\./g, '');
675
+ // Remove namespace prefixes
676
+ cleaned = cleaned.replace(/\w+\./g, (match) => {
677
+ // Keep Temporal prefix for special types
678
+ if (match === 'Temporal.') {
679
+ return match;
680
+ }
681
+ return '';
682
+ });
683
+ return cleaned.trim();
684
+ }
685
+ }
686
+ //# sourceMappingURL=controller-parser.js.map