@valkyrjaio/sindri 26.0.1

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 (83) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +111 -0
  3. package/package.json +63 -0
  4. package/src/Sindri/Ast/Abstract/AstReader.ts +710 -0
  5. package/src/Sindri/Ast/Abstract/RouteAttributeReader.ts +50 -0
  6. package/src/Sindri/Ast/CliRouteAttributeReader.ts +219 -0
  7. package/src/Sindri/Ast/CliRouteParameterReader.ts +204 -0
  8. package/src/Sindri/Ast/ComponentProviderReader.ts +41 -0
  9. package/src/Sindri/Ast/ConfigReader.ts +213 -0
  10. package/src/Sindri/Ast/Contract/CliRouteAttributeReaderContract.ts +14 -0
  11. package/src/Sindri/Ast/Contract/CliRouteParameterReaderContract.ts +34 -0
  12. package/src/Sindri/Ast/Contract/ComponentProviderReaderContract.ts +14 -0
  13. package/src/Sindri/Ast/Contract/ConfigReaderContract.ts +14 -0
  14. package/src/Sindri/Ast/Contract/HttpRouteAttributeReaderContract.ts +14 -0
  15. package/src/Sindri/Ast/Contract/HttpRouteMiddlewareReaderContract.ts +61 -0
  16. package/src/Sindri/Ast/Contract/HttpRouteParameterReaderContract.ts +26 -0
  17. package/src/Sindri/Ast/Contract/ListenerAttributeReaderContract.ts +14 -0
  18. package/src/Sindri/Ast/Contract/ListenerProviderReaderContract.ts +14 -0
  19. package/src/Sindri/Ast/Contract/RouteProviderReaderContract.ts +14 -0
  20. package/src/Sindri/Ast/Contract/ServiceProviderReaderContract.ts +14 -0
  21. package/src/Sindri/Ast/Data/CliArgumentParameterData.ts +20 -0
  22. package/src/Sindri/Ast/Data/CliOptionParameterData.ts +24 -0
  23. package/src/Sindri/Ast/Data/CliRouteData.ts +32 -0
  24. package/src/Sindri/Ast/Data/ConfigData.ts +20 -0
  25. package/src/Sindri/Ast/Data/Contract/CliArgumentParameterDataContract.ts +18 -0
  26. package/src/Sindri/Ast/Data/Contract/CliOptionParameterDataContract.ts +22 -0
  27. package/src/Sindri/Ast/Data/Contract/CliRouteDataContract.ts +25 -0
  28. package/src/Sindri/Ast/Data/Contract/ConfigDataContract.ts +16 -0
  29. package/src/Sindri/Ast/Data/Contract/HandlerDataContract.ts +13 -0
  30. package/src/Sindri/Ast/Data/Contract/HttpParameterDataContract.ts +17 -0
  31. package/src/Sindri/Ast/Data/Contract/HttpRouteDataContract.ts +28 -0
  32. package/src/Sindri/Ast/Data/Contract/ListenerDataContract.ts +16 -0
  33. package/src/Sindri/Ast/Data/HandlerData.ts +20 -0
  34. package/src/Sindri/Ast/Data/HttpParameterData.ts +20 -0
  35. package/src/Sindri/Ast/Data/HttpRouteData.ts +30 -0
  36. package/src/Sindri/Ast/Data/ListenerData.ts +19 -0
  37. package/src/Sindri/Ast/Data/Result/CliRouteAttributeResult.ts +14 -0
  38. package/src/Sindri/Ast/Data/Result/ComponentProviderResult.ts +28 -0
  39. package/src/Sindri/Ast/Data/Result/ConfigResult.ts +18 -0
  40. package/src/Sindri/Ast/Data/Result/HttpRouteAttributeResult.ts +19 -0
  41. package/src/Sindri/Ast/Data/Result/ListenerAttributeResult.ts +14 -0
  42. package/src/Sindri/Ast/Data/Result/ListenerProviderResult.ts +24 -0
  43. package/src/Sindri/Ast/Data/Result/RouteProviderResult.ts +24 -0
  44. package/src/Sindri/Ast/Data/Result/ServiceProviderResult.ts +22 -0
  45. package/src/Sindri/Ast/HttpRouteAttributeReader.ts +315 -0
  46. package/src/Sindri/Ast/HttpRouteMiddlewareReader.ts +214 -0
  47. package/src/Sindri/Ast/HttpRouteParameterReader.ts +193 -0
  48. package/src/Sindri/Ast/ListenerAttributeReader.ts +131 -0
  49. package/src/Sindri/Ast/ListenerProviderReader.ts +45 -0
  50. package/src/Sindri/Ast/RouteProviderReader.ts +45 -0
  51. package/src/Sindri/Ast/ServiceProviderReader.ts +124 -0
  52. package/src/Sindri/Ast/Throwable/Contract/AstThrowable.ts +12 -0
  53. package/src/Sindri/Ast/Throwable/Exception/Abstract/AstInvalidArgumentException.ts +14 -0
  54. package/src/Sindri/Ast/Throwable/Exception/Abstract/AstRuntimeException.ts +14 -0
  55. package/src/Sindri/Ast/Throwable/Exception/AstFileReadException.ts +12 -0
  56. package/src/Sindri/Cli/Command/GenerateDataFromConfigCommand.ts +83 -0
  57. package/src/Sindri/Constant/CommandName.ts +12 -0
  58. package/src/Sindri/Constant/SindriInfo.ts +20 -0
  59. package/src/Sindri/Constant/SindriServiceId.ts +32 -0
  60. package/src/Sindri/Generate/Abstract/GenerateDataFromAst.ts +391 -0
  61. package/src/Sindri/Generator/Abstract/AstFileGenerator.ts +54 -0
  62. package/src/Sindri/Generator/Abstract/FileGenerator.ts +59 -0
  63. package/src/Sindri/Generator/Ast/Cli/AstCliDataFileGenerator.ts +88 -0
  64. package/src/Sindri/Generator/Ast/Container/AstContainerDataFileGenerator.ts +86 -0
  65. package/src/Sindri/Generator/Ast/Event/AstEventDataFileGenerator.ts +79 -0
  66. package/src/Sindri/Generator/Ast/Http/AstHttpDataFileGenerator.ts +306 -0
  67. package/src/Sindri/Generator/Cli/Contract/CliDataFileGeneratorContract.ts +23 -0
  68. package/src/Sindri/Generator/Container/Contract/ContainerDataFileGeneratorContract.ts +21 -0
  69. package/src/Sindri/Generator/Contract/FileGeneratorContract.ts +15 -0
  70. package/src/Sindri/Generator/Enum/GenerateStatus.ts +14 -0
  71. package/src/Sindri/Generator/Event/Contract/EventDataFileGeneratorContract.ts +23 -0
  72. package/src/Sindri/Generator/Http/Contract/HttpDataFileGeneratorContract.ts +25 -0
  73. package/src/Sindri/Generator/Throwable/Contract/GeneratorThrowable.ts +12 -0
  74. package/src/Sindri/Generator/Throwable/Exception/Abstract/GeneratorInvalidArgumentException.ts +16 -0
  75. package/src/Sindri/Generator/Throwable/Exception/Abstract/GeneratorRuntimeException.ts +14 -0
  76. package/src/Sindri/Generator/Throwable/Exception/GeneratorUnreachableException.ts +12 -0
  77. package/src/Sindri/Provider/SindriAstServiceProvider.ts +182 -0
  78. package/src/Sindri/Provider/SindriCliRouteProvider.ts +32 -0
  79. package/src/Sindri/Provider/SindriCommandServiceProvider.ts +71 -0
  80. package/src/Sindri/Provider/SindriComponentProvider.ts +41 -0
  81. package/src/Sindri/Throwable/Contract/SindriThrowable.ts +12 -0
  82. package/src/Sindri/Throwable/Exception/Abstract/SindriInvalidArgumentException.ts +16 -0
  83. package/src/Sindri/Throwable/Exception/Abstract/SindriRuntimeException.ts +14 -0
