@effected/cli-kit 0.0.0-stage → 0.27.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/Cancelled.js +43 -0
- package/CliAudience.js +177 -0
- package/CliColor.js +66 -0
- package/CliEnv.js +101 -0
- package/CliExit.js +117 -0
- package/CliFailure.js +287 -0
- package/CliFormat.js +124 -0
- package/CliInteractive.js +94 -0
- package/CliLinks.js +153 -0
- package/CliLog.js +367 -0
- package/CliLogger.js +91 -0
- package/CliMessage.js +79 -0
- package/CliPrompt.js +110 -0
- package/CliRuntime.js +254 -0
- package/CliTest.js +240 -0
- package/CliTheme.js +142 -0
- package/ConfigIssueRenderer.js +62 -0
- package/Doc.js +548 -0
- package/Fmt.js +295 -0
- package/GithubAnnotation.js +39 -0
- package/Glyphs.js +82 -0
- package/LICENSE +21 -0
- package/NotInteractive.js +41 -0
- package/README.md +213 -2
- package/Render.js +280 -0
- package/SchemaIssueRenderer.js +47 -0
- package/Status.js +165 -0
- package/TestTerminal.js +107 -0
- package/Token.js +70 -0
- package/index.d.ts +4035 -0
- package/index.js +27 -0
- package/internal/ExitRequested.js +21 -0
- package/internal/HelpRouting.js +63 -0
- package/internal/ansi.js +229 -0
- package/internal/autoFormat.js +33 -0
- package/internal/canPrompt.js +14 -0
- package/internal/counts.js +83 -0
- package/internal/diagnostics.js +31 -0
- package/internal/displayPath.js +46 -0
- package/internal/displayWidth.js +42 -0
- package/internal/failureTarget.js +195 -0
- package/internal/fallbackAnswer.js +17 -0
- package/internal/fileSink.js +61 -0
- package/internal/format.js +173 -0
- package/internal/inspect.js +40 -0
- package/internal/isExitCode.js +11 -0
- package/internal/layout.js +249 -0
- package/internal/linkScheme.js +29 -0
- package/internal/linkTarget.js +49 -0
- package/internal/logSafety.js +45 -0
- package/internal/renderAnsi.js +51 -0
- package/internal/renderDoc.js +322 -0
- package/internal/renderGithubLog.js +59 -0
- package/internal/renderMarkdown.js +367 -0
- package/internal/renderPlain.js +49 -0
- package/internal/scanAudience.js +105 -0
- package/internal/splitFrame.js +55 -0
- package/internal/terminalCapabilities.js +23 -0
- package/internal/wizardGate.js +17 -0
- package/package.json +87 -5
- package/testing.d.ts +283 -0
- package/testing.js +4 -0
- package/tsdoc-metadata.json +11 -0
- package/ui/CliUi.js +509 -0
- package/ui/CliUiDetached.js +36 -0
- package/ui/CliUiLive.js +436 -0
- package/ui/CliUiRouteStream.js +72 -0
- package/ui/Confirm.js +244 -0
- package/ui/DocView.js +76 -0
- package/ui/KeyHelp.js +61 -0
- package/ui/KeyTable.js +198 -0
- package/ui/MultiSelect.js +259 -0
- package/ui/Select.js +229 -0
- package/ui/Tabs.js +201 -0
- package/ui/TextInput.js +289 -0
- package/ui/Toggle.js +31 -0
- package/ui/UiKey.js +48 -0
- package/ui/UiProvider.js +62 -0
- package/ui/UiStreams.js +17 -0
- package/ui/UiTheme.js +166 -0
- package/ui/Viewport.js +203 -0
- package/ui/internal/ErrorBoundary.js +29 -0
- package/ui/internal/Holder.js +73 -0
- package/ui/internal/ScreenContext.js +51 -0
- package/ui/internal/UiProviders.js +38 -0
- package/ui/internal/ink.js +121 -0
- package/ui/internal/inkChalk.js +47 -0
- package/ui/internal/inkConsole.js +150 -0
- package/ui/internal/lazyView.js +73 -0
- package/ui/internal/lineRouter.js +70 -0
- package/ui/internal/lineText.js +18 -0
- package/ui/internal/mountPermit.js +15 -0
- package/ui/internal/processStreams.js +18 -0
- package/ui/internal/renderOptions.js +12 -0
- package/ui/testing/CliUiTest.js +792 -0
- package/ui/testing/fakeStreams.js +104 -0
- package/ui/testing/terminalModel.js +67 -0
- package/ui-testing-serializer.d.ts +12 -0
- package/ui-testing-serializer.js +32 -0
- package/ui-testing.d.ts +558 -0
- package/ui-testing.js +3 -0
- package/ui.d.ts +2013 -0
- package/ui.js +17 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { PassThrough, Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
// src/ui/testing/fakeStreams.ts
|
|
4
|
+
/** The hide-cursor escape `cli-cursor` writes to a TTY stream as it arms the restore. */
|
|
5
|
+
const HIDE_CURSOR = `${String.fromCharCode(27)}[?25l`;
|
|
6
|
+
/** A cursor-show escape in a listener's source, as `restore-cursor` writes it (`'\u001B[?25h'`) or as the raw byte. */
|
|
7
|
+
const CURSOR_SHOW = /\[\?25h/;
|
|
8
|
+
/**
|
|
9
|
+
* Disarm the real terminal's cursor restore that a fake TTY arms, so no byte of a run on the fakes reaches the
|
|
10
|
+
* process's own streams (#983). The fake streams call it as the hide escape is written to them.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Ink's frame writer hides the cursor through `cli-cursor` on its first render, and `cli-cursor` does it only for a
|
|
14
|
+
* stream that says it is a TTY, which the fakes do. Hiding it also arms `restore-cursor`, once per loaded copy of it
|
|
15
|
+
* (once per test file where the runner isolates modules): a `signal-exit` hook (`alwaysLast`, so on the `afterexit`
|
|
16
|
+
* event) that writes the cursor-show escape to the REAL `process.stderr` when the process exits or is signalled,
|
|
17
|
+
* whatever stream Ink was given and whether or not stderr is a terminal. Nothing on the fakes hid the real cursor, so
|
|
18
|
+
* that restore is only ever a stray byte in the test runner's output: this removes every `afterexit` listener that
|
|
19
|
+
* writes a cursor-show escape, right after the copy that armed it wrote its hide escape. The emitter is `signal-exit` 3's process-wide one, the version `restore-cursor` 4
|
|
20
|
+
* (the one Ink 8's `cli-cursor` takes) loads; without one there is nothing to disarm.
|
|
21
|
+
*
|
|
22
|
+
* The one read of `process` in the testing fakes, under the process-streams licence: it touches no stream.
|
|
23
|
+
*/
|
|
24
|
+
const disarmCursorRestore = () => {
|
|
25
|
+
const emitter = process.__signal_exit_emitter__;
|
|
26
|
+
if (emitter === void 0) return;
|
|
27
|
+
for (const listener of emitter.listeners("afterexit")) if (CURSOR_SHOW.test(String(listener))) emitter.removeListener("afterexit", listener);
|
|
28
|
+
};
|
|
29
|
+
const capture = (columns, rows, both, onWrite) => {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
const stream = new Writable({ write(chunk, _encoding, callback) {
|
|
32
|
+
const text = chunk.toString();
|
|
33
|
+
if (text.includes(HIDE_CURSOR)) disarmCursorRestore();
|
|
34
|
+
chunks.push(text);
|
|
35
|
+
both.push(text);
|
|
36
|
+
onWrite?.(text);
|
|
37
|
+
callback();
|
|
38
|
+
} });
|
|
39
|
+
Object.assign(stream, {
|
|
40
|
+
isTTY: true,
|
|
41
|
+
columns,
|
|
42
|
+
rows
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
stream,
|
|
46
|
+
text: () => chunks.join("")
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Make in-memory stdin, stdout and stderr that satisfy Ink's stream contract: TTYs with a size, a recorded
|
|
51
|
+
* `setRawMode`, `ref` and `unref`, and captured writes.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* The third file licensed to touch Node, testing only: Ink's stream
|
|
55
|
+
* contract is Node's, so the fakes are `node:stream` streams rather than a hand-rolled emitter that would have to
|
|
56
|
+
* reproduce `readable`, `read()`, `setEncoding` and the write-callback barrier Ink waits on at unmount.
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
const makeFakeStreams = (options = {}) => {
|
|
61
|
+
const columns = options.columns ?? 80;
|
|
62
|
+
const rows = options.rows ?? 24;
|
|
63
|
+
const rawModes = [];
|
|
64
|
+
const stdin = new PassThrough();
|
|
65
|
+
Object.assign(stdin, {
|
|
66
|
+
isTTY: true,
|
|
67
|
+
setRawMode: (mode) => {
|
|
68
|
+
rawModes.push(mode);
|
|
69
|
+
return stdin;
|
|
70
|
+
},
|
|
71
|
+
ref: () => stdin,
|
|
72
|
+
unref: () => stdin
|
|
73
|
+
});
|
|
74
|
+
const both = [];
|
|
75
|
+
const stdout = capture(columns, rows, both, options.onStdoutWrite);
|
|
76
|
+
const stderr = capture(columns, rows, both);
|
|
77
|
+
return {
|
|
78
|
+
streams: {
|
|
79
|
+
stdin,
|
|
80
|
+
stdout: stdout.stream,
|
|
81
|
+
stderr: stderr.stream
|
|
82
|
+
},
|
|
83
|
+
rawModes,
|
|
84
|
+
stdout: stdout.text,
|
|
85
|
+
stderr: stderr.text,
|
|
86
|
+
written: () => both.join(""),
|
|
87
|
+
input: (data) => {
|
|
88
|
+
stdin.write(data);
|
|
89
|
+
},
|
|
90
|
+
resize: (nextColumns, nextRows) => {
|
|
91
|
+
Object.assign(stdout.stream, {
|
|
92
|
+
columns: nextColumns,
|
|
93
|
+
rows: nextRows
|
|
94
|
+
});
|
|
95
|
+
Object.assign(stderr.stream, {
|
|
96
|
+
columns: nextColumns,
|
|
97
|
+
rows: nextRows
|
|
98
|
+
});
|
|
99
|
+
stdout.stream.emit("resize");
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export { makeFakeStreams };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/ui/testing/terminalModel.ts
|
|
2
|
+
const ESC = String.fromCharCode(27);
|
|
3
|
+
/**
|
|
4
|
+
* A small terminal model for the production render path: what a terminal shows after `written`, as its non-empty
|
|
5
|
+
* lines.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* It applies printable text, line feeds, the erase and cursor moves Ink's log-update writes (erase line, cursor up,
|
|
9
|
+
* down and forward, cursor to a column, cursor to the next line), and the clears of Ink's full-clear frame: `ESC[J`
|
|
10
|
+
* erases from the cursor to the end of the screen, `ESC[2J` blanks the visible screen, `ESC[3J` drops the scrollback
|
|
11
|
+
* above it, `ESC[H` homes the cursor to the screen's top left. The visible screen is the last
|
|
12
|
+
* `rows` lines; with `rows` unknown, the whole buffer counts as the screen, so a clear takes everything. Every other
|
|
13
|
+
* escape is ignored. It does not wrap a line wider than the terminal. Shared by `CliUiTest.live`'s transcript and the
|
|
14
|
+
* kit's own production-path tests.
|
|
15
|
+
*
|
|
16
|
+
* @param written - every byte written to the terminal
|
|
17
|
+
* @param rows - the terminal's height, which decides how much of the buffer a clear takes as the screen
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
const screenAfter = (written, rows) => {
|
|
22
|
+
const lines = [""];
|
|
23
|
+
let row = 0;
|
|
24
|
+
let column = 0;
|
|
25
|
+
const screenTop = () => rows === void 0 ? 0 : Math.max(0, lines.length - rows);
|
|
26
|
+
const sequence = new RegExp(`${ESC}\\[([0-9;?]*)([A-Za-z])|${ESC}\\][^\\u0007]*\\u0007|([\\s\\S])`, "g");
|
|
27
|
+
for (const match of written.matchAll(sequence)) {
|
|
28
|
+
const [, params, command, character] = match;
|
|
29
|
+
if (character !== void 0) {
|
|
30
|
+
if (character === "\n") {
|
|
31
|
+
row++;
|
|
32
|
+
column = 0;
|
|
33
|
+
while (lines.length <= row) lines.push("");
|
|
34
|
+
} else if (character === "\r") column = 0;
|
|
35
|
+
else if (character >= " ") {
|
|
36
|
+
const line = lines[row] ?? "";
|
|
37
|
+
lines[row] = `${line.slice(0, column).padEnd(column)}${character}${line.slice(column + 1)}`;
|
|
38
|
+
column++;
|
|
39
|
+
}
|
|
40
|
+
} else if (command === "K") lines[row] = "";
|
|
41
|
+
else if (command === "A") row = Math.max(0, row - Number(params === "" ? 1 : params));
|
|
42
|
+
else if (command === "B" || command === "E") {
|
|
43
|
+
row += Number(params === "" ? 1 : params);
|
|
44
|
+
if (command === "E") column = 0;
|
|
45
|
+
while (lines.length <= row) lines.push("");
|
|
46
|
+
} else if (command === "C") column += Number(params === "" ? 1 : params);
|
|
47
|
+
else if (command === "G") column = Math.max(0, Number(params === "" ? 1 : params) - 1);
|
|
48
|
+
else if (command === "J" && (params === "" || params === "0")) {
|
|
49
|
+
lines[row] = (lines[row] ?? "").slice(0, column);
|
|
50
|
+
for (let index = row + 1; index < lines.length; index++) lines[index] = "";
|
|
51
|
+
} else if (command === "J" && params === "2") for (let index = screenTop(); index < lines.length; index++) lines[index] = "";
|
|
52
|
+
else if (command === "J" && params === "3") {
|
|
53
|
+
const top = screenTop();
|
|
54
|
+
lines.splice(0, top);
|
|
55
|
+
row = Math.max(0, row - top);
|
|
56
|
+
if (lines.length === 0) lines.push("");
|
|
57
|
+
} else if (command === "H") {
|
|
58
|
+
const [line, col] = params === "" ? [1, 1] : params.split(";").map((part) => Number(part === "" ? 1 : part));
|
|
59
|
+
row = screenTop() + (line ?? 1) - 1;
|
|
60
|
+
column = (col ?? 1) - 1;
|
|
61
|
+
while (lines.length <= row) lines.push("");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return lines.map((line) => line.trimEnd()).filter((line) => line !== "");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export { screenAfter };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CliUiTest.serializer`: it claims a string carrying escapes or token markup and prints it as token markup with each
|
|
3
|
+
* line's trailing spaces trimmed. See `CliUiTest.serializer` for what it claims.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
declare const serializer: {
|
|
8
|
+
readonly test: (value: unknown) => boolean;
|
|
9
|
+
readonly serialize: (value: unknown) => string;
|
|
10
|
+
};
|
|
11
|
+
export { serializer as default };
|
|
12
|
+
//# sourceMappingURL=ui-testing-serializer.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CliUiTest } from "./ui/testing/CliUiTest.js";
|
|
2
|
+
|
|
3
|
+
// src/ui-testing-serializer.ts
|
|
4
|
+
/**
|
|
5
|
+
* `CliUiTest.serializer` as a module's default export, for Vitest's `snapshotSerializers` config.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Vitest's `test.snapshotSerializers` takes module paths whose default export is a serializer, so registering this
|
|
9
|
+
* one needs no `expect.addSnapshotSerializer` call and no shim file of your own:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // vitest.config.ts
|
|
13
|
+
* import { defineConfig } from "vitest/config"
|
|
14
|
+
*
|
|
15
|
+
* export default defineConfig({
|
|
16
|
+
* test: { snapshotSerializers: ["@effected/cli-kit/ui/testing/serializer"] },
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* A separate entrypoint so a program's runtime import graph never loads test code.
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* `CliUiTest.serializer`: it claims a string carrying escapes or token markup and prints it as token markup with each
|
|
26
|
+
* line's trailing spaces trimmed. See `CliUiTest.serializer` for what it claims.
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
const serializer = CliUiTest.serializer;
|
|
31
|
+
|
|
32
|
+
export { serializer as default };
|