@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
|
@@ -211,6 +211,157 @@ 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
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build the specifier a generated data file should import a class file by.
|
|
227
|
+
*
|
|
228
|
+
* Application source is imported by a path relative to the data directory;
|
|
229
|
+
* a file belonging to an installed package is imported by that package's
|
|
230
|
+
* own public specifier — so the framework classes the cache references are
|
|
231
|
+
* written exactly as hand-written application code writes them
|
|
232
|
+
* (`@valkyrjaio/valkyrja/Cli/Routing/Data/Route.ts`), never as a brittle
|
|
233
|
+
* reach into `node_modules`.
|
|
234
|
+
*/
|
|
235
|
+
protected importSpecifier(fromDir: string, toFile: string): string {
|
|
236
|
+
return this.packageSpecifier(toFile) ?? this.relativeSpecifier(fromDir, toFile);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Map an absolute path inside an installed package back to the specifier
|
|
241
|
+
* that package publicly exposes it as, or undefined when the file is not
|
|
242
|
+
* part of one.
|
|
243
|
+
*
|
|
244
|
+
* The package's `exports` map is inverted: for `"./*.ts": "./src/*.ts"` a
|
|
245
|
+
* file at `<package>/src/Foo.ts` maps back to `<name>/Foo.ts`. Packages
|
|
246
|
+
* with no matching pattern fall back to the path as written under the
|
|
247
|
+
* package root, which is what a package without an `exports` map exposes.
|
|
248
|
+
*/
|
|
249
|
+
protected packageSpecifier(filePath: string): string | undefined {
|
|
250
|
+
const normalized = filePath.split(path.sep).join('/');
|
|
251
|
+
const marker = normalized.lastIndexOf('/node_modules/');
|
|
252
|
+
|
|
253
|
+
if (marker === -1) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const subject = normalized.substring(marker + '/node_modules/'.length);
|
|
258
|
+
const segments = subject.split('/');
|
|
259
|
+
const nameLength = subject.startsWith('@') ? 2 : 1;
|
|
260
|
+
|
|
261
|
+
if (segments.length <= nameLength) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const packageName = segments.slice(0, nameLength).join('/');
|
|
266
|
+
const packageDir = normalized.substring(0, marker + '/node_modules/'.length) + packageName;
|
|
267
|
+
const relative = segments.slice(nameLength).join('/');
|
|
268
|
+
|
|
269
|
+
return `${packageName}/${this.exportedSubpath(packageDir, relative) ?? relative}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Invert a package's `exports` patterns to find the subpath a file inside
|
|
274
|
+
* it is exposed as, or undefined when no pattern matches it.
|
|
275
|
+
*/
|
|
276
|
+
protected exportedSubpath(packageDir: string, relative: string): string | undefined {
|
|
277
|
+
for (const [key, target] of Object.entries(this.readPackageExports(packageDir))) {
|
|
278
|
+
const star = key.indexOf('*');
|
|
279
|
+
const targetStar = target.indexOf('*');
|
|
280
|
+
|
|
281
|
+
if (star === -1 || targetStar === -1) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const prefix = target.substring(0, targetStar).replace(/^\.\//, '');
|
|
286
|
+
const suffix = target.substring(targetStar + 1);
|
|
287
|
+
|
|
288
|
+
if (!relative.startsWith(prefix) || !relative.endsWith(suffix)) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const value = relative.substring(prefix.length, relative.length - suffix.length);
|
|
293
|
+
|
|
294
|
+
return (key.substring(0, star) + value + key.substring(star + 1)).replace(/^\.\//, '');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Read a package's `exports` map, flattened to subpath → file target.
|
|
302
|
+
*
|
|
303
|
+
* Conditional targets are collapsed to their first string value; every
|
|
304
|
+
* condition a package declares for a subpath points at the same source
|
|
305
|
+
* file in the layouts sindri generates for.
|
|
306
|
+
*/
|
|
307
|
+
protected readPackageExports(packageDir: string): Record<string, string> {
|
|
308
|
+
const manifestPath = path.join(packageDir, 'package.json');
|
|
309
|
+
|
|
310
|
+
if (!fs.existsSync(manifestPath)) {
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let manifest: { exports?: unknown };
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { exports?: unknown };
|
|
318
|
+
} catch {
|
|
319
|
+
return {};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const exports = manifest.exports;
|
|
323
|
+
|
|
324
|
+
if (typeof exports !== 'object' || exports === null) {
|
|
325
|
+
return {};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const flattened: Record<string, string> = {};
|
|
329
|
+
|
|
330
|
+
for (const [subpath, target] of Object.entries(exports as Record<string, unknown>)) {
|
|
331
|
+
const resolved = this.firstStringTarget(target);
|
|
332
|
+
|
|
333
|
+
if (resolved !== undefined) {
|
|
334
|
+
flattened[subpath] = resolved;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return flattened;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Reduce an `exports` target — a string, or a nested map of conditions — to
|
|
343
|
+
* the first file path it names.
|
|
344
|
+
*/
|
|
345
|
+
protected firstStringTarget(target: unknown): string | undefined {
|
|
346
|
+
if (typeof target === 'string') {
|
|
347
|
+
return target;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (typeof target !== 'object' || target === null) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const value of Object.values(target as Record<string, unknown>)) {
|
|
355
|
+
const resolved = this.firstStringTarget(value);
|
|
356
|
+
|
|
357
|
+
if (resolved !== undefined) {
|
|
358
|
+
return resolved;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
214
365
|
protected generateContainerData(
|
|
215
366
|
serviceProviders: readonly string[],
|
|
216
367
|
config: ConfigResult,
|
|
@@ -221,6 +372,7 @@ export abstract class GenerateDataFromAst {
|
|
|
221
372
|
.writeMessages();
|
|
222
373
|
|
|
223
374
|
const publishers: Record<string, readonly [string, string]> = {};
|
|
375
|
+
const importMap: Record<string, string> = {};
|
|
224
376
|
|
|
225
377
|
for (const providerClass of serviceProviders) {
|
|
226
378
|
const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
|
|
@@ -231,9 +383,17 @@ export abstract class GenerateDataFromAst {
|
|
|
231
383
|
|
|
232
384
|
const result = this.serviceProviderReader.readFile(filePath);
|
|
233
385
|
|
|
386
|
+
if (Object.keys(result.publishers).length > 0) {
|
|
387
|
+
// The publisher callbacks reference the provider's static methods,
|
|
388
|
+
// so the generated data cache must import the provider class.
|
|
389
|
+
importMap[path.basename(filePath, '.ts')] = this.importSpecifier(config.dataPath, filePath);
|
|
390
|
+
}
|
|
391
|
+
|
|
234
392
|
Object.assign(publishers, result.publishers);
|
|
235
393
|
}
|
|
236
394
|
|
|
395
|
+
this.containerGenerator.classImportMap = importMap;
|
|
396
|
+
|
|
237
397
|
const status = this.containerGenerator.generateFile(
|
|
238
398
|
config.dataPath,
|
|
239
399
|
'AppContainerData',
|
|
@@ -297,6 +457,8 @@ export abstract class GenerateDataFromAst {
|
|
|
297
457
|
.writeMessages();
|
|
298
458
|
|
|
299
459
|
const allRoutes: Record<string, ts.Expression> = {};
|
|
460
|
+
const imperativeRoutes: ts.Expression[] = [];
|
|
461
|
+
const importMap: Record<string, string> = {};
|
|
300
462
|
|
|
301
463
|
for (const providerClass of cliRouteProviders) {
|
|
302
464
|
const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
|
|
@@ -307,6 +469,19 @@ export abstract class GenerateDataFromAst {
|
|
|
307
469
|
|
|
308
470
|
const providerResult = this.routeProviderReader.readFile(filePath);
|
|
309
471
|
|
|
472
|
+
if (providerResult.routes.length > 0) {
|
|
473
|
+
// Imperative routes reference the provider's static handlers,
|
|
474
|
+
// so the generated data cache must import the provider class —
|
|
475
|
+
// along with every parameter/constant class the route arguments
|
|
476
|
+
// themselves name, since those are emitted verbatim.
|
|
477
|
+
imperativeRoutes.push(...providerResult.routes);
|
|
478
|
+
importMap[path.basename(filePath, '.ts')] = this.importSpecifier(config.dataPath, filePath);
|
|
479
|
+
|
|
480
|
+
for (const [name, importPath] of Object.entries(providerResult.routeImports)) {
|
|
481
|
+
importMap[name] = this.importSpecifier(config.dataPath, importPath);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
310
485
|
for (const controllerClass of providerResult.controllerClasses) {
|
|
311
486
|
const controllerPath = this.fqnToFilePath(controllerClass, config.namespace, config.dir);
|
|
312
487
|
|
|
@@ -320,12 +495,17 @@ export abstract class GenerateDataFromAst {
|
|
|
320
495
|
}
|
|
321
496
|
}
|
|
322
497
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
498
|
+
this.cliGenerator.classImportMap = importMap;
|
|
499
|
+
|
|
500
|
+
const status =
|
|
501
|
+
imperativeRoutes.length > 0
|
|
502
|
+
? this.cliGenerator.generateFileFromRoutes(
|
|
503
|
+
config.dataPath,
|
|
504
|
+
'AppCliRoutingData',
|
|
505
|
+
config.dataNamespace,
|
|
506
|
+
imperativeRoutes,
|
|
507
|
+
)
|
|
508
|
+
: this.cliGenerator.generateFile(config.dataPath, 'AppCliRoutingData', config.dataNamespace, allRoutes);
|
|
329
509
|
|
|
330
510
|
return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
|
|
331
511
|
}
|
|
@@ -341,6 +521,8 @@ export abstract class GenerateDataFromAst {
|
|
|
341
521
|
|
|
342
522
|
const allRoutes: Record<string, ts.Expression> = {};
|
|
343
523
|
const allRouteData: Record<string, HttpRouteData> = {};
|
|
524
|
+
const imperativeRoutes: ts.Expression[] = [];
|
|
525
|
+
const importMap: Record<string, string> = {};
|
|
344
526
|
|
|
345
527
|
for (const providerClass of httpRouteProviders) {
|
|
346
528
|
const filePath = this.fqnToFilePath(providerClass, config.namespace, config.dir);
|
|
@@ -351,6 +533,19 @@ export abstract class GenerateDataFromAst {
|
|
|
351
533
|
|
|
352
534
|
const providerResult = this.routeProviderReader.readFile(filePath);
|
|
353
535
|
|
|
536
|
+
if (providerResult.routes.length > 0) {
|
|
537
|
+
// Imperative routes reference the provider's static handlers,
|
|
538
|
+
// so the generated data cache must import the provider class —
|
|
539
|
+
// along with every parameter/constant class the route arguments
|
|
540
|
+
// themselves name, since those are emitted verbatim.
|
|
541
|
+
imperativeRoutes.push(...providerResult.routes);
|
|
542
|
+
importMap[path.basename(filePath, '.ts')] = this.importSpecifier(config.dataPath, filePath);
|
|
543
|
+
|
|
544
|
+
for (const [name, importPath] of Object.entries(providerResult.routeImports)) {
|
|
545
|
+
importMap[name] = this.importSpecifier(config.dataPath, importPath);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
354
549
|
for (const controllerClass of providerResult.controllerClasses) {
|
|
355
550
|
const controllerPath = this.fqnToFilePath(controllerClass, config.namespace, config.dir);
|
|
356
551
|
|
|
@@ -365,13 +560,23 @@ export abstract class GenerateDataFromAst {
|
|
|
365
560
|
}
|
|
366
561
|
}
|
|
367
562
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
563
|
+
this.httpGenerator.classImportMap = importMap;
|
|
564
|
+
|
|
565
|
+
const status =
|
|
566
|
+
imperativeRoutes.length > 0
|
|
567
|
+
? this.httpGenerator.generateFileFromRoutes(
|
|
568
|
+
config.dataPath,
|
|
569
|
+
'AppHttpRoutingData',
|
|
570
|
+
config.dataNamespace,
|
|
571
|
+
imperativeRoutes,
|
|
572
|
+
)
|
|
573
|
+
: this.httpGenerator.generateFile(
|
|
574
|
+
config.dataPath,
|
|
575
|
+
'AppHttpRoutingData',
|
|
576
|
+
config.dataNamespace,
|
|
577
|
+
allRoutes,
|
|
578
|
+
allRouteData,
|
|
579
|
+
);
|
|
375
580
|
|
|
376
581
|
return this.addMessagesForGenerateStatus(output, status).withAddedMessages(new NewLine()).writeMessages();
|
|
377
582
|
}
|
|
@@ -31,6 +31,33 @@ 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
|
+
* Names the generated file's fixed framework header already binds are
|
|
40
|
+
* dropped: those are the same framework classes under the same names, and
|
|
41
|
+
* emitting them twice is a duplicate-identifier error.
|
|
42
|
+
*/
|
|
43
|
+
protected buildUserImportsBlock(classImportMap: Record<string, string>, reservedNames: readonly string[]): string {
|
|
44
|
+
const userImports = Object.entries(classImportMap)
|
|
45
|
+
.filter(([name]) => !reservedNames.includes(name))
|
|
46
|
+
.map(([name, specifier]) => `import { ${name} } from '${specifier}';`)
|
|
47
|
+
.join('\n');
|
|
48
|
+
|
|
49
|
+
return userImports ? `${userImports}\n` : '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Render the fixed framework imports a generated data file always carries.
|
|
54
|
+
*/
|
|
55
|
+
protected buildFrameworkImportLines(imports: Readonly<Record<string, string>>, isType: boolean = false): string[] {
|
|
56
|
+
const keyword = isType ? 'import type' : 'import';
|
|
57
|
+
|
|
58
|
+
return Object.entries(imports).map(([name, specifier]) => `${keyword} { ${name} } from '${specifier}';`);
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
protected writeFile(directory: string, className: string, data: string): GenerateStatus {
|
|
35
62
|
const filePath = directory.replace(/\/$/, '') + `/${className}.ts`;
|
|
36
63
|
|
|
@@ -14,6 +14,23 @@ import type { CliDataFileGeneratorContract } from '../../Cli/Contract/CliDataFil
|
|
|
14
14
|
import type { GenerateStatus } from '../../Enum/GenerateStatus.ts';
|
|
15
15
|
|
|
16
16
|
export class AstCliDataFileGenerator extends AstFileGenerator implements CliDataFileGeneratorContract {
|
|
17
|
+
/** The framework classes every generated CLI data file imports as values. */
|
|
18
|
+
protected static readonly FRAMEWORK_IMPORTS: Readonly<Record<string, string>> = {
|
|
19
|
+
CliRoutingData: '@valkyrjaio/valkyrja/Cli/Routing/Data/CliRoutingData.ts',
|
|
20
|
+
Route: '@valkyrjaio/valkyrja/Cli/Routing/Data/Route.ts',
|
|
21
|
+
ArgumentParameter: '@valkyrjaio/valkyrja/Cli/Routing/Data/ArgumentParameter.ts',
|
|
22
|
+
OptionParameter: '@valkyrjaio/valkyrja/Cli/Routing/Data/OptionParameter.ts',
|
|
23
|
+
ArgumentMode: '@valkyrjaio/valkyrja/Cli/Routing/Enum/ArgumentMode.ts',
|
|
24
|
+
ArgumentValueMode: '@valkyrjaio/valkyrja/Cli/Routing/Enum/ArgumentValueMode.ts',
|
|
25
|
+
OptionMode: '@valkyrjaio/valkyrja/Cli/Routing/Enum/OptionMode.ts',
|
|
26
|
+
OptionValueMode: '@valkyrjaio/valkyrja/Cli/Routing/Enum/OptionValueMode.ts',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** The framework types every generated CLI data file imports. */
|
|
30
|
+
protected static readonly FRAMEWORK_TYPE_IMPORTS: Readonly<Record<string, string>> = {
|
|
31
|
+
RouteContract: '@valkyrjaio/valkyrja/Cli/Routing/Data/Contract/RouteContract.ts',
|
|
32
|
+
};
|
|
33
|
+
|
|
17
34
|
public classImportMap: Record<string, string> = {};
|
|
18
35
|
|
|
19
36
|
private readonly printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
@@ -32,26 +49,18 @@ export class AstCliDataFileGenerator extends AstFileGenerator implements CliData
|
|
|
32
49
|
namespace: string,
|
|
33
50
|
routes: Record<string, ts.Expression>,
|
|
34
51
|
): GenerateStatus {
|
|
35
|
-
const
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
const userImportsBlock = userImports ? `${userImports}\n` : '';
|
|
52
|
+
const userImportsBlock = this.buildUserImportsBlock(this.classImportMap, [
|
|
53
|
+
...Object.keys(AstCliDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
54
|
+
...Object.keys(AstCliDataFileGenerator.FRAMEWORK_TYPE_IMPORTS),
|
|
55
|
+
]);
|
|
40
56
|
|
|
41
57
|
const contents = this.generateClassContents(routes);
|
|
42
58
|
|
|
43
59
|
const data = [
|
|
44
60
|
'// This file was automatically generated by Sindri.',
|
|
45
61
|
'',
|
|
46
|
-
|
|
47
|
-
|
|
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';`,
|
|
62
|
+
...this.buildFrameworkImportLines(AstCliDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
63
|
+
...this.buildFrameworkImportLines(AstCliDataFileGenerator.FRAMEWORK_TYPE_IMPORTS, true),
|
|
55
64
|
userImportsBlock,
|
|
56
65
|
`export class ${className} extends CliRoutingData {`,
|
|
57
66
|
` constructor() {`,
|
|
@@ -64,6 +73,61 @@ export class AstCliDataFileGenerator extends AstFileGenerator implements CliData
|
|
|
64
73
|
return this.writeFile(directory, className, data);
|
|
65
74
|
}
|
|
66
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Generate the CLI routing data file from imperative `getRoutes()` route
|
|
78
|
+
* objects — each `new Route(name, description, handler, ...)` is keyed by
|
|
79
|
+
* its command name (the first constructor argument) and emitted verbatim.
|
|
80
|
+
*/
|
|
81
|
+
public generateFileFromRoutes(
|
|
82
|
+
directory: string,
|
|
83
|
+
className: string,
|
|
84
|
+
namespace: string,
|
|
85
|
+
routeExprs: readonly ts.Expression[],
|
|
86
|
+
): GenerateStatus {
|
|
87
|
+
const routes: Record<string, ts.Expression> = {};
|
|
88
|
+
|
|
89
|
+
for (const expr of routeExprs) {
|
|
90
|
+
if (!ts.isNewExpression(expr)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const key = this.routeKey((expr.arguments ?? [])[0]);
|
|
95
|
+
|
|
96
|
+
if (key === undefined) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
routes[key] = expr;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return this.generateFile(directory, className, namespace, routes);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Derive the map key for a route from its name argument.
|
|
108
|
+
*
|
|
109
|
+
* A literal name keys the map by its own text. A constant reference
|
|
110
|
+
* (`CliCommandName.LIST` — how the framework's own commands name
|
|
111
|
+
* themselves) is kept as a `Class::CASE` marker, so the generated file
|
|
112
|
+
* emits the constant reference as a computed key rather than baking in the
|
|
113
|
+
* value it happens to hold today.
|
|
114
|
+
*/
|
|
115
|
+
protected routeKey(nameArg: ts.Expression | undefined): string | undefined {
|
|
116
|
+
if (nameArg === undefined) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (ts.isStringLiteral(nameArg)) {
|
|
121
|
+
return nameArg.text;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (ts.isPropertyAccessExpression(nameArg) && ts.isIdentifier(nameArg.expression)) {
|
|
125
|
+
return `${nameArg.expression.text}::${nameArg.name.text}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
67
131
|
public generateClassContents(routes: Record<string, ts.Expression>): string {
|
|
68
132
|
const entries = Object.entries(routes);
|
|
69
133
|
|
|
@@ -74,15 +138,25 @@ export class AstCliDataFileGenerator extends AstFileGenerator implements CliData
|
|
|
74
138
|
const lines: string[] = [];
|
|
75
139
|
|
|
76
140
|
for (const [key, valueExpr] of entries) {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const formattedKey = key.includes('::') ? `[${printedKey}]` : `['${printedKey}']`;
|
|
141
|
+
const printedValue = this.printExpr(valueExpr);
|
|
142
|
+
const formattedKey = key.includes('::')
|
|
143
|
+
? `[${this.printer.printNode(ts.EmitHint.Unspecified, this.buildEnumCaseExpr(key), this.dummySourceFile)}]`
|
|
144
|
+
: `['${key}']`;
|
|
82
145
|
|
|
83
146
|
lines.push(` ${formattedKey}: (): RouteContract => ${printedValue},`);
|
|
84
147
|
}
|
|
85
148
|
|
|
86
|
-
return ['super({',
|
|
149
|
+
return ['super({', ...lines, ' });'].join('\n ');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Print an expression to source. Parsed nodes (from a `getRoutes()` body)
|
|
154
|
+
* are emitted via their original source text; synthesized nodes (which
|
|
155
|
+
* carry no source positions) are emitted through the printer.
|
|
156
|
+
*/
|
|
157
|
+
protected printExpr(expr: ts.Expression): string {
|
|
158
|
+
return expr.pos >= 0
|
|
159
|
+
? expr.getText()
|
|
160
|
+
: this.printer.printNode(ts.EmitHint.Unspecified, expr, this.dummySourceFile);
|
|
87
161
|
}
|
|
88
162
|
}
|
|
@@ -14,6 +14,16 @@ import type { ContainerDataFileGeneratorContract } from '../../Container/Contrac
|
|
|
14
14
|
import type { GenerateStatus } from '../../Enum/GenerateStatus.ts';
|
|
15
15
|
|
|
16
16
|
export class AstContainerDataFileGenerator extends AstFileGenerator implements ContainerDataFileGeneratorContract {
|
|
17
|
+
/** The framework class every generated container data file imports as a value. */
|
|
18
|
+
protected static readonly FRAMEWORK_IMPORTS: Readonly<Record<string, string>> = {
|
|
19
|
+
ContainerData: '@valkyrjaio/valkyrja/Container/Data/ContainerData.ts',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** The framework type every generated container data file imports. */
|
|
23
|
+
protected static readonly FRAMEWORK_TYPE_IMPORTS: Readonly<Record<string, string>> = {
|
|
24
|
+
ContainerContract: '@valkyrjaio/valkyrja/Container/Manager/Contract/ContainerContract.ts',
|
|
25
|
+
};
|
|
26
|
+
|
|
17
27
|
public classImportMap: Record<string, string> = {};
|
|
18
28
|
|
|
19
29
|
private readonly printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
@@ -32,19 +42,18 @@ export class AstContainerDataFileGenerator extends AstFileGenerator implements C
|
|
|
32
42
|
namespace: string,
|
|
33
43
|
publishers: Readonly<Record<string, readonly [string, string]>>,
|
|
34
44
|
): GenerateStatus {
|
|
35
|
-
const
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
const userImportsBlock = userImports ? `${userImports}\n` : '';
|
|
45
|
+
const userImportsBlock = this.buildUserImportsBlock(this.classImportMap, [
|
|
46
|
+
...Object.keys(AstContainerDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
47
|
+
...Object.keys(AstContainerDataFileGenerator.FRAMEWORK_TYPE_IMPORTS),
|
|
48
|
+
]);
|
|
40
49
|
|
|
41
50
|
const contents = this.generateClassContents(publishers);
|
|
42
51
|
|
|
43
52
|
const data = [
|
|
44
53
|
'// This file was automatically generated by Sindri.',
|
|
45
54
|
'',
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
...this.buildFrameworkImportLines(AstContainerDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
56
|
+
...this.buildFrameworkImportLines(AstContainerDataFileGenerator.FRAMEWORK_TYPE_IMPORTS, true),
|
|
48
57
|
userImportsBlock,
|
|
49
58
|
`export class ${className} extends ContainerData {`,
|
|
50
59
|
` constructor() {`,
|
|
@@ -67,12 +76,11 @@ export class AstContainerDataFileGenerator extends AstFileGenerator implements C
|
|
|
67
76
|
const lines: string[] = [];
|
|
68
77
|
|
|
69
78
|
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
79
|
const shortProviderClass = providerClass.slice(providerClass.lastIndexOf('\\') + 1);
|
|
74
80
|
|
|
75
|
-
const formattedKey = serviceId.includes('::')
|
|
81
|
+
const formattedKey = serviceId.includes('::')
|
|
82
|
+
? `[${this.printer.printNode(ts.EmitHint.Unspecified, this.buildEnumCaseExpr(serviceId), this.dummySourceFile)}]`
|
|
83
|
+
: `['${serviceId}']`;
|
|
76
84
|
|
|
77
85
|
lines.push(
|
|
78
86
|
` ${formattedKey}: (container: ContainerContract) => ${shortProviderClass}.${methodName}(container),`,
|
|
@@ -14,6 +14,21 @@ import type { EventDataFileGeneratorContract } from '../../Event/Contract/EventD
|
|
|
14
14
|
import type { GenerateStatus } from '../../Enum/GenerateStatus.ts';
|
|
15
15
|
|
|
16
16
|
export class AstEventDataFileGenerator extends AstFileGenerator implements EventDataFileGeneratorContract {
|
|
17
|
+
/** The framework class every generated event data file imports as a value. */
|
|
18
|
+
protected static readonly FRAMEWORK_IMPORTS: Readonly<Record<string, string>> = {
|
|
19
|
+
EventData: '@valkyrjaio/valkyrja/Event/Data/EventData.ts',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** The framework classes a generated event data file imports only when it holds listeners. */
|
|
23
|
+
protected static readonly LISTENER_IMPORTS: Readonly<Record<string, string>> = {
|
|
24
|
+
Listener: '@valkyrjaio/valkyrja/Event/Data/Listener.ts',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The framework types a generated event data file imports only when it holds listeners. */
|
|
28
|
+
protected static readonly LISTENER_TYPE_IMPORTS: Readonly<Record<string, string>> = {
|
|
29
|
+
ListenerContract: '@valkyrjaio/valkyrja/Event/Data/Contract/ListenerContract.ts',
|
|
30
|
+
};
|
|
31
|
+
|
|
17
32
|
public classImportMap: Record<string, string> = {};
|
|
18
33
|
|
|
19
34
|
private readonly printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
@@ -32,20 +47,30 @@ export class AstEventDataFileGenerator extends AstFileGenerator implements Event
|
|
|
32
47
|
namespace: string,
|
|
33
48
|
listeners: Record<string, ts.Expression>,
|
|
34
49
|
): GenerateStatus {
|
|
35
|
-
const
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
const userImportsBlock = this.buildUserImportsBlock(this.classImportMap, [
|
|
51
|
+
...Object.keys(AstEventDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
52
|
+
...Object.keys(AstEventDataFileGenerator.LISTENER_IMPORTS),
|
|
53
|
+
...Object.keys(AstEventDataFileGenerator.LISTENER_TYPE_IMPORTS),
|
|
54
|
+
]);
|
|
40
55
|
|
|
41
56
|
const contents = this.generateClassContents(listeners);
|
|
42
57
|
|
|
58
|
+
// The Listener / ListenerContract imports are only referenced by the
|
|
59
|
+
// generated listener closures — omit them when there are no listeners
|
|
60
|
+
// so empty event data does not carry unused (and unresolved) imports.
|
|
61
|
+
const listenerImports =
|
|
62
|
+
Object.keys(listeners).length > 0
|
|
63
|
+
? [
|
|
64
|
+
...this.buildFrameworkImportLines(AstEventDataFileGenerator.LISTENER_IMPORTS),
|
|
65
|
+
...this.buildFrameworkImportLines(AstEventDataFileGenerator.LISTENER_TYPE_IMPORTS, true),
|
|
66
|
+
]
|
|
67
|
+
: [];
|
|
68
|
+
|
|
43
69
|
const data = [
|
|
44
70
|
'// This file was automatically generated by Sindri.',
|
|
45
71
|
'',
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`import type { ListenerContract } from '@valkyrjaio/valkyrja/Event/Data/Contract/ListenerContract.ts';`,
|
|
72
|
+
...this.buildFrameworkImportLines(AstEventDataFileGenerator.FRAMEWORK_IMPORTS),
|
|
73
|
+
...listenerImports,
|
|
49
74
|
userImportsBlock,
|
|
50
75
|
`export class ${className} extends EventData {`,
|
|
51
76
|
` constructor() {`,
|