@@ -0,0 +1,710 @@
1
+ /*
2
+ * This file is part of the Sindri package.
3
+ *
4
+ * (c) Melech Mizrachi <melechmizrachi@gmail.com>
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+
13
+ import {
14
+ ClassDeclaration,
15
+ Decorator,
16
+ MethodDeclaration,
17
+ Node,
18
+ ParameterDeclaration,
19
+ Project,
20
+ SourceFile,
21
+ SyntaxKind,
22
+ ts,
23
+ } from 'ts-morph';
24
+
25
+ import { AstFileReadException } from '../Throwable/Exception/AstFileReadException.ts';
26
+ import { HandlerData } from '../Data/HandlerData.ts';
27
+
28
+ /**
29
+ * Shared AST parsing utilities for all provider reader implementations.
30
+ *
31
+ * Uses ts-morph for high-level class/method/decorator discovery and the
32
+ * TypeScript compiler API for fine-grained body traversal.
33
+ *
34
+ * All methods are protected so subclasses can override any step of the pipeline.
35
+ */
36
+ export abstract class AstReader {
37
+ protected parseFileToSourceFile(filePath: string): SourceFile {
38
+ if (!fs.existsSync(filePath)) {
39
+ throw new AstFileReadException(`Cannot read file '${filePath}'.`);
40
+ }
41
+
42
+ const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true });
43
+
44
+ return project.addSourceFileAtPath(filePath);
45
+ }
46
+
47
+ /**
48
+ * Build a map of alias/short-name → import module specifier from import declarations.
49
+ */
50
+ protected buildUseMap(sourceFile: SourceFile): Record<string, string> {
51
+ const map: Record<string, string> = {};
52
+
53
+ for (const importDecl of sourceFile.getImportDeclarations()) {
54
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
55
+
56
+ for (const namedImport of importDecl.getNamedImports()) {
57
+ const alias = namedImport.getAliasNode()?.getText() ?? namedImport.getName();
58
+ map[alias] = moduleSpecifier;
59
+ }
60
+
61
+ const defaultImport = importDecl.getDefaultImport();
62
+
63
+ if (defaultImport !== undefined) {
64
+ map[defaultImport.getText()] = moduleSpecifier;
65
+ }
66
+
67
+ const namespaceImport = importDecl.getNamespaceImport();
68
+
69
+ if (namespaceImport !== undefined) {
70
+ map[namespaceImport.getText()] = moduleSpecifier;
71
+ }
72
+ }
73
+
74
+ return map;
75
+ }
76
+
77
+ protected findClass(sourceFile: SourceFile): ClassDeclaration | undefined {
78
+ return sourceFile.getClasses()[0];
79
+ }
80
+
81
+ protected indexMethods(classDecl: ClassDeclaration): Record<string, MethodDeclaration> {
82
+ const index: Record<string, MethodDeclaration> = {};
83
+
84
+ for (const method of classDecl.getMethods()) {
85
+ index[method.getName()] = method;
86
+ }
87
+
88
+ return index;
89
+ }
90
+
91
+ /**
92
+ * Resolve the file path for an imported class name using the use map and current file directory.
93
+ */
94
+ protected resolveImportToFilePath(name: string, useMap: Record<string, string>, currentFilePath: string): string {
95
+ const moduleSpecifier = useMap[name];
96
+
97
+ if (moduleSpecifier === undefined) {
98
+ return '';
99
+ }
100
+
101
+ if (!moduleSpecifier.startsWith('.')) {
102
+ return '';
103
+ }
104
+
105
+ const dir = path.dirname(currentFilePath);
106
+ const resolved = path.resolve(dir, moduleSpecifier.replace(/\.js$/, '.ts'));
107
+
108
+ return fs.existsSync(resolved) ? resolved : '';
109
+ }
110
+
111
+ /**
112
+ * Extract class names from a method returning [new A(), new B(), ...].
113
+ */
114
+ protected extractClassListFromValues(
115
+ method: MethodDeclaration | undefined,
116
+ useMap: Record<string, string>,
117
+ currentFilePath: string,
118
+ ): string[] {
119
+ if (method === undefined) {
120
+ return [];
121
+ }
122
+
123
+ const array = this.findReturnedArray(method);
124
+
125
+ if (array === undefined) {
126
+ return [];
127
+ }
128
+
129
+ const classes: string[] = [];
130
+
131
+ for (const element of array.elements) {
132
+ const resolved =
133
+ this.newExprToClassName(element, useMap, currentFilePath) ??
134
+ this.classNameFromNode(element, useMap, currentFilePath);
135
+
136
+ if (resolved !== undefined) {
137
+ classes.push(resolved);
138
+ }
139
+ }
140
+
141
+ return classes;
142
+ }
143
+
144
+ /**
145
+ * Extract service ID strings from keys of an object-returning method.
146
+ *
147
+ * Used for publishers() where keys are service IDs (strings or computed references).
148
+ */
149
+ protected extractClassListFromKeys(
150
+ method: MethodDeclaration | undefined,
151
+ useMap: Record<string, string>,
152
+ currentFilePath: string,
153
+ ): string[] {
154
+ if (method === undefined) {
155
+ return [];
156
+ }
157
+
158
+ const obj = this.findReturnedObject(method);
159
+
160
+ if (obj === undefined) {
161
+ return [];
162
+ }
163
+
164
+ const classes: string[] = [];
165
+
166
+ for (const prop of obj.properties) {
167
+ if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) {
168
+ continue;
169
+ }
170
+
171
+ const resolved = this.resolvePropertyKeyToString(prop.name, useMap, currentFilePath);
172
+
173
+ if (resolved !== undefined) {
174
+ classes.push(resolved);
175
+ }
176
+ }
177
+
178
+ return classes;
179
+ }
180
+
181
+ /**
182
+ * Find the first returned ArrayLiteralExpression in a method's body.
183
+ */
184
+ protected findReturnedArray(method: MethodDeclaration): ts.ArrayLiteralExpression | undefined {
185
+ const body = method.compilerNode.body;
186
+
187
+ if (body === undefined) {
188
+ return undefined;
189
+ }
190
+
191
+ for (const stmt of body.statements) {
192
+ if (
193
+ ts.isReturnStatement(stmt) &&
194
+ stmt.expression !== undefined &&
195
+ ts.isArrayLiteralExpression(stmt.expression)
196
+ ) {
197
+ return stmt.expression;
198
+ }
199
+ }
200
+
201
+ return undefined;
202
+ }
203
+
204
+ /**
205
+ * Find the first returned ObjectLiteralExpression in a method's body.
206
+ */
207
+ protected findReturnedObject(method: MethodDeclaration): ts.ObjectLiteralExpression | undefined {
208
+ const body = method.compilerNode.body;
209
+
210
+ if (body === undefined) {
211
+ return undefined;
212
+ }
213
+
214
+ for (const stmt of body.statements) {
215
+ if (
216
+ ts.isReturnStatement(stmt) &&
217
+ stmt.expression !== undefined &&
218
+ ts.isObjectLiteralExpression(stmt.expression)
219
+ ) {
220
+ return stmt.expression;
221
+ }
222
+ }
223
+
224
+ return undefined;
225
+ }
226
+
227
+ /**
228
+ * Extract a class name from a NewExpression TS compiler node.
229
+ */
230
+ protected newExprToClassName(
231
+ node: ts.Node,
232
+ useMap: Record<string, string>,
233
+ currentFilePath: string,
234
+ ): string | undefined {
235
+ if (!ts.isNewExpression(node)) {
236
+ return undefined;
237
+ }
238
+
239
+ const name = ts.isIdentifier(node.expression) ? node.expression.text : node.expression.getText();
240
+
241
+ return this.resolveClassName(name, useMap, currentFilePath);
242
+ }
243
+
244
+ /**
245
+ * Extract a class name from a general TS compiler node (identifier).
246
+ */
247
+ protected classNameFromNode(
248
+ node: ts.Node,
249
+ useMap: Record<string, string>,
250
+ currentFilePath: string,
251
+ ): string | undefined {
252
+ if (ts.isIdentifier(node)) {
253
+ return this.resolveClassName(node.text, useMap, currentFilePath);
254
+ }
255
+
256
+ return undefined;
257
+ }
258
+
259
+ /**
260
+ * Resolve a property key node to a string value.
261
+ *
262
+ * Handles: string literals, identifiers, computed properties [SomeId.Constant].
263
+ */
264
+ protected resolvePropertyKeyToString(
265
+ keyNode: ts.PropertyName,
266
+ useMap: Record<string, string>,
267
+ currentFilePath: string,
268
+ ): string | undefined {
269
+ if (ts.isStringLiteral(keyNode)) {
270
+ return keyNode.text;
271
+ }
272
+
273
+ if (ts.isIdentifier(keyNode)) {
274
+ return keyNode.text;
275
+ }
276
+
277
+ if (ts.isComputedPropertyName(keyNode)) {
278
+ return this.resolveNodeToString(keyNode.expression, useMap, currentFilePath);
279
+ }
280
+
281
+ return undefined;
282
+ }
283
+
284
+ /**
285
+ * Attempt to resolve a TS node to a string value.
286
+ *
287
+ * Handles: string literals, identifiers, property accesses like ServiceId.Constant.
288
+ */
289
+ protected resolveNodeToString(
290
+ node: ts.Expression,
291
+ useMap: Record<string, string>,
292
+ currentFilePath: string,
293
+ ): string | undefined {
294
+ if (ts.isStringLiteral(node)) {
295
+ return node.text;
296
+ }
297
+
298
+ if (ts.isIdentifier(node)) {
299
+ return this.resolveStaticProperty(node.text, undefined, useMap, currentFilePath);
300
+ }
301
+
302
+ if (ts.isPropertyAccessExpression(node)) {
303
+ const objName = ts.isIdentifier(node.expression) ? node.expression.text : undefined;
304
+
305
+ if (objName !== undefined) {
306
+ return this.resolveStaticProperty(objName, node.name.text, useMap, currentFilePath);
307
+ }
308
+ }
309
+
310
+ return undefined;
311
+ }
312
+
313
+ /**
314
+ * Resolve a static property value by reading the imported source file.
315
+ */
316
+ protected resolveStaticProperty(
317
+ className: string,
318
+ propName: string | undefined,
319
+ useMap: Record<string, string>,
320
+ currentFilePath: string,
321
+ ): string | undefined {
322
+ const importedFilePath = this.resolveImportToFilePath(className, useMap, currentFilePath);
323
+
324
+ if (importedFilePath === '') {
325
+ return undefined;
326
+ }
327
+
328
+ try {
329
+ const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true });
330
+ const importedSource = project.addSourceFileAtPath(importedFilePath);
331
+ const importedClass = importedSource.getClass(className);
332
+
333
+ if (importedClass === undefined || propName === undefined) {
334
+ return undefined;
335
+ }
336
+
337
+ const prop = importedClass.getStaticProperty(propName);
338
+
339
+ if (prop === undefined) {
340
+ return undefined;
341
+ }
342
+
343
+ if (Node.isPropertyDeclaration(prop)) {
344
+ const initializer = prop.getInitializer();
345
+
346
+ if (initializer?.isKind(SyntaxKind.StringLiteral) === true) {
347
+ return (initializer as Node<ts.StringLiteral>).compilerNode.text;
348
+ }
349
+ }
350
+ } catch {
351
+ // Silently skip unresolvable references
352
+ }
353
+
354
+ return undefined;
355
+ }
356
+
357
+ /**
358
+ * Resolve a short class name — returns the name as-is (class names in TS are local).
359
+ */
360
+ protected resolveClassName(name: string, useMap: Record<string, string>, _currentFilePath: string): string {
361
+ if (name in useMap) {
362
+ return name;
363
+ }
364
+
365
+ return name;
366
+ }
367
+
368
+ /**
369
+ * Resolve a class short name to an absolute file path using the use map.
370
+ */
371
+ protected resolveClassToFilePath(name: string, useMap: Record<string, string>, currentFilePath: string): string {
372
+ return this.resolveImportToFilePath(name, useMap, currentFilePath);
373
+ }
374
+
375
+ /**
376
+ * Collect all Decorator nodes on a given node whose name matches decoratorName.
377
+ */
378
+ protected findDecoratorsOnNode(
379
+ node: ClassDeclaration | MethodDeclaration | ParameterDeclaration,
380
+ decoratorName: string,
381
+ useMap: Record<string, string>,
382
+ currentFilePath: string,
383
+ ): Decorator[] {
384
+ return node.getDecorators().filter((d: Decorator) => {
385
+ const name = d.getName();
386
+ return name === decoratorName || this.resolveClassName(name, useMap, currentFilePath) === decoratorName;
387
+ });
388
+ }
389
+
390
+ protected getDecoratorArg(decorator: Decorator, position: number): Node | undefined {
391
+ return decorator.getArguments()[position];
392
+ }
393
+
394
+ /**
395
+ * Convert a simple expression node to a scalar value or HandlerData.
396
+ */
397
+ protected extractExprValue(
398
+ node: Node | ts.Node | undefined,
399
+ useMap: Record<string, string>,
400
+ currentFilePath: string,
401
+ currentClass: string = '',
402
+ ): string | number | boolean | HandlerData | null | undefined {
403
+ if (node === undefined) {
404
+ return undefined;
405
+ }
406
+
407
+ const tsNode = Node.isNode(node) ? node.compilerNode : node;
408
+
409
+ if (ts.isStringLiteral(tsNode)) {
410
+ return tsNode.text;
411
+ }
412
+
413
+ if (ts.isNumericLiteral(tsNode)) {
414
+ return Number(tsNode.text);
415
+ }
416
+
417
+ if (tsNode.kind === ts.SyntaxKind.TrueKeyword) {
418
+ return true;
419
+ }
420
+
421
+ if (tsNode.kind === ts.SyntaxKind.FalseKeyword) {
422
+ return false;
423
+ }
424
+
425
+ if (tsNode.kind === ts.SyntaxKind.NullKeyword) {
426
+ return null;
427
+ }
428
+
429
+ if (ts.isPrefixUnaryExpression(tsNode) && tsNode.operator === ts.SyntaxKind.MinusToken) {
430
+ const inner = this.extractExprValue(tsNode.operand, useMap, currentFilePath, currentClass);
431
+ if (typeof inner === 'number') return -inner;
432
+ return undefined;
433
+ }
434
+
435
+ if (ts.isIdentifier(tsNode)) {
436
+ const text = tsNode.text;
437
+ if (text === 'null') return null;
438
+ if (text === 'true') return true;
439
+ if (text === 'false') return false;
440
+ return this.resolveClassName(text, useMap, currentFilePath);
441
+ }
442
+
443
+ if (ts.isPropertyAccessExpression(tsNode)) {
444
+ const objName = ts.isIdentifier(tsNode.expression) ? tsNode.expression.text : undefined;
445
+ const propName = tsNode.name.text;
446
+
447
+ if (objName !== undefined) {
448
+ const resolved = this.resolveStaticProperty(objName, propName, useMap, currentFilePath);
449
+ if (resolved !== undefined) {
450
+ return resolved;
451
+ }
452
+ const className = this.resolveClassName(objName, useMap, currentFilePath);
453
+ return `${className}::${propName}`;
454
+ }
455
+ }
456
+
457
+ if (ts.isArrayLiteralExpression(tsNode)) {
458
+ return this.extractHandlerFromTsArray(tsNode, useMap, currentFilePath, currentClass);
459
+ }
460
+
461
+ return undefined;
462
+ }
463
+
464
+ protected extractHandlerFromTsArray(
465
+ array: ts.ArrayLiteralExpression,
466
+ useMap: Record<string, string>,
467
+ currentFilePath: string,
468
+ currentClass: string = '',
469
+ ): HandlerData | undefined {
470
+ if (array.elements.length !== 2) {
471
+ return undefined;
472
+ }
473
+
474
+ const classValue = this.extractExprValue(array.elements[0], useMap, currentFilePath, currentClass);
475
+ const methodValue = this.extractExprValue(array.elements[1], useMap, currentFilePath, currentClass);
476
+
477
+ if (typeof classValue !== 'string' || classValue === '') {
478
+ return undefined;
479
+ }
480
+
481
+ if (typeof methodValue !== 'string' || methodValue === '') {
482
+ return undefined;
483
+ }
484
+
485
+ return new HandlerData(classValue, methodValue);
486
+ }
487
+
488
+ protected extractClassListFromArrayExpr(
489
+ array: ts.ArrayLiteralExpression,
490
+ useMap: Record<string, string>,
491
+ currentFilePath: string,
492
+ currentClass: string = '',
493
+ ): string[] {
494
+ const classes: string[] = [];
495
+
496
+ for (const element of array.elements) {
497
+ const value = this.extractExprValue(element, useMap, currentFilePath, currentClass);
498
+
499
+ if (typeof value === 'string' && value !== '') {
500
+ classes.push(value);
501
+ }
502
+ }
503
+
504
+ return classes;
505
+ }
506
+
507
+ protected parseClassFile(
508
+ filePath: string,
509
+ ):
510
+ | { sourceFile: SourceFile; classDecl: ClassDeclaration; useMap: Record<string, string>; currentClass: string }
511
+ | undefined {
512
+ const sourceFile = this.parseFileToSourceFile(filePath);
513
+ const classDecl = this.findClass(sourceFile);
514
+
515
+ if (classDecl === undefined) {
516
+ return undefined;
517
+ }
518
+
519
+ const useMap = this.buildUseMap(sourceFile);
520
+ const currentClass = classDecl.getName() ?? '';
521
+
522
+ return { sourceFile, classDecl, useMap, currentClass };
523
+ }
524
+
525
+ protected extractStringArg(
526
+ decorator: Decorator,
527
+ position: number,
528
+ useMap: Record<string, string>,
529
+ currentFilePath: string,
530
+ currentClass: string,
531
+ defaultValue: string = '',
532
+ ): string {
533
+ const node = this.getDecoratorArg(decorator, position);
534
+ const value = this.extractExprValue(node, useMap, currentFilePath, currentClass);
535
+ return typeof value === 'string' ? value : defaultValue;
536
+ }
537
+
538
+ protected extractBoolArg(
539
+ decorator: Decorator,
540
+ position: number,
541
+ useMap: Record<string, string>,
542
+ currentFilePath: string,
543
+ currentClass: string,
544
+ defaultValue: boolean = false,
545
+ ): boolean {
546
+ const node = this.getDecoratorArg(decorator, position);
547
+ const value = this.extractExprValue(node, useMap, currentFilePath, currentClass);
548
+ return typeof value === 'boolean' ? value : defaultValue;
549
+ }
550
+
551
+ protected extractClassListArgFromDecorator(
552
+ decorator: Decorator,
553
+ position: number,
554
+ useMap: Record<string, string>,
555
+ currentFilePath: string,
556
+ currentClass: string,
557
+ ): string[] {
558
+ const node = this.getDecoratorArg(decorator, position);
559
+
560
+ if (node === undefined) {
561
+ return [];
562
+ }
563
+
564
+ const tsNode = node.compilerNode;
565
+
566
+ if (!ts.isArrayLiteralExpression(tsNode)) {
567
+ return [];
568
+ }
569
+
570
+ return this.extractClassListFromArrayExpr(tsNode, useMap, currentFilePath, currentClass);
571
+ }
572
+
573
+ protected extractStringListArgFromDecorator(
574
+ decorator: Decorator,
575
+ position: number,
576
+ useMap: Record<string, string>,
577
+ currentFilePath: string,
578
+ currentClass: string,
579
+ ): string[] {
580
+ const node = this.getDecoratorArg(decorator, position);
581
+
582
+ if (node === undefined) {
583
+ return [];
584
+ }
585
+
586
+ const tsNode = node.compilerNode;
587
+
588
+ if (!ts.isArrayLiteralExpression(tsNode)) {
589
+ return [];
590
+ }
591
+
592
+ const values: string[] = [];
593
+
594
+ for (const element of tsNode.elements) {
595
+ const value = this.extractExprValue(element, useMap, currentFilePath, currentClass);
596
+ if (typeof value === 'string') {
597
+ values.push(value);
598
+ }
599
+ }
600
+
601
+ return values;
602
+ }
603
+
604
+ // -------------------------------------------------------------------------
605
+ // AST-building helpers — produce ts.Expression nodes for code generation
606
+ // -------------------------------------------------------------------------
607
+
608
+ protected buildStringExpr(value: string): ts.StringLiteral {
609
+ return ts.factory.createStringLiteral(value);
610
+ }
611
+
612
+ protected buildClassConstExpr(serviceId: string): ts.StringLiteral {
613
+ return ts.factory.createStringLiteral(serviceId);
614
+ }
615
+
616
+ /**
617
+ * Build an enum/constant case expression from a "ClassName::CASE" string.
618
+ *
619
+ * Produces `ClassName.CASE` property access, or a string literal fallback.
620
+ */
621
+ protected buildEnumCaseExpr(fqnColonCase: string): ts.Expression {
622
+ const pos = fqnColonCase.indexOf('::');
623
+
624
+ if (pos === -1) {
625
+ return ts.factory.createStringLiteral(fqnColonCase);
626
+ }
627
+
628
+ const fqn = fqnColonCase.substring(0, pos);
629
+ const caseName = fqnColonCase.substring(pos + 2);
630
+ const className = fqn.split('\\').pop() ?? fqn;
631
+
632
+ return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(className), caseName);
633
+ }
634
+
635
+ /**
636
+ * Build a `ClassName.methodName` property access from a HandlerData.
637
+ */
638
+ protected buildHandlerExpr(handler: HandlerData): ts.PropertyAccessExpression {
639
+ const className = handler.class.split('\\').pop() ?? handler.class;
640
+
641
+ return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(className), handler.method);
642
+ }
643
+
644
+ protected buildClassArrayExpr(classes: readonly string[]): ts.ArrayLiteralExpression {
645
+ const elements = classes.map((c) => ts.factory.createStringLiteral(c));
646
+ return ts.factory.createArrayLiteralExpression(elements);
647
+ }
648
+
649
+ protected buildNewExpr(className: string, args: ts.Expression[]): ts.NewExpression {
650
+ const shortName = className.split('\\').pop() ?? className;
651
+
652
+ return ts.factory.createNewExpression(ts.factory.createIdentifier(shortName), undefined, args);
653
+ }
654
+
655
+ protected buildBoolExpr(value: boolean): ts.BooleanLiteral {
656
+ return value ? ts.factory.createTrue() : ts.factory.createFalse();
657
+ }
658
+
659
+ protected buildStringArrayExpr(values: readonly string[]): ts.ArrayLiteralExpression {
660
+ const elements = values.map((v) => ts.factory.createStringLiteral(v));
661
+ return ts.factory.createArrayLiteralExpression(elements);
662
+ }
663
+
664
+ protected buildEnumCaseArrayExpr(enumCases: readonly string[]): ts.ArrayLiteralExpression {
665
+ const elements = enumCases.map((c) => this.buildEnumCaseExpr(c));
666
+ return ts.factory.createArrayLiteralExpression(elements);
667
+ }
668
+
669
+ protected buildNullExpr(): ts.NullLiteral {
670
+ return ts.factory.createNull();
671
+ }
672
+
673
+ protected buildClassIdentifierArrayExpr(classes: readonly string[]): ts.ArrayLiteralExpression {
674
+ const elements = classes.map((c) => ts.factory.createIdentifier(c.split('\\').pop() ?? c));
675
+ return ts.factory.createArrayLiteralExpression(elements);
676
+ }
677
+
678
+ protected classImplementsInterface(
679
+ className: string,
680
+ interfaceName: string,
681
+ useMap: Record<string, string>,
682
+ currentFilePath: string,
683
+ ): boolean {
684
+ const filePath = this.resolveImportToFilePath(className, useMap, currentFilePath);
685
+
686
+ if (filePath === '') {
687
+ return false;
688
+ }
689
+
690
+ try {
691
+ const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true });
692
+ const sourceFile = project.addSourceFileAtPath(filePath);
693
+ const classDecl = sourceFile.getClass(className);
694
+
695
+ if (classDecl === undefined) {
696
+ return false;
697
+ }
698
+
699
+ for (const impl of classDecl.getImplements()) {
700
+ if (impl.getExpression().getText() === interfaceName) {
701
+ return true;
702
+ }
703
+ }
704
+ } catch {
705
+ // Silently skip unresolvable
706
+ }
707
+
708
+ return false;
709
+ }
710
+ }