@valkyrjaio/sindri 26.2.0 → 26.3.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 +395 -129
- package/package.json +2 -2
- package/src/Sindri/Ast/Abstract/AstReader.ts +80 -0
- package/src/Sindri/Ast/ComponentProviderReader.ts +9 -5
- package/src/Sindri/Ast/ConfigReader.ts +14 -6
- package/src/Sindri/Ast/RouteProviderReader.ts +24 -3
- package/src/Sindri/Constant/SindriInfo.ts +2 -2
- package/src/Sindri/Generate/Abstract/GenerateDataFromAst.ts +66 -13
- package/src/Sindri/Generator/Abstract/AstFileGenerator.ts +13 -0
- package/src/Sindri/Generator/Ast/Cli/AstCliDataFileGenerator.ts +47 -11
- package/src/Sindri/Generator/Ast/Container/AstContainerDataFileGenerator.ts +4 -9
- package/src/Sindri/Generator/Ast/Event/AstEventDataFileGenerator.ts +13 -7
- package/src/Sindri/Generator/Ast/Http/AstHttpDataFileGenerator.ts +176 -12
- 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.3.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.3.0",
|
|
42
42
|
"ts-morph": "^28.0.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
@@ -141,6 +141,86 @@ export abstract class AstReader {
|
|
|
141
141
|
return classes;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Resolve a `new X()` or bare `X` identifier element to the absolute file
|
|
146
|
+
* path of the class it references, using the current file's import map.
|
|
147
|
+
*
|
|
148
|
+
* Returns undefined when the element is neither form, or when the name is
|
|
149
|
+
* not imported via a relative specifier (e.g. framework classes imported
|
|
150
|
+
* from a package — those are intentionally skipped by the provider walk).
|
|
151
|
+
*/
|
|
152
|
+
protected resolveElementToFilePath(
|
|
153
|
+
element: ts.Expression,
|
|
154
|
+
useMap: Record<string, string>,
|
|
155
|
+
currentFilePath: string,
|
|
156
|
+
): string | undefined {
|
|
157
|
+
let name: string | undefined;
|
|
158
|
+
|
|
159
|
+
if (ts.isNewExpression(element)) {
|
|
160
|
+
name = ts.isIdentifier(element.expression) ? element.expression.text : undefined;
|
|
161
|
+
} else if (ts.isIdentifier(element)) {
|
|
162
|
+
name = element.text;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (name === undefined) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const resolved = this.resolveImportToFilePath(name, useMap, currentFilePath);
|
|
170
|
+
|
|
171
|
+
return resolved !== '' ? resolved : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Extract the absolute file paths of the classes referenced by a method
|
|
176
|
+
* returning [new A(), new B(), ...] (or [A, B, ...]).
|
|
177
|
+
*
|
|
178
|
+
* Unlike {@link extractClassListFromValues} (which returns bare class
|
|
179
|
+
* names), this resolves each reference through the import map so provider
|
|
180
|
+
* classes are located unambiguously — critical when the same short class
|
|
181
|
+
* name exists in more than one component tree (e.g. an Http and a Cli
|
|
182
|
+
* `ComponentProvider`).
|
|
183
|
+
*/
|
|
184
|
+
protected extractClassPathListFromValues(
|
|
185
|
+
method: MethodDeclaration | undefined,
|
|
186
|
+
useMap: Record<string, string>,
|
|
187
|
+
currentFilePath: string,
|
|
188
|
+
): string[] {
|
|
189
|
+
if (method === undefined) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const array = this.findReturnedArray(method);
|
|
194
|
+
|
|
195
|
+
if (array === undefined) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return this.extractClassPathListFromArrayExpr(array, useMap, currentFilePath);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extract the absolute file paths of the classes referenced by the elements
|
|
204
|
+
* of an array literal, using the current file's import map.
|
|
205
|
+
*/
|
|
206
|
+
protected extractClassPathListFromArrayExpr(
|
|
207
|
+
array: ts.ArrayLiteralExpression,
|
|
208
|
+
useMap: Record<string, string>,
|
|
209
|
+
currentFilePath: string,
|
|
210
|
+
): string[] {
|
|
211
|
+
const paths: string[] = [];
|
|
212
|
+
|
|
213
|
+
for (const element of array.elements) {
|
|
214
|
+
const resolved = this.resolveElementToFilePath(element, useMap, currentFilePath);
|
|
215
|
+
|
|
216
|
+
if (resolved !== undefined) {
|
|
217
|
+
paths.push(resolved);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return paths;
|
|
222
|
+
}
|
|
223
|
+
|
|
144
224
|
/**
|
|
145
225
|
* Extract service ID strings from keys of an object-returning method.
|
|
146
226
|
*
|
|
@@ -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
|
|
|
@@ -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';
|
|
@@ -35,11 +35,32 @@ export class RouteProviderReader extends AstReader implements RouteProviderReade
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Extract the route objects returned by a provider's `getRoutes()` method.
|
|
40
|
+
*
|
|
41
|
+
* TS route providers register routes imperatively — `getRoutes()` returns
|
|
42
|
+
* the concrete `new Route(...)` / `new DynamicRoute(...)` (or CLI
|
|
43
|
+
* `new Route(...)`) instances. Those expressions are returned verbatim for
|
|
44
|
+
* the data-cache generator to emit as route closures and to derive the
|
|
45
|
+
* path/regex lookup maps from. (Attribute/decorator scanning — the other,
|
|
46
|
+
* optional source of routes — is handled separately via
|
|
47
|
+
* {@link readFile}'s controller-class list.)
|
|
48
|
+
*/
|
|
38
49
|
protected extractRoutes(
|
|
39
|
-
|
|
50
|
+
method: ReturnType<typeof this.indexMethods>[string] | undefined,
|
|
40
51
|
_useMap: Record<string, string>,
|
|
41
52
|
_filePath: string,
|
|
42
53
|
): ts.Expression[] {
|
|
43
|
-
|
|
54
|
+
if (method === undefined) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const array = this.findReturnedArray(method);
|
|
59
|
+
|
|
60
|
+
if (array === undefined) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return array.elements.filter((element): element is ts.NewExpression => ts.isNewExpression(element));
|
|
44
65
|
}
|
|
45
66
|
}
|
|
@@ -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 25 2026
|
|
11
|
+
static readonly VERSION = '26.3.0' as const;
|
|
12
|
+
static readonly VERSION_BUILD_DATE_TIME = 'July 25 2026 21:11:03 MST' as const;
|
|
13
13
|
static readonly ICON = `\
|
|
14
14
|
██████████
|
|
15
15
|
████ ████
|
|
@@ -211,6 +211,17 @@ export abstract class GenerateDataFromAst {
|
|
|
211
211
|
return '';
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Build the `import { X } from '...'` specifier for a class file, relative
|
|
216
|
+
* to the generated data file's output directory. Used to import the
|
|
217
|
+
* provider/handler classes referenced by the generated data caches.
|
|
218
|
+
*/
|
|
219
|
+
protected relativeSpecifier(fromDir: string, toFile: string): string {
|
|
220
|
+
const relative = path.relative(fromDir, toFile).split(path.sep).join('/');
|
|
221
|
+
|
|
222
|
+
return relative.startsWith('.') ? relative : `./${relative}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
214
225
|
protected generateContainerData(
|
|
215
226
|
serviceProviders: readonly string[],
|
|
216
227
|
config: ConfigResult,
|
|
@@ -221,6 +232,7 @@ export abstract class GenerateDataFromAst {
|
|
|
221
232
|
.writeMessages();
|
|
222
233
|
|
|
223
234
|
const publishers: Record<string, readonly [string, string]> = {};
|
|
235
|
+
const importMap: Record<string, string> = {};
|
|
224
236
|
|
|
225
237
|
for (const providerClass of serviceProviders) {
|
|
226
238
|
const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
|
|
@@ -231,9 +243,17 @@ export abstract class GenerateDataFromAst {
|
|
|
231
243
|
|
|
232
244
|
const result = this.serviceProviderReader.readFile(filePath);
|
|
233
245
|
|
|
246
|
+
if (Object.keys(result.publishers).length > 0) {
|
|
247
|
+
// The publisher callbacks reference the provider's static methods,
|
|
248
|
+
// so the generated data cache must import the provider class.
|
|
249
|
+
importMap[path.basename(filePath, '.ts')] = this.relativeSpecifier(config.dataPath, filePath);
|
|
250
|
+
}
|
|
251
|
+
|
|
234
252
|
Object.assign(publishers, result.publishers);
|
|
235
253
|
}
|
|
236
254
|
|
|
255
|
+
this.containerGenerator.classImportMap = importMap;
|
|
256
|
+
|
|
237
257
|
const status = this.containerGenerator.generateFile(
|
|
238
258
|
config.dataPath,
|
|
239
259
|
'AppContainerData',
|
|
@@ -297,6 +317,8 @@ export abstract class GenerateDataFromAst {
|
|
|
297
317
|
.writeMessages();
|
|
298
318
|
|
|
299
319
|
const allRoutes: Record<string, ts.Expression> = {};
|
|
320
|
+
const imperativeRoutes: ts.Expression[] = [];
|
|
321
|
+
const importMap: Record<string, string> = {};
|
|
300
322
|
|
|
301
323
|
for (const providerClass of cliRouteProviders) {
|
|
302
324
|
const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
|
|
@@ -307,6 +329,13 @@ export abstract class GenerateDataFromAst {
|
|
|
307
329
|
|
|
308
330
|
const providerResult = this.routeProviderReader.readFile(filePath);
|
|
309
331
|
|
|
332
|
+
if (providerResult.routes.length > 0) {
|
|
333
|
+
// Imperative routes reference the provider's static handlers,
|
|
334
|
+
// so the generated data cache must import the provider class.
|
|
335
|
+
imperativeRoutes.push(...providerResult.routes);
|
|
336
|
+
importMap[path.basename(filePath, '.ts')] = this.relativeSpecifier(config.dataPath, filePath);
|
|
337
|
+
}
|
|
338
|
+
|
|
310
339
|
for (const controllerClass of providerResult.controllerClasses) {
|
|
311
340
|
const controllerPath = this.fqnToFilePath(controllerClass, config.namespace, config.dir);
|
|
312
341
|
|
|
@@ -320,12 +349,17 @@ export abstract class GenerateDataFromAst {
|
|
|
320
349
|
}
|
|
321
350
|
}
|
|
322
351
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
352
|
+
this.cliGenerator.classImportMap = importMap;
|
|
353
|
+
|
|
354
|
+
const status =
|
|
355
|
+
imperativeRoutes.length > 0
|
|
356
|
+
? this.cliGenerator.generateFileFromRoutes(
|
|
357
|
+
config.dataPath,
|
|
358
|
+
'AppCliRoutingData',
|
|
359
|
+
config.dataNamespace,
|
|
360
|
+
imperativeRoutes,
|
|
361
|
+
)
|
|
362
|
+
: this.cliGenerator.generateFile(config.dataPath, 'AppCliRoutingData', config.dataNamespace, allRoutes);
|
|
329
363
|
|
|
330
364
|
return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
|
|
331
365
|
}
|
|
@@ -341,6 +375,8 @@ export abstract class GenerateDataFromAst {
|
|
|
341
375
|
|
|
342
376
|
const allRoutes: Record<string, ts.Expression> = {};
|
|
343
377
|
const allRouteData: Record<string, HttpRouteData> = {};
|
|
378
|
+
const imperativeRoutes: ts.Expression[] = [];
|
|
379
|
+
const importMap: Record<string, string> = {};
|
|
344
380
|
|
|
345
381
|
for (const providerClass of httpRouteProviders) {
|
|
346
382
|
const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
|
|
@@ -351,6 +387,13 @@ export abstract class GenerateDataFromAst {
|
|
|
351
387
|
|
|
352
388
|
const providerResult = this.routeProviderReader.readFile(filePath);
|
|
353
389
|
|
|
390
|
+
if (providerResult.routes.length > 0) {
|
|
391
|
+
// Imperative routes reference the provider's static handlers,
|
|
392
|
+
// so the generated data cache must import the provider class.
|
|
393
|
+
imperativeRoutes.push(...providerResult.routes);
|
|
394
|
+
importMap[path.basename(filePath, '.ts')] = this.relativeSpecifier(config.dataPath, filePath);
|
|
395
|
+
}
|
|
396
|
+
|
|
354
397
|
for (const controllerClass of providerResult.controllerClasses) {
|
|
355
398
|
const controllerPath = this.fqnToFilePath(controllerClass, config.namespace, config.dir);
|
|
356
399
|
|
|
@@ -365,13 +408,23 @@ export abstract class GenerateDataFromAst {
|
|
|
365
408
|
}
|
|
366
409
|
}
|
|
367
410
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
411
|
+
this.httpGenerator.classImportMap = importMap;
|
|
412
|
+
|
|
413
|
+
const status =
|
|
414
|
+
imperativeRoutes.length > 0
|
|
415
|
+
? this.httpGenerator.generateFileFromRoutes(
|
|
416
|
+
config.dataPath,
|
|
417
|
+
'AppHttpRoutingData',
|
|
418
|
+
config.dataNamespace,
|
|
419
|
+
imperativeRoutes,
|
|
420
|
+
)
|
|
421
|
+
: this.httpGenerator.generateFile(
|
|
422
|
+
config.dataPath,
|
|
423
|
+
'AppHttpRoutingData',
|
|
424
|
+
config.dataNamespace,
|
|
425
|
+
allRoutes,
|
|
426
|
+
allRouteData,
|
|
427
|
+
);
|
|
375
428
|
|
|
376
429
|
return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
|
|
377
430
|
}
|
|
@@ -31,6 +31,19 @@ export abstract class AstFileGenerator {
|
|
|
31
31
|
return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(className), caseName);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Build the `import { X } from '...';` block for the provider/handler classes
|
|
36
|
+
* a generated data cache references, followed by a trailing newline (or an
|
|
37
|
+
* empty string when there are no such imports).
|
|
38
|
+
*/
|
|
39
|
+
protected buildUserImportsBlock(classImportMap: Record<string, string>): string {
|
|
40
|
+
const userImports = Object.entries(classImportMap)
|
|
41
|
+
.map(([name, specifier]) => `import { ${name} } from '${specifier}';`)
|
|
42
|
+
.join('\n');
|
|
43
|
+
|
|
44
|
+
return userImports ? `${userImports}\n` : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
protected writeFile(directory: string, className: string, data: string): GenerateStatus {
|
|
35
48
|
const filePath = directory.replace(/\/$/, '') + `/${className}.ts`;
|
|
36
49
|
|
|
@@ -32,11 +32,7 @@ export class AstCliDataFileGenerator extends AstFileGenerator implements CliData
|
|
|
32
32
|
namespace: string,
|
|
33
33
|
routes: Record<string, ts.Expression>,
|
|
34
34
|
): GenerateStatus {
|
|
35
|
-
const
|
|
36
|
-
.map(([name, specifier]) => `import { ${name} } from '${specifier}';`)
|
|
37
|
-
.join('\n');
|
|
38
|
-
|
|
39
|
-
const userImportsBlock = userImports ? `${userImports}\n` : '';
|
|
35
|
+
const userImportsBlock = this.buildUserImportsBlock(this.classImportMap);
|
|
40
36
|
|
|
41
37
|
const contents = this.generateClassContents(routes);
|
|
42
38
|
|
|
@@ -64,6 +60,36 @@ export class AstCliDataFileGenerator extends AstFileGenerator implements CliData
|
|
|
64
60
|
return this.writeFile(directory, className, data);
|
|
65
61
|
}
|
|
66
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Generate the CLI routing data file from imperative `getRoutes()` route
|
|
65
|
+
* objects — each `new Route(name, description, handler, ...)` is keyed by
|
|
66
|
+
* its command name (the first constructor argument) and emitted verbatim.
|
|
67
|
+
*/
|
|
68
|
+
public generateFileFromRoutes(
|
|
69
|
+
directory: string,
|
|
70
|
+
className: string,
|
|
71
|
+
namespace: string,
|
|
72
|
+
routeExprs: readonly ts.Expression[],
|
|
73
|
+
): GenerateStatus {
|
|
74
|
+
const routes: Record<string, ts.Expression> = {};
|
|
75
|
+
|
|
76
|
+
for (const expr of routeExprs) {
|
|
77
|
+
if (!ts.isNewExpression(expr)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const nameArg = (expr.arguments ?? [])[0];
|
|
82
|
+
|
|
83
|
+
if (nameArg === undefined || !ts.isStringLiteral(nameArg)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
routes[nameArg.text] = expr;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return this.generateFile(directory, className, namespace, routes);
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
public generateClassContents(routes: Record<string, ts.Expression>): string {
|
|
68
94
|
const entries = Object.entries(routes);
|
|
69
95
|
|
|
@@ -74,15 +100,25 @@ export class AstCliDataFileGenerator extends AstFileGenerator implements CliData
|
|
|
74
100
|
const lines: string[] = [];
|
|
75
101
|
|
|
76
102
|
for (const [key, valueExpr] of entries) {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const formattedKey = key.includes('::') ? `[${printedKey}]` : `['${printedKey}']`;
|
|
103
|
+
const printedValue = this.printExpr(valueExpr);
|
|
104
|
+
const formattedKey = key.includes('::')
|
|
105
|
+
? `[${this.printer.printNode(ts.EmitHint.Unspecified, this.buildEnumCaseExpr(key), this.dummySourceFile)}]`
|
|
106
|
+
: `['${key}']`;
|
|
82
107
|
|
|
83
108
|
lines.push(` ${formattedKey}: (): RouteContract => ${printedValue},`);
|
|
84
109
|
}
|
|
85
110
|
|
|
86
|
-
return ['super({',
|
|
111
|
+
return ['super({', ...lines, ' });'].join('\n ');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Print an expression to source. Parsed nodes (from a `getRoutes()` body)
|
|
116
|
+
* are emitted via their original source text; synthesized nodes (which
|
|
117
|
+
* carry no source positions) are emitted through the printer.
|
|
118
|
+
*/
|
|
119
|
+
protected printExpr(expr: ts.Expression): string {
|
|
120
|
+
return expr.pos >= 0
|
|
121
|
+
? expr.getText()
|
|
122
|
+
: this.printer.printNode(ts.EmitHint.Unspecified, expr, this.dummySourceFile);
|
|
87
123
|
}
|
|
88
124
|
}
|
|
@@ -32,11 +32,7 @@ export class AstContainerDataFileGenerator extends AstFileGenerator implements C
|
|
|
32
32
|
namespace: string,
|
|
33
33
|
publishers: Readonly<Record<string, readonly [string, string]>>,
|
|
34
34
|
): GenerateStatus {
|
|
35
|
-
const
|
|
36
|
-
.map(([name, specifier]) => `import { ${name} } from '${specifier}';`)
|
|
37
|
-
.join('\n');
|
|
38
|
-
|
|
39
|
-
const userImportsBlock = userImports ? `${userImports}\n` : '';
|
|
35
|
+
const userImportsBlock = this.buildUserImportsBlock(this.classImportMap);
|
|
40
36
|
|
|
41
37
|
const contents = this.generateClassContents(publishers);
|
|
42
38
|
|
|
@@ -67,12 +63,11 @@ export class AstContainerDataFileGenerator extends AstFileGenerator implements C
|
|
|
67
63
|
const lines: string[] = [];
|
|
68
64
|
|
|
69
65
|
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
66
|
const shortProviderClass = providerClass.slice(providerClass.lastIndexOf('\\') + 1);
|
|
74
67
|
|
|
75
|
-
const formattedKey = serviceId.includes('::')
|
|
68
|
+
const formattedKey = serviceId.includes('::')
|
|
69
|
+
? `[${this.printer.printNode(ts.EmitHint.Unspecified, this.buildEnumCaseExpr(serviceId), this.dummySourceFile)}]`
|
|
70
|
+
: `['${serviceId}']`;
|
|
76
71
|
|
|
77
72
|
lines.push(
|
|
78
73
|
` ${formattedKey}: (container: ContainerContract) => ${shortProviderClass}.${methodName}(container),`,
|
|
@@ -32,20 +32,26 @@ export class AstEventDataFileGenerator extends AstFileGenerator implements Event
|
|
|
32
32
|
namespace: string,
|
|
33
33
|
listeners: Record<string, ts.Expression>,
|
|
34
34
|
): GenerateStatus {
|
|
35
|
-
const
|
|
36
|
-
.map(([name, specifier]) => `import { ${name} } from '${specifier}';`)
|
|
37
|
-
.join('\n');
|
|
38
|
-
|
|
39
|
-
const userImportsBlock = userImports ? `${userImports}\n` : '';
|
|
35
|
+
const userImportsBlock = this.buildUserImportsBlock(this.classImportMap);
|
|
40
36
|
|
|
41
37
|
const contents = this.generateClassContents(listeners);
|
|
42
38
|
|
|
39
|
+
// The Listener / ListenerContract imports are only referenced by the
|
|
40
|
+
// generated listener closures — omit them when there are no listeners
|
|
41
|
+
// so empty event data does not carry unused (and unresolved) imports.
|
|
42
|
+
const listenerImports =
|
|
43
|
+
Object.keys(listeners).length > 0
|
|
44
|
+
? [
|
|
45
|
+
`import { Listener } from '@valkyrjaio/valkyrja/Event/Data/Listener.ts';`,
|
|
46
|
+
`import type { ListenerContract } from '@valkyrjaio/valkyrja/Event/Data/Contract/ListenerContract.ts';`,
|
|
47
|
+
]
|
|
48
|
+
: [];
|
|
49
|
+
|
|
43
50
|
const data = [
|
|
44
51
|
'// This file was automatically generated by Sindri.',
|
|
45
52
|
'',
|
|
46
53
|
`import { EventData } from '@valkyrjaio/valkyrja/Event/Data/EventData.ts';`,
|
|
47
|
-
|
|
48
|
-
`import type { ListenerContract } from '@valkyrjaio/valkyrja/Event/Data/Contract/ListenerContract.ts';`,
|
|
54
|
+
...listenerImports,
|
|
49
55
|
userImportsBlock,
|
|
50
56
|
`export class ${className} extends EventData {`,
|
|
51
57
|
` constructor() {`,
|