@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,391 @@
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 { ErrorFormatter } from '@valkyrjaio/valkyrja/Cli/Interaction/Formatter/ErrorFormatter.ts';
14
+ import { HighlightedTextFormatter } from '@valkyrjaio/valkyrja/Cli/Interaction/Formatter/HighlightedTextFormatter.ts';
15
+ import { SuccessFormatter } from '@valkyrjaio/valkyrja/Cli/Interaction/Formatter/SuccessFormatter.ts';
16
+ import { WarningFormatter } from '@valkyrjaio/valkyrja/Cli/Interaction/Formatter/WarningFormatter.ts';
17
+ import { Header } from '@valkyrjaio/valkyrja/Cli/Interaction/Message/Header.ts';
18
+ import { Message } from '@valkyrjaio/valkyrja/Cli/Interaction/Message/Message.ts';
19
+ import { NewLine } from '@valkyrjaio/valkyrja/Cli/Interaction/Message/NewLine.ts';
20
+
21
+ import { CliRouteAttributeReader } from '../../Ast/CliRouteAttributeReader.ts';
22
+ import { ComponentProviderReader } from '../../Ast/ComponentProviderReader.ts';
23
+ import { ConfigReader } from '../../Ast/ConfigReader.ts';
24
+ import { HttpRouteAttributeReader } from '../../Ast/HttpRouteAttributeReader.ts';
25
+ import { ListenerAttributeReader } from '../../Ast/ListenerAttributeReader.ts';
26
+ import { ListenerProviderReader } from '../../Ast/ListenerProviderReader.ts';
27
+ import { RouteProviderReader } from '../../Ast/RouteProviderReader.ts';
28
+ import { ServiceProviderReader } from '../../Ast/ServiceProviderReader.ts';
29
+ import { AstCliDataFileGenerator } from '../../Generator/Ast/Cli/AstCliDataFileGenerator.ts';
30
+ import { AstContainerDataFileGenerator } from '../../Generator/Ast/Container/AstContainerDataFileGenerator.ts';
31
+ import { AstEventDataFileGenerator } from '../../Generator/Ast/Event/AstEventDataFileGenerator.ts';
32
+ import { AstHttpDataFileGenerator } from '../../Generator/Ast/Http/AstHttpDataFileGenerator.ts';
33
+ import { GenerateStatus } from '../../Generator/Enum/GenerateStatus.ts';
34
+ import { SindriInfo } from '../../Constant/SindriInfo.ts';
35
+
36
+ import type { CliDataFileGeneratorContract } from '../../Generator/Cli/Contract/CliDataFileGeneratorContract.ts';
37
+ import type { ContainerDataFileGeneratorContract } from '../../Generator/Container/Contract/ContainerDataFileGeneratorContract.ts';
38
+ import type { EventDataFileGeneratorContract } from '../../Generator/Event/Contract/EventDataFileGeneratorContract.ts';
39
+ import type { HttpDataFileGeneratorContract } from '../../Generator/Http/Contract/HttpDataFileGeneratorContract.ts';
40
+ import type { CliRouteAttributeReaderContract } from '../../Ast/Contract/CliRouteAttributeReaderContract.ts';
41
+ import type { ComponentProviderReaderContract } from '../../Ast/Contract/ComponentProviderReaderContract.ts';
42
+ import type { ConfigReaderContract } from '../../Ast/Contract/ConfigReaderContract.ts';
43
+ import type { HttpRouteAttributeReaderContract } from '../../Ast/Contract/HttpRouteAttributeReaderContract.ts';
44
+ import type { ListenerAttributeReaderContract } from '../../Ast/Contract/ListenerAttributeReaderContract.ts';
45
+ import type { ListenerProviderReaderContract } from '../../Ast/Contract/ListenerProviderReaderContract.ts';
46
+ import type { RouteProviderReaderContract } from '../../Ast/Contract/RouteProviderReaderContract.ts';
47
+ import type { ServiceProviderReaderContract } from '../../Ast/Contract/ServiceProviderReaderContract.ts';
48
+ import { ComponentProviderResult } from '../../Ast/Data/Result/ComponentProviderResult.ts';
49
+ import type { ConfigResult } from '../../Ast/Data/Result/ConfigResult.ts';
50
+ import type { OutputContract } from '@valkyrjaio/valkyrja/Cli/Interaction/Output/Contract/OutputContract.ts';
51
+ import type { OutputFactoryContract } from '@valkyrjaio/valkyrja/Cli/Interaction/Output/Factory/Contract/OutputFactoryContract.ts';
52
+ import type { ts } from 'ts-morph';
53
+ import type { HttpRouteData } from '../../Ast/Data/HttpRouteData.ts';
54
+ import type { RouteContract } from '@valkyrjaio/valkyrja/Cli/Routing/Data/Contract/RouteContract.ts';
55
+
56
+ export abstract class GenerateDataFromAst {
57
+ public constructor(
58
+ protected readonly outputFactory: OutputFactoryContract,
59
+ protected readonly route: RouteContract,
60
+ protected readonly title: string = 'Generating Data',
61
+ protected readonly configReader: ConfigReaderContract = new ConfigReader(),
62
+ protected readonly componentProviderReader: ComponentProviderReaderContract = new ComponentProviderReader(),
63
+ protected readonly routeProviderReader: RouteProviderReaderContract = new RouteProviderReader(),
64
+ protected readonly listenerProviderReader: ListenerProviderReaderContract = new ListenerProviderReader(),
65
+ protected readonly serviceProviderReader: ServiceProviderReaderContract = new ServiceProviderReader(),
66
+ protected readonly cliRouteAttributeReader: CliRouteAttributeReaderContract = new CliRouteAttributeReader(),
67
+ protected readonly httpRouteAttributeReader: HttpRouteAttributeReaderContract = new HttpRouteAttributeReader(),
68
+ protected readonly listenerAttributeReader: ListenerAttributeReaderContract = new ListenerAttributeReader(),
69
+ protected readonly containerGenerator: ContainerDataFileGeneratorContract = new AstContainerDataFileGenerator(),
70
+ protected readonly eventGenerator: EventDataFileGeneratorContract = new AstEventDataFileGenerator(),
71
+ protected readonly cliGenerator: CliDataFileGeneratorContract = new AstCliDataFileGenerator(),
72
+ protected readonly httpGenerator: HttpDataFileGeneratorContract = new AstHttpDataFileGenerator(),
73
+ ) {}
74
+
75
+ protected generateData(): OutputContract {
76
+ let output = this.getOutput();
77
+ const config = this.configReader.readFile(this.getConfigFilePath());
78
+
79
+ const providers = this.walkComponentProviders(config);
80
+
81
+ output = this.generateContainerData(providers.serviceProviders, config, output);
82
+ output = this.generateEventData(providers.listenerProviders, config, output);
83
+ output = this.generateCliData(providers.cliRouteProviders, config, output);
84
+ output = this.generateHttpData(providers.httpRouteProviders, config, output);
85
+
86
+ return output.withAddedMessages(new NewLine());
87
+ }
88
+
89
+ protected getOutput(): OutputContract {
90
+ return this.outputFactory
91
+ .createOutput()
92
+ .withAddedMessages(
93
+ new Header('Sindri', SindriInfo.VERSION, this.route, SindriInfo.ICON),
94
+ new NewLine(),
95
+ new NewLine(),
96
+ new Message(`${this.title}:`, new HighlightedTextFormatter()),
97
+ new NewLine(),
98
+ new NewLine(),
99
+ )
100
+ .writeMessages();
101
+ }
102
+
103
+ /**
104
+ * Walk the component provider tree and collect all provider class lists.
105
+ *
106
+ * Each entry in config.providers is fully expanded before moving to the
107
+ * next, so the declaration order in the config controls the order providers
108
+ * appear in the result. Already-visited classes are skipped to prevent loops.
109
+ */
110
+ protected walkComponentProviders(config: ConfigResult): ComponentProviderResult {
111
+ let result = new ComponentProviderResult();
112
+ const visited: Record<string, true> = {};
113
+
114
+ for (const providerClass of config.providers) {
115
+ result = result.merge(this.walkProvider(providerClass, config, visited));
116
+ }
117
+
118
+ return result;
119
+ }
120
+
121
+ /**
122
+ * Recursively expand a single component provider.
123
+ *
124
+ * Sub-components are expanded inline in the order they are declared, then
125
+ * the current provider's own lists are appended. The caller controls load
126
+ * order entirely through the config — this method imposes no additional rules.
127
+ */
128
+ protected walkProvider(
129
+ providerClass: string,
130
+ config: ConfigResult,
131
+ visited: Record<string, true>,
132
+ ): ComponentProviderResult {
133
+ if (visited[providerClass] === true) {
134
+ return new ComponentProviderResult();
135
+ }
136
+
137
+ visited[providerClass] = true;
138
+
139
+ const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
140
+
141
+ if (filePath === '' || !fs.existsSync(filePath)) {
142
+ return new ComponentProviderResult();
143
+ }
144
+
145
+ const providerResult = this.componentProviderReader.readFile(filePath);
146
+
147
+ let aggregated = new ComponentProviderResult();
148
+
149
+ for (const subProvider of providerResult.componentProviders) {
150
+ aggregated = aggregated.merge(this.walkProvider(subProvider, config, visited));
151
+ }
152
+
153
+ return aggregated.merge(providerResult);
154
+ }
155
+
156
+ /**
157
+ * Derive a file path from a class name or module specifier.
158
+ *
159
+ * For relative/absolute paths, resolves from srcDir.
160
+ * For short class names, searches srcDir recursively for a matching .ts file.
161
+ */
162
+ protected fqnToFilePath(className: string, namespace: string, srcDir: string): string {
163
+ if (className.startsWith('./') || className.startsWith('../')) {
164
+ const resolved = path.resolve(srcDir, className.replace(/\.js$/, '.ts'));
165
+ return fs.existsSync(resolved) ? resolved : '';
166
+ }
167
+
168
+ if (path.isAbsolute(className)) {
169
+ return fs.existsSync(className) ? className : '';
170
+ }
171
+
172
+ return this.findFileInDir(className, srcDir);
173
+ }
174
+
175
+ /**
176
+ * Recursively search srcDir for a TypeScript file whose basename matches className.
177
+ */
178
+ protected findFileInDir(className: string, dir: string): string {
179
+ if (!fs.existsSync(dir)) {
180
+ return '';
181
+ }
182
+
183
+ let entries: fs.Dirent[];
184
+
185
+ try {
186
+ entries = fs.readdirSync(dir, { withFileTypes: true });
187
+ } catch {
188
+ return '';
189
+ }
190
+
191
+ for (const entry of entries) {
192
+ const fullPath = path.join(dir, entry.name);
193
+
194
+ if (entry.isDirectory()) {
195
+ const found = this.findFileInDir(className, fullPath);
196
+
197
+ if (found !== '') {
198
+ return found;
199
+ }
200
+ } else if (entry.isFile() && entry.name === `${className}.ts`) {
201
+ return fullPath;
202
+ }
203
+ }
204
+
205
+ return '';
206
+ }
207
+
208
+ protected generateContainerData(
209
+ serviceProviders: readonly string[],
210
+ config: ConfigResult,
211
+ output: OutputContract,
212
+ ): OutputContract {
213
+ output = output
214
+ .withAddedMessages(new Message('Generating Container Data......................'))
215
+ .writeMessages();
216
+
217
+ const publishers: Record<string, readonly [string, string]> = {};
218
+
219
+ for (const providerClass of serviceProviders) {
220
+ const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
221
+
222
+ if (filePath === '' || !fs.existsSync(filePath)) {
223
+ continue;
224
+ }
225
+
226
+ const result = this.serviceProviderReader.readFile(filePath);
227
+
228
+ Object.assign(publishers, result.publishers);
229
+ }
230
+
231
+ const status = this.containerGenerator.generateFile(
232
+ config.dataPath,
233
+ 'AppContainerData',
234
+ config.dataNamespace,
235
+ publishers,
236
+ );
237
+
238
+ return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
239
+ }
240
+
241
+ protected generateEventData(
242
+ listenerProviders: readonly string[],
243
+ config: ConfigResult,
244
+ output: OutputContract,
245
+ ): OutputContract {
246
+ output = output
247
+ .withAddedMessages(new Message('Generating Event Data..........................'))
248
+ .writeMessages();
249
+
250
+ const allListeners: Record<string, ts.Expression> = {};
251
+
252
+ for (const providerClass of listenerProviders) {
253
+ const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
254
+
255
+ if (filePath === '' || !fs.existsSync(filePath)) {
256
+ continue;
257
+ }
258
+
259
+ const providerResult = this.listenerProviderReader.readFile(filePath);
260
+
261
+ for (const listenerClass of providerResult.listenerClasses) {
262
+ const listenerPath = this.fqnToFilePath(listenerClass, config.namespace, config.dir);
263
+
264
+ if (listenerPath === '' || !fs.existsSync(listenerPath)) {
265
+ continue;
266
+ }
267
+
268
+ const attrResult = this.listenerAttributeReader.readFile(listenerPath);
269
+
270
+ Object.assign(allListeners, attrResult.listeners);
271
+ }
272
+ }
273
+
274
+ const status = this.eventGenerator.generateFile(
275
+ config.dataPath,
276
+ 'AppEventData',
277
+ config.dataNamespace,
278
+ allListeners,
279
+ );
280
+
281
+ return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
282
+ }
283
+
284
+ protected generateCliData(
285
+ cliRouteProviders: readonly string[],
286
+ config: ConfigResult,
287
+ output: OutputContract,
288
+ ): OutputContract {
289
+ output = output
290
+ .withAddedMessages(new Message('Generating Cli Routes Data.....................'))
291
+ .writeMessages();
292
+
293
+ const allRoutes: Record<string, ts.Expression> = {};
294
+
295
+ for (const providerClass of cliRouteProviders) {
296
+ const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
297
+
298
+ if (filePath === '' || !fs.existsSync(filePath)) {
299
+ continue;
300
+ }
301
+
302
+ const providerResult = this.routeProviderReader.readFile(filePath);
303
+
304
+ for (const controllerClass of providerResult.controllerClasses) {
305
+ const controllerPath = this.fqnToFilePath(controllerClass, config.namespace, config.dir);
306
+
307
+ if (controllerPath === '' || !fs.existsSync(controllerPath)) {
308
+ continue;
309
+ }
310
+
311
+ const attrResult = this.cliRouteAttributeReader.readFile(controllerPath);
312
+
313
+ Object.assign(allRoutes, attrResult.routes);
314
+ }
315
+ }
316
+
317
+ const status = this.cliGenerator.generateFile(
318
+ config.dataPath,
319
+ 'AppCliRoutingData',
320
+ config.dataNamespace,
321
+ allRoutes,
322
+ );
323
+
324
+ return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
325
+ }
326
+
327
+ protected generateHttpData(
328
+ httpRouteProviders: readonly string[],
329
+ config: ConfigResult,
330
+ output: OutputContract,
331
+ ): OutputContract {
332
+ output = output
333
+ .withAddedMessages(new Message('Generating Http Routes Data....................'))
334
+ .writeMessages();
335
+
336
+ const allRoutes: Record<string, ts.Expression> = {};
337
+ const allRouteData: Record<string, HttpRouteData> = {};
338
+
339
+ for (const providerClass of httpRouteProviders) {
340
+ const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
341
+
342
+ if (filePath === '' || !fs.existsSync(filePath)) {
343
+ continue;
344
+ }
345
+
346
+ const providerResult = this.routeProviderReader.readFile(filePath);
347
+
348
+ for (const controllerClass of providerResult.controllerClasses) {
349
+ const controllerPath = this.fqnToFilePath(controllerClass, config.namespace, config.dir);
350
+
351
+ if (controllerPath === '' || !fs.existsSync(controllerPath)) {
352
+ continue;
353
+ }
354
+
355
+ const attrResult = this.httpRouteAttributeReader.readFile(controllerPath);
356
+
357
+ Object.assign(allRoutes, attrResult.routes);
358
+ Object.assign(allRouteData, attrResult.routeData);
359
+ }
360
+ }
361
+
362
+ const status = this.httpGenerator.generateFile(
363
+ config.dataPath,
364
+ 'AppHttpRoutingData',
365
+ config.dataNamespace,
366
+ allRoutes,
367
+ allRouteData,
368
+ );
369
+
370
+ return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
371
+ }
372
+
373
+ protected addMessagesForGenerateStatus(output: OutputContract, status: GenerateStatus): OutputContract {
374
+ let text = 'Failed';
375
+ let formatter = new ErrorFormatter();
376
+
377
+ if (status === GenerateStatus.SUCCESS) {
378
+ text = 'Success';
379
+ formatter = new SuccessFormatter();
380
+ }
381
+
382
+ if (status === GenerateStatus.SKIPPED) {
383
+ text = 'Skipped';
384
+ formatter = new WarningFormatter();
385
+ }
386
+
387
+ return output.withAddedMessages(new Message(text, formatter), new NewLine());
388
+ }
389
+
390
+ protected abstract getConfigFilePath(): string;
391
+ }
@@ -0,0 +1,54 @@
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
+
12
+ import { ts } from 'ts-morph';
13
+
14
+ import { GenerateStatus } from '../Enum/GenerateStatus.ts';
15
+
16
+ export abstract class AstFileGenerator {
17
+ /**
18
+ * Build a property access or string literal expression from a "ClassName::CASE" string.
19
+ */
20
+ protected buildEnumCaseExpr(fqnColonCase: string): ts.Expression {
21
+ const pos = fqnColonCase.indexOf('::');
22
+
23
+ if (pos === -1) {
24
+ return ts.factory.createStringLiteral(fqnColonCase);
25
+ }
26
+
27
+ const fqn = fqnColonCase.substring(0, pos);
28
+ const caseName = fqnColonCase.substring(pos + 2);
29
+ const className = fqn.split('\\').pop() ?? fqn;
30
+
31
+ return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(className), caseName);
32
+ }
33
+
34
+ protected writeFile(directory: string, className: string, data: string): GenerateStatus {
35
+ const filePath = directory.replace(/\/$/, '') + `/${className}.ts`;
36
+
37
+ try {
38
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : false;
39
+
40
+ if (existing === data) {
41
+ return GenerateStatus.SKIPPED;
42
+ }
43
+
44
+ fs.mkdirSync(directory, { recursive: true });
45
+ fs.writeFileSync(filePath, data, 'utf-8');
46
+
47
+ return GenerateStatus.SUCCESS;
48
+ } catch {
49
+ // Fallthrough
50
+ }
51
+
52
+ return GenerateStatus.FAILURE;
53
+ }
54
+ }
@@ -0,0 +1,59 @@
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
+
12
+ import { GenerateStatus } from '../Enum/GenerateStatus.ts';
13
+
14
+ import type { FileGeneratorContract } from '../Contract/FileGeneratorContract.ts';
15
+
16
+ export abstract class FileGenerator implements FileGeneratorContract {
17
+ protected filePath: string;
18
+
19
+ constructor(
20
+ protected directory: string,
21
+ protected className: string,
22
+ ) {
23
+ this.filePath = directory.replace(/\/$/, '') + `/${className}.ts`;
24
+ }
25
+
26
+ generateFile(): GenerateStatus {
27
+ try {
28
+ const data = this.generateFileContents();
29
+ const existing = this.fileGetContents();
30
+
31
+ if (existing === data) {
32
+ return GenerateStatus.SKIPPED;
33
+ }
34
+
35
+ this.filePutContents(data);
36
+
37
+ return GenerateStatus.SUCCESS;
38
+ } catch {
39
+ // Fallthrough
40
+ }
41
+
42
+ return GenerateStatus.FAILURE;
43
+ }
44
+
45
+ abstract generateFileContents(): string;
46
+
47
+ protected fileGetContents(): string | false {
48
+ if (!fs.existsSync(this.filePath)) {
49
+ return false;
50
+ }
51
+
52
+ return fs.readFileSync(this.filePath, 'utf-8');
53
+ }
54
+
55
+ protected filePutContents(data: string): void {
56
+ fs.mkdirSync(this.directory, { recursive: true });
57
+ fs.writeFileSync(this.filePath, data, 'utf-8');
58
+ }
59
+ }
@@ -0,0 +1,88 @@
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 { ts } from 'ts-morph';
11
+
12
+ import { AstFileGenerator } from '../../Abstract/AstFileGenerator.ts';
13
+ import type { CliDataFileGeneratorContract } from '../../Cli/Contract/CliDataFileGeneratorContract.ts';
14
+ import type { GenerateStatus } from '../../Enum/GenerateStatus.ts';
15
+
16
+ export class AstCliDataFileGenerator extends AstFileGenerator implements CliDataFileGeneratorContract {
17
+ public classImportMap: Record<string, string> = {};
18
+
19
+ private readonly printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
20
+
21
+ private readonly dummySourceFile = ts.createSourceFile(
22
+ '_dummy.ts',
23
+ '',
24
+ ts.ScriptTarget.ESNext,
25
+ false,
26
+ ts.ScriptKind.TS,
27
+ );
28
+
29
+ public generateFile(
30
+ directory: string,
31
+ className: string,
32
+ namespace: string,
33
+ routes: Record<string, ts.Expression>,
34
+ ): GenerateStatus {
35
+ const userImports = Object.entries(this.classImportMap)
36
+ .map(([name, specifier]) => `import { ${name} } from '${specifier}';`)
37
+ .join('\n');
38
+
39
+ const userImportsBlock = userImports ? `${userImports}\n` : '';
40
+
41
+ const contents = this.generateClassContents(routes);
42
+
43
+ const data = [
44
+ '// This file was automatically generated by Sindri.',
45
+ '',
46
+ `import { CliRoutingData } from '@valkyrjaio/valkyrja/Cli/Routing/Data/CliRoutingData.ts';`,
47
+ `import { Route } from '@valkyrjaio/valkyrja/Cli/Routing/Data/Route.ts';`,
48
+ `import { ArgumentParameter } from '@valkyrjaio/valkyrja/Cli/Routing/Data/ArgumentParameter.ts';`,
49
+ `import { OptionParameter } from '@valkyrjaio/valkyrja/Cli/Routing/Data/OptionParameter.ts';`,
50
+ `import { ArgumentMode } from '@valkyrjaio/valkyrja/Cli/Routing/Enum/ArgumentMode.ts';`,
51
+ `import { ArgumentValueMode } from '@valkyrjaio/valkyrja/Cli/Routing/Enum/ArgumentValueMode.ts';`,
52
+ `import { OptionMode } from '@valkyrjaio/valkyrja/Cli/Routing/Enum/OptionMode.ts';`,
53
+ `import { OptionValueMode } from '@valkyrjaio/valkyrja/Cli/Routing/Enum/OptionValueMode.ts';`,
54
+ `import type { RouteContract } from '@valkyrjaio/valkyrja/Cli/Routing/Data/Contract/RouteContract.ts';`,
55
+ userImportsBlock,
56
+ `export class ${className} extends CliRoutingData {`,
57
+ ` constructor() {`,
58
+ ` ${contents}`,
59
+ ` }`,
60
+ `}`,
61
+ '',
62
+ ].join('\n');
63
+
64
+ return this.writeFile(directory, className, data);
65
+ }
66
+
67
+ public generateClassContents(routes: Record<string, ts.Expression>): string {
68
+ const entries = Object.entries(routes);
69
+
70
+ if (entries.length === 0) {
71
+ return 'super({});';
72
+ }
73
+
74
+ const lines: string[] = [];
75
+
76
+ for (const [key, valueExpr] of entries) {
77
+ const keyExpr = this.buildEnumCaseExpr(key);
78
+ const printedKey = this.printer.printNode(ts.EmitHint.Unspecified, keyExpr, this.dummySourceFile);
79
+ const printedValue = this.printer.printNode(ts.EmitHint.Unspecified, valueExpr, this.dummySourceFile);
80
+
81
+ const formattedKey = key.includes('::') ? `[${printedKey}]` : `['${printedKey}']`;
82
+
83
+ lines.push(` ${formattedKey}: (): RouteContract => ${printedValue},`);
84
+ }
85
+
86
+ return ['super({', ' routes: {', ...lines, ' },', ' });'].join('\n ');
87
+ }
88
+ }
@@ -0,0 +1,86 @@
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 { ts } from 'ts-morph';
11
+
12
+ import { AstFileGenerator } from '../../Abstract/AstFileGenerator.ts';
13
+ import type { ContainerDataFileGeneratorContract } from '../../Container/Contract/ContainerDataFileGeneratorContract.ts';
14
+ import type { GenerateStatus } from '../../Enum/GenerateStatus.ts';
15
+
16
+ export class AstContainerDataFileGenerator extends AstFileGenerator implements ContainerDataFileGeneratorContract {
17
+ public classImportMap: Record<string, string> = {};
18
+
19
+ private readonly printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
20
+
21
+ private readonly dummySourceFile = ts.createSourceFile(
22
+ '_dummy.ts',
23
+ '',
24
+ ts.ScriptTarget.ESNext,
25
+ false,
26
+ ts.ScriptKind.TS,
27
+ );
28
+
29
+ public generateFile(
30
+ directory: string,
31
+ className: string,
32
+ namespace: string,
33
+ publishers: Readonly<Record<string, readonly [string, string]>>,
34
+ ): GenerateStatus {
35
+ const userImports = Object.entries(this.classImportMap)
36
+ .map(([name, specifier]) => `import { ${name} } from '${specifier}';`)
37
+ .join('\n');
38
+
39
+ const userImportsBlock = userImports ? `${userImports}\n` : '';
40
+
41
+ const contents = this.generateClassContents(publishers);
42
+
43
+ const data = [
44
+ '// This file was automatically generated by Sindri.',
45
+ '',
46
+ `import { ContainerData } from '@valkyrjaio/valkyrja/Container/Data/ContainerData.ts';`,
47
+ `import type { ContainerContract } from '@valkyrjaio/valkyrja/Container/Manager/Contract/ContainerContract.ts';`,
48
+ userImportsBlock,
49
+ `export class ${className} extends ContainerData {`,
50
+ ` constructor() {`,
51
+ ` ${contents}`,
52
+ ` }`,
53
+ `}`,
54
+ '',
55
+ ].join('\n');
56
+
57
+ return this.writeFile(directory, className, data);
58
+ }
59
+
60
+ public generateClassContents(publishers: Readonly<Record<string, readonly [string, string]>>): string {
61
+ const entries = Object.entries(publishers);
62
+
63
+ if (entries.length === 0) {
64
+ return 'super({});';
65
+ }
66
+
67
+ const lines: string[] = [];
68
+
69
+ for (const [serviceId, [providerClass, methodName]] of entries) {
70
+ const keyExpr = this.buildEnumCaseExpr(serviceId);
71
+ const printedKey = this.printer.printNode(ts.EmitHint.Unspecified, keyExpr, this.dummySourceFile);
72
+
73
+ const shortProviderClass = providerClass.split('\\').pop() ?? providerClass;
74
+
75
+ const formattedKey = serviceId.includes('::') ? `[${printedKey}]` : `['${printedKey}']`;
76
+
77
+ lines.push(
78
+ ` ${formattedKey}: (container: ContainerContract) => ${shortProviderClass}.${methodName}(container),`,
79
+ );
80
+ }
81
+
82
+ return ['super({', ' deferredCallback: {', ...lines, ' },', ' });'].join(
83
+ '\n ',
84
+ );
85
+ }
86
+ }