@valkyrjaio/sindri 26.5.1 → 26.6.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 +574 -268
- package/package.json +2 -2
- package/src/Sindri/Ast/Abstract/AstReader.ts +185 -2
- package/src/Sindri/Ast/CliRouteAttributeReader.ts +100 -14
- package/src/Sindri/Ast/CliRouteParameterReader.ts +42 -23
- package/src/Sindri/Ast/Contract/HttpRouteMiddlewareReaderContract.ts +3 -7
- package/src/Sindri/Ast/Contract/HttpRouteParameterReaderContract.ts +1 -4
- package/src/Sindri/Ast/Data/Contract/HttpParameterDataContract.ts +2 -0
- package/src/Sindri/Ast/Data/HttpParameterData.ts +1 -0
- package/src/Sindri/Ast/Data/Result/CliRouteAttributeResult.ts +5 -1
- package/src/Sindri/Ast/Data/Result/HttpRouteAttributeResult.ts +2 -0
- package/src/Sindri/Ast/HttpRouteAttributeReader.ts +178 -48
- package/src/Sindri/Ast/HttpRouteMiddlewareReader.ts +36 -15
- package/src/Sindri/Ast/HttpRouteParameterReader.ts +48 -106
- package/src/Sindri/Constant/SindriInfo.ts +2 -2
- package/src/Sindri/Generate/Abstract/GenerateDataFromAst.ts +37 -24
- package/src/Sindri/Generator/Ast/Cli/AstCliDataFileGenerator.ts +19 -3
- package/src/Sindri/Generator/Ast/Http/AstHttpDataFileGenerator.ts +81 -42
- package/src/Sindri/Generator/Cli/Contract/CliDataFileGeneratorContract.ts +8 -0
- package/src/Sindri/Generator/Http/Contract/HttpDataFileGeneratorContract.ts +9 -0
|
@@ -344,20 +344,20 @@ export abstract class GenerateDataFromAst extends GenerateFromAst {
|
|
|
344
344
|
const attrResult = this.cliRouteAttributeReader.readFile(controllerPath);
|
|
345
345
|
|
|
346
346
|
Object.assign(allRoutes, attrResult.routes);
|
|
347
|
+
this.mergeReaderImports(importMap, attrResult.importMap, config);
|
|
347
348
|
}
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
this.cliGenerator.classImportMap = importMap;
|
|
351
352
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
: this.cliGenerator.generateFile(config.dataPath, 'AppCliRoutingData', config.dataNamespace, allRoutes);
|
|
353
|
+
// Merge attribute-scanned command routes with imperative getRoutes() routes.
|
|
354
|
+
const status = this.cliGenerator.generateMergedFile(
|
|
355
|
+
config.dataPath,
|
|
356
|
+
'AppCliRoutingData',
|
|
357
|
+
config.dataNamespace,
|
|
358
|
+
allRoutes,
|
|
359
|
+
imperativeRoutes,
|
|
360
|
+
);
|
|
361
361
|
|
|
362
362
|
return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
|
|
363
363
|
}
|
|
@@ -409,30 +409,43 @@ export abstract class GenerateDataFromAst extends GenerateFromAst {
|
|
|
409
409
|
|
|
410
410
|
Object.assign(allRoutes, attrResult.routes);
|
|
411
411
|
Object.assign(allRouteData, attrResult.routeData);
|
|
412
|
+
this.mergeReaderImports(importMap, attrResult.importMap, config);
|
|
412
413
|
}
|
|
413
414
|
}
|
|
414
415
|
|
|
415
416
|
this.httpGenerator.classImportMap = importMap;
|
|
416
417
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
config.dataPath,
|
|
427
|
-
'AppHttpRoutingData',
|
|
428
|
-
config.dataNamespace,
|
|
429
|
-
allRoutes,
|
|
430
|
-
allRouteData,
|
|
431
|
-
);
|
|
418
|
+
// Merge attribute-scanned routes with imperative getRoutes() routes.
|
|
419
|
+
const status = this.httpGenerator.generateMergedFile(
|
|
420
|
+
config.dataPath,
|
|
421
|
+
'AppHttpRoutingData',
|
|
422
|
+
config.dataNamespace,
|
|
423
|
+
allRoutes,
|
|
424
|
+
allRouteData,
|
|
425
|
+
imperativeRoutes,
|
|
426
|
+
);
|
|
432
427
|
|
|
433
428
|
return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
|
|
434
429
|
}
|
|
435
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Merge the class-name → absolute-file-path imports a route attribute reader
|
|
433
|
+
* resolved (handler/help-text/middleware classes) into the generated data
|
|
434
|
+
* file's import map, converting each absolute path to the specifier the
|
|
435
|
+
* generated file imports it by — relative to the output directory for
|
|
436
|
+
* application source, or the owning package's own specifier for a class an
|
|
437
|
+
* installed package exposes.
|
|
438
|
+
*/
|
|
439
|
+
protected mergeReaderImports(
|
|
440
|
+
importMap: Record<string, string>,
|
|
441
|
+
readerImportMap: Record<string, string>,
|
|
442
|
+
config: ConfigResult,
|
|
443
|
+
): void {
|
|
444
|
+
for (const [name, absolutePath] of Object.entries(readerImportMap)) {
|
|
445
|
+
importMap[name] = this.importSpecifier(config.dataPath, absolutePath);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
436
449
|
protected addMessagesForGenerateStatus(output: OutputContract, status: GenerateStatus): OutputContract {
|
|
437
450
|
let text = 'Failed';
|
|
438
451
|
let formatter = new ErrorFormatter();
|
|
@@ -84,7 +84,23 @@ export class AstCliDataFileGenerator extends AstFileGenerator implements CliData
|
|
|
84
84
|
namespace: string,
|
|
85
85
|
routeExprs: readonly ts.Expression[],
|
|
86
86
|
): GenerateStatus {
|
|
87
|
-
|
|
87
|
+
return this.generateMergedFile(directory, className, namespace, {}, routeExprs);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate the CLI routing data file by MERGING attribute-scanned command
|
|
92
|
+
* routes with imperative `getRoutes()` route objects — both are keyed by
|
|
93
|
+
* command name into a single routes map. Attribute entries come first;
|
|
94
|
+
* imperative entries override on a name collision.
|
|
95
|
+
*/
|
|
96
|
+
public generateMergedFile(
|
|
97
|
+
directory: string,
|
|
98
|
+
className: string,
|
|
99
|
+
namespace: string,
|
|
100
|
+
routes: Record<string, ts.Expression>,
|
|
101
|
+
routeExprs: readonly ts.Expression[],
|
|
102
|
+
): GenerateStatus {
|
|
103
|
+
const merged: Record<string, ts.Expression> = { ...routes };
|
|
88
104
|
|
|
89
105
|
for (const expr of routeExprs) {
|
|
90
106
|
if (!ts.isNewExpression(expr)) {
|
|
@@ -97,10 +113,10 @@ export class AstCliDataFileGenerator extends AstFileGenerator implements CliData
|
|
|
97
113
|
continue;
|
|
98
114
|
}
|
|
99
115
|
|
|
100
|
-
|
|
116
|
+
merged[key] = expr;
|
|
101
117
|
}
|
|
102
118
|
|
|
103
|
-
return this.generateFile(directory, className, namespace,
|
|
119
|
+
return this.generateFile(directory, className, namespace, merged);
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
/**
|
|
@@ -75,19 +75,7 @@ export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDa
|
|
|
75
75
|
routes: Record<string, ts.Expression>,
|
|
76
76
|
routeData: Record<string, HttpRouteData>,
|
|
77
77
|
): GenerateStatus {
|
|
78
|
-
|
|
79
|
-
...Object.keys(AstHttpDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
80
|
-
...Object.keys(AstHttpDataFileGenerator.FRAMEWORK_TYPE_IMPORTS),
|
|
81
|
-
]);
|
|
82
|
-
|
|
83
|
-
const routesContent = this.getRoutesAsContent(routes, routeData);
|
|
84
|
-
const paths = this.printNestedObject(this.buildPaths(routeData));
|
|
85
|
-
const dynamicPaths = this.printNestedObject(this.buildDynamicPaths(routeData));
|
|
86
|
-
const regexes = this.printNestedObject(this.buildRegexes(routeData));
|
|
87
|
-
|
|
88
|
-
const data = this.assembleFile(className, userImportsBlock, routesContent, paths, dynamicPaths, regexes);
|
|
89
|
-
|
|
90
|
-
return this.writeFile(directory, className, data);
|
|
78
|
+
return this.generateMergedFile(directory, className, namespace, routes, routeData, []);
|
|
91
79
|
}
|
|
92
80
|
|
|
93
81
|
/**
|
|
@@ -100,9 +88,26 @@ export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDa
|
|
|
100
88
|
* (literal path and regex, request methods defaulting to `[HEAD, GET]`).
|
|
101
89
|
*/
|
|
102
90
|
public generateFileFromRoutes(
|
|
91
|
+
directory: string,
|
|
92
|
+
className: string,
|
|
93
|
+
namespace: string,
|
|
94
|
+
routeExprs: readonly ts.Expression[],
|
|
95
|
+
): GenerateStatus {
|
|
96
|
+
return this.generateMergedFile(directory, className, namespace, {}, {}, routeExprs);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate the routing data file by MERGING attribute-scanned routes with
|
|
101
|
+
* imperative `getRoutes()` route objects — the four lookup maps (routes,
|
|
102
|
+
* paths, dynamicPaths, regexes) are built from both sources and combined.
|
|
103
|
+
* Attribute entries are emitted first, then imperative ones.
|
|
104
|
+
*/
|
|
105
|
+
public generateMergedFile(
|
|
103
106
|
directory: string,
|
|
104
107
|
className: string,
|
|
105
108
|
_namespace: string,
|
|
109
|
+
routes: Record<string, ts.Expression>,
|
|
110
|
+
routeData: Record<string, HttpRouteData>,
|
|
106
111
|
routeExprs: readonly ts.Expression[],
|
|
107
112
|
): GenerateStatus {
|
|
108
113
|
const userImportsBlock = this.buildUserImportsBlock(this.classImportMap, [
|
|
@@ -114,16 +119,39 @@ export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDa
|
|
|
114
119
|
.map((expr) => this.extractRouteMeta(expr))
|
|
115
120
|
.filter((meta): meta is ImperativeRouteMeta => meta !== undefined);
|
|
116
121
|
|
|
117
|
-
const routesContent = this.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
const routesContent = this.wrapRoutes([
|
|
123
|
+
...this.getRouteLines(routes, routeData),
|
|
124
|
+
...this.getImperativeRouteLines(metas),
|
|
125
|
+
]);
|
|
126
|
+
const paths = this.printNestedObject(
|
|
127
|
+
this.mergeNested(this.buildPaths(routeData), this.buildImperativePaths(metas, false)),
|
|
128
|
+
);
|
|
129
|
+
const dynamicPaths = this.printNestedObject(
|
|
130
|
+
this.mergeNested(this.buildDynamicPaths(routeData), this.buildImperativePaths(metas, true)),
|
|
131
|
+
);
|
|
132
|
+
const regexes = this.printNestedObject(
|
|
133
|
+
this.mergeNested(this.buildRegexes(routeData), this.buildImperativeRegexes(metas)),
|
|
134
|
+
);
|
|
121
135
|
|
|
122
136
|
const data = this.assembleFile(className, userImportsBlock, routesContent, paths, dynamicPaths, regexes);
|
|
123
137
|
|
|
124
138
|
return this.writeFile(directory, className, data);
|
|
125
139
|
}
|
|
126
140
|
|
|
141
|
+
/** Deep-merge two `method → key → name` maps; entries in `b` override `a`. */
|
|
142
|
+
protected mergeNested(
|
|
143
|
+
a: Record<string, Record<string, string>>,
|
|
144
|
+
b: Record<string, Record<string, string>>,
|
|
145
|
+
): Record<string, Record<string, string>> {
|
|
146
|
+
const merged: Record<string, Record<string, string>> = {};
|
|
147
|
+
|
|
148
|
+
for (const [outerKey, innerMap] of [...Object.entries(a), ...Object.entries(b)]) {
|
|
149
|
+
merged[outerKey] = { ...(merged[outerKey] ?? {}), ...innerMap };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return merged;
|
|
153
|
+
}
|
|
154
|
+
|
|
127
155
|
protected assembleFile(
|
|
128
156
|
className: string,
|
|
129
157
|
userImportsBlock: string,
|
|
@@ -339,23 +367,25 @@ export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDa
|
|
|
339
367
|
routes: Record<string, ts.Expression>,
|
|
340
368
|
routeData: Record<string, HttpRouteData>,
|
|
341
369
|
): string {
|
|
342
|
-
|
|
370
|
+
return this.wrapRoutes(this.getRouteLines(routes, routeData));
|
|
371
|
+
}
|
|
343
372
|
|
|
344
|
-
|
|
373
|
+
/** Wrap route closure lines in the `{ ... }` object-literal body (or `{}`). */
|
|
374
|
+
protected wrapRoutes(lines: readonly string[]): string {
|
|
375
|
+
if (lines.length === 0) {
|
|
345
376
|
return '{}';
|
|
346
377
|
}
|
|
347
378
|
|
|
379
|
+
return ['{', ...lines, ' }'].join('\n ');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Build the per-route closure lines for attribute-scanned routes. */
|
|
383
|
+
protected getRouteLines(routes: Record<string, ts.Expression>, routeData: Record<string, HttpRouteData>): string[] {
|
|
348
384
|
const lines: string[] = [];
|
|
349
385
|
|
|
350
|
-
for (const [key, routeExpr] of entries) {
|
|
386
|
+
for (const [key, routeExpr] of Object.entries(routes)) {
|
|
351
387
|
const data = routeData[key] ?? null;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (data !== null && data.isDynamic && ts.isNewExpression(expr)) {
|
|
355
|
-
const computedRegex = data.parameters.length > 0 ? this.computeRegex(data) : '';
|
|
356
|
-
const args = [...(expr.arguments ?? []), ts.factory.createStringLiteral(computedRegex)];
|
|
357
|
-
expr = ts.factory.createNewExpression(expr.expression, expr.typeArguments, args);
|
|
358
|
-
}
|
|
388
|
+
const expr = data !== null ? this.injectRegex(routeExpr, data) : routeExpr;
|
|
359
389
|
|
|
360
390
|
const printedRoute = this.printer.printNode(ts.EmitHint.Unspecified, expr, this.dummySourceFile);
|
|
361
391
|
const formattedKey = key.includes('::')
|
|
@@ -365,7 +395,25 @@ export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDa
|
|
|
365
395
|
lines.push(` ${formattedKey}: (): RouteContract => ${printedRoute},`);
|
|
366
396
|
}
|
|
367
397
|
|
|
368
|
-
return
|
|
398
|
+
return lines;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Replace the regex argument (slot 2) of a dynamic `new DynamicRoute(...)`
|
|
403
|
+
* expression with the regex computed from the route's parameters. The reader
|
|
404
|
+
* emits an empty-string placeholder there; the generator owns the routing
|
|
405
|
+
* `Processor` and computes the real value here.
|
|
406
|
+
*/
|
|
407
|
+
protected injectRegex(expr: ts.Expression, data: HttpRouteData): ts.Expression {
|
|
408
|
+
if (!(data.isDynamic && ts.isNewExpression(expr))) {
|
|
409
|
+
return expr;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const computedRegex = data.parameters.length > 0 ? this.computeRegex(data) : '';
|
|
413
|
+
const original = expr.arguments ?? ts.factory.createNodeArray<ts.Expression>();
|
|
414
|
+
const args = original.map((arg, index) => (index === 2 ? ts.factory.createStringLiteral(computedRegex) : arg));
|
|
415
|
+
|
|
416
|
+
return ts.factory.createNewExpression(expr.expression, expr.typeArguments, args);
|
|
369
417
|
}
|
|
370
418
|
|
|
371
419
|
// -------------------------------------------------------------------------
|
|
@@ -433,20 +481,11 @@ export class AstHttpDataFileGenerator extends AstFileGenerator implements HttpDa
|
|
|
433
481
|
return methods;
|
|
434
482
|
}
|
|
435
483
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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 ');
|
|
484
|
+
/** Build the per-route closure lines for imperative `getRoutes()` routes. */
|
|
485
|
+
protected getImperativeRouteLines(metas: readonly ImperativeRouteMeta[]): string[] {
|
|
486
|
+
// Imperative route expressions come parsed from a provider's `getRoutes()`
|
|
487
|
+
// body, so they are emitted verbatim via their original source text.
|
|
488
|
+
return metas.map((meta) => ` ['${meta.name}']: (): RouteContract => ${meta.expr.getText()},`);
|
|
450
489
|
}
|
|
451
490
|
|
|
452
491
|
protected buildImperativePaths(
|
|
@@ -29,5 +29,13 @@ export interface CliDataFileGeneratorContract {
|
|
|
29
29
|
routeExprs: readonly ts.Expression[],
|
|
30
30
|
): GenerateStatus;
|
|
31
31
|
|
|
32
|
+
generateMergedFile(
|
|
33
|
+
directory: string,
|
|
34
|
+
className: string,
|
|
35
|
+
namespace: string,
|
|
36
|
+
routes: Record<string, ts.Expression>,
|
|
37
|
+
routeExprs: readonly ts.Expression[],
|
|
38
|
+
): GenerateStatus;
|
|
39
|
+
|
|
32
40
|
generateClassContents(routes: Record<string, ts.Expression>): string;
|
|
33
41
|
}
|
|
@@ -31,5 +31,14 @@ export interface HttpDataFileGeneratorContract {
|
|
|
31
31
|
routeExprs: readonly ts.Expression[],
|
|
32
32
|
): GenerateStatus;
|
|
33
33
|
|
|
34
|
+
generateMergedFile(
|
|
35
|
+
directory: string,
|
|
36
|
+
className: string,
|
|
37
|
+
namespace: string,
|
|
38
|
+
routes: Record<string, ts.Expression>,
|
|
39
|
+
routeData: Record<string, HttpRouteData>,
|
|
40
|
+
routeExprs: readonly ts.Expression[],
|
|
41
|
+
): GenerateStatus;
|
|
42
|
+
|
|
34
43
|
generateClassContents(routes: Record<string, ts.Expression>, routeData: Record<string, HttpRouteData>): string;
|
|
35
44
|
}
|