@valkyrjaio/valkyrja 26.7.20 → 26.8.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/Application/README.md +484 -0
- package/src/Valkyrja/Cli/Interaction/Output/FileOutput.ts +12 -4
- package/src/Valkyrja/Cli/Interaction/Output/Output.ts +25 -14
- package/src/Valkyrja/Cli/Interaction/Output/StreamOutput.ts +5 -4
- package/src/Valkyrja/Cli/Interaction/Throwable/Exception/CliInteractionFileWriteException.ts +11 -0
- package/src/Valkyrja/Cli/README.md +655 -0
- package/src/Valkyrja/Cli/Routing/Data/Abstract/Parameter.ts +1 -0
- package/src/Valkyrja/Cli/Routing/Data/ArgumentParameter.ts +5 -1
- package/src/Valkyrja/Cli/Routing/Data/Contract/ParameterContract.ts +1 -0
- package/src/Valkyrja/Cli/Routing/Data/Contract/RouteContract.ts +4 -0
- package/src/Valkyrja/Cli/Routing/Data/OptionParameter.ts +5 -1
- package/src/Valkyrja/Cli/Routing/Data/Route.ts +32 -0
- package/src/Valkyrja/Cli/Routing/Dispatcher/Router.ts +7 -6
- package/src/Valkyrja/Cli/Server/Command/HelpCommand.ts +1 -1
- package/src/Valkyrja/Cli/Server/Command/ListBashCommand.ts +3 -2
- package/src/Valkyrja/Cli/Server/Command/ListCommand.ts +2 -3
- package/src/Valkyrja/Cli/Server/Handler/InputHandler.ts +193 -21
- package/src/Valkyrja/Cli/Server/Handler/SyncInputHandler.ts +20 -0
- package/src/Valkyrja/Cli/Server/Support/Exiter.ts +9 -0
- package/src/Valkyrja/Container/README.md +442 -0
- package/src/Valkyrja/Event/README.md +339 -0
- package/src/Valkyrja/Http/README.md +530 -0
- package/src/Valkyrja/Log/README.md +88 -0
- package/src/Valkyrja/Throwable/README.md +191 -0
- package/src/Valkyrja/Type/README.md +120 -0
- package/src/Valkyrja/Validation/README.md +241 -0
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export class ApplicationInfo {
|
|
10
|
-
static readonly VERSION = '26.
|
|
11
|
-
static readonly VERSION_BUILD_DATE_TIME = 'September
|
|
10
|
+
static readonly VERSION = '26.8.0' as const;
|
|
11
|
+
static readonly VERSION_BUILD_DATE_TIME = 'September 4 2026 09:10:02 MST' as const;
|
|
12
12
|
static readonly ASCII = `
|
|
13
13
|
_ _ _
|
|
14
14
|
/\\ /\\__ _| | | ___ _ _ __(_) __ _
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# The Application
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
The Application component boots the framework. An entry class takes a config
|
|
6
|
+
object, builds the container, registers every provider, and hands control to a
|
|
7
|
+
protocol handler.
|
|
8
|
+
|
|
9
|
+
The application holds no service itself. It holds the config, the container, and
|
|
10
|
+
the provider tree. See [Container](../Container/README.md) for the container.
|
|
11
|
+
|
|
12
|
+
## Entry points
|
|
13
|
+
|
|
14
|
+
The component ships three entry classes over one abstract base:
|
|
15
|
+
|
|
16
|
+
| Class | Runtime | Boots |
|
|
17
|
+
| :----------- | :-------------------------------- | :---------------- |
|
|
18
|
+
| `Http` | Node `http`, one boot per request | On each request |
|
|
19
|
+
| `WorkerHttp` | Node `http`, persistent | Once, at startup |
|
|
20
|
+
| `Cli` | A command-line process | Once, per process |
|
|
21
|
+
| `App` | The abstract base | — |
|
|
22
|
+
|
|
23
|
+
**This port ships no OpenSwoole, RoadRunner, or FrankenPHP entry.** The PHP
|
|
24
|
+
reference ships all three.
|
|
25
|
+
|
|
26
|
+
### HTTP
|
|
27
|
+
|
|
28
|
+
`Http.run()` opens a server and boots the application for each request:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { Http } from '@valkyrjaio/valkyrja/Application/Entry/Http.ts';
|
|
32
|
+
|
|
33
|
+
Http.run(new HttpConfig(), 3000);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Warning: `Http.handle()` calls `start()` on every request, so every request pays
|
|
37
|
+
the full boot cost. Use `WorkerHttp` for a process that serves more than one
|
|
38
|
+
request.
|
|
39
|
+
|
|
40
|
+
### CLI
|
|
41
|
+
|
|
42
|
+
`Cli.run()` boots once and runs the input handler:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Cli } from '@valkyrjaio/valkyrja/Application/Entry/Cli.ts';
|
|
46
|
+
|
|
47
|
+
Cli.run(new CliConfig());
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`Cli.getInput()` drops the first element of `process.argv`, because that element
|
|
51
|
+
is the interpreter path. The factory expects the vector to start with the
|
|
52
|
+
caller. See [Cli](../Cli/README.md) for the command-line syntax.
|
|
53
|
+
|
|
54
|
+
### The persistent worker
|
|
55
|
+
|
|
56
|
+
`WorkerHttp.run()` boots once, captures the container data, and builds a child
|
|
57
|
+
container for each request:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
static handle(
|
|
61
|
+
app: ApplicationContract,
|
|
62
|
+
data: ContainerData,
|
|
63
|
+
nodeRequest: IncomingMessage,
|
|
64
|
+
nodeResponse: ServerResponse,
|
|
65
|
+
): void {
|
|
66
|
+
const request = this.getRequest(nodeRequest);
|
|
67
|
+
const childContainer = this.getChildContainer(app, data);
|
|
68
|
+
const childApp = this.getChildApplication(app, childContainer);
|
|
69
|
+
|
|
70
|
+
this.bootstrapChildContainer(childApp, childContainer);
|
|
71
|
+
this.handleRequest(childContainer, request, nodeResponse);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The child container keeps request state out of the parent. See
|
|
76
|
+
[Container](../Container/README.md) for the resolution order and for what the
|
|
77
|
+
child copies.
|
|
78
|
+
|
|
79
|
+
`bootstrapParentServices()` resolves the route collection once in the parent, so
|
|
80
|
+
every child reads one collection.
|
|
81
|
+
|
|
82
|
+
### Customizing an entry class
|
|
83
|
+
|
|
84
|
+
Every method on `App` is `static`, and a subclass overrides the one it needs:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
export class AppHttp extends Http {
|
|
88
|
+
static override getThrowableHandler(): ThrowableHandlerContract {
|
|
89
|
+
return new AppThrowableHandler();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`createServer()` and `listen()` are runtime seams. A test substitutes a double
|
|
95
|
+
for each one, so no test opens a socket.
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
The application config is a class, and not a file. `ConfigContract` declares the
|
|
100
|
+
base properties:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
export interface ConfigContract {
|
|
104
|
+
readonly namespace: string;
|
|
105
|
+
readonly dir: string;
|
|
106
|
+
readonly version: string;
|
|
107
|
+
readonly environment: string;
|
|
108
|
+
readonly debugMode: boolean;
|
|
109
|
+
readonly timezone: string;
|
|
110
|
+
readonly key: string;
|
|
111
|
+
readonly dataPath: string;
|
|
112
|
+
readonly dataNamespace: string;
|
|
113
|
+
readonly providers: ComponentProviderContract[];
|
|
114
|
+
readonly callbacks: ((app: ApplicationContract) => void)[];
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Base properties
|
|
119
|
+
|
|
120
|
+
| Property | Default | Holds |
|
|
121
|
+
| :-------------- | :------------------------ | :------------------------------------------ |
|
|
122
|
+
| `namespace` | `'App'` | The application's own namespace |
|
|
123
|
+
| `dir` | `process.cwd()` | The base directory |
|
|
124
|
+
| `version` | `ApplicationInfo.VERSION` | The framework version |
|
|
125
|
+
| `environment` | `'production'` | The environment name |
|
|
126
|
+
| `debugMode` | `false` | Rebuild the data, and enable the handler |
|
|
127
|
+
| `timezone` | `'UTC'` | The value written to `process.env.TZ` |
|
|
128
|
+
| `key` | `'some_secret_app_key'` | The application key |
|
|
129
|
+
| `dataPath` | `'App/Provider/Data'` | Where the generated data lives |
|
|
130
|
+
| `dataNamespace` | `'App/Provider/Data'` | The namespace of the generated data |
|
|
131
|
+
| `providers` | One component provider | The root of the provider tree |
|
|
132
|
+
| `callbacks` | `[]` | Functions the application runs at bootstrap |
|
|
133
|
+
|
|
134
|
+
Warning: `key` has a default, and that default is not a secret. Set it in the
|
|
135
|
+
application config.
|
|
136
|
+
|
|
137
|
+
### The three config classes
|
|
138
|
+
|
|
139
|
+
| Class | Implements | Default root provider |
|
|
140
|
+
| :----------- | :------------------- | :---------------------------------------- |
|
|
141
|
+
| `Config` | `ConfigContract` | `ApplicationComponentProvider` |
|
|
142
|
+
| `HttpConfig` | `HttpConfigContract` | `HttpApplicationComponentProvider` |
|
|
143
|
+
| `CliConfig` | `CliConfigContract` | `CliWithHttpApplicationComponentProvider` |
|
|
144
|
+
|
|
145
|
+
`HttpConfig` and `CliConfig` each add the middleware lists for their protocol.
|
|
146
|
+
Each list holds binding keys, and not classes:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
export interface HttpConfigContract extends ConfigContract {
|
|
150
|
+
readonly requestReceivedMiddleware: string[];
|
|
151
|
+
readonly routeMatchedMiddleware: string[];
|
|
152
|
+
readonly routeNotMatchedMiddleware: string[];
|
|
153
|
+
readonly routeDispatchedMiddleware: string[];
|
|
154
|
+
readonly throwableCaughtMiddleware: string[];
|
|
155
|
+
readonly sendingResponseMiddleware: string[];
|
|
156
|
+
readonly responseSentMiddleware: string[];
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`CliConfigContract` adds `applicationName`, `defaultCommandName`, and its own
|
|
161
|
+
six lists. `CliConfig` ships a default for three of them:
|
|
162
|
+
|
|
163
|
+
| List | Default |
|
|
164
|
+
| :-------------------------- | :--------------------------------------------------------------------- |
|
|
165
|
+
| `inputReceivedMiddleware` | The help, version, and global interaction option middleware |
|
|
166
|
+
| `routeNotMatchedMiddleware` | `CheckCommandForTypoMiddleware` |
|
|
167
|
+
| `throwableCaughtMiddleware` | `LogThrowableCaughtMiddleware`, then `OutputThrowableCaughtMiddleware` |
|
|
168
|
+
|
|
169
|
+
`HttpConfig` ships an empty list for each of its seven. See
|
|
170
|
+
[Http](../Http/README.md) and [Cli](../Cli/README.md) for what each stage does.
|
|
171
|
+
|
|
172
|
+
### Your own config class
|
|
173
|
+
|
|
174
|
+
Extend a config class, or implement the contract:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
export class AppConfig extends HttpConfig {
|
|
178
|
+
constructor() {
|
|
179
|
+
super('App', process.cwd(), ApplicationInfo.VERSION, 'local', true);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Note that `HttpConfig` and `CliConfig` each carry a `static readonly id`. The
|
|
185
|
+
bootstrap reads that property and registers the config under it as well:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const concreteConfigId = (config.constructor as { id?: string }).id;
|
|
189
|
+
|
|
190
|
+
if (concreteConfigId !== undefined && concreteConfigId !== ApplicationServiceId.Config) {
|
|
191
|
+
container.setSingleton(concreteConfigId, config);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`Config` declares no `id`, so a plain `Config` registers under the two base keys
|
|
196
|
+
only.
|
|
197
|
+
|
|
198
|
+
**This port reads no environment file.** There is no `env()` helper and no
|
|
199
|
+
`.env` loader. The one environment write is the timezone:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
protected bootstrapTimezone(): void {
|
|
203
|
+
process.env['TZ'] = this.config.timezone;
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Config callbacks
|
|
208
|
+
|
|
209
|
+
The application runs each callback during bootstrap, after it registers the
|
|
210
|
+
config and the container:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
publishProviderCallbacks(): void {
|
|
214
|
+
for (const callback of this.config.callbacks) {
|
|
215
|
+
callback(this);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A callback takes the application, so it reaches the container through
|
|
221
|
+
`getContainer()`.
|
|
222
|
+
|
|
223
|
+
## The bootstrap sequence
|
|
224
|
+
|
|
225
|
+
`App.start()` runs four steps:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
static start(config: ConfigContract): ApplicationContract {
|
|
229
|
+
if (config.debugMode) {
|
|
230
|
+
this.defaultExceptionHandler();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.appStart();
|
|
234
|
+
this.directory(config.dir);
|
|
235
|
+
|
|
236
|
+
return this.app(config);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
1. **The debug handler.** `defaultExceptionHandler()` runs in debug mode. The
|
|
241
|
+
base method does nothing, and a subclass overrides it.
|
|
242
|
+
2. **The start time.** `appStart()` records `performance.now()` once. A second
|
|
243
|
+
call keeps the first value.
|
|
244
|
+
3. **The base directory.** `directory()` writes `config.dir` to
|
|
245
|
+
`Directory.basePath`.
|
|
246
|
+
4. **The application.** `app()` builds the container, builds the `Valkyrja`
|
|
247
|
+
application, and registers the services.
|
|
248
|
+
|
|
249
|
+
`bootstrapServices()` then does the rest:
|
|
250
|
+
|
|
251
|
+
1. Register the config under `ConfigContract`, under `Config`, and under the
|
|
252
|
+
concrete id.
|
|
253
|
+
2. Register the container under `ContainerServiceId.Contract`.
|
|
254
|
+
3. Register the application under `ApplicationServiceId.ApplicationContract`.
|
|
255
|
+
4. Run the config callbacks.
|
|
256
|
+
5. Load the container data.
|
|
257
|
+
|
|
258
|
+
`loadContainerData()` publishes the data when the container does not hold it
|
|
259
|
+
already, then merges it:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
static loadContainerData(container: ContainerContract): void {
|
|
263
|
+
if (!container.isSingleton(ContainerServiceId.Data)) {
|
|
264
|
+
this.publishContainerData(container);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const containerData = container.getSingleton<ContainerData>(ContainerServiceId.Data);
|
|
268
|
+
|
|
269
|
+
container.setFromData(containerData);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## The application
|
|
274
|
+
|
|
275
|
+
`ApplicationContract` declares ten methods:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
export interface ApplicationContract {
|
|
279
|
+
getContainer(): ContainerContract;
|
|
280
|
+
publishProviderCallbacks(): void;
|
|
281
|
+
getProviders(): ComponentProviderContract[];
|
|
282
|
+
getContainerProviders(): ServiceProviderContract[];
|
|
283
|
+
getEventProviders(): ListenerProviderContract[];
|
|
284
|
+
getCliProviders(): CliRouteProviderContract[];
|
|
285
|
+
getHttpProviders(): HttpRouteProviderContract[];
|
|
286
|
+
getDebugMode(): boolean;
|
|
287
|
+
getEnvironment(): string;
|
|
288
|
+
getVersion(): string;
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`Valkyrja` is the implementation. It caches each provider list, so a second call
|
|
293
|
+
for a list that holds one entry or more does no work.
|
|
294
|
+
|
|
295
|
+
Warning: each cache guard tests the list for a length. A list that resolves to
|
|
296
|
+
empty therefore has no cached state, and every later call builds it again. An
|
|
297
|
+
application that registers no listener provider rebuilds the event provider list
|
|
298
|
+
on each `getEventProviders()` call.
|
|
299
|
+
|
|
300
|
+
`ChildApplication` wraps a parent and one container. Every method delegates to
|
|
301
|
+
the parent, and `getContainer()` returns the child container.
|
|
302
|
+
|
|
303
|
+
## The provider hierarchy
|
|
304
|
+
|
|
305
|
+
A component provider names the providers for one component:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
export interface ComponentProviderContract {
|
|
309
|
+
getComponentProviders(app: ApplicationContract): ComponentProviderContract[];
|
|
310
|
+
getContainerProviders(app: ApplicationContract): ServiceProviderContract[];
|
|
311
|
+
getEventProviders(app: ApplicationContract): ListenerProviderContract[];
|
|
312
|
+
getCliProviders(app: ApplicationContract): CliRouteProviderContract[];
|
|
313
|
+
getHttpProviders(app: ApplicationContract): HttpRouteProviderContract[];
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
| Method | Returns |
|
|
318
|
+
| :------------------------ | :------------------------------------------------------------- |
|
|
319
|
+
| `getComponentProviders()` | The component providers this one depends on |
|
|
320
|
+
| `getContainerProviders()` | The service providers, see [Container](../Container/README.md) |
|
|
321
|
+
| `getEventProviders()` | The listener providers, see [Event](../Event/README.md) |
|
|
322
|
+
| `getCliProviders()` | The CLI route providers, see [Cli](../Cli/README.md) |
|
|
323
|
+
| `getHttpProviders()` | The HTTP route providers, see [Http](../Http/README.md) |
|
|
324
|
+
|
|
325
|
+
### Loading order
|
|
326
|
+
|
|
327
|
+
`getProviders()` walks the tree depth first. A child provider lands in the list
|
|
328
|
+
before its parent:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
protected collectProviders(provider: ComponentProviderContract): void {
|
|
332
|
+
for (const subProvider of provider.getComponentProviders(this)) {
|
|
333
|
+
this.collectProviders(subProvider);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this.providers.push(provider);
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
A component that another component depends on therefore registers first.
|
|
341
|
+
|
|
342
|
+
Each of the four typed lists is flattened and deduplicated:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
this.serviceProviders = [...new Set(providers.flat())];
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Warning: the `Set` removes a duplicate object reference, and it does not remove
|
|
349
|
+
two separate instances of one provider class. Two providers that each return
|
|
350
|
+
`new ContainerComponentProvider()` produce two entries.
|
|
351
|
+
|
|
352
|
+
### The built-in component providers
|
|
353
|
+
|
|
354
|
+
| Class | Returns |
|
|
355
|
+
| :---------------------------------------- | :-------------------------------------------------------------------------------------- |
|
|
356
|
+
| `ApplicationComponentProvider` | Container and Event |
|
|
357
|
+
| `HttpApplicationComponentProvider` | Container, and the HTTP message, middleware, routing, routing CLI, and server providers |
|
|
358
|
+
| `CliApplicationComponentProvider` | Container and the four CLI components |
|
|
359
|
+
| `CliWithHttpApplicationComponentProvider` | The CLI set, and the HTTP routing CLI provider |
|
|
360
|
+
|
|
361
|
+
Each of the last three extends `ApplicationComponentProvider` and overrides
|
|
362
|
+
`getComponentProviders()`.
|
|
363
|
+
|
|
364
|
+
Note that `PublishableComponentProviderContract` is declared, and nothing in
|
|
365
|
+
this port implements or reads it.
|
|
366
|
+
|
|
367
|
+
### Writing a component provider
|
|
368
|
+
|
|
369
|
+
Return the providers for your component, and return an empty array for each kind
|
|
370
|
+
your component does not use:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
export class AppComponentProvider implements ComponentProviderContract {
|
|
374
|
+
getComponentProviders(_app: ApplicationContract): ComponentProviderContract[] {
|
|
375
|
+
return [new HttpApplicationComponentProvider()];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getContainerProviders(_app: ApplicationContract): ServiceProviderContract[] {
|
|
379
|
+
return [new AppServiceProvider()];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
getEventProviders(_app: ApplicationContract): ListenerProviderContract[] {
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
getCliProviders(_app: ApplicationContract): CliRouteProviderContract[] {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
getHttpProviders(_app: ApplicationContract): HttpRouteProviderContract[] {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Directories
|
|
397
|
+
|
|
398
|
+
`Directory` holds a static path segment for each directory, and a method that
|
|
399
|
+
joins it onto the base path:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
Directory.basePath = config.dir;
|
|
403
|
+
|
|
404
|
+
Directory.storageDirectory('logs/app.log');
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
| Method | Reads |
|
|
408
|
+
| :--------------------------------- | :------------------------ |
|
|
409
|
+
| `baseDirectory()` | `basePath` |
|
|
410
|
+
| `appDirectory()` | `app` |
|
|
411
|
+
| `dataDirectory()` | `data` |
|
|
412
|
+
| `envDirectory()` | `env` |
|
|
413
|
+
| `publicDirectory()` | `public` |
|
|
414
|
+
| `resourcesDirectory()` | `resources` |
|
|
415
|
+
| `srcDirectory()` | `src` |
|
|
416
|
+
| `storageDirectory()` | `storage` |
|
|
417
|
+
| `frameworkStorageDirectory()` | `storage/framework` |
|
|
418
|
+
| `frameworkStorageCacheDirectory()` | `storage/framework/cache` |
|
|
419
|
+
| `logsStorageDirectory()` | `storage/logs` |
|
|
420
|
+
| `testsDirectory()` | `tests` |
|
|
421
|
+
| `vendorDirectory()` | `node_modules` |
|
|
422
|
+
|
|
423
|
+
Each static segment is writable, so an application changes one at bootstrap.
|
|
424
|
+
`path()` adds a leading `/` when the argument does not start with one.
|
|
425
|
+
|
|
426
|
+
Note that `vendorPath` is `node_modules`, and the PHP reference uses `vendor`.
|
|
427
|
+
|
|
428
|
+
## Framework information
|
|
429
|
+
|
|
430
|
+
`ApplicationInfo` holds four constants:
|
|
431
|
+
|
|
432
|
+
| Constant | Holds |
|
|
433
|
+
| :------------------------ | :-------------------------------------- |
|
|
434
|
+
| `VERSION` | The released version of the framework |
|
|
435
|
+
| `VERSION_BUILD_DATE_TIME` | When the release job built that version |
|
|
436
|
+
| `ASCII` | The Valkyrja word mark, as ASCII art |
|
|
437
|
+
| `ICON` | The Valkyrja icon, as block characters |
|
|
438
|
+
|
|
439
|
+
The release job rewrites `VERSION` and `VERSION_BUILD_DATE_TIME`. Read
|
|
440
|
+
`getVersion()` on the application instead of the constant, because the config
|
|
441
|
+
holds the version the application runs under.
|
|
442
|
+
|
|
443
|
+
## Debug mode
|
|
444
|
+
|
|
445
|
+
`debugMode` changes six things:
|
|
446
|
+
|
|
447
|
+
- `App.start()` calls `defaultExceptionHandler()`.
|
|
448
|
+
- The entry point registers a throwable handler. See
|
|
449
|
+
[Throwable](../Throwable/README.md).
|
|
450
|
+
- `EventServiceProvider` rebuilds the listener data from the providers, instead
|
|
451
|
+
of loading the cached data. See [Event](../Event/README.md).
|
|
452
|
+
- `HttpRoutingServiceProvider` rebuilds the HTTP route data the same way. See
|
|
453
|
+
[Http](../Http/README.md).
|
|
454
|
+
- `CliRoutingServiceProvider` rebuilds the CLI route data the same way. See
|
|
455
|
+
[Cli](../Cli/README.md).
|
|
456
|
+
- `HttpServerServiceProvider` passes the flag into `RequestHandler`, and the
|
|
457
|
+
handler then rethrows a throwable instead of building a response. See
|
|
458
|
+
[Http](../Http/README.md).
|
|
459
|
+
|
|
460
|
+
Warning: route data is not loaded from the cache in debug mode. Both routing
|
|
461
|
+
components build it again on each boot.
|
|
462
|
+
|
|
463
|
+
Warning: keep `debugMode` false in production. A debug boot walks every provider
|
|
464
|
+
on each start.
|
|
465
|
+
|
|
466
|
+
## Container bindings
|
|
467
|
+
|
|
468
|
+
| Id | Holds |
|
|
469
|
+
| :----------------------------------------- | :------------------------- |
|
|
470
|
+
| `ApplicationServiceId.ApplicationContract` | The application |
|
|
471
|
+
| `ApplicationServiceId.ConfigContract` | The config |
|
|
472
|
+
| `ApplicationServiceId.Config` | The config |
|
|
473
|
+
| `ApplicationServiceId.HttpConfigContract` | An `HttpConfig`, when used |
|
|
474
|
+
| `ApplicationServiceId.CliConfigContract` | A `CliConfig`, when used |
|
|
475
|
+
|
|
476
|
+
The entry point registers each of these directly. No service provider publishes
|
|
477
|
+
them.
|
|
478
|
+
|
|
479
|
+
## Exceptions
|
|
480
|
+
|
|
481
|
+
`ApplicationRuntimeException` and `ApplicationInvalidArgumentException` are the
|
|
482
|
+
abstract bases. Both implement `ApplicationThrowable`. This port declares no
|
|
483
|
+
concrete application exception. See [Throwable](../Throwable/README.md) for the
|
|
484
|
+
hierarchy.
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* Released under the MIT License. See LICENSE.md for details.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { appendFileSync } from 'node:fs';
|
|
10
|
+
|
|
9
11
|
import type { MessageContract } from '../Message/Contract/MessageContract.ts';
|
|
10
12
|
import type { FileOutputContract } from './Contract/FileOutputContract.ts';
|
|
11
13
|
import { ExitCode } from '../Enum/ExitCode.ts';
|
|
12
14
|
import { Output } from './Output.ts';
|
|
13
|
-
import {
|
|
15
|
+
import { CliInteractionFileWriteException } from '../Throwable/Exception/CliInteractionFileWriteException.ts';
|
|
14
16
|
|
|
15
17
|
export class FileOutput extends Output implements FileOutputContract {
|
|
16
18
|
constructor(
|
|
@@ -29,12 +31,18 @@ export class FileOutput extends Output implements FileOutputContract {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
withFilepath(filepath: string): this {
|
|
32
|
-
const clone =
|
|
34
|
+
const clone = this.cloneOutput();
|
|
33
35
|
clone.filepath = filepath;
|
|
34
36
|
return clone;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
protected override outputMessage(
|
|
38
|
-
|
|
39
|
+
protected override outputMessage(message: MessageContract): void {
|
|
40
|
+
try {
|
|
41
|
+
appendFileSync(this.filepath, message.getFormattedText());
|
|
42
|
+
} catch (throwable) {
|
|
43
|
+
throw new CliInteractionFileWriteException(`Unable to write to the file \`${this.filepath}\``, {
|
|
44
|
+
cause: throwable,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
}
|
|
@@ -50,25 +50,25 @@ export class Output implements OutputContract {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
withMessages(...messages: MessageContract[]): this {
|
|
53
|
-
const clone =
|
|
53
|
+
const clone = this.cloneOutput();
|
|
54
54
|
clone.unwrittenMessages = messages;
|
|
55
55
|
return clone;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
withAddedMessages(...messages: MessageContract[]): this {
|
|
59
|
-
const clone =
|
|
59
|
+
const clone = this.cloneOutput();
|
|
60
60
|
clone.unwrittenMessages = [...this.unwrittenMessages, ...messages];
|
|
61
61
|
return clone;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
withAddedMessage(message: MessageContract): this {
|
|
65
|
-
const clone =
|
|
65
|
+
const clone = this.cloneOutput();
|
|
66
66
|
clone.unwrittenMessages = [...this.unwrittenMessages, message];
|
|
67
67
|
return clone;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
writeMessages(): this {
|
|
71
|
-
let clone =
|
|
71
|
+
let clone = this.cloneOutput();
|
|
72
72
|
const unwrittenMessages = this.unwrittenMessages;
|
|
73
73
|
clone.unwrittenMessages = [];
|
|
74
74
|
|
|
@@ -80,14 +80,12 @@ export class Output implements OutputContract {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
writeMessage(message: MessageContract): this {
|
|
83
|
-
this.setMessageAsWritten(message);
|
|
84
|
-
|
|
85
83
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
86
|
-
if (this.silent
|
|
87
|
-
|
|
84
|
+
if (!this.silent && !(this.quiet && this.exitCode === ExitCode.SUCCESS)) {
|
|
85
|
+
this.outputMessage(message);
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
this.
|
|
88
|
+
this.setMessageAsWritten(message);
|
|
91
89
|
|
|
92
90
|
return this;
|
|
93
91
|
}
|
|
@@ -97,7 +95,7 @@ export class Output implements OutputContract {
|
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
withWriters(...writers: WriterContract[]): this {
|
|
100
|
-
const clone =
|
|
98
|
+
const clone = this.cloneOutput();
|
|
101
99
|
clone.writers = writers;
|
|
102
100
|
return clone;
|
|
103
101
|
}
|
|
@@ -107,7 +105,7 @@ export class Output implements OutputContract {
|
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
withIsInteractive(isInteractive: boolean): this {
|
|
110
|
-
const clone =
|
|
108
|
+
const clone = this.cloneOutput();
|
|
111
109
|
clone.interactive = isInteractive;
|
|
112
110
|
return clone;
|
|
113
111
|
}
|
|
@@ -117,7 +115,7 @@ export class Output implements OutputContract {
|
|
|
117
115
|
}
|
|
118
116
|
|
|
119
117
|
withIsQuiet(isQuiet: boolean): this {
|
|
120
|
-
const clone =
|
|
118
|
+
const clone = this.cloneOutput();
|
|
121
119
|
clone.quiet = isQuiet;
|
|
122
120
|
return clone;
|
|
123
121
|
}
|
|
@@ -127,7 +125,7 @@ export class Output implements OutputContract {
|
|
|
127
125
|
}
|
|
128
126
|
|
|
129
127
|
withIsSilent(isSilent: boolean): this {
|
|
130
|
-
const clone =
|
|
128
|
+
const clone = this.cloneOutput();
|
|
131
129
|
clone.silent = isSilent;
|
|
132
130
|
return clone;
|
|
133
131
|
}
|
|
@@ -137,7 +135,7 @@ export class Output implements OutputContract {
|
|
|
137
135
|
}
|
|
138
136
|
|
|
139
137
|
withExitCode(exitCode: ExitCode | number): this {
|
|
140
|
-
const clone =
|
|
138
|
+
const clone = this.cloneOutput();
|
|
141
139
|
clone.exitCode = exitCode;
|
|
142
140
|
return clone;
|
|
143
141
|
}
|
|
@@ -156,6 +154,19 @@ export class Output implements OutputContract {
|
|
|
156
154
|
this.writtenMessages.push(message);
|
|
157
155
|
}
|
|
158
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Copy this output, giving the copy message lists of its own.
|
|
159
|
+
*
|
|
160
|
+
* A write on the copy reaches no list this output holds, so a failed write leaves this
|
|
161
|
+
* output counting no message it never wrote.
|
|
162
|
+
*/
|
|
163
|
+
protected cloneOutput(): this {
|
|
164
|
+
const clone = ObjectFactory.clone(this);
|
|
165
|
+
clone.unwrittenMessages = [...this.unwrittenMessages];
|
|
166
|
+
clone.writtenMessages = [...this.writtenMessages];
|
|
167
|
+
return clone;
|
|
168
|
+
}
|
|
169
|
+
|
|
159
170
|
protected outputMessage(message: MessageContract): void {
|
|
160
171
|
process.stdout.write(message.getFormattedText());
|
|
161
172
|
}
|
|
@@ -10,7 +10,6 @@ import type { MessageContract } from '../Message/Contract/MessageContract.ts';
|
|
|
10
10
|
import type { StreamOutputContract } from './Contract/StreamOutputContract.ts';
|
|
11
11
|
import { ExitCode } from '../Enum/ExitCode.ts';
|
|
12
12
|
import { Output } from './Output.ts';
|
|
13
|
-
import { ObjectFactory } from '../../../Type/Object/Factory/ObjectFactory.ts';
|
|
14
13
|
|
|
15
14
|
export class StreamOutput extends Output implements StreamOutputContract {
|
|
16
15
|
constructor(
|
|
@@ -29,12 +28,14 @@ export class StreamOutput extends Output implements StreamOutputContract {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
withStream(stream: NodeJS.WritableStream): this {
|
|
32
|
-
const clone =
|
|
31
|
+
const clone = this.cloneOutput();
|
|
33
32
|
clone.stream = stream;
|
|
34
33
|
return clone;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
protected override outputMessage(
|
|
38
|
-
//
|
|
36
|
+
protected override outputMessage(message: MessageContract): void {
|
|
37
|
+
// A Node writable reports a failed write on an 'error' event rather than to this caller,
|
|
38
|
+
// so the application attaches that listener before it hands the stream over.
|
|
39
|
+
this.stream.write(message.getFormattedText());
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja Framework package.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2016-present Melech Mizrachi
|
|
5
|
+
*
|
|
6
|
+
* Released under the MIT License. See LICENSE.md for details.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { CliInteractionRuntimeException } from './Abstract/CliInteractionRuntimeException.ts';
|
|
10
|
+
|
|
11
|
+
export class CliInteractionFileWriteException extends CliInteractionRuntimeException {}
|