@valkyrjaio/valkyrja 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/package.json +1 -1
- package/src/Valkyrja/Application/Constant/ApplicationInfo.ts +2 -2
- package/src/Valkyrja/Cli/Routing/Attribute/ArgumentParameter.ts +23 -0
- package/src/Valkyrja/Cli/Routing/Attribute/OptionParameter.ts +23 -0
- package/src/Valkyrja/Cli/Routing/Attribute/Route/Middleware.ts +24 -0
- package/src/Valkyrja/Cli/Routing/Attribute/Route/Name.ts +29 -0
- package/src/Valkyrja/Cli/Routing/Attribute/Route/RouteHandler.ts +30 -0
- package/src/Valkyrja/Cli/Routing/Attribute/Route.ts +25 -0
- package/src/Valkyrja/Cli/Routing/Attribute/RouteAttributeMetadata.ts +227 -0
- package/src/Valkyrja/Cli/Routing/Attribute/RouteOptions.ts +66 -0
- package/src/Valkyrja/Cli/Routing/Collector/AttributeRouteCollector.ts +196 -0
- package/src/Valkyrja/Cli/Routing/Constant/CliRoutingServiceId.ts +1 -0
- package/src/Valkyrja/Cli/Routing/Provider/CliRoutingCliRouteProvider.ts +4 -0
- package/src/Valkyrja/Cli/Routing/Provider/CliRoutingServiceProvider.ts +26 -1
- package/src/Valkyrja/Cli/Routing/Provider/Contract/CliRouteProviderContract.ts +5 -0
- package/src/Valkyrja/Http/Message/Uri/Constant/Char.ts +58 -0
- package/src/Valkyrja/Http/Message/Uri/Factory/UriFactory.ts +63 -3
- package/src/Valkyrja/Http/Message/Uri/Uri.ts +3 -3
- package/src/Valkyrja/Http/Routing/Attribute/DynamicRoute.ts +28 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/Middleware.ts +24 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/Name.ts +29 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/Path.ts +29 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Any.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Connect.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Delete.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Get.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Head.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Options.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Patch.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Post.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Put.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Trace.ts +20 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod.ts +25 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RequestStruct.ts +23 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/ResponseStruct.ts +23 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route/RouteHandler.ts +30 -0
- package/src/Valkyrja/Http/Routing/Attribute/Route.ts +32 -0
- package/src/Valkyrja/Http/Routing/Attribute/RouteAttributeMetadata.ts +235 -0
- package/src/Valkyrja/Http/Routing/Attribute/RouteOptions.ts +61 -0
- package/src/Valkyrja/Http/Routing/Collector/AttributeRouteCollector.ts +237 -0
- package/src/Valkyrja/Http/Routing/Constant/Regex.ts +2 -0
- package/src/Valkyrja/Http/Routing/Provider/Contract/HttpRouteProviderContract.ts +5 -0
- package/src/Valkyrja/Http/Routing/Provider/HttpRoutingCliRouteProvider.ts +4 -0
- package/src/Valkyrja/Http/Routing/Provider/HttpRoutingServiceProvider.ts +32 -2
package/package.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export class ApplicationInfo {
|
|
11
|
-
static readonly VERSION = '26.
|
|
12
|
-
static readonly VERSION_BUILD_DATE_TIME = 'August 1 2026 07:
|
|
11
|
+
static readonly VERSION = '26.6.0' as const;
|
|
12
|
+
static readonly VERSION_BUILD_DATE_TIME = 'August 1 2026 18:07:10 MST' as const;
|
|
13
13
|
static readonly ASCII = `
|
|
14
14
|
_ _ _
|
|
15
15
|
/\\ /\\__ _| | | ___ _ _ __(_) __ _
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja package.
|
|
3
|
+
*
|
|
4
|
+
* (c) Melech Mizrachi <melechmizrachi@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ensureCliRouteMethodMetadata } from './RouteAttributeMetadata.ts';
|
|
11
|
+
|
|
12
|
+
import type { ArgumentParameterOptions } from './RouteOptions.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Declare a command argument on a controller method.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors PHP's repeatable `#[ArgumentParameter]`.
|
|
18
|
+
*/
|
|
19
|
+
export function ArgumentParameter(options: ArgumentParameterOptions) {
|
|
20
|
+
return function (_value: unknown, context: ClassMethodDecoratorContext): void {
|
|
21
|
+
ensureCliRouteMethodMetadata(context.metadata, context.name).arguments.push(options);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja package.
|
|
3
|
+
*
|
|
4
|
+
* (c) Melech Mizrachi <melechmizrachi@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ensureCliRouteMethodMetadata } from './RouteAttributeMetadata.ts';
|
|
11
|
+
|
|
12
|
+
import type { OptionParameterOptions } from './RouteOptions.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Declare a command option on a controller method.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors PHP's repeatable `#[OptionParameter]`.
|
|
18
|
+
*/
|
|
19
|
+
export function OptionParameter(options: OptionParameterOptions) {
|
|
20
|
+
return function (_value: unknown, context: ClassMethodDecoratorContext): void {
|
|
21
|
+
ensureCliRouteMethodMetadata(context.metadata, context.name).options.push(options);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja package.
|
|
3
|
+
*
|
|
4
|
+
* (c) Melech Mizrachi <melechmizrachi@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ensureCliRouteMethodMetadata } from '../RouteAttributeMetadata.ts';
|
|
11
|
+
|
|
12
|
+
import type { CliMiddlewareReference } from '../RouteAttributeMetadata.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Add a middleware to the command(s) declared on a controller method.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors PHP's repeatable `#[Middleware]`. The collector routes the class into
|
|
18
|
+
* whichever middleware buckets it satisfies.
|
|
19
|
+
*/
|
|
20
|
+
export function Middleware(middleware: CliMiddlewareReference) {
|
|
21
|
+
return function (_value: unknown, context: ClassMethodDecoratorContext): void {
|
|
22
|
+
ensureCliRouteMethodMetadata(context.metadata, context.name).middleware.push(middleware);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja package.
|
|
3
|
+
*
|
|
4
|
+
* (c) Melech Mizrachi <melechmizrachi@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ensureCliRouteMetadata, ensureCliRouteMethodMetadata } from '../RouteAttributeMetadata.ts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Add a name segment to a command.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors PHP's `#[Name]` (targets a class or a method): a class-level name
|
|
16
|
+
* prefixes every command in the controller, a method-level name is appended to
|
|
17
|
+
* that method's command names.
|
|
18
|
+
*/
|
|
19
|
+
export function Name(value: string) {
|
|
20
|
+
return function (_value: unknown, context: ClassDecoratorContext | ClassMethodDecoratorContext): void {
|
|
21
|
+
if (context.kind === 'class') {
|
|
22
|
+
ensureCliRouteMetadata(context.metadata).classNames.push(value);
|
|
23
|
+
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ensureCliRouteMethodMetadata(context.metadata, context.name).names.push(value);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja package.
|
|
3
|
+
*
|
|
4
|
+
* (c) Melech Mizrachi <melechmizrachi@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ensureCliRouteMethodMetadata } from '../RouteAttributeMetadata.ts';
|
|
11
|
+
|
|
12
|
+
import type { CliHandlerReference } from '../RouteAttributeMetadata.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Assign the handler for the command(s) declared on a controller method.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors PHP's `#[RouteHandler([CliRouteProvider::class, 'testCommandHandler'])]`,
|
|
18
|
+
* written here as `@RouteHandler([() => CliRouteProvider, 'testCommandHandler'])`.
|
|
19
|
+
*
|
|
20
|
+
* The two pieces of that shape solve two independent problems (see
|
|
21
|
+
* `CliHandlerReference`): the **thunk** keeps the class binding untouched at
|
|
22
|
+
* decoration time so a circular or self-referential import cannot trip the
|
|
23
|
+
* temporal dead zone, and the generic **`THandler`** makes the method name a
|
|
24
|
+
* checked key of that class rather than an arbitrary string.
|
|
25
|
+
*/
|
|
26
|
+
export function RouteHandler<THandler>(handler: CliHandlerReference<THandler>) {
|
|
27
|
+
return function (_value: unknown, context: ClassMethodDecoratorContext): void {
|
|
28
|
+
ensureCliRouteMethodMetadata(context.metadata, context.name).handler = handler;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja package.
|
|
3
|
+
*
|
|
4
|
+
* (c) Melech Mizrachi <melechmizrachi@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createCliRouteDefinition, ensureCliRouteMethodMetadata } from './RouteAttributeMetadata.ts';
|
|
11
|
+
|
|
12
|
+
import type { CliRouteOptions } from './RouteOptions.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Declare a CLI command on a controller method.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors PHP's repeatable `#[Route]`: stack it to declare several commands for
|
|
18
|
+
* one method. Sindri reads it statically to build the command cache; in debug
|
|
19
|
+
* mode the runtime `AttributeRouteCollector` reads it from the class metadata.
|
|
20
|
+
*/
|
|
21
|
+
export function Route<THandler = unknown, THelpText = unknown>(options: CliRouteOptions<THandler, THelpText>) {
|
|
22
|
+
return function (_value: unknown, context: ClassMethodDecoratorContext): void {
|
|
23
|
+
ensureCliRouteMethodMetadata(context.metadata, context.name).routes.push(createCliRouteDefinition(options));
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja package.
|
|
3
|
+
*
|
|
4
|
+
* (c) Melech Mizrachi <melechmizrachi@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ContainerContract } from '../../../Container/Manager/Contract/ContainerContract.ts';
|
|
11
|
+
import type { MessageContract } from '../../Interaction/Message/Contract/MessageContract.ts';
|
|
12
|
+
import type { OutputContract } from '../../Interaction/Output/Contract/OutputContract.ts';
|
|
13
|
+
import type { RouteDispatchedMiddlewareContract } from '../../Middleware/Contract/RouteDispatchedMiddlewareContract.ts';
|
|
14
|
+
import type { RouteMatchedMiddlewareContract } from '../../Middleware/Contract/RouteMatchedMiddlewareContract.ts';
|
|
15
|
+
import type { ThrowableCaughtMiddlewareContract } from '../../Middleware/Contract/ThrowableCaughtMiddlewareContract.ts';
|
|
16
|
+
import type { ProcessExitingMiddlewareContract } from '../../Middleware/Contract/ProcessExitingMiddlewareContract.ts';
|
|
17
|
+
import type { RouteContract } from '../Data/Contract/RouteContract.ts';
|
|
18
|
+
import type { ArgumentParameterOptions, CliRouteOptions, OptionParameterOptions } from './RouteOptions.ts';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The signature every CLI command handler must have — the typed closure the
|
|
22
|
+
* dispatcher invokes (mirrors PHP's `OutputContract` handler contract).
|
|
23
|
+
*/
|
|
24
|
+
export type CliHandler = (container: ContainerContract, route: RouteContract) => OutputContract;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The signature every help-text provider must have.
|
|
28
|
+
*/
|
|
29
|
+
export type CliHelpText = () => MessageContract;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The keys of `T` whose value is a valid `CliHandler`.
|
|
33
|
+
*
|
|
34
|
+
* Fix 2 (types) — independent of the thunk below. A bare `string` method name
|
|
35
|
+
* lets a typo or a wrong-signature method compile and only blow up at run time;
|
|
36
|
+
* this mapped type turns both into compile errors. Do not relax it to `string`.
|
|
37
|
+
*/
|
|
38
|
+
type CliHandlerKeys<T> = { [K in keyof T]: T[K] extends CliHandler ? K : never }[keyof T];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The keys of `T` whose value returns a `MessageContract` — the help-text
|
|
42
|
+
* counterpart of `CliHandlerKeys` (Fix 2).
|
|
43
|
+
*/
|
|
44
|
+
type CliHelpTextKeys<T> = { [K in keyof T]: T[K] extends CliHelpText ? K : never }[keyof T];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A command handler reference: a **thunk** returning the provider class that
|
|
48
|
+
* holds the static handler method, paired with the name of that method —
|
|
49
|
+
* mirrors PHP's `[CliRouteProvider::class, 'testCommandHandler']`.
|
|
50
|
+
*
|
|
51
|
+
* Fix 1 (runtime) — the thunk is load-bearing, not decoration. A TC39 Stage-3
|
|
52
|
+
* method decorator runs while the enclosing class binding is still in its
|
|
53
|
+
* temporal dead zone, so naming a class directly (`[CliRouteProvider, 'x']`)
|
|
54
|
+
* dereferences it at class-definition time and throws `ReferenceError: Cannot
|
|
55
|
+
* access 'X' before initialization` whenever the referenced module is still
|
|
56
|
+
* initializing (a circular command ↔ provider import, or a command naming
|
|
57
|
+
* itself — which is exactly why `helpText` had to be dropped before). Building a
|
|
58
|
+
* closure never touches the binding, so the TDZ cannot fire; the collector calls
|
|
59
|
+
* `ref[0]()` later, once every module is initialized. Do not "simplify" this
|
|
60
|
+
* back to a bare class reference — the bare form is an order-dependent footgun
|
|
61
|
+
* and is deliberately not accepted.
|
|
62
|
+
*/
|
|
63
|
+
export type CliHandlerReference<T> = [() => T, CliHandlerKeys<T> & string];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A help-text reference: a thunk returning the class holding a static method
|
|
67
|
+
* that returns the help message — mirrors PHP's `helpText: [self::class,
|
|
68
|
+
* 'help']`, written here as `helpText: [() => TestCommand, 'help']`. The thunk
|
|
69
|
+
* is what makes the self-reference legal (Fix 1).
|
|
70
|
+
*/
|
|
71
|
+
export type CliHelpTextReference<T> = [() => T, CliHelpTextKeys<T> & string];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The loose storage forms of the references above. Only the authoring types are
|
|
75
|
+
* strict; once the metadata is written the concrete class type is no longer
|
|
76
|
+
* needed, so the stored tuple erases it.
|
|
77
|
+
*/
|
|
78
|
+
export type CliHandlerReferenceMetadata = [() => unknown, string];
|
|
79
|
+
|
|
80
|
+
export type CliHelpTextReferenceMetadata = [() => unknown, string];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A middleware class reference. The buckets it belongs to are resolved
|
|
84
|
+
* structurally by the collector, mirroring PHP's independent `is_a` checks.
|
|
85
|
+
*/
|
|
86
|
+
export type CliMiddlewareClass = new (
|
|
87
|
+
...args: unknown[]
|
|
88
|
+
) =>
|
|
89
|
+
| RouteMatchedMiddlewareContract
|
|
90
|
+
| RouteDispatchedMiddlewareContract
|
|
91
|
+
| ThrowableCaughtMiddlewareContract
|
|
92
|
+
| ProcessExitingMiddlewareContract;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A middleware reference: a **thunk** returning the middleware class.
|
|
96
|
+
*
|
|
97
|
+
* Fix 1 (runtime) — the thunk is load-bearing, for the same reason as
|
|
98
|
+
* `CliHandlerReference`. Naming the class directly dereferences it while the
|
|
99
|
+
* decorator runs, which throws `ReferenceError: Cannot access 'X' before
|
|
100
|
+
* initialization` if that module is still initializing. Building a closure never
|
|
101
|
+
* touches the binding; the collector calls it once every module is ready.
|
|
102
|
+
*/
|
|
103
|
+
export type CliMiddlewareReference = () => CliMiddlewareClass;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The accumulated metadata for a single `@Route` attribute on a command method.
|
|
107
|
+
* Repeatable decorators produce one definition each.
|
|
108
|
+
*/
|
|
109
|
+
export interface CliRouteDefinition {
|
|
110
|
+
name: string;
|
|
111
|
+
description: string;
|
|
112
|
+
handler: CliHandlerReferenceMetadata | null;
|
|
113
|
+
helpText: CliHelpTextReferenceMetadata | null;
|
|
114
|
+
middleware: CliMiddlewareReference[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The accumulated metadata for a single command method: its route definitions
|
|
119
|
+
* plus the modifiers contributed by the other method-level decorators.
|
|
120
|
+
*/
|
|
121
|
+
export interface CliRouteMethodMetadata {
|
|
122
|
+
routes: CliRouteDefinition[];
|
|
123
|
+
handler: CliHandlerReferenceMetadata | null;
|
|
124
|
+
names: string[];
|
|
125
|
+
middleware: CliMiddlewareReference[];
|
|
126
|
+
arguments: ArgumentParameterOptions[];
|
|
127
|
+
options: OptionParameterOptions[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The full attribute metadata attached to a command controller class.
|
|
132
|
+
*/
|
|
133
|
+
export interface CliRouteAttributeMetadata {
|
|
134
|
+
classNames: string[];
|
|
135
|
+
methods: Map<string, CliRouteMethodMetadata>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The property key under which the CLI routing metadata is stored on the
|
|
140
|
+
* standard Stage-3 decorator metadata object.
|
|
141
|
+
*/
|
|
142
|
+
export const CLI_ROUTE_METADATA_KEY: unique symbol = Symbol('Valkyrja.Cli.Routing.Attribute.Metadata');
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Ensure `Symbol.metadata` exists so the collector can read decorator metadata
|
|
146
|
+
* off a class in runtimes that predate native support (Node, Vitest).
|
|
147
|
+
*/
|
|
148
|
+
export function ensureSymbolMetadata(): symbol {
|
|
149
|
+
const symbolWithMetadata = Symbol as { metadata: symbol | undefined };
|
|
150
|
+
|
|
151
|
+
symbolWithMetadata.metadata ??= Symbol.for('Symbol.metadata');
|
|
152
|
+
|
|
153
|
+
return symbolWithMetadata.metadata;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get (creating if absent) the CLI routing metadata container on a decorator
|
|
158
|
+
* metadata object.
|
|
159
|
+
*/
|
|
160
|
+
export function ensureCliRouteMetadata(metadata: DecoratorMetadataObject): CliRouteAttributeMetadata {
|
|
161
|
+
const store = metadata as Record<symbol, CliRouteAttributeMetadata | undefined>;
|
|
162
|
+
|
|
163
|
+
return (store[CLI_ROUTE_METADATA_KEY] ??= {
|
|
164
|
+
classNames: [],
|
|
165
|
+
methods: new Map<string, CliRouteMethodMetadata>(),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get (creating if absent) the metadata for a single command method.
|
|
171
|
+
*/
|
|
172
|
+
export function ensureCliRouteMethodMetadata(
|
|
173
|
+
metadata: DecoratorMetadataObject,
|
|
174
|
+
method: string | symbol,
|
|
175
|
+
): CliRouteMethodMetadata {
|
|
176
|
+
const store = ensureCliRouteMetadata(metadata);
|
|
177
|
+
const name = method.toString();
|
|
178
|
+
|
|
179
|
+
let entry = store.methods.get(name);
|
|
180
|
+
|
|
181
|
+
if (entry === undefined) {
|
|
182
|
+
entry = {
|
|
183
|
+
routes: [],
|
|
184
|
+
handler: null,
|
|
185
|
+
names: [],
|
|
186
|
+
middleware: [],
|
|
187
|
+
arguments: [],
|
|
188
|
+
options: [],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
store.methods.set(name, entry);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return entry;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build a route definition from decorator options.
|
|
199
|
+
*/
|
|
200
|
+
export function createCliRouteDefinition<THandler, THelpText>(
|
|
201
|
+
options: CliRouteOptions<THandler, THelpText>,
|
|
202
|
+
): CliRouteDefinition {
|
|
203
|
+
return {
|
|
204
|
+
name: options.name,
|
|
205
|
+
description: options.description,
|
|
206
|
+
handler: options.handler ?? null,
|
|
207
|
+
helpText: options.helpText ?? null,
|
|
208
|
+
middleware: options.middleware ?? [],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Read the CLI routing metadata a decorator wrote onto a command class, or
|
|
214
|
+
* `null` when the class carries no routing attributes.
|
|
215
|
+
*/
|
|
216
|
+
export function readCliRouteMetadata(target: new (...args: unknown[]) => unknown): CliRouteAttributeMetadata | null {
|
|
217
|
+
const metadataSymbol = ensureSymbolMetadata();
|
|
218
|
+
const metadata = (target as unknown as Record<symbol, DecoratorMetadataObject | undefined>)[metadataSymbol];
|
|
219
|
+
|
|
220
|
+
if (metadata === undefined) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const store = metadata as Record<symbol, CliRouteAttributeMetadata | undefined>;
|
|
225
|
+
|
|
226
|
+
return store[CLI_ROUTE_METADATA_KEY] ?? null;
|
|
227
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja package.
|
|
3
|
+
*
|
|
4
|
+
* (c) Melech Mizrachi <melechmizrachi@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Cast } from '../../../Type/Data/Cast.ts';
|
|
11
|
+
import type { ArgumentMode } from '../Enum/ArgumentMode.ts';
|
|
12
|
+
import type { ArgumentValueMode } from '../Enum/ArgumentValueMode.ts';
|
|
13
|
+
import type { OptionMode } from '../Enum/OptionMode.ts';
|
|
14
|
+
import type { OptionValueMode } from '../Enum/OptionValueMode.ts';
|
|
15
|
+
import type { CliHandlerReference, CliHelpTextReference, CliMiddlewareReference } from './RouteAttributeMetadata.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The options accepted by the CLI `@Route` decorator, mirroring the named
|
|
19
|
+
* constructor arguments of PHP's `Valkyrja\Cli\Routing\Attribute\Route`.
|
|
20
|
+
*/
|
|
21
|
+
export interface CliRouteOptions<THandler = unknown, THelpText = unknown> {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
/**
|
|
25
|
+
* The handler thunk/method-name pair. See `CliHandlerReference`: the thunk
|
|
26
|
+
* (Fix 1) sidesteps the decorator-time temporal dead zone, and the generic
|
|
27
|
+
* `THandler` (Fix 2) constrains the method name to a real handler on the
|
|
28
|
+
* referenced class.
|
|
29
|
+
*/
|
|
30
|
+
handler?: CliHandlerReference<THandler>;
|
|
31
|
+
/**
|
|
32
|
+
* The help-text thunk/method-name pair, e.g. `[() => TestCommand, 'help']`.
|
|
33
|
+
* The thunk is what lets a command reference *itself* here (PHP's
|
|
34
|
+
* `self::class`) without tripping the temporal dead zone.
|
|
35
|
+
*/
|
|
36
|
+
helpText?: CliHelpTextReference<THelpText>;
|
|
37
|
+
middleware?: CliMiddlewareReference[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The options accepted by the `@ArgumentParameter` decorator, mirroring PHP's
|
|
42
|
+
* `Valkyrja\Cli\Routing\Attribute\ArgumentParameter`.
|
|
43
|
+
*/
|
|
44
|
+
export interface ArgumentParameterOptions {
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
cast?: Cast | null;
|
|
48
|
+
mode?: ArgumentMode;
|
|
49
|
+
valueMode?: ArgumentValueMode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The options accepted by the `@OptionParameter` decorator, mirroring PHP's
|
|
54
|
+
* `Valkyrja\Cli\Routing\Attribute\OptionParameter`.
|
|
55
|
+
*/
|
|
56
|
+
export interface OptionParameterOptions {
|
|
57
|
+
name: string;
|
|
58
|
+
description: string;
|
|
59
|
+
valueDisplayName?: string;
|
|
60
|
+
cast?: Cast | null;
|
|
61
|
+
defaultValue?: string;
|
|
62
|
+
shortNames?: string[];
|
|
63
|
+
validValues?: string[];
|
|
64
|
+
mode?: OptionMode;
|
|
65
|
+
valueMode?: OptionValueMode;
|
|
66
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja package.
|
|
3
|
+
*
|
|
4
|
+
* (c) Melech Mizrachi <melechmizrachi@gmail.com>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Output } from '../../Interaction/Output/Output.ts';
|
|
11
|
+
import { readCliRouteMetadata } from '../Attribute/RouteAttributeMetadata.ts';
|
|
12
|
+
import { ArgumentParameter } from '../Data/ArgumentParameter.ts';
|
|
13
|
+
import { OptionParameter } from '../Data/OptionParameter.ts';
|
|
14
|
+
import { Route } from '../Data/Route.ts';
|
|
15
|
+
import { ArgumentMode } from '../Enum/ArgumentMode.ts';
|
|
16
|
+
import { ArgumentValueMode } from '../Enum/ArgumentValueMode.ts';
|
|
17
|
+
import { OptionMode } from '../Enum/OptionMode.ts';
|
|
18
|
+
import { OptionValueMode } from '../Enum/OptionValueMode.ts';
|
|
19
|
+
|
|
20
|
+
import type { OutputContract } from '../../Interaction/Output/Contract/OutputContract.ts';
|
|
21
|
+
import type {
|
|
22
|
+
CliHandler,
|
|
23
|
+
CliHandlerReferenceMetadata,
|
|
24
|
+
CliHelpText,
|
|
25
|
+
CliHelpTextReferenceMetadata,
|
|
26
|
+
CliMiddlewareReference,
|
|
27
|
+
CliRouteDefinition,
|
|
28
|
+
CliRouteMethodMetadata,
|
|
29
|
+
} from '../Attribute/RouteAttributeMetadata.ts';
|
|
30
|
+
import type { ArgumentParameterOptions, OptionParameterOptions } from '../Attribute/RouteOptions.ts';
|
|
31
|
+
import type { ArgumentParameterContract } from '../Data/Contract/ArgumentParameterContract.ts';
|
|
32
|
+
import type { OptionParameterContract } from '../Data/Contract/OptionParameterContract.ts';
|
|
33
|
+
import type { RouteContract } from '../Data/Contract/RouteContract.ts';
|
|
34
|
+
import type { RouteCollectorContract } from './Contract/RouteCollectorContract.ts';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Builds CLI commands from the decorator metadata attached to controller
|
|
38
|
+
* classes, without executing or re-parsing any source.
|
|
39
|
+
*
|
|
40
|
+
* Mirrors the framework's PHP `Cli\Routing\AttributeRouteCollector`, reading the
|
|
41
|
+
* standard Stage-3 decorator metadata (`Controller[Symbol.metadata]`) the CLI
|
|
42
|
+
* routing decorators write. Used on the uncached (debug) path.
|
|
43
|
+
*/
|
|
44
|
+
export class AttributeRouteCollector implements RouteCollectorContract {
|
|
45
|
+
getRoutes(...classes: Array<new (...args: unknown[]) => unknown>): RouteContract[] {
|
|
46
|
+
const routes: RouteContract[] = [];
|
|
47
|
+
|
|
48
|
+
for (const controller of classes) {
|
|
49
|
+
const metadata = readCliRouteMetadata(controller);
|
|
50
|
+
|
|
51
|
+
if (metadata === null) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const className = metadata.classNames[0] ?? '';
|
|
56
|
+
|
|
57
|
+
for (const method of metadata.methods.values()) {
|
|
58
|
+
for (const definition of method.routes) {
|
|
59
|
+
routes.push(this.buildRoute(definition, method, className));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return routes;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected buildRoute(
|
|
68
|
+
definition: CliRouteDefinition,
|
|
69
|
+
method: CliRouteMethodMetadata,
|
|
70
|
+
className: string,
|
|
71
|
+
): RouteContract {
|
|
72
|
+
const handler = this.resolveHandler(method.handler ?? definition.handler);
|
|
73
|
+
const helpText = this.resolveHelpText(definition.helpText);
|
|
74
|
+
|
|
75
|
+
let route: RouteContract = new Route(
|
|
76
|
+
definition.name,
|
|
77
|
+
definition.description,
|
|
78
|
+
handler,
|
|
79
|
+
helpText,
|
|
80
|
+
[],
|
|
81
|
+
[],
|
|
82
|
+
[],
|
|
83
|
+
[],
|
|
84
|
+
method.arguments.map((argument) => this.buildArgument(argument)),
|
|
85
|
+
method.options.map((option) => this.buildOption(option)),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
route = this.applyName(route, className, method.names);
|
|
89
|
+
route = this.applyMiddleware(route, [...definition.middleware, ...method.middleware]);
|
|
90
|
+
|
|
91
|
+
return route;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the stored `[thunk, methodName]` pair into the handler closure.
|
|
96
|
+
*
|
|
97
|
+
* The first element is a thunk (Fix 1: it defers dereferencing the class
|
|
98
|
+
* past the decorator's temporal dead zone), so it must be *called* to get
|
|
99
|
+
* the class before the method can be indexed off it.
|
|
100
|
+
*/
|
|
101
|
+
protected resolveHandler(reference: CliHandlerReferenceMetadata | null): CliHandler {
|
|
102
|
+
if (reference === null) {
|
|
103
|
+
return AttributeRouteCollector.defaultHandler;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const [provider, methodName] = reference;
|
|
107
|
+
const handler = (provider() as Record<string, CliHandler | undefined>)[methodName];
|
|
108
|
+
|
|
109
|
+
return handler ?? AttributeRouteCollector.defaultHandler;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the stored help-text `[thunk, methodName]` pair, calling the thunk
|
|
114
|
+
* for the same reason as `resolveHandler` — and it is precisely that
|
|
115
|
+
* deferral which lets a command point `helpText` at itself.
|
|
116
|
+
*/
|
|
117
|
+
protected resolveHelpText(reference: CliHelpTextReferenceMetadata | null): CliHelpText | null {
|
|
118
|
+
if (reference === null) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const [source, methodName] = reference;
|
|
123
|
+
|
|
124
|
+
return (source() as Record<string, CliHelpText | undefined>)[methodName] ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
protected applyName(route: RouteContract, className: string, methodNames: string[]): RouteContract {
|
|
128
|
+
if (className !== '') {
|
|
129
|
+
route = route.withName(className + '.' + route.getName());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const methodName = methodNames[0];
|
|
133
|
+
|
|
134
|
+
if (methodName !== undefined) {
|
|
135
|
+
route = route.withName(route.getName() + '.' + methodName);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return route;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
protected applyMiddleware(route: RouteContract, middleware: CliMiddlewareReference[]): RouteContract {
|
|
142
|
+
for (const thunk of middleware) {
|
|
143
|
+
// The reference is a thunk (see `CliMiddlewareReference`), so call it
|
|
144
|
+
// to get the class before inspecting its prototype.
|
|
145
|
+
const reference = thunk();
|
|
146
|
+
const prototype = reference.prototype as unknown as Record<string, unknown>;
|
|
147
|
+
|
|
148
|
+
if (typeof prototype.routeMatched === 'function') {
|
|
149
|
+
route = route.withAddedRouteMatchedMiddleware(reference.name);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof prototype.routeDispatched === 'function') {
|
|
153
|
+
route = route.withAddedRouteDispatchedMiddleware(reference.name);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (typeof prototype.throwableCaught === 'function') {
|
|
157
|
+
route = route.withAddedThrowableCaughtMiddleware(reference.name);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof prototype.processExiting === 'function') {
|
|
161
|
+
route = route.withAddedProcessExitingMiddleware(reference.name);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return route;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected buildArgument(options: ArgumentParameterOptions): ArgumentParameterContract {
|
|
169
|
+
return new ArgumentParameter(
|
|
170
|
+
options.name,
|
|
171
|
+
options.description,
|
|
172
|
+
options.cast ?? null,
|
|
173
|
+
options.mode ?? ArgumentMode.OPTIONAL,
|
|
174
|
+
options.valueMode ?? ArgumentValueMode.DEFAULT,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
protected buildOption(options: OptionParameterOptions): OptionParameterContract {
|
|
179
|
+
return new OptionParameter(
|
|
180
|
+
options.name,
|
|
181
|
+
options.description,
|
|
182
|
+
options.valueDisplayName ?? '',
|
|
183
|
+
options.cast ?? null,
|
|
184
|
+
options.defaultValue ?? '',
|
|
185
|
+
options.shortNames ?? [],
|
|
186
|
+
options.validValues ?? [],
|
|
187
|
+
[],
|
|
188
|
+
options.mode ?? OptionMode.OPTIONAL,
|
|
189
|
+
options.valueMode ?? OptionValueMode.DEFAULT,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
protected static defaultHandler(this: void): OutputContract {
|
|
194
|
+
return new Output();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export class CliRoutingServiceId {
|
|
11
11
|
static readonly RouterContract = 'Valkyrja.Cli.Routing.Dispatcher.RouterContract' as const;
|
|
12
12
|
static readonly RouteCollectionContract = 'Valkyrja.Cli.Routing.Collection.RouteCollectionContract' as const;
|
|
13
|
+
static readonly RouteCollectorContract = 'Valkyrja.Cli.Routing.Collector.RouteCollectorContract' as const;
|
|
13
14
|
static readonly RouteContract = 'Valkyrja.Cli.Routing.Data.RouteContract' as const;
|
|
14
15
|
static readonly CliRoutingData = 'Valkyrja.Cli.Routing.Data.CliRoutingData' as const;
|
|
15
16
|
static readonly CliRoutingConfig = 'Valkyrja.Cli.Routing.Data.CliRoutingConfig' as const;
|