@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valkyrjaio/sindri",
|
|
3
|
-
"version": "26.
|
|
3
|
+
"version": "26.4.0",
|
|
4
4
|
"description": "The Valkyrja Sindri build tool.",
|
|
5
5
|
"homepage": "https://www.valkyrja.io/sindri",
|
|
6
6
|
"keywords": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"node": ">=22"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@valkyrjaio/valkyrja": "^26.
|
|
41
|
+
"@valkyrjaio/valkyrja": "^26.4.0",
|
|
42
42
|
"ts-morph": "^28.0.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
@@ -13,12 +13,12 @@ import * as path from 'path';
|
|
|
13
13
|
import {
|
|
14
14
|
ClassDeclaration,
|
|
15
15
|
Decorator,
|
|
16
|
+
Expression,
|
|
16
17
|
MethodDeclaration,
|
|
17
18
|
Node,
|
|
18
19
|
ParameterDeclaration,
|
|
19
20
|
Project,
|
|
20
21
|
SourceFile,
|
|
21
|
-
SyntaxKind,
|
|
22
22
|
ts,
|
|
23
23
|
} from 'ts-morph';
|
|
24
24
|
|
|
@@ -34,6 +34,20 @@ import { HandlerData } from '../Data/HandlerData.ts';
|
|
|
34
34
|
* All methods are protected so subclasses can override any step of the pipeline.
|
|
35
35
|
*/
|
|
36
36
|
export abstract class AstReader {
|
|
37
|
+
/**
|
|
38
|
+
* Module resolution settings for {@link resolveModuleSpecifierToFilePath}.
|
|
39
|
+
*
|
|
40
|
+
* NodeNext mirrors how the application itself resolves its imports, and
|
|
41
|
+
* `allowImportingTsExtensions` matches the framework convention of writing
|
|
42
|
+
* the `.ts` extension in every import specifier.
|
|
43
|
+
*/
|
|
44
|
+
protected static readonly MODULE_RESOLUTION_OPTIONS: ts.CompilerOptions = {
|
|
45
|
+
module: ts.ModuleKind.NodeNext,
|
|
46
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
47
|
+
allowImportingTsExtensions: true,
|
|
48
|
+
noEmit: true,
|
|
49
|
+
};
|
|
50
|
+
|
|
37
51
|
protected parseFileToSourceFile(filePath: string): SourceFile {
|
|
38
52
|
if (!fs.existsSync(filePath)) {
|
|
39
53
|
throw new AstFileReadException(`Cannot read file '${filePath}'.`);
|
|
@@ -98,14 +112,44 @@ export abstract class AstReader {
|
|
|
98
112
|
return '';
|
|
99
113
|
}
|
|
100
114
|
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
return this.resolveModuleSpecifierToFilePath(moduleSpecifier, currentFilePath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a module specifier to the absolute path of the TypeScript file it points at.
|
|
120
|
+
*
|
|
121
|
+
* Relative specifiers resolve against the importing file's directory. Bare
|
|
122
|
+
* package specifiers (`@valkyrjaio/valkyrja/Cli/Routing/Data/Route.ts`) go
|
|
123
|
+
* through the TypeScript compiler's NodeNext resolver, which honors the
|
|
124
|
+
* package's `exports` map — so a provider tree that reaches into an
|
|
125
|
+
* installed package (the framework's own component providers, reached from
|
|
126
|
+
* the app config) is walked exactly like application source.
|
|
127
|
+
*
|
|
128
|
+
* Returns '' when the specifier cannot be resolved to an existing `.ts`
|
|
129
|
+
* source file. A package shipping only compiled `.js` — or only `.d.ts`
|
|
130
|
+
* declarations, which carry no `getRoutes()` body to read — has nothing for
|
|
131
|
+
* the AST readers to walk.
|
|
132
|
+
*/
|
|
133
|
+
protected resolveModuleSpecifierToFilePath(moduleSpecifier: string, currentFilePath: string): string {
|
|
134
|
+
if (moduleSpecifier.startsWith('.')) {
|
|
135
|
+
const dir = path.dirname(currentFilePath);
|
|
136
|
+
const resolved = path.resolve(dir, moduleSpecifier.replace(/\.js$/, '.ts'));
|
|
137
|
+
|
|
138
|
+
return fs.existsSync(resolved) ? resolved : '';
|
|
103
139
|
}
|
|
104
140
|
|
|
105
|
-
const
|
|
106
|
-
|
|
141
|
+
const resolved = ts.resolveModuleName(
|
|
142
|
+
moduleSpecifier,
|
|
143
|
+
currentFilePath,
|
|
144
|
+
AstReader.MODULE_RESOLUTION_OPTIONS,
|
|
145
|
+
ts.sys,
|
|
146
|
+
).resolvedModule?.resolvedFileName;
|
|
107
147
|
|
|
108
|
-
|
|
148
|
+
if (resolved === undefined || !resolved.endsWith('.ts') || resolved.endsWith('.d.ts')) {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return resolved;
|
|
109
153
|
}
|
|
110
154
|
|
|
111
155
|
/**
|
|
@@ -141,6 +185,122 @@ export abstract class AstReader {
|
|
|
141
185
|
return classes;
|
|
142
186
|
}
|
|
143
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Resolve a `new X()` or bare `X` identifier element to the absolute file
|
|
190
|
+
* path of the class it references, using the current file's import map.
|
|
191
|
+
*
|
|
192
|
+
* Returns undefined when the element is neither form, or when the name is
|
|
193
|
+
* not imported via a relative specifier (e.g. framework classes imported
|
|
194
|
+
* from a package — those are intentionally skipped by the provider walk).
|
|
195
|
+
*/
|
|
196
|
+
protected resolveElementToFilePath(
|
|
197
|
+
element: ts.Expression,
|
|
198
|
+
useMap: Record<string, string>,
|
|
199
|
+
currentFilePath: string,
|
|
200
|
+
): string | undefined {
|
|
201
|
+
let name: string | undefined;
|
|
202
|
+
|
|
203
|
+
if (ts.isNewExpression(element)) {
|
|
204
|
+
name = ts.isIdentifier(element.expression) ? element.expression.text : undefined;
|
|
205
|
+
} else if (ts.isIdentifier(element)) {
|
|
206
|
+
name = element.text;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (name === undefined) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const resolved = this.resolveImportToFilePath(name, useMap, currentFilePath);
|
|
214
|
+
|
|
215
|
+
return resolved !== '' ? resolved : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extract the absolute file paths of the classes referenced by a method
|
|
220
|
+
* returning [new A(), new B(), ...] (or [A, B, ...]).
|
|
221
|
+
*
|
|
222
|
+
* Unlike {@link extractClassListFromValues} (which returns bare class
|
|
223
|
+
* names), this resolves each reference through the import map so provider
|
|
224
|
+
* classes are located unambiguously — critical when the same short class
|
|
225
|
+
* name exists in more than one component tree (e.g. an Http and a Cli
|
|
226
|
+
* `ComponentProvider`).
|
|
227
|
+
*/
|
|
228
|
+
protected extractClassPathListFromValues(
|
|
229
|
+
method: MethodDeclaration | undefined,
|
|
230
|
+
useMap: Record<string, string>,
|
|
231
|
+
currentFilePath: string,
|
|
232
|
+
): string[] {
|
|
233
|
+
if (method === undefined) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const array = this.findReturnedArray(method);
|
|
238
|
+
|
|
239
|
+
if (array === undefined) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return this.extractClassPathListFromArrayExpr(array, useMap, currentFilePath);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Extract the absolute file paths of the classes referenced by the elements
|
|
248
|
+
* of an array literal, using the current file's import map.
|
|
249
|
+
*/
|
|
250
|
+
protected extractClassPathListFromArrayExpr(
|
|
251
|
+
array: ts.ArrayLiteralExpression,
|
|
252
|
+
useMap: Record<string, string>,
|
|
253
|
+
currentFilePath: string,
|
|
254
|
+
): string[] {
|
|
255
|
+
const paths: string[] = [];
|
|
256
|
+
|
|
257
|
+
for (const element of array.elements) {
|
|
258
|
+
const resolved = this.resolveElementToFilePath(element, useMap, currentFilePath);
|
|
259
|
+
|
|
260
|
+
if (resolved !== undefined) {
|
|
261
|
+
paths.push(resolved);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return paths;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Collect every identifier an expression references as a value.
|
|
270
|
+
*
|
|
271
|
+
* Expressions taken from a provider's `getRoutes()` body are emitted into
|
|
272
|
+
* the generated data cache verbatim, so the cache must import everything
|
|
273
|
+
* they name — not just the provider class, but the parameter/enum/constant
|
|
274
|
+
* classes the route arguments reach for. Property *names* are skipped (the
|
|
275
|
+
* `HELP` of `CommandName.HELP` is not an import) as are type positions,
|
|
276
|
+
* which erase at runtime and must never become a value import.
|
|
277
|
+
*/
|
|
278
|
+
protected collectReferencedIdentifiers(node: ts.Node, names: Set<string> = new Set<string>()): Set<string> {
|
|
279
|
+
if (ts.isTypeNode(node)) {
|
|
280
|
+
return names;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (ts.isIdentifier(node)) {
|
|
284
|
+
names.add(node.text);
|
|
285
|
+
|
|
286
|
+
return names;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
290
|
+
return this.collectReferencedIdentifiers(node.expression, names);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (ts.isPropertyAssignment(node)) {
|
|
294
|
+
return this.collectReferencedIdentifiers(node.initializer, names);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
ts.forEachChild(node, (child: ts.Node): void => {
|
|
298
|
+
this.collectReferencedIdentifiers(child, names);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return names;
|
|
302
|
+
}
|
|
303
|
+
|
|
144
304
|
/**
|
|
145
305
|
* Extract service ID strings from keys of an object-returning method.
|
|
146
306
|
*
|
|
@@ -296,7 +456,7 @@ export abstract class AstReader {
|
|
|
296
456
|
}
|
|
297
457
|
|
|
298
458
|
if (ts.isIdentifier(node)) {
|
|
299
|
-
return this.
|
|
459
|
+
return this.resolveConstant(node.text, useMap, currentFilePath);
|
|
300
460
|
}
|
|
301
461
|
|
|
302
462
|
if (ts.isPropertyAccessExpression(node)) {
|
|
@@ -311,47 +471,108 @@ export abstract class AstReader {
|
|
|
311
471
|
}
|
|
312
472
|
|
|
313
473
|
/**
|
|
314
|
-
* Resolve a static property value by reading the
|
|
474
|
+
* Resolve a static property value by reading the file that declares its class.
|
|
315
475
|
*/
|
|
316
476
|
protected resolveStaticProperty(
|
|
317
477
|
className: string,
|
|
318
|
-
propName: string
|
|
478
|
+
propName: string,
|
|
319
479
|
useMap: Record<string, string>,
|
|
320
480
|
currentFilePath: string,
|
|
321
481
|
): string | undefined {
|
|
322
|
-
const
|
|
482
|
+
const declaringClass = this.parseDeclaringSourceFile(className, useMap, currentFilePath)?.getClass(className);
|
|
323
483
|
|
|
324
|
-
if (
|
|
484
|
+
if (declaringClass === undefined) {
|
|
325
485
|
return undefined;
|
|
326
486
|
}
|
|
327
487
|
|
|
328
|
-
|
|
329
|
-
const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true });
|
|
330
|
-
const importedSource = project.addSourceFileAtPath(importedFilePath);
|
|
331
|
-
const importedClass = importedSource.getClass(className);
|
|
488
|
+
const prop = declaringClass.getStaticProperty(propName);
|
|
332
489
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
490
|
+
if (prop === undefined || !Node.isPropertyDeclaration(prop)) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
336
493
|
|
|
337
|
-
|
|
494
|
+
return this.stringValueOf(prop.getInitializer());
|
|
495
|
+
}
|
|
338
496
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
497
|
+
/**
|
|
498
|
+
* Resolve a module-level `const` to its string value.
|
|
499
|
+
*
|
|
500
|
+
* Not every binding key is a class static — some are plain exported
|
|
501
|
+
* constants, e.g. `export const LoggerContractId = 'Valkyrja.Log.Logger.LoggerContract' as const;`
|
|
502
|
+
*/
|
|
503
|
+
protected resolveConstant(
|
|
504
|
+
name: string,
|
|
505
|
+
useMap: Record<string, string>,
|
|
506
|
+
currentFilePath: string,
|
|
507
|
+
): string | undefined {
|
|
508
|
+
const declaration = this.parseDeclaringSourceFile(name, useMap, currentFilePath)?.getVariableDeclaration(name);
|
|
342
509
|
|
|
343
|
-
|
|
344
|
-
|
|
510
|
+
return this.stringValueOf(declaration?.getInitializer());
|
|
511
|
+
}
|
|
345
512
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
513
|
+
/**
|
|
514
|
+
* Parse the file that declares a name: the module it is imported from, or
|
|
515
|
+
* the current file when the name is not imported.
|
|
516
|
+
*
|
|
517
|
+
* A provider commonly declares the binding keys for its own services
|
|
518
|
+
* alongside the publishers that use them, so `[Provider.SomeId]` names a
|
|
519
|
+
* constant that no import map can resolve — it lives in the file being read.
|
|
520
|
+
*
|
|
521
|
+
* Returns undefined when the file cannot be read.
|
|
522
|
+
*/
|
|
523
|
+
protected parseDeclaringSourceFile(
|
|
524
|
+
name: string,
|
|
525
|
+
useMap: Record<string, string>,
|
|
526
|
+
currentFilePath: string,
|
|
527
|
+
): SourceFile | undefined {
|
|
528
|
+
const importedFilePath = this.resolveImportToFilePath(name, useMap, currentFilePath);
|
|
529
|
+
const filePath = importedFilePath !== '' ? importedFilePath : currentFilePath;
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true });
|
|
533
|
+
|
|
534
|
+
return project.addSourceFileAtPath(filePath);
|
|
350
535
|
} catch {
|
|
351
536
|
// Silently skip unresolvable references
|
|
537
|
+
return undefined;
|
|
352
538
|
}
|
|
539
|
+
}
|
|
353
540
|
|
|
354
|
-
|
|
541
|
+
/**
|
|
542
|
+
* The string a declaration's initializer holds, once its type-only wrappers are stripped.
|
|
543
|
+
*/
|
|
544
|
+
protected stringValueOf(initializer: Expression | undefined): string | undefined {
|
|
545
|
+
if (initializer === undefined) {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const value = this.unwrapTypeAssertions(initializer.compilerNode);
|
|
550
|
+
|
|
551
|
+
return ts.isStringLiteral(value) ? value.text : undefined;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Strip the type-only wrappers an expression may be dressed in — `as`,
|
|
556
|
+
* `satisfies` and parentheses — to reach the value node underneath.
|
|
557
|
+
*
|
|
558
|
+
* Binding-key constants are declared `static readonly Data = 'Some.Id' as
|
|
559
|
+
* const;`, so the initializer node is an `AsExpression`, not the
|
|
560
|
+
* `StringLiteral` holding the value. Without unwrapping, every such key
|
|
561
|
+
* resolves to nothing and the publishers it names are dropped from the
|
|
562
|
+
* generated cache.
|
|
563
|
+
*/
|
|
564
|
+
protected unwrapTypeAssertions(node: ts.Expression): ts.Expression {
|
|
565
|
+
let current = node;
|
|
566
|
+
|
|
567
|
+
while (
|
|
568
|
+
ts.isAsExpression(current) ||
|
|
569
|
+
ts.isSatisfiesExpression(current) ||
|
|
570
|
+
ts.isParenthesizedExpression(current)
|
|
571
|
+
) {
|
|
572
|
+
current = current.expression;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return current;
|
|
355
576
|
}
|
|
356
577
|
|
|
357
578
|
/**
|
|
@@ -30,12 +30,16 @@ export class ComponentProviderReader extends AstReader implements ComponentProvi
|
|
|
30
30
|
const useMap = this.buildUseMap(sourceFile);
|
|
31
31
|
const methods = this.indexMethods(classDecl);
|
|
32
32
|
|
|
33
|
+
// Provider references are resolved through the file's import map to
|
|
34
|
+
// absolute paths (not bare names) so the walk locates each provider
|
|
35
|
+
// unambiguously — the same short name (e.g. `ComponentProvider`,
|
|
36
|
+
// `DataServiceProvider`) exists in both the Http and Cli trees.
|
|
33
37
|
return new ComponentProviderResult(
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
37
|
-
this.
|
|
38
|
-
this.
|
|
38
|
+
this.extractClassPathListFromValues(methods[ComponentProviderReader.METHOD_COMPONENT], useMap, filePath),
|
|
39
|
+
this.extractClassPathListFromValues(methods[ComponentProviderReader.METHOD_CONTAINER], useMap, filePath),
|
|
40
|
+
this.extractClassPathListFromValues(methods[ComponentProviderReader.METHOD_EVENT], useMap, filePath),
|
|
41
|
+
this.extractClassPathListFromValues(methods[ComponentProviderReader.METHOD_CLI], useMap, filePath),
|
|
42
|
+
this.extractClassPathListFromValues(methods[ComponentProviderReader.METHOD_HTTP], useMap, filePath),
|
|
39
43
|
);
|
|
40
44
|
}
|
|
41
45
|
}
|
|
@@ -190,21 +190,29 @@ export class ConfigReader extends AstReader implements ConfigReaderContract {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
* Extract
|
|
194
|
-
*
|
|
193
|
+
* Extract the absolute file paths of the configured provider classes.
|
|
194
|
+
*
|
|
195
|
+
* The providers array is the first array-literal argument after the
|
|
196
|
+
* `dataNamespace` (index 8) — every preceding argument is a scalar. Its
|
|
197
|
+
* exact position varies by config type (index 9 in `HttpConfig`, index 11
|
|
198
|
+
* in `CliConfig` after `applicationName`/`defaultCommandName`), so the
|
|
199
|
+
* array is located by scanning rather than by a fixed index.
|
|
200
|
+
*
|
|
201
|
+
* Each element (`new ComponentProvider()` or a bare `ComponentProvider`
|
|
202
|
+
* identifier) is resolved through the config file's import map to an
|
|
203
|
+
* absolute path, so the walk locates the right provider even when the same
|
|
204
|
+
* short name exists in multiple trees.
|
|
195
205
|
*/
|
|
196
206
|
protected extractProvidersFromArgs(
|
|
197
207
|
args: ts.NodeArray<ts.Expression>,
|
|
198
208
|
useMap: Record<string, string>,
|
|
199
209
|
filePath: string,
|
|
200
210
|
): string[] {
|
|
201
|
-
|
|
202
|
-
// It's typically at index 11 in CliConfig
|
|
203
|
-
for (let i = 10; i < args.length; i++) {
|
|
211
|
+
for (let i = 9; i < args.length; i++) {
|
|
204
212
|
const arg = args[i];
|
|
205
213
|
|
|
206
214
|
if (arg !== undefined && ts.isArrayLiteralExpression(arg)) {
|
|
207
|
-
return this.
|
|
215
|
+
return this.extractClassPathListFromArrayExpr(arg, useMap, filePath);
|
|
208
216
|
}
|
|
209
217
|
}
|
|
210
218
|
|
|
@@ -10,15 +10,22 @@
|
|
|
10
10
|
import type { ts } from 'ts-morph';
|
|
11
11
|
|
|
12
12
|
export class RouteProviderResult {
|
|
13
|
+
/**
|
|
14
|
+
* @param routeImports Local name → absolute file path for every class the
|
|
15
|
+
* imperative `routes` expressions reference, so the
|
|
16
|
+
* generated data cache can import them.
|
|
17
|
+
*/
|
|
13
18
|
constructor(
|
|
14
19
|
readonly controllerClasses: readonly string[] = [],
|
|
15
20
|
readonly routes: readonly ts.Expression[] = [],
|
|
21
|
+
readonly routeImports: Readonly<Record<string, string>> = {},
|
|
16
22
|
) {}
|
|
17
23
|
|
|
18
24
|
merge(other: RouteProviderResult): RouteProviderResult {
|
|
19
25
|
return new RouteProviderResult(
|
|
20
26
|
[...new Set([...this.controllerClasses, ...other.controllerClasses])],
|
|
21
27
|
[...this.routes, ...other.routes],
|
|
28
|
+
{ ...this.routeImports, ...other.routeImports },
|
|
22
29
|
);
|
|
23
30
|
}
|
|
24
31
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* file that was distributed with this source code.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import { ts } from 'ts-morph';
|
|
11
11
|
|
|
12
12
|
import { AstReader } from './Abstract/AstReader.ts';
|
|
13
13
|
import { RouteProviderResult } from './Data/Result/RouteProviderResult.ts';
|
|
@@ -28,18 +28,69 @@ export class RouteProviderReader extends AstReader implements RouteProviderReade
|
|
|
28
28
|
|
|
29
29
|
const useMap = this.buildUseMap(sourceFile);
|
|
30
30
|
const methods = this.indexMethods(classDecl);
|
|
31
|
+
const routes = this.extractRoutes(methods[RouteProviderReader.METHOD_ROUTES], useMap, filePath);
|
|
31
32
|
|
|
32
33
|
return new RouteProviderResult(
|
|
33
34
|
this.extractClassListFromValues(methods[RouteProviderReader.METHOD_CONTROLLER_CLASSES], useMap, filePath),
|
|
34
|
-
|
|
35
|
+
routes,
|
|
36
|
+
this.extractRouteImports(routes, useMap, filePath),
|
|
35
37
|
);
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Resolve every class the imperative route expressions reference to its
|
|
42
|
+
* absolute file path, so the generated data cache can import them.
|
|
43
|
+
*
|
|
44
|
+
* Names that are not imported by the provider file — locals, globals, and
|
|
45
|
+
* the provider's own class (which the generator imports separately) — have
|
|
46
|
+
* no entry in the use map and are skipped.
|
|
47
|
+
*/
|
|
48
|
+
protected extractRouteImports(
|
|
49
|
+
routes: readonly ts.Expression[],
|
|
50
|
+
useMap: Record<string, string>,
|
|
51
|
+
filePath: string,
|
|
52
|
+
): Record<string, string> {
|
|
53
|
+
const imports: Record<string, string> = {};
|
|
54
|
+
|
|
55
|
+
for (const route of routes) {
|
|
56
|
+
for (const name of this.collectReferencedIdentifiers(route)) {
|
|
57
|
+
const resolved = this.resolveImportToFilePath(name, useMap, filePath);
|
|
58
|
+
|
|
59
|
+
if (resolved !== '') {
|
|
60
|
+
imports[name] = resolved;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return imports;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract the route objects returned by a provider's `getRoutes()` method.
|
|
70
|
+
*
|
|
71
|
+
* TS route providers register routes imperatively — `getRoutes()` returns
|
|
72
|
+
* the concrete `new Route(...)` / `new DynamicRoute(...)` (or CLI
|
|
73
|
+
* `new Route(...)`) instances. Those expressions are returned verbatim for
|
|
74
|
+
* the data-cache generator to emit as route closures and to derive the
|
|
75
|
+
* path/regex lookup maps from. (Attribute/decorator scanning — the other,
|
|
76
|
+
* optional source of routes — is handled separately via
|
|
77
|
+
* {@link readFile}'s controller-class list.)
|
|
78
|
+
*/
|
|
38
79
|
protected extractRoutes(
|
|
39
|
-
|
|
80
|
+
method: ReturnType<typeof this.indexMethods>[string] | undefined,
|
|
40
81
|
_useMap: Record<string, string>,
|
|
41
82
|
_filePath: string,
|
|
42
83
|
): ts.Expression[] {
|
|
43
|
-
|
|
84
|
+
if (method === undefined) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const array = this.findReturnedArray(method);
|
|
89
|
+
|
|
90
|
+
if (array === undefined) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return array.elements.filter((element): element is ts.NewExpression => ts.isNewExpression(element));
|
|
44
95
|
}
|
|
45
96
|
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export class SindriInfo {
|
|
11
|
-
static readonly VERSION = '26.
|
|
12
|
-
static readonly VERSION_BUILD_DATE_TIME = 'July
|
|
11
|
+
static readonly VERSION = '26.4.0' as const;
|
|
12
|
+
static readonly VERSION_BUILD_DATE_TIME = 'July 27 2026 22:16:03 MST' as const;
|
|
13
13
|
static readonly ICON = `\
|
|
14
14
|
██████████
|
|
15
15
|
████ ████
|