@valkyrjaio/valkyrja 26.5.0 → 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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/Valkyrja/Application/Constant/ApplicationInfo.ts +2 -2
  3. package/src/Valkyrja/Cli/Routing/Attribute/ArgumentParameter.ts +23 -0
  4. package/src/Valkyrja/Cli/Routing/Attribute/OptionParameter.ts +23 -0
  5. package/src/Valkyrja/Cli/Routing/Attribute/Route/Middleware.ts +24 -0
  6. package/src/Valkyrja/Cli/Routing/Attribute/Route/Name.ts +29 -0
  7. package/src/Valkyrja/Cli/Routing/Attribute/Route/RouteHandler.ts +30 -0
  8. package/src/Valkyrja/Cli/Routing/Attribute/Route.ts +25 -0
  9. package/src/Valkyrja/Cli/Routing/Attribute/RouteAttributeMetadata.ts +227 -0
  10. package/src/Valkyrja/Cli/Routing/Attribute/RouteOptions.ts +66 -0
  11. package/src/Valkyrja/Cli/Routing/Collector/AttributeRouteCollector.ts +196 -0
  12. package/src/Valkyrja/Cli/Routing/Constant/CliRoutingServiceId.ts +1 -0
  13. package/src/Valkyrja/Cli/Routing/Provider/CliRoutingCliRouteProvider.ts +4 -0
  14. package/src/Valkyrja/Cli/Routing/Provider/CliRoutingServiceProvider.ts +26 -1
  15. package/src/Valkyrja/Cli/Routing/Provider/Contract/CliRouteProviderContract.ts +5 -0
  16. package/src/Valkyrja/Http/Message/Uri/Constant/Char.ts +58 -0
  17. package/src/Valkyrja/Http/Message/Uri/Factory/UriFactory.ts +63 -3
  18. package/src/Valkyrja/Http/Message/Uri/Uri.ts +3 -3
  19. package/src/Valkyrja/Http/Routing/Attribute/DynamicRoute.ts +28 -0
  20. package/src/Valkyrja/Http/Routing/Attribute/Route/Middleware.ts +24 -0
  21. package/src/Valkyrja/Http/Routing/Attribute/Route/Name.ts +29 -0
  22. package/src/Valkyrja/Http/Routing/Attribute/Route/Path.ts +29 -0
  23. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Any.ts +20 -0
  24. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Connect.ts +20 -0
  25. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Delete.ts +20 -0
  26. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Get.ts +20 -0
  27. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Head.ts +20 -0
  28. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Options.ts +20 -0
  29. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Patch.ts +20 -0
  30. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Post.ts +20 -0
  31. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Put.ts +20 -0
  32. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod/Trace.ts +20 -0
  33. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestMethod.ts +25 -0
  34. package/src/Valkyrja/Http/Routing/Attribute/Route/RequestStruct.ts +23 -0
  35. package/src/Valkyrja/Http/Routing/Attribute/Route/ResponseStruct.ts +23 -0
  36. package/src/Valkyrja/Http/Routing/Attribute/Route/RouteHandler.ts +30 -0
  37. package/src/Valkyrja/Http/Routing/Attribute/Route.ts +32 -0
  38. package/src/Valkyrja/Http/Routing/Attribute/RouteAttributeMetadata.ts +235 -0
  39. package/src/Valkyrja/Http/Routing/Attribute/RouteOptions.ts +61 -0
  40. package/src/Valkyrja/Http/Routing/Collector/AttributeRouteCollector.ts +237 -0
  41. package/src/Valkyrja/Http/Routing/Constant/Regex.ts +2 -0
  42. package/src/Valkyrja/Http/Routing/Provider/Contract/HttpRouteProviderContract.ts +5 -0
  43. package/src/Valkyrja/Http/Routing/Provider/HttpRoutingCliRouteProvider.ts +4 -0
  44. package/src/Valkyrja/Http/Routing/Provider/HttpRoutingServiceProvider.ts +32 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valkyrjaio/valkyrja",
3
- "version": "26.5.0",
3
+ "version": "26.6.0",
4
4
  "description": "The Valkyrja Framework.",
5
5
  "keywords": [
6
6
  "framework",
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  export class ApplicationInfo {
11
- static readonly VERSION = '26.5.0' as const;
12
- static readonly VERSION_BUILD_DATE_TIME = 'July 31 2026 08:59:22 MST' as const;
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;