@effected/cli 0.23.0 → 0.24.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/CliEnv.js +15 -3
- package/CliFormat.js +71 -16
- package/CliInteractive.js +28 -4
- package/CliPrompt.js +7 -0
- package/CliRuntime.js +10 -1
- package/CliTest.js +134 -22
- package/ConfigIssueRenderer.js +8 -2
- package/Fmt.js +94 -22
- package/index.d.ts +183 -30
- package/internal/HelpRouting.js +21 -75
- package/internal/format.js +74 -3
- package/internal/logSafety.js +1 -1
- package/internal/terminalCapabilities.js +23 -0
- package/package.json +8 -8
- package/testing.d.ts +68 -5
- package/ui/CliUi.js +4 -1
- package/ui.d.ts +4 -1
- package/internal/causeMessage.js +0 -57
package/testing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Effect, FileSystem, Layer, Path, PlatformError, Scope, Terminal } from "effect";
|
|
2
|
-
import { ChildProcessSpawner } from "effect/process";
|
|
1
|
+
import { Duration, Effect, FileSystem, Layer, Path, PlatformError, Scope, Stream, Terminal } from "effect";
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
3
|
/**
|
|
4
4
|
* A hermetic temp directory minted by {@link CliTest.sandbox}, removed when its
|
|
5
5
|
* scope closes.
|
|
@@ -39,6 +39,12 @@ interface RunOptions {
|
|
|
39
39
|
* the child gets an already-ended empty input, never an open pipe.
|
|
40
40
|
*/
|
|
41
41
|
readonly stdin?: string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* How long a child has to exit after `SIGTERM` (or the signal `kill` sends), when its scope closes or
|
|
44
|
+
* {@link SpawnedBin.kill} is called, before it is sent `SIGKILL`; `2 seconds` by default. A finite duration: a
|
|
45
|
+
* child that ignores the signal would otherwise keep the scope from closing forever, and leave the process behind.
|
|
46
|
+
*/
|
|
47
|
+
readonly forceKillAfter?: Duration.Input | undefined;
|
|
42
48
|
}
|
|
43
49
|
/**
|
|
44
50
|
* What a spawned bin did, as data: a non-zero exit is a result, not a failure.
|
|
@@ -46,13 +52,46 @@ interface RunOptions {
|
|
|
46
52
|
* @public
|
|
47
53
|
*/
|
|
48
54
|
interface RunResult {
|
|
49
|
-
/** The child's exit code. */
|
|
50
|
-
readonly exitCode: number;
|
|
55
|
+
/** The child's exit code, or `null` when a signal ended it. */
|
|
56
|
+
readonly exitCode: number | null;
|
|
57
|
+
/** The signal that ended the child, or `null` when it exited with a code. */
|
|
58
|
+
readonly signal: ChildProcess.Signal | null;
|
|
51
59
|
/** Everything the child wrote to stdout, decoded as UTF-8. */
|
|
52
60
|
readonly stdout: string;
|
|
53
61
|
/** Everything the child wrote to stderr, decoded as UTF-8. */
|
|
54
62
|
readonly stderr: string;
|
|
55
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* A bin {@link CliTest.spawn} started and has not waited for: its output as it arrives, a way to signal it, and its
|
|
66
|
+
* exit.
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
interface SpawnedBin {
|
|
71
|
+
/**
|
|
72
|
+
* The child's stdout as lines, each without its line break (`\r\n` included), from the first byte it wrote,
|
|
73
|
+
* ending when the child closes stdout; a last line with no line break comes when it does. Every subscription replays
|
|
74
|
+
* from the start, so a test may read it more than once, and output a test never reads is still drained, so the
|
|
75
|
+
* child never blocks on a full pipe.
|
|
76
|
+
*/
|
|
77
|
+
readonly stdout: Stream.Stream<string>;
|
|
78
|
+
/** The child's stderr as lines, as `stdout` gives stdout. */
|
|
79
|
+
readonly stderr: Stream.Stream<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Send `signal` (`SIGTERM` by default) and wait for the child to exit. A child still running after
|
|
82
|
+
* {@link RunOptions.forceKillAfter} is sent `SIGKILL`, so this always ends. A child that has already exited, on its
|
|
83
|
+
* own or at the same moment, is not an error: this returns normally, and `exitCode` and `result` keep reporting how
|
|
84
|
+
* it ended. A kill that fails on a child still running fails with that `PlatformError`.
|
|
85
|
+
*/
|
|
86
|
+
readonly kill: (signal?: ChildProcess.Signal) => Effect.Effect<void, PlatformError.PlatformError>;
|
|
87
|
+
/**
|
|
88
|
+
* The child's exit code once it exits, or `null` when a signal ended it (the default `kill` included): `result`
|
|
89
|
+
* names the signal. Re-reading it after the exit answers at once.
|
|
90
|
+
*/
|
|
91
|
+
readonly exitCode: Effect.Effect<number | null, PlatformError.PlatformError>;
|
|
92
|
+
/** Wait for the exit and the end of both streams, and read everything, as {@link CliTest.run} returns it. */
|
|
93
|
+
readonly result: Effect.Effect<RunResult, PlatformError.PlatformError>;
|
|
94
|
+
}
|
|
56
95
|
/**
|
|
57
96
|
* Spawn a built CLI bin hermetically and read its exit code and streams as data.
|
|
58
97
|
*
|
|
@@ -103,6 +142,30 @@ export declare class CliTest {
|
|
|
103
142
|
* ends it.
|
|
104
143
|
*/
|
|
105
144
|
static readonly run: (bin: string, args: ReadonlyArray<string>, options: RunOptions) => Effect.Effect<RunResult, PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner>;
|
|
145
|
+
/**
|
|
146
|
+
* Start `execPath bin ...args` and return at once with a handle on it: its output as lines while it runs, a signal,
|
|
147
|
+
* and its exit, for a long-running command (a `--follow`, a watch mode, a server) that {@link CliTest.run} would wait
|
|
148
|
+
* on forever.
|
|
149
|
+
*
|
|
150
|
+
* @remarks
|
|
151
|
+
* The child lives in the caller's scope: when the scope closes first, the child is sent `SIGTERM`, then `SIGKILL`
|
|
152
|
+
* after `forceKillAfter` (`2 seconds` by default), so a test that ends early leaves nothing running and never waits
|
|
153
|
+
* on a child that ignores the signal. A child a signal ended reports `exitCode: null` and the signal in `result`. Both streams are drained from the start whether or not the test reads them, so a
|
|
154
|
+
* chatty child never blocks on a full pipe, and each stream replays from its first line to every reader. The
|
|
155
|
+
* options are {@link CliTest.run}'s, `stdin` included: the child gets that text, then an ended input.
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* const child = yield* CliTest.spawn("dist/bin.js", ["logs", "--follow"], { sandbox, execPath: process.execPath })
|
|
159
|
+
* yield* Stream.runHead(child.stdout) // the first line: it is following
|
|
160
|
+
* yield* child.kill("SIGINT")
|
|
161
|
+
* assert.strictEqual(yield* child.exitCode, 0)
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* @param bin - the built bin to run
|
|
165
|
+
* @param args - its arguments
|
|
166
|
+
* @param options - the sandbox, the node binary, and the working directory, environment and stdin
|
|
167
|
+
*/
|
|
168
|
+
static readonly spawn: (bin: string, args: ReadonlyArray<string>, options: RunOptions) => Effect.Effect<SpawnedBin, PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope>;
|
|
106
169
|
}
|
|
107
170
|
/**
|
|
108
171
|
* One key press for {@link TestTerminal}.
|
|
@@ -172,5 +235,5 @@ export declare class TestTerminal {
|
|
|
172
235
|
readonly rows?: number | undefined;
|
|
173
236
|
}) => Effect.Effect<TestTerminalHandle>;
|
|
174
237
|
}
|
|
175
|
-
export type { KeyInput, RunOptions, RunResult, Sandbox, TestTerminalHandle };
|
|
238
|
+
export type { KeyInput, RunOptions, RunResult, Sandbox, SpawnedBin, TestTerminalHandle };
|
|
176
239
|
//# sourceMappingURL=testing.d.ts.map
|
package/ui/CliUi.js
CHANGED
|
@@ -384,7 +384,10 @@ var CliUi = class CliUi {
|
|
|
384
384
|
* It runs during parsing, whose environment is core's alone, so it reads `CliTheme` if one is there: with
|
|
385
385
|
* `CliRuntime.main`'s `env` (`CliEnv.layer`), or provided around the program. With no theme it treats the run as
|
|
386
386
|
* not interactive, and when `CliInteractive` is on it says so once, at debug level. Interactivity is
|
|
387
|
-
* `CliInteractive`, which an audience flag can set before parsing under `CliAudience`.
|
|
387
|
+
* `CliInteractive`, which an audience flag can set before parsing under `CliAudience`. Core calls a fallback only
|
|
388
|
+
* when its `TerminalCapabilities.canPrompt` is `true`; `CliInteractive.layer` and `layerTest` hold it there so that
|
|
389
|
+
* `otherwise` survives a piped run, and a `canPrompt: false` provided inside the program bypasses this fallback,
|
|
390
|
+
* as it does `CliPrompt.fallback`.
|
|
388
391
|
*
|
|
389
392
|
* As with `CliPrompt.fallback`, the screen runs here rather than being handed to core, whose fallback runner
|
|
390
393
|
* turns a quit into the missing-parameter error, which would exit `64`. A cancel (Esc, Ctrl-C) is `Cancelled`,
|
package/ui.d.ts
CHANGED
|
@@ -687,7 +687,10 @@ export declare class CliUi {
|
|
|
687
687
|
* It runs during parsing, whose environment is core's alone, so it reads `CliTheme` if one is there: with
|
|
688
688
|
* `CliRuntime.main`'s `env` (`CliEnv.layer`), or provided around the program. With no theme it treats the run as
|
|
689
689
|
* not interactive, and when `CliInteractive` is on it says so once, at debug level. Interactivity is
|
|
690
|
-
* `CliInteractive`, which an audience flag can set before parsing under `CliAudience`.
|
|
690
|
+
* `CliInteractive`, which an audience flag can set before parsing under `CliAudience`. Core calls a fallback only
|
|
691
|
+
* when its `TerminalCapabilities.canPrompt` is `true`; `CliInteractive.layer` and `layerTest` hold it there so that
|
|
692
|
+
* `otherwise` survives a piped run, and a `canPrompt: false` provided inside the program bypasses this fallback,
|
|
693
|
+
* as it does `CliPrompt.fallback`.
|
|
691
694
|
*
|
|
692
695
|
* As with `CliPrompt.fallback`, the screen runs here rather than being handed to core, whose fallback runner
|
|
693
696
|
* turns a quit into the missing-parameter error, which would exit `64`. A cancel (Esc, Ctrl-C) is `Cancelled`,
|
package/internal/causeMessage.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { Predicate } from "effect";
|
|
2
|
-
|
|
3
|
-
// src/internal/causeMessage.ts
|
|
4
|
-
/** How many nodes of a cause chain the walk reads before stopping, so a cyclic chain cannot spin. */
|
|
5
|
-
const CAUSE_DEPTH = 8;
|
|
6
|
-
/**
|
|
7
|
-
* The message a single node carries: a thrown string, or a non-empty string `message` property. A getter or
|
|
8
|
-
* proxy trap that throws is a node with no message.
|
|
9
|
-
*/
|
|
10
|
-
const messageAt = (node) => {
|
|
11
|
-
try {
|
|
12
|
-
const text = Predicate.isString(node) ? node : Predicate.hasProperty(node, "message") && Predicate.isString(node.message) ? node.message : void 0;
|
|
13
|
-
return text === void 0 || text.trim() === "" ? void 0 : text;
|
|
14
|
-
} catch {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
/** The next link of the chain: a node's `cause`, or `undefined` when it has none or reading it throws. */
|
|
19
|
-
const causeAt = (node) => {
|
|
20
|
-
try {
|
|
21
|
-
return Predicate.hasProperty(node, "cause") ? node.cause : void 0;
|
|
22
|
-
} catch {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Every distinct non-empty message a preserved cause chain carries, nearest
|
|
28
|
-
* first and joined with `: `, or `undefined` when nothing along it has one.
|
|
29
|
-
*
|
|
30
|
-
* A `Schema.Defect()` field holds whatever was thrown, so the read is guarded
|
|
31
|
-
* rather than assumed: `Predicate.hasProperty` + `Predicate.isString` accepts
|
|
32
|
-
* an `Error`, a cross-realm `Error` that fails `instanceof`, a plain
|
|
33
|
-
* `{ message }` object and a thrown string alike. An empty or whitespace-only
|
|
34
|
-
* message counts as none. The walk follows `cause` links because a wrapper's
|
|
35
|
-
* text is often the generic summary and the driver's text underneath is the
|
|
36
|
-
* actionable detail, yet a wrapper that composes its own context must not
|
|
37
|
-
* lose it either; so nothing is dropped, and a deeper text the nearer one
|
|
38
|
-
* already contains is not repeated. The walk is bounded, so a cycle terminates.
|
|
39
|
-
*
|
|
40
|
-
* It never throws: every property read is guarded, so a `message` getter that throws leaves that link
|
|
41
|
-
* without text, and a `cause` getter that throws ends the walk with what was found so far.
|
|
42
|
-
*/
|
|
43
|
-
const causeMessage = (cause) => {
|
|
44
|
-
let message;
|
|
45
|
-
let node = cause;
|
|
46
|
-
for (let depth = 0; depth < CAUSE_DEPTH && node !== void 0 && node !== null; depth++) {
|
|
47
|
-
const text = messageAt(node);
|
|
48
|
-
if (text !== void 0) {
|
|
49
|
-
if (message === void 0 || text.includes(message)) message = text;
|
|
50
|
-
else if (!message.includes(text)) message = `${message}: ${text}`;
|
|
51
|
-
}
|
|
52
|
-
node = causeAt(node);
|
|
53
|
-
}
|
|
54
|
-
return message;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export { causeMessage };
|