@alchemy.run/sigil 0.0.0-alpha.4 → 0.0.0-alpha.6
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 +21 -9
- package/THIRD_PARTY_NOTICES.md +39 -1
- package/dist/Text-BobFKi74.d.ts +452 -0
- package/dist/ansi.d.ts +122 -131
- package/dist/ansi.js +86 -6
- 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/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-DZ88EXJv.d.ts +21 -0
- package/dist/index.d.ts +143 -498
- package/dist/index.js +1026 -2244
- 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-BOSLQ8fF.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -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-aZr9O8h3.js +665 -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-BzqAJi0n.js +1337 -0
- package/package.json +41 -9
- package/src/ansi/chalk.ts +5 -3
- package/src/ansi/escapes.ts +14 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +3 -1
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/tokenize.ts +3 -4
- 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 +98 -10
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +0 -8
- package/src/components/CursorContext.ts +1 -1
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +22 -45
- package/src/components/Transform.tsx +1 -1
- package/src/dom.ts +11 -2
- package/src/global.d.ts +3 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +2 -2
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/index.ts +45 -1
- package/src/ink.tsx +213 -153
- package/src/{render-node-to-output.ts → paint-tree.ts} +75 -48
- package/src/reconciler.ts +24 -3
- package/src/render-background.ts +36 -15
- package/src/render-border.ts +94 -61
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +21 -6
- package/src/render.ts +15 -6
- 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/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -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 +305 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/squash-text-nodes.ts +2 -5
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +19 -14
- 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.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/wrap-text.ts +4 -0
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-Cr6xVFMa.js +0 -2330
- package/src/ansi/supports-color.ts +0 -207
- package/src/colorize.ts +0 -60
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
|
@@ -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/wrap-text.ts
CHANGED
|
@@ -6,6 +6,10 @@ import { type Styles } from "#/styles.ts";
|
|
|
6
6
|
export const wrapTextCache = new QuickLRU<string, string>({ maxSize: 4096 });
|
|
7
7
|
|
|
8
8
|
export const wrapText = (text: string, maxWidth: number, wrapType: Styles["textWrap"]): string => {
|
|
9
|
+
if (wrapType === "none") {
|
|
10
|
+
return text;
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
const cacheKey = text + String(maxWidth) + String(wrapType);
|
|
10
14
|
const cachedText = wrapTextCache.get(cacheKey);
|
|
11
15
|
|
package/dist/sgr-CMfEpjSk.d.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
//#region src/ansi/sgr.d.ts
|
|
2
|
-
type StylePair = {
|
|
3
|
-
readonly open: string;
|
|
4
|
-
readonly close: string;
|
|
5
|
-
};
|
|
6
|
-
declare const modifierCodes: {
|
|
7
|
-
readonly reset: readonly [0, 0];
|
|
8
|
-
readonly bold: readonly [1, 22];
|
|
9
|
-
readonly dim: readonly [2, 22];
|
|
10
|
-
readonly italic: readonly [3, 23];
|
|
11
|
-
readonly underline: readonly [4, 24];
|
|
12
|
-
readonly overline: readonly [53, 55];
|
|
13
|
-
readonly inverse: readonly [7, 27];
|
|
14
|
-
readonly hidden: readonly [8, 28];
|
|
15
|
-
readonly strikethrough: readonly [9, 29];
|
|
16
|
-
};
|
|
17
|
-
declare const colorCodes: {
|
|
18
|
-
readonly black: readonly [30, 39];
|
|
19
|
-
readonly red: readonly [31, 39];
|
|
20
|
-
readonly green: readonly [32, 39];
|
|
21
|
-
readonly yellow: readonly [33, 39];
|
|
22
|
-
readonly blue: readonly [34, 39];
|
|
23
|
-
readonly magenta: readonly [35, 39];
|
|
24
|
-
readonly cyan: readonly [36, 39];
|
|
25
|
-
readonly white: readonly [37, 39];
|
|
26
|
-
readonly blackBright: readonly [90, 39];
|
|
27
|
-
readonly gray: readonly [90, 39];
|
|
28
|
-
readonly grey: readonly [90, 39];
|
|
29
|
-
readonly redBright: readonly [91, 39];
|
|
30
|
-
readonly greenBright: readonly [92, 39];
|
|
31
|
-
readonly yellowBright: readonly [93, 39];
|
|
32
|
-
readonly blueBright: readonly [94, 39];
|
|
33
|
-
readonly magentaBright: readonly [95, 39];
|
|
34
|
-
readonly cyanBright: readonly [96, 39];
|
|
35
|
-
readonly whiteBright: readonly [97, 39];
|
|
36
|
-
};
|
|
37
|
-
declare const bgColorCodes: {
|
|
38
|
-
readonly bgBlack: readonly [40, 49];
|
|
39
|
-
readonly bgRed: readonly [41, 49];
|
|
40
|
-
readonly bgGreen: readonly [42, 49];
|
|
41
|
-
readonly bgYellow: readonly [43, 49];
|
|
42
|
-
readonly bgBlue: readonly [44, 49];
|
|
43
|
-
readonly bgMagenta: readonly [45, 49];
|
|
44
|
-
readonly bgCyan: readonly [46, 49];
|
|
45
|
-
readonly bgWhite: readonly [47, 49];
|
|
46
|
-
readonly bgBlackBright: readonly [100, 49];
|
|
47
|
-
readonly bgGray: readonly [100, 49];
|
|
48
|
-
readonly bgGrey: readonly [100, 49];
|
|
49
|
-
readonly bgRedBright: readonly [101, 49];
|
|
50
|
-
readonly bgGreenBright: readonly [102, 49];
|
|
51
|
-
readonly bgYellowBright: readonly [103, 49];
|
|
52
|
-
readonly bgBlueBright: readonly [104, 49];
|
|
53
|
-
readonly bgMagentaBright: readonly [105, 49];
|
|
54
|
-
readonly bgCyanBright: readonly [106, 49];
|
|
55
|
-
readonly bgWhiteBright: readonly [107, 49];
|
|
56
|
-
};
|
|
57
|
-
type ModifierName = keyof typeof modifierCodes;
|
|
58
|
-
type ForegroundColorName = keyof typeof colorCodes;
|
|
59
|
-
type BackgroundColorName = keyof typeof bgColorCodes;
|
|
60
|
-
type StyleName = ModifierName | ForegroundColorName | BackgroundColorName;
|
|
61
|
-
declare const modifierNames: ModifierName[];
|
|
62
|
-
declare const foregroundColorNames: ForegroundColorName[];
|
|
63
|
-
declare const backgroundColorNames: BackgroundColorName[];
|
|
64
|
-
/**
|
|
65
|
-
Named SGR styles as `{open, close}` escape sequence pairs.
|
|
66
|
-
*/
|
|
67
|
-
declare const styles: Record<StyleName, StylePair>;
|
|
68
|
-
/**
|
|
69
|
-
Raw SGR code numbers: open code → close code.
|
|
70
|
-
*/
|
|
71
|
-
declare const codes: ReadonlyMap<number, number>;
|
|
72
|
-
declare const foreground: {
|
|
73
|
-
close: string;
|
|
74
|
-
ansi: (code: number) => string;
|
|
75
|
-
ansi256: (code: number) => string;
|
|
76
|
-
ansi16m: (red: number, green: number, blue: number) => string;
|
|
77
|
-
};
|
|
78
|
-
declare const background: {
|
|
79
|
-
close: string;
|
|
80
|
-
ansi: (code: number) => string;
|
|
81
|
-
ansi256: (code: number) => string;
|
|
82
|
-
ansi16m: (red: number, green: number, blue: number) => string;
|
|
83
|
-
};
|
|
84
|
-
declare const rgbToAnsi256: (red: number, green: number, blue: number) => number;
|
|
85
|
-
declare const hexToRgb: (hex: string) => [number, number, number];
|
|
86
|
-
declare const hexToAnsi256: (hex: string) => number;
|
|
87
|
-
declare const ansi256ToAnsi: (code: number) => number;
|
|
88
|
-
declare const rgbToAnsi: (red: number, green: number, blue: number) => number;
|
|
89
|
-
declare const hexToAnsi: (hex: string) => number;
|
|
90
|
-
//#endregion
|
|
91
|
-
export { rgbToAnsi256 as _, StylePair as a, backgroundColorNames as c, foregroundColorNames as d, hexToAnsi as f, rgbToAnsi as g, modifierNames as h, StyleName as i, codes as l, hexToRgb as m, ForegroundColorName as n, ansi256ToAnsi as o, hexToAnsi256 as p, ModifierName as r, background as s, BackgroundColorName as t, foreground as u, styles as v };
|