@valkyrjaio/valkyrja 26.7.19 → 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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/src/Valkyrja/Application/Constant/ApplicationInfo.ts +2 -2
  3. package/src/Valkyrja/Application/README.md +484 -0
  4. package/src/Valkyrja/Cli/Interaction/Output/FileOutput.ts +12 -4
  5. package/src/Valkyrja/Cli/Interaction/Output/Output.ts +25 -14
  6. package/src/Valkyrja/Cli/Interaction/Output/StreamOutput.ts +5 -4
  7. package/src/Valkyrja/Cli/Interaction/Throwable/Exception/CliInteractionFileWriteException.ts +11 -0
  8. package/src/Valkyrja/Cli/README.md +655 -0
  9. package/src/Valkyrja/Cli/Routing/Data/Abstract/Parameter.ts +1 -0
  10. package/src/Valkyrja/Cli/Routing/Data/ArgumentParameter.ts +5 -1
  11. package/src/Valkyrja/Cli/Routing/Data/Contract/ParameterContract.ts +1 -0
  12. package/src/Valkyrja/Cli/Routing/Data/Contract/RouteContract.ts +4 -0
  13. package/src/Valkyrja/Cli/Routing/Data/OptionParameter.ts +5 -1
  14. package/src/Valkyrja/Cli/Routing/Data/Route.ts +32 -0
  15. package/src/Valkyrja/Cli/Routing/Dispatcher/Router.ts +7 -6
  16. package/src/Valkyrja/Cli/Server/Command/HelpCommand.ts +1 -1
  17. package/src/Valkyrja/Cli/Server/Command/ListBashCommand.ts +3 -2
  18. package/src/Valkyrja/Cli/Server/Command/ListCommand.ts +2 -3
  19. package/src/Valkyrja/Cli/Server/Handler/InputHandler.ts +193 -21
  20. package/src/Valkyrja/Cli/Server/Handler/SyncInputHandler.ts +20 -0
  21. package/src/Valkyrja/Cli/Server/Support/Exiter.ts +9 -0
  22. package/src/Valkyrja/Container/README.md +442 -0
  23. package/src/Valkyrja/Event/README.md +339 -0
  24. package/src/Valkyrja/Http/README.md +530 -0
  25. package/src/Valkyrja/Log/README.md +88 -0
  26. package/src/Valkyrja/Throwable/README.md +191 -0
  27. package/src/Valkyrja/Type/README.md +120 -0
  28. package/src/Valkyrja/Validation/README.md +241 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valkyrjaio/valkyrja",
3
- "version": "26.7.19",
3
+ "version": "26.8.0",
4
4
  "description": "The Valkyrja Framework.",
5
5
  "keywords": [
6
6
  "framework",
@@ -7,8 +7,8 @@
7
7
  */
8
8
 
9
9
  export class ApplicationInfo {
10
- static readonly VERSION = '26.7.19' as const;
11
- static readonly VERSION_BUILD_DATE_TIME = 'September 1 2026 09:26:38 MST' as const;
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 { ObjectFactory } from '../../../Type/Object/Factory/ObjectFactory.ts';
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 = ObjectFactory.clone(this);
34
+ const clone = this.cloneOutput();
33
35
  clone.filepath = filepath;
34
36
  return clone;
35
37
  }
36
38
 
37
- protected override outputMessage(_message: MessageContract): void {
38
- // TODO: Implement
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 = ObjectFactory.clone(this);
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 = ObjectFactory.clone(this);
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 = ObjectFactory.clone(this);
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 = ObjectFactory.clone(this);
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 || (this.quiet && this.exitCode === ExitCode.SUCCESS)) {
87
- return this;
84
+ if (!this.silent && !(this.quiet && this.exitCode === ExitCode.SUCCESS)) {
85
+ this.outputMessage(message);
88
86
  }
89
87
 
90
- this.outputMessage(message);
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 = ObjectFactory.clone(this);
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 = ObjectFactory.clone(this);
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 = ObjectFactory.clone(this);
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 = ObjectFactory.clone(this);
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 = ObjectFactory.clone(this);
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 = ObjectFactory.clone(this);
31
+ const clone = this.cloneOutput();
33
32
  clone.stream = stream;
34
33
  return clone;
35
34
  }
36
35
 
37
- protected override outputMessage(_message: MessageContract): void {
38
- // TODO: Implement
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 {}