@valkyrjaio/sindri 26.2.0 → 26.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/sindri.cjs +820 -212
- package/package.json +2 -2
- package/src/Sindri/Ast/Abstract/AstReader.ts +250 -29
- package/src/Sindri/Ast/ComponentProviderReader.ts +9 -5
- package/src/Sindri/Ast/ConfigReader.ts +14 -6
- package/src/Sindri/Ast/Data/Result/RouteProviderResult.ts +7 -0
- package/src/Sindri/Ast/RouteProviderReader.ts +55 -4
- package/src/Sindri/Constant/SindriInfo.ts +2 -2
- package/src/Sindri/Generate/Abstract/GenerateDataFromAst.ts +218 -13
- package/src/Sindri/Generator/Abstract/AstFileGenerator.ts +27 -0
- package/src/Sindri/Generator/Ast/Cli/AstCliDataFileGenerator.ts +94 -20
- package/src/Sindri/Generator/Ast/Container/AstContainerDataFileGenerator.ts +19 -11
- package/src/Sindri/Generator/Ast/Event/AstEventDataFileGenerator.ts +33 -8
- package/src/Sindri/Generator/Ast/Http/AstHttpDataFileGenerator.ts +199 -19
- package/src/Sindri/Generator/Cli/Contract/CliDataFileGeneratorContract.ts +10 -0
- package/src/Sindri/Generator/Container/Contract/ContainerDataFileGeneratorContract.ts +3 -0
- package/src/Sindri/Generator/Http/Contract/HttpDataFileGeneratorContract.ts +10 -0
|
@@ -13,6 +13,7 @@ import { DynamicRoute } from '@valkyrjaio/valkyrja/Http/Routing/Data/DynamicRout
|
|
|
13
13
|
import { Parameter } from '@valkyrjaio/valkyrja/Http/Routing/Data/Parameter.ts';
|
|
14
14
|
import { Regex } from '@valkyrjaio/valkyrja/Http/Routing/Constant/Regex.ts';
|
|
15
15
|
import { Processor } from '@valkyrjaio/valkyrja/Http/Routing/Processor/Processor.ts';
|
|
16
|
+
import { RequestMethod, allRequestMethods } from '@valkyrjaio/valkyrja/Http/Message/Enum/RequestMethod.ts';
|
|
16
17
|
|
|
17
18
|
import { AstFileGenerator } from '../../Abstract/AstFileGenerator.ts';
|
|
18
19
|
import { GeneratorUnreachableException } from '../../Throwable/Exception/GeneratorUnreachableException.ts';
|
|
@@ -22,7 +23,32 @@ import type { HttpParameterData } from '../../../Ast/Data/HttpParameterData.ts';
|
|
|
22
23
|
import type { HttpRouteData } from '../../../Ast/Data/HttpRouteData.ts';
|
|
23
24
|
import type { ProcessorContract } from '@valkyrjaio/valkyrja/Http/Routing/Processor/Contract/ProcessorContract.ts';
|
|
24
25
|
|
|
26
|
+
/** Metadata extracted from an imperative `new Route(...)` / `new DynamicRoute(...)` expression. */
|
|
27
|
+
interface ImperativeRouteMeta {
|
|
28
|
+
readonly expr: ts.Expression;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly path: string;
|
|
31
|
+
readonly isDynamic: boolean;
|
|
32
|
+
readonly regex: string;
|
|
33
|
+
readonly methods: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDataFileGeneratorContract {
|
|
37
|
+
/** The framework classes every generated HTTP data file imports as values. */
|
|
38
|
+
protected static readonly FRAMEWORK_IMPORTS: Readonly<Record<string, string>> = {
|
|
39
|
+
HttpRoutingData: '@valkyrjaio/valkyrja/Http/Routing/Data/HttpRoutingData.ts',
|
|
40
|
+
DynamicRoute: '@valkyrjaio/valkyrja/Http/Routing/Data/DynamicRoute.ts',
|
|
41
|
+
Route: '@valkyrjaio/valkyrja/Http/Routing/Data/Route.ts',
|
|
42
|
+
Parameter: '@valkyrjaio/valkyrja/Http/Routing/Data/Parameter.ts',
|
|
43
|
+
RequestMethod: '@valkyrjaio/valkyrja/Http/Message/Enum/RequestMethod.ts',
|
|
44
|
+
Regex: '@valkyrjaio/valkyrja/Http/Routing/Constant/Regex.ts',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** The framework types every generated HTTP data file imports. */
|
|
48
|
+
protected static readonly FRAMEWORK_TYPE_IMPORTS: Readonly<Record<string, string>> = {
|
|
49
|
+
RouteContract: '@valkyrjaio/valkyrja/Http/Routing/Data/Contract/RouteContract.ts',
|
|
50
|
+
};
|
|
51
|
+
|
|
26
52
|
public classImportMap: Record<string, string> = {};
|
|
27
53
|
|
|
28
54
|
protected readonly processor: ProcessorContract;
|
|
@@ -49,27 +75,68 @@ export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDa
|
|
|
49
75
|
routes: Record<string, ts.Expression>,
|
|
50
76
|
routeData: Record<string, HttpRouteData>,
|
|
51
77
|
): GenerateStatus {
|
|
52
|
-
const
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
const userImportsBlock = userImports ? `${userImports}\n` : '';
|
|
78
|
+
const userImportsBlock = this.buildUserImportsBlock(this.classImportMap, [
|
|
79
|
+
...Object.keys(AstHttpDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
80
|
+
...Object.keys(AstHttpDataFileGenerator.FRAMEWORK_TYPE_IMPORTS),
|
|
81
|
+
]);
|
|
57
82
|
|
|
58
83
|
const routesContent = this.getRoutesAsContent(routes, routeData);
|
|
59
84
|
const paths = this.printNestedObject(this.buildPaths(routeData));
|
|
60
85
|
const dynamicPaths = this.printNestedObject(this.buildDynamicPaths(routeData));
|
|
61
86
|
const regexes = this.printNestedObject(this.buildRegexes(routeData));
|
|
62
87
|
|
|
63
|
-
const data =
|
|
88
|
+
const data = this.assembleFile(className, userImportsBlock, routesContent, paths, dynamicPaths, regexes);
|
|
89
|
+
|
|
90
|
+
return this.writeFile(directory, className, data);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate the routing data file from imperative `getRoutes()` route
|
|
95
|
+
* objects (as opposed to attribute-scanned routes).
|
|
96
|
+
*
|
|
97
|
+
* Each route expression is emitted verbatim as a route closure and the
|
|
98
|
+
* path/dynamic-path/regex lookup maps are derived from the route objects'
|
|
99
|
+
* own arguments, mirroring the framework's runtime `RouteCollection`
|
|
100
|
+
* (literal path and regex, request methods defaulting to `[HEAD, GET]`).
|
|
101
|
+
*/
|
|
102
|
+
public generateFileFromRoutes(
|
|
103
|
+
directory: string,
|
|
104
|
+
className: string,
|
|
105
|
+
_namespace: string,
|
|
106
|
+
routeExprs: readonly ts.Expression[],
|
|
107
|
+
): GenerateStatus {
|
|
108
|
+
const userImportsBlock = this.buildUserImportsBlock(this.classImportMap, [
|
|
109
|
+
...Object.keys(AstHttpDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
110
|
+
...Object.keys(AstHttpDataFileGenerator.FRAMEWORK_TYPE_IMPORTS),
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const metas = routeExprs
|
|
114
|
+
.map((expr) => this.extractRouteMeta(expr))
|
|
115
|
+
.filter((meta): meta is ImperativeRouteMeta => meta !== undefined);
|
|
116
|
+
|
|
117
|
+
const routesContent = this.getImperativeRoutesContent(metas);
|
|
118
|
+
const paths = this.printNestedObject(this.buildImperativePaths(metas, false));
|
|
119
|
+
const dynamicPaths = this.printNestedObject(this.buildImperativePaths(metas, true));
|
|
120
|
+
const regexes = this.printNestedObject(this.buildImperativeRegexes(metas));
|
|
121
|
+
|
|
122
|
+
const data = this.assembleFile(className, userImportsBlock, routesContent, paths, dynamicPaths, regexes);
|
|
123
|
+
|
|
124
|
+
return this.writeFile(directory, className, data);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
protected assembleFile(
|
|
128
|
+
className: string,
|
|
129
|
+
userImportsBlock: string,
|
|
130
|
+
routesContent: string,
|
|
131
|
+
paths: string,
|
|
132
|
+
dynamicPaths: string,
|
|
133
|
+
regexes: string,
|
|
134
|
+
): string {
|
|
135
|
+
return [
|
|
64
136
|
'// This file was automatically generated by Sindri.',
|
|
65
137
|
'',
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`import { Route } from '@valkyrjaio/valkyrja/Http/Routing/Data/Route.ts';`,
|
|
69
|
-
`import { Parameter } from '@valkyrjaio/valkyrja/Http/Routing/Data/Parameter.ts';`,
|
|
70
|
-
`import { RequestMethod } from '@valkyrjaio/valkyrja/Http/Message/Enum/RequestMethod.ts';`,
|
|
71
|
-
`import { Regex } from '@valkyrjaio/valkyrja/Http/Routing/Constant/Regex.ts';`,
|
|
72
|
-
`import type { RouteContract } from '@valkyrjaio/valkyrja/Http/Routing/Data/Contract/RouteContract.ts';`,
|
|
138
|
+
...this.buildFrameworkImportLines(AstHttpDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
139
|
+
...this.buildFrameworkImportLines(AstHttpDataFileGenerator.FRAMEWORK_TYPE_IMPORTS, true),
|
|
73
140
|
userImportsBlock,
|
|
74
141
|
`export class ${className} extends HttpRoutingData {`,
|
|
75
142
|
` constructor() {`,
|
|
@@ -83,8 +150,6 @@ export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDa
|
|
|
83
150
|
`}`,
|
|
84
151
|
'',
|
|
85
152
|
].join('\n');
|
|
86
|
-
|
|
87
|
-
return this.writeFile(directory, className, data);
|
|
88
153
|
}
|
|
89
154
|
|
|
90
155
|
public generateClassContents(
|
|
@@ -292,15 +357,130 @@ export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDa
|
|
|
292
357
|
expr = ts.factory.createNewExpression(expr.expression, expr.typeArguments, args);
|
|
293
358
|
}
|
|
294
359
|
|
|
295
|
-
const keyExpr = this.buildEnumCaseExpr(key);
|
|
296
|
-
const printedKey = this.printer.printNode(ts.EmitHint.Unspecified, keyExpr, this.dummySourceFile);
|
|
297
360
|
const printedRoute = this.printer.printNode(ts.EmitHint.Unspecified, expr, this.dummySourceFile);
|
|
298
|
-
|
|
299
|
-
|
|
361
|
+
const formattedKey = key.includes('::')
|
|
362
|
+
? `[${this.printer.printNode(ts.EmitHint.Unspecified, this.buildEnumCaseExpr(key), this.dummySourceFile)}]`
|
|
363
|
+
: `['${key}']`;
|
|
300
364
|
|
|
301
365
|
lines.push(` ${formattedKey}: (): RouteContract => ${printedRoute},`);
|
|
302
366
|
}
|
|
303
367
|
|
|
304
368
|
return ['{', ...lines, ' }'].join('\n ');
|
|
305
369
|
}
|
|
370
|
+
|
|
371
|
+
// -------------------------------------------------------------------------
|
|
372
|
+
// Imperative `getRoutes()` support
|
|
373
|
+
// -------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Extract routing metadata from a `new Route(...)` / `new DynamicRoute(...)`
|
|
377
|
+
* expression, matching the framework constructor argument order:
|
|
378
|
+
* Route(path, name, handler, requestMethods?)
|
|
379
|
+
* DynamicRoute(path, name, regex, parameters, handler, requestMethods?)
|
|
380
|
+
*/
|
|
381
|
+
protected extractRouteMeta(expr: ts.Expression): ImperativeRouteMeta | undefined {
|
|
382
|
+
if (!ts.isNewExpression(expr) || !ts.isIdentifier(expr.expression)) {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const isDynamic = expr.expression.text === 'DynamicRoute';
|
|
387
|
+
const args = expr.arguments ?? ts.factory.createNodeArray<ts.Expression>();
|
|
388
|
+
|
|
389
|
+
const path = this.stringArg(args[0]);
|
|
390
|
+
const name = this.stringArg(args[1]);
|
|
391
|
+
|
|
392
|
+
if (name === undefined || path === undefined) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const regex = isDynamic ? (this.stringArg(args[2]) ?? '') : '';
|
|
397
|
+
const methodsArg = isDynamic ? args[5] : args[3];
|
|
398
|
+
const methods = this.extractMethodNamesFromArg(methodsArg);
|
|
399
|
+
|
|
400
|
+
return { expr, name, path, isDynamic, regex, methods };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
protected stringArg(node: ts.Expression | undefined): string | undefined {
|
|
404
|
+
return node !== undefined && ts.isStringLiteral(node) ? node.text : undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Resolve a `[RequestMethod.GET, ...]` argument to method-name strings,
|
|
409
|
+
* defaulting to `[HEAD, GET]` (the framework `Route` default) when omitted
|
|
410
|
+
* and expanding `RequestMethod.ANY` to every request method.
|
|
411
|
+
*/
|
|
412
|
+
protected extractMethodNamesFromArg(node: ts.Expression | undefined): string[] {
|
|
413
|
+
if (node === undefined || !ts.isArrayLiteralExpression(node)) {
|
|
414
|
+
return [RequestMethod.HEAD, RequestMethod.GET];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const methods: string[] = [];
|
|
418
|
+
|
|
419
|
+
for (const element of node.elements) {
|
|
420
|
+
if (!ts.isPropertyAccessExpression(element)) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const method = element.name.text;
|
|
425
|
+
|
|
426
|
+
if (method === 'ANY') {
|
|
427
|
+
return allRequestMethods();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
methods.push(method);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return methods;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
protected getImperativeRoutesContent(metas: readonly ImperativeRouteMeta[]): string {
|
|
437
|
+
if (metas.length === 0) {
|
|
438
|
+
return '{}';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const lines: string[] = [];
|
|
442
|
+
|
|
443
|
+
for (const meta of metas) {
|
|
444
|
+
// Imperative route expressions come parsed from a provider's `getRoutes()`
|
|
445
|
+
// body, so they are emitted verbatim via their original source text.
|
|
446
|
+
lines.push(` ['${meta.name}']: (): RouteContract => ${meta.expr.getText()},`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return ['{', ...lines, ' }'].join('\n ');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
protected buildImperativePaths(
|
|
453
|
+
metas: readonly ImperativeRouteMeta[],
|
|
454
|
+
dynamic: boolean,
|
|
455
|
+
): Record<string, Record<string, string>> {
|
|
456
|
+
const paths: Record<string, Record<string, string>> = {};
|
|
457
|
+
|
|
458
|
+
for (const meta of metas) {
|
|
459
|
+
if (meta.isDynamic !== dynamic) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
for (const method of meta.methods) {
|
|
464
|
+
(paths[method] ??= {})[meta.path] = meta.name;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return paths;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
protected buildImperativeRegexes(metas: readonly ImperativeRouteMeta[]): Record<string, Record<string, string>> {
|
|
472
|
+
const regexes: Record<string, Record<string, string>> = {};
|
|
473
|
+
|
|
474
|
+
for (const meta of metas) {
|
|
475
|
+
if (!meta.isDynamic || meta.regex === '') {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
for (const method of meta.methods) {
|
|
480
|
+
(regexes[method] ??= {})[meta.regex] = meta.name;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return regexes;
|
|
485
|
+
}
|
|
306
486
|
}
|
|
@@ -12,6 +12,9 @@ import type { ts } from 'ts-morph';
|
|
|
12
12
|
import type { GenerateStatus } from '../../Enum/GenerateStatus.ts';
|
|
13
13
|
|
|
14
14
|
export interface CliDataFileGeneratorContract {
|
|
15
|
+
/** Map of class name → module specifier for the provider imports the data cache references. */
|
|
16
|
+
classImportMap: Record<string, string>;
|
|
17
|
+
|
|
15
18
|
generateFile(
|
|
16
19
|
directory: string,
|
|
17
20
|
className: string,
|
|
@@ -19,5 +22,12 @@ export interface CliDataFileGeneratorContract {
|
|
|
19
22
|
routes: Record<string, ts.Expression>,
|
|
20
23
|
): GenerateStatus;
|
|
21
24
|
|
|
25
|
+
generateFileFromRoutes(
|
|
26
|
+
directory: string,
|
|
27
|
+
className: string,
|
|
28
|
+
namespace: string,
|
|
29
|
+
routeExprs: readonly ts.Expression[],
|
|
30
|
+
): GenerateStatus;
|
|
31
|
+
|
|
22
32
|
generateClassContents(routes: Record<string, ts.Expression>): string;
|
|
23
33
|
}
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
import type { GenerateStatus } from '../../Enum/GenerateStatus.ts';
|
|
11
11
|
|
|
12
12
|
export interface ContainerDataFileGeneratorContract {
|
|
13
|
+
/** Map of class name → module specifier for the provider imports the data cache references. */
|
|
14
|
+
classImportMap: Record<string, string>;
|
|
15
|
+
|
|
13
16
|
generateFile(
|
|
14
17
|
directory: string,
|
|
15
18
|
className: string,
|
|
@@ -13,6 +13,9 @@ import type { HttpRouteData } from '../../../Ast/Data/HttpRouteData.ts';
|
|
|
13
13
|
import type { GenerateStatus } from '../../Enum/GenerateStatus.ts';
|
|
14
14
|
|
|
15
15
|
export interface HttpDataFileGeneratorContract {
|
|
16
|
+
/** Map of class name → module specifier for the provider imports the data cache references. */
|
|
17
|
+
classImportMap: Record<string, string>;
|
|
18
|
+
|
|
16
19
|
generateFile(
|
|
17
20
|
directory: string,
|
|
18
21
|
className: string,
|
|
@@ -21,5 +24,12 @@ export interface HttpDataFileGeneratorContract {
|
|
|
21
24
|
routeData: Record<string, HttpRouteData>,
|
|
22
25
|
): GenerateStatus;
|
|
23
26
|
|
|
27
|
+
generateFileFromRoutes(
|
|
28
|
+
directory: string,
|
|
29
|
+
className: string,
|
|
30
|
+
namespace: string,
|
|
31
|
+
routeExprs: readonly ts.Expression[],
|
|
32
|
+
): GenerateStatus;
|
|
33
|
+
|
|
24
34
|
generateClassContents(routes: Record<string, ts.Expression>, routeData: Record<string, HttpRouteData>): string;
|
|
25
35
|
}
|