@alchemy.run/sigil 0.0.0-alpha.3 → 0.0.0-alpha.5
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/README.md +23 -13
- package/THIRD_PARTY_NOTICES.md +70 -23
- package/dist/Text-D5HUf3Fj.d.ts +452 -0
- package/dist/ansi.d.ts +137 -143
- package/dist/ansi.js +87 -2
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
- package/dist/color-policy-SVj1pYTA.js +560 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-Bh4yGP6w.d.ts +186 -0
- package/dist/detect-BuTXtY6e.js +373 -0
- package/dist/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-Bmc2tRPk.d.ts +21 -0
- package/dist/index.d.ts +235 -754
- package/dist/index.js +1362 -3001
- package/dist/osc-CCH7xDoS.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-C19minOS.d.ts +81 -0
- package/dist/query-vaIeGOkH.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-BOh__-65.js +324 -0
- package/dist/screen-BReKIheE.d.ts +40 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-BJmzX2NJ.js +664 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-CgrG9K4y.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +118 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-C2sciZOo.js +1335 -0
- package/package.json +43 -17
- package/src/ansi/chalk.ts +12 -53
- package/src/ansi/cursor.ts +2 -4
- package/src/ansi/east-asian-width.ts +44 -0
- package/src/ansi/escapes.ts +18 -26
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +14 -14
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/sgr.ts +2 -5
- package/src/ansi/slice.ts +1 -1
- package/src/ansi/string-width.ts +91 -206
- package/src/ansi/strip.ts +13 -33
- package/src/ansi/tokenize.ts +96 -169
- package/src/ansi/truncate.ts +5 -3
- package/src/ansi/wrap.ts +13 -14
- package/src/ansi-tokenizer.ts +1 -1
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +594 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +657 -0
- package/src/capabilities/store.ts +379 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +109 -22
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +43 -60
- package/src/components/CursorContext.ts +1 -1
- package/src/components/ErrorBoundary.tsx +1 -1
- package/src/components/ErrorOverview.tsx +8 -7
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/Spacer.tsx +1 -1
- package/src/components/Static.tsx +1 -1
- package/src/components/StderrContext.ts +0 -2
- package/src/components/StdinContext.ts +0 -1
- package/src/components/StdoutContext.ts +1 -3
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +23 -46
- package/src/components/Transform.tsx +2 -2
- package/src/cursor-position.ts +1 -1
- package/src/devtools.ts +84 -24
- package/src/dom.ts +11 -8
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +1 -1
- package/src/global.d.ts +6 -4
- package/src/{boxes.ts → glyphs.ts} +17 -18
- package/src/hooks/use-animation.ts +1 -1
- package/src/hooks/use-app.ts +1 -1
- package/src/hooks/use-box-metrics.ts +1 -1
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +3 -3
- package/src/hooks/use-focus-manager.ts +1 -1
- package/src/hooks/use-focus.ts +5 -6
- package/src/hooks/use-input.ts +4 -4
- package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
- package/src/hooks/use-paste.ts +2 -2
- package/src/hooks/use-stderr.ts +1 -1
- package/src/hooks/use-stdin.ts +1 -1
- package/src/hooks/use-stdout.ts +1 -1
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/hooks/use-window-size.ts +2 -2
- package/src/index.ts +88 -44
- package/src/ink.tsx +620 -745
- package/src/input-parser.ts +1 -1
- package/src/instances.ts +1 -1
- package/src/kitty-keyboard.ts +128 -0
- package/src/measure-element.ts +1 -1
- package/src/measure-text.ts +2 -2
- package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
- package/src/parse-keypress.ts +4 -5
- package/src/parse-stack-line.ts +0 -1
- package/src/patch-console.ts +44 -0
- package/src/reconciler.ts +36 -62
- package/src/render-background.ts +37 -16
- package/src/render-border.ts +97 -64
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +24 -9
- package/src/render.ts +24 -16
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/sanitize-ansi.ts +1 -1
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +137 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +272 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/signal-exit.ts +4 -5
- package/src/squash-text-nodes.ts +4 -7
- package/src/stream.ts +2 -4
- package/src/structured-text.ts +321 -0
- package/src/styles.ts +16 -17
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +86 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +188 -0
- package/src/terminal/session.ts +407 -0
- package/src/terminal-size.ts +9 -8
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +349 -0
- package/src/testing/vitest.ts +157 -0
- package/src/transform-adapter.ts +14 -0
- package/src/types.ts +1 -6
- package/src/utils.ts +2 -2
- package/src/wrap-text.ts +4 -4
- package/src/yoga/config.ts +2 -2
- package/src/yoga/core/absoluteLayout.ts +15 -15
- package/src/yoga/core/baseline.ts +3 -3
- package/src/yoga/core/cache.ts +4 -4
- package/src/yoga/core/calculateLayout.ts +33 -27
- package/src/yoga/core/config.ts +1 -1
- package/src/yoga/core/flexLine.ts +3 -3
- package/src/yoga/core/helpers.ts +3 -3
- package/src/yoga/core/layoutResults.ts +4 -4
- package/src/yoga/core/node.ts +16 -16
- package/src/yoga/core/pixelGrid.ts +4 -4
- package/src/yoga/core/style.ts +11 -11
- package/src/yoga/core/types.ts +2 -2
- package/src/yoga/index.ts +6 -6
- package/src/yoga/node.ts +8 -8
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-CBiyyZzw.js +0 -2156
- package/src/ansi/supports-color.ts +0 -213
- package/src/ansi/widest-line.ts +0 -12
- package/src/auto-bind.ts +0 -41
- package/src/colorize.ts +0 -60
- package/src/devtools-window-polyfill.ts +0 -73
- package/src/indent-string.ts +0 -16
- package/src/is-in-ci.ts +0 -7
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
- package/src/write-synchronized.ts +0 -9
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// End-to-end terminal testing: launch an app in a real PTY wired to a real
|
|
2
|
+
// terminal emulator engine (Ghostty's VT core or xterm.js), then interact
|
|
3
|
+
// and assert Playwright-style — locators, auto-waiting, key presses.
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createEmulator,
|
|
8
|
+
type Emulator,
|
|
9
|
+
type EmulatorCell,
|
|
10
|
+
type EmulatorName,
|
|
11
|
+
} from "#/testing/emulators.ts";
|
|
12
|
+
import { keyToSequence } from "#/testing/keys.ts";
|
|
13
|
+
import { connectLiveClient, currentTestName } from "#/testing/live.ts";
|
|
14
|
+
|
|
15
|
+
export type LaunchOptions = {
|
|
16
|
+
/**
|
|
17
|
+
Which emulator engine answers the app's terminal queries and interprets
|
|
18
|
+
its output.
|
|
19
|
+
|
|
20
|
+
@default "ghostty"
|
|
21
|
+
*/
|
|
22
|
+
emulator?: EmulatorName;
|
|
23
|
+
|
|
24
|
+
columns?: number;
|
|
25
|
+
rows?: number;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
The emulated OS color scheme at startup (Ghostty engine only).
|
|
29
|
+
|
|
30
|
+
@default "dark"
|
|
31
|
+
*/
|
|
32
|
+
colorScheme?: "dark" | "light";
|
|
33
|
+
|
|
34
|
+
cwd?: string;
|
|
35
|
+
env?: Record<string, string>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type WaitForOptions = {
|
|
39
|
+
/**
|
|
40
|
+
@default 5000
|
|
41
|
+
*/
|
|
42
|
+
timeout?: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
Wait for the text to appear (`"visible"`, default) or disappear
|
|
46
|
+
(`"hidden"`).
|
|
47
|
+
*/
|
|
48
|
+
state?: "visible" | "hidden";
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type TerminalLocator = {
|
|
52
|
+
/**
|
|
53
|
+
Whether the text is on the visible screen right now.
|
|
54
|
+
*/
|
|
55
|
+
isVisible: () => boolean;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
The full screen line containing the match, if any.
|
|
59
|
+
*/
|
|
60
|
+
line: () => string | undefined;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
Waits (polling) until the text appears — or disappears with
|
|
64
|
+
`state: "hidden"`. Throws with a screen dump on timeout.
|
|
65
|
+
*/
|
|
66
|
+
waitFor: (options?: WaitForOptions) => Promise<void>;
|
|
67
|
+
|
|
68
|
+
description: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type TerminalApp = {
|
|
72
|
+
readonly emulator: EmulatorName;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
The visible screen as right-trimmed lines.
|
|
76
|
+
*/
|
|
77
|
+
lines: () => string[];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
The visible screen as one string, trailing blank lines removed.
|
|
81
|
+
*/
|
|
82
|
+
text: () => string;
|
|
83
|
+
|
|
84
|
+
getByText: (text: string | RegExp) => TerminalLocator;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
Polls until the predicate holds. Throws with a screen dump on timeout.
|
|
88
|
+
*/
|
|
89
|
+
waitFor: (predicate: () => boolean, options?: { timeout?: number }) => Promise<void>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
Presses named keys in order: `press("Tab", "Enter")`, `press("Ctrl+C")`.
|
|
93
|
+
Keys are spaced out slightly, like real typing, so each key's effect
|
|
94
|
+
(focus moves, state updates) lands before the next one.
|
|
95
|
+
*/
|
|
96
|
+
press: (...keys: string[]) => Promise<void>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
Types text verbatim.
|
|
100
|
+
*/
|
|
101
|
+
type: (text: string) => void;
|
|
102
|
+
|
|
103
|
+
resize: (columns: number, rows: number) => void;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
Flips the emulated OS color scheme (Ghostty engine only) — the app
|
|
107
|
+
receives a real color scheme report if it enabled them.
|
|
108
|
+
*/
|
|
109
|
+
setColorScheme: (scheme: "dark" | "light") => void;
|
|
110
|
+
|
|
111
|
+
cellAt: (x: number, y: number) => EmulatorCell | undefined;
|
|
112
|
+
|
|
113
|
+
cursor: () => { x: number; y: number };
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
Everything the app has written, unprocessed.
|
|
117
|
+
*/
|
|
118
|
+
output: () => string;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
The exit code once the app has exited.
|
|
122
|
+
*/
|
|
123
|
+
exitCode: () => number | undefined;
|
|
124
|
+
|
|
125
|
+
waitForExit: (options?: { timeout?: number }) => Promise<number>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
Kills the app (if still running) and disposes the emulator.
|
|
129
|
+
*/
|
|
130
|
+
close: () => void;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const defaultTimeout = 5000;
|
|
134
|
+
const pollInterval = 25;
|
|
135
|
+
|
|
136
|
+
const screenDump = (emulator: Emulator): string =>
|
|
137
|
+
["", "── screen ──", ...emulator.lines().map((line) => `│${line}`), "────────────"].join("\n");
|
|
138
|
+
|
|
139
|
+
const poll = async (
|
|
140
|
+
check: () => boolean,
|
|
141
|
+
timeout: number,
|
|
142
|
+
onTimeout: () => Error,
|
|
143
|
+
): Promise<void> => {
|
|
144
|
+
const deadline = Date.now() + timeout;
|
|
145
|
+
for (;;) {
|
|
146
|
+
if (check()) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Date.now() > deadline) {
|
|
151
|
+
throw onTimeout();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
Launches a command inside a PTY attached to a real terminal emulator engine.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const app = await launchTerminal(["node", "--import=tsx", "examples/router/index.ts"]);
|
|
163
|
+
await app.getByText("Home").waitFor();
|
|
164
|
+
app.press("Tab", "Enter");
|
|
165
|
+
await app.getByText("Users").waitFor();
|
|
166
|
+
app.close();
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
A string command runs through `sh -c`.
|
|
170
|
+
*/
|
|
171
|
+
export const launchTerminal = async (
|
|
172
|
+
command: string | string[],
|
|
173
|
+
{
|
|
174
|
+
emulator: emulatorName = "ghostty",
|
|
175
|
+
columns = 100,
|
|
176
|
+
rows = 30,
|
|
177
|
+
colorScheme = "dark",
|
|
178
|
+
cwd = process.cwd(),
|
|
179
|
+
env = {},
|
|
180
|
+
}: LaunchOptions = {},
|
|
181
|
+
): Promise<TerminalApp> => {
|
|
182
|
+
const zigpty = await import("zigpty").catch((error) => {
|
|
183
|
+
throw new Error(
|
|
184
|
+
'The "zigpty" package is required to launch terminal apps. Install it as a dev dependency: pnpm add -D zigpty',
|
|
185
|
+
{ cause: error },
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const emulator = await createEmulator(emulatorName, { columns, rows, colorScheme });
|
|
190
|
+
|
|
191
|
+
const argv = Array.isArray(command) ? command : ["/bin/sh", "-c", command];
|
|
192
|
+
// Tests should behave like a user's interactive truecolor session, even on
|
|
193
|
+
// CI — both emulator engines understand 24-bit SGR.
|
|
194
|
+
const childEnv: Record<string, string> = {
|
|
195
|
+
...(process.env as Record<string, string>),
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
197
|
+
COLORTERM: "truecolor",
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
199
|
+
NODE_NO_WARNINGS: "1",
|
|
200
|
+
...env,
|
|
201
|
+
};
|
|
202
|
+
delete childEnv["CI"];
|
|
203
|
+
const child = zigpty.spawn(argv[0], argv.slice(1), {
|
|
204
|
+
name: "xterm-256color",
|
|
205
|
+
cols: columns,
|
|
206
|
+
rows,
|
|
207
|
+
cwd,
|
|
208
|
+
env: childEnv,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
let output = "";
|
|
212
|
+
let exitCode: number | undefined;
|
|
213
|
+
let closed = false;
|
|
214
|
+
|
|
215
|
+
// Mirror the session to the live hub (explorer /live page) when one is
|
|
216
|
+
// configured, so the run can be watched in the browser as it happens.
|
|
217
|
+
const live = connectLiveClient();
|
|
218
|
+
const liveId = randomUUID();
|
|
219
|
+
if (live) {
|
|
220
|
+
const commandTitle = Array.isArray(command) ? command.join(" ") : command;
|
|
221
|
+
live.send({ type: "start", id: liveId, title: commandTitle, columns, rows });
|
|
222
|
+
// Inside Vitest, relabel with the test that launched this session.
|
|
223
|
+
void currentTestName().then((name) => {
|
|
224
|
+
if (name) {
|
|
225
|
+
live.send({ type: "title", id: liveId, title: name });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
child.onData((data: string | Buffer) => {
|
|
231
|
+
if (closed) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const chunk = typeof data === "string" ? data : data.toString("latin1");
|
|
236
|
+
output += chunk;
|
|
237
|
+
emulator.feed(chunk);
|
|
238
|
+
live?.send({ type: "data", id: liveId, data: chunk });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
emulator.onResponse((data) => {
|
|
242
|
+
if (exitCode === undefined) {
|
|
243
|
+
child.write(data);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
child.onExit(({ exitCode: code }: { exitCode: number }) => {
|
|
248
|
+
exitCode = code;
|
|
249
|
+
live?.send({ type: "end", id: liveId, code });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const lines = (): string[] => emulator.lines();
|
|
253
|
+
|
|
254
|
+
const getByText = (matcher: string | RegExp): TerminalLocator => {
|
|
255
|
+
const description = typeof matcher === "string" ? JSON.stringify(matcher) : String(matcher);
|
|
256
|
+
const findLine = (): string | undefined =>
|
|
257
|
+
lines().find((line) =>
|
|
258
|
+
typeof matcher === "string" ? line.includes(matcher) : matcher.test(line),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
description,
|
|
263
|
+
isVisible: () => findLine() !== undefined,
|
|
264
|
+
line: findLine,
|
|
265
|
+
waitFor: async ({ timeout = defaultTimeout, state = "visible" }: WaitForOptions = {}) => {
|
|
266
|
+
const check =
|
|
267
|
+
state === "visible" ? () => findLine() !== undefined : () => findLine() === undefined;
|
|
268
|
+
await poll(check, timeout, () =>
|
|
269
|
+
state === "visible"
|
|
270
|
+
? new Error(`Timed out waiting for ${description} to appear.${screenDump(emulator)}`)
|
|
271
|
+
: new Error(
|
|
272
|
+
`Timed out waiting for ${description} to disappear.${screenDump(emulator)}`,
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
emulator: emulatorName,
|
|
281
|
+
lines,
|
|
282
|
+
text: () => lines().join("\n").replace(/\n+$/, ""),
|
|
283
|
+
getByText,
|
|
284
|
+
waitFor: async (predicate, { timeout = defaultTimeout } = {}) => {
|
|
285
|
+
await poll(
|
|
286
|
+
predicate,
|
|
287
|
+
timeout,
|
|
288
|
+
() => new Error(`Timed out waiting for condition.${screenDump(emulator)}`),
|
|
289
|
+
);
|
|
290
|
+
},
|
|
291
|
+
press: async (...keys) => {
|
|
292
|
+
for (const key of keys) {
|
|
293
|
+
const outputBefore = output.length;
|
|
294
|
+
child.write(keyToSequence(key));
|
|
295
|
+
// Wait for the key's effect to render before the next key — a fixed
|
|
296
|
+
// gap flakes under load when a re-render (e.g. a focus move) hasn't
|
|
297
|
+
// committed yet. Keys without visible effect give up after the cap.
|
|
298
|
+
const deadline = Date.now() + 500;
|
|
299
|
+
while (Date.now() < deadline && output.length === outputBefore) {
|
|
300
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
type: (text) => {
|
|
307
|
+
child.write(text);
|
|
308
|
+
},
|
|
309
|
+
resize: (nextColumns, nextRows) => {
|
|
310
|
+
child.resize(nextColumns, nextRows);
|
|
311
|
+
emulator.resize(nextColumns, nextRows);
|
|
312
|
+
},
|
|
313
|
+
setColorScheme: (scheme) => {
|
|
314
|
+
if (!emulator.setColorScheme) {
|
|
315
|
+
throw new Error(`The ${emulatorName} emulator cannot change color scheme.`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
emulator.setColorScheme(scheme);
|
|
319
|
+
},
|
|
320
|
+
cellAt: emulator.cellAt,
|
|
321
|
+
cursor: emulator.cursor,
|
|
322
|
+
output: () => output,
|
|
323
|
+
exitCode: () => exitCode,
|
|
324
|
+
waitForExit: async ({ timeout = defaultTimeout } = {}) => {
|
|
325
|
+
await poll(
|
|
326
|
+
() => exitCode !== undefined,
|
|
327
|
+
timeout,
|
|
328
|
+
() => new Error(`Timed out waiting for the app to exit.${screenDump(emulator)}`),
|
|
329
|
+
);
|
|
330
|
+
return exitCode!;
|
|
331
|
+
},
|
|
332
|
+
close: () => {
|
|
333
|
+
if (closed) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
closed = true;
|
|
338
|
+
if (exitCode === undefined) {
|
|
339
|
+
try {
|
|
340
|
+
child.kill();
|
|
341
|
+
} catch {}
|
|
342
|
+
live?.send({ type: "end", id: liveId, code: undefined });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
live?.close();
|
|
346
|
+
emulator.dispose();
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Vitest as an embedded engine: the explorer UI drives test discovery and
|
|
2
|
+
// runs through vitest's Node API and streams state changes out through a
|
|
3
|
+
// reporter, instead of shelling out to the CLI or the stock UI.
|
|
4
|
+
export type SerializedTask = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
type: "suite" | "test";
|
|
8
|
+
state: string | undefined;
|
|
9
|
+
duration: number | undefined;
|
|
10
|
+
errors: string[];
|
|
11
|
+
tasks: SerializedTask[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type TestEngine = {
|
|
15
|
+
/**
|
|
16
|
+
The current task tree (files → suites → tests) with results.
|
|
17
|
+
*/
|
|
18
|
+
tree: () => SerializedTask[];
|
|
19
|
+
|
|
20
|
+
running: () => boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
Runs everything under the repo's test dir. Fire-and-forget; progress
|
|
24
|
+
arrives via `onUpdate`.
|
|
25
|
+
*/
|
|
26
|
+
runAll: () => void;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
Reruns a single file, suite, or test by task id.
|
|
30
|
+
*/
|
|
31
|
+
runTask: (id: string) => void;
|
|
32
|
+
|
|
33
|
+
onUpdate: (listener: () => void) => () => void;
|
|
34
|
+
|
|
35
|
+
close: () => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type VitestTask = {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
type: string;
|
|
42
|
+
filepath?: string;
|
|
43
|
+
result?: { state?: string; duration?: number; errors?: Array<{ message?: string }> };
|
|
44
|
+
tasks?: VitestTask[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const serializeTask = (task: VitestTask): SerializedTask => ({
|
|
48
|
+
id: task.id,
|
|
49
|
+
name: task.name,
|
|
50
|
+
type: task.type === "test" ? "test" : "suite",
|
|
51
|
+
state: task.result?.state,
|
|
52
|
+
duration: task.result?.duration,
|
|
53
|
+
errors: (task.result?.errors ?? [])
|
|
54
|
+
.map((error) => error.message ?? "")
|
|
55
|
+
.filter((message) => message.length > 0),
|
|
56
|
+
tasks: (task.tasks ?? []).map(serializeTask),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
Creates a live Vitest instance scoped to the repo's own `test/` directory.
|
|
61
|
+
Set any environment the test workers should inherit (e.g. `SIGIL_LIVE_URL`
|
|
62
|
+
for live terminal mirroring) before calling this.
|
|
63
|
+
*/
|
|
64
|
+
export const createVitestEngine = async (): Promise<TestEngine> => {
|
|
65
|
+
const { createVitest } = await import("vitest/node").catch((error) => {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'The "vitest" package is required for the test engine. Install it as a dev dependency: pnpm add -D vitest',
|
|
68
|
+
{ cause: error },
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const listeners = new Set<() => void>();
|
|
73
|
+
let running = false;
|
|
74
|
+
let notifyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
75
|
+
|
|
76
|
+
const notify = (): void => {
|
|
77
|
+
// Reporter hooks fire densely during a run; coalesce.
|
|
78
|
+
notifyTimer ??= setTimeout(() => {
|
|
79
|
+
notifyTimer = undefined;
|
|
80
|
+
for (const listener of listeners) {
|
|
81
|
+
listener();
|
|
82
|
+
}
|
|
83
|
+
}, 80);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const reporter = {
|
|
87
|
+
onTestRunStart: () => {
|
|
88
|
+
running = true;
|
|
89
|
+
notify();
|
|
90
|
+
},
|
|
91
|
+
onTestRunEnd: () => {
|
|
92
|
+
running = false;
|
|
93
|
+
notify();
|
|
94
|
+
},
|
|
95
|
+
onTestModuleCollected: notify,
|
|
96
|
+
onTestModuleEnd: notify,
|
|
97
|
+
onTestSuiteResult: notify,
|
|
98
|
+
onTestCaseResult: notify,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const ctx = await createVitest("test", { watch: true, reporters: [reporter] });
|
|
102
|
+
|
|
103
|
+
const isRelevant = (moduleId: string): boolean =>
|
|
104
|
+
moduleId.includes("/test/") && !moduleId.includes(".vendor");
|
|
105
|
+
|
|
106
|
+
const relevantSpecifications = async () =>
|
|
107
|
+
(await ctx.globTestSpecifications()).filter((specification) =>
|
|
108
|
+
isRelevant(specification.moduleId),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Populate the tree up front without running anything.
|
|
112
|
+
await ctx.collect(["test/"]);
|
|
113
|
+
notify();
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
tree: () =>
|
|
117
|
+
ctx.state
|
|
118
|
+
.getFiles()
|
|
119
|
+
.filter((file) => isRelevant((file as VitestTask).filepath ?? ""))
|
|
120
|
+
.map((file) => serializeTask(file as VitestTask)),
|
|
121
|
+
running: () => running,
|
|
122
|
+
runAll: () => {
|
|
123
|
+
if (running) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
void relevantSpecifications()
|
|
128
|
+
.then((specifications) => ctx.runTestSpecifications(specifications, false))
|
|
129
|
+
.catch(() => {
|
|
130
|
+
running = false;
|
|
131
|
+
notify();
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
runTask: (id) => {
|
|
135
|
+
if (running) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Present at runtime (the stock UI uses it) but absent from the
|
|
140
|
+
// public Vitest type.
|
|
141
|
+
const runner = ctx as unknown as { rerunTask: (taskId: string) => Promise<unknown> };
|
|
142
|
+
void Promise.resolve(runner.rerunTask(id)).catch(() => {
|
|
143
|
+
running = false;
|
|
144
|
+
notify();
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
onUpdate: (listener) => {
|
|
148
|
+
listeners.add(listener);
|
|
149
|
+
return () => {
|
|
150
|
+
listeners.delete(listener);
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
close: async () => {
|
|
154
|
+
await ctx.close();
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { styledCharsFromTokens, type StyledChar, tokenize } from "#/ansi/tokenize.ts";
|
|
2
|
+
|
|
3
|
+
export type AnsiTransformer = (text: string, line: number) => string;
|
|
4
|
+
|
|
5
|
+
/** The single ANSI compatibility boundary used by Transform and external styled strings. */
|
|
6
|
+
export function transformAnsiLine(
|
|
7
|
+
serializedSubtree: string,
|
|
8
|
+
lineIndex: number,
|
|
9
|
+
transformers: readonly AnsiTransformer[],
|
|
10
|
+
): readonly StyledChar[] {
|
|
11
|
+
let transformed = serializedSubtree;
|
|
12
|
+
for (const transformer of transformers) transformed = transformer(transformed, lineIndex);
|
|
13
|
+
return styledCharsFromTokens(tokenize(transformed));
|
|
14
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The one `type-fest` type Ink used, inlined (MIT, Sindre Sorhus).
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
Allows creating a union type by combining primitive types and literal types
|
|
@@ -8,8 +8,3 @@ union.
|
|
|
8
8
|
export type LiteralUnion<LiteralType, BaseType extends string | number> =
|
|
9
9
|
| LiteralType
|
|
10
10
|
| (BaseType & Record<never, never>);
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
Create a type from an object type without certain keys.
|
|
14
|
-
*/
|
|
15
|
-
export type Except<ObjectType, KeysType extends keyof ObjectType> = Omit<ObjectType, KeysType>;
|
package/src/utils.ts
CHANGED
package/src/wrap-text.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { cliTruncate } from "
|
|
2
|
-
import { wrapAnsi } from "
|
|
3
|
-
import { QuickLru as QuickLRU } from "
|
|
4
|
-
import { type Styles } from "
|
|
1
|
+
import { cliTruncate } from "#/ansi/truncate.ts";
|
|
2
|
+
import { wrapAnsi } from "#/ansi/wrap.ts";
|
|
3
|
+
import { QuickLru as QuickLRU } from "#/quick-lru.ts";
|
|
4
|
+
import { type Styles } from "#/styles.ts";
|
|
5
5
|
|
|
6
6
|
export const wrapTextCache = new QuickLRU<string, string>({ maxSize: 4096 });
|
|
7
7
|
|
package/src/yoga/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Derived from Yoga. See THIRD_PARTY_NOTICES.md.
|
|
3
3
|
|
|
4
|
-
import { Config as CoreConfig } from "
|
|
5
|
-
import type { Errata, ExperimentalFeature } from "
|
|
4
|
+
import { Config as CoreConfig } from "#/yoga/core/config.ts";
|
|
5
|
+
import type { Errata, ExperimentalFeature } from "#/yoga/generated/YGEnums.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Public Config wrapper over the core config, exposing the same surface as
|
|
@@ -4,23 +4,12 @@
|
|
|
4
4
|
// Port of yoga/algorithm/AbsoluteLayout.cpp. Grid-specific branches are kept
|
|
5
5
|
// where they read from style, but Display.Grid is not otherwise supported.
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
Align,
|
|
9
|
-
Dimension,
|
|
10
|
-
Direction,
|
|
11
|
-
Display,
|
|
12
|
-
Errata,
|
|
13
|
-
FlexDirection,
|
|
14
|
-
Justify,
|
|
15
|
-
PositionType,
|
|
16
|
-
Wrap,
|
|
17
|
-
} from "../generated/YGEnums.ts";
|
|
18
7
|
import {
|
|
19
8
|
type LayoutData,
|
|
20
9
|
LayoutPassReason,
|
|
21
10
|
calculateLayoutInternal,
|
|
22
11
|
cleanupContentsNodesRecursively,
|
|
23
|
-
} from "
|
|
12
|
+
} from "#/yoga/core/calculateLayout.ts";
|
|
24
13
|
import {
|
|
25
14
|
PhysicalEdge,
|
|
26
15
|
SizingMode,
|
|
@@ -37,9 +26,20 @@ import {
|
|
|
37
26
|
resolveCrossDirection,
|
|
38
27
|
resolveDirection,
|
|
39
28
|
setChildTrailingPosition,
|
|
40
|
-
} from "
|
|
41
|
-
import type { Node } from "
|
|
42
|
-
import { isDefined, isUndefined } from "
|
|
29
|
+
} from "#/yoga/core/helpers.ts";
|
|
30
|
+
import type { Node } from "#/yoga/core/node.ts";
|
|
31
|
+
import { isDefined, isUndefined } from "#/yoga/core/numeric.ts";
|
|
32
|
+
import {
|
|
33
|
+
Align,
|
|
34
|
+
Dimension,
|
|
35
|
+
Direction,
|
|
36
|
+
Display,
|
|
37
|
+
Errata,
|
|
38
|
+
FlexDirection,
|
|
39
|
+
Justify,
|
|
40
|
+
PositionType,
|
|
41
|
+
Wrap,
|
|
42
|
+
} from "#/yoga/generated/YGEnums.ts";
|
|
43
43
|
|
|
44
44
|
function setFlexStartLayoutPosition(
|
|
45
45
|
parent: Node,
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
// Port of yoga/algorithm/Baseline.cpp.
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
6
|
+
import { PhysicalEdge, isColumn, resolveChildAlignment } from "#/yoga/core/helpers.ts";
|
|
7
|
+
import type { Node } from "#/yoga/core/node.ts";
|
|
8
|
+
import { Align, Dimension, PositionType } from "#/yoga/generated/YGEnums.ts";
|
|
9
9
|
|
|
10
10
|
export function calculateBaseline(node: Node): number {
|
|
11
11
|
if (node.hasBaselineFunc()) {
|
package/src/yoga/core/cache.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
// Port of yoga/algorithm/Cache.cpp.
|
|
5
5
|
|
|
6
|
-
import type { Config } from "
|
|
7
|
-
import { SizingMode } from "
|
|
8
|
-
import { inexactEquals, isDefined } from "
|
|
9
|
-
import { roundValueToPixelGrid } from "
|
|
6
|
+
import type { Config } from "#/yoga/core/config.ts";
|
|
7
|
+
import { SizingMode } from "#/yoga/core/helpers.ts";
|
|
8
|
+
import { inexactEquals, isDefined } from "#/yoga/core/numeric.ts";
|
|
9
|
+
import { roundValueToPixelGrid } from "#/yoga/core/pixelGrid.ts";
|
|
10
10
|
|
|
11
11
|
function sizeIsExactAndMatchesOldMeasuredSize(
|
|
12
12
|
sizeMode: SizingMode,
|