@swifttui/web 0.0.14 → 0.0.16
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 +24 -10
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +2 -0
- package/dist/src/AccessibilityTree.js +156 -0
- package/dist/src/AccessibilityTree.js.map +1 -0
- package/dist/src/BoxDrawingRenderer.js +1106 -0
- package/dist/src/BoxDrawingRenderer.js.map +1 -0
- package/dist/src/WebHostApp.d.ts +41 -0
- package/dist/src/WebHostApp.js +135 -0
- package/dist/src/WebHostApp.js.map +1 -0
- package/dist/src/WebHostSceneManifest.d.ts +18 -0
- package/dist/src/WebHostSceneManifest.js +70 -0
- package/dist/src/WebHostSceneManifest.js.map +1 -0
- package/dist/src/WebHostSceneRuntime.d.ts +112 -0
- package/dist/src/WebHostSceneRuntime.js +651 -0
- package/dist/src/WebHostSceneRuntime.js.map +1 -0
- package/dist/src/WebHostSurfaceTransport.d.ts +166 -0
- package/dist/src/WebHostSurfaceTransport.js +252 -0
- package/dist/src/WebHostSurfaceTransport.js.map +1 -0
- package/dist/src/WebHostTerminalStyle.d.ts +92 -0
- package/dist/src/WebHostTerminalStyle.js +277 -0
- package/dist/src/WebHostTerminalStyle.js.map +1 -0
- package/dist/src/WebHostTestFixtures.d.ts +5 -0
- package/dist/src/WebHostTestFixtures.js +9 -0
- package/dist/src/WebHostTestFixtures.js.map +1 -0
- package/dist/src/WebSocketSceneBridge.d.ts +53 -0
- package/dist/src/WebSocketSceneBridge.js +124 -0
- package/dist/src/WebSocketSceneBridge.js.map +1 -0
- package/dist/src/wasi/BrowserWASIBridge.d.ts +33 -0
- package/dist/src/wasi/BrowserWASIBridge.js +97 -0
- package/dist/src/wasi/BrowserWASIBridge.js.map +1 -0
- package/dist/src/wasi/SharedInputQueue.d.ts +31 -0
- package/dist/src/wasi/SharedInputQueue.js +102 -0
- package/dist/src/wasi/SharedInputQueue.js.map +1 -0
- package/dist/src/wasi/StdIOPipe.d.ts +15 -0
- package/dist/src/wasi/StdIOPipe.js +56 -0
- package/dist/src/wasi/StdIOPipe.js.map +1 -0
- package/dist/src/wasi/WasiPollScheduler.js +114 -0
- package/dist/src/wasi/WasiPollScheduler.js.map +1 -0
- package/dist/src/wasi/WasmSceneRuntime.d.ts +23 -0
- package/dist/src/wasi/WasmSceneRuntime.js +119 -0
- package/dist/src/wasi/WasmSceneRuntime.js.map +1 -0
- package/dist/src/wasi/WasmSceneWorker.d.ts +27 -0
- package/dist/src/wasi/WasmSceneWorker.js +109 -0
- package/dist/src/wasi/WasmSceneWorker.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +2 -0
- package/dist/wasi-worker.d.ts +2 -0
- package/dist/wasi-worker.js +2 -0
- package/dist/wasi.d.ts +6 -0
- package/dist/wasi.js +6 -0
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +2 -0
- package/package.json +49 -18
- package/AGENTS.md +0 -52
- package/cli.ts +0 -168
- package/index.html +0 -50
- package/index.ts +0 -8
- package/manifest.ts +0 -1
- package/src/AccessibilityTree.ts +0 -262
- package/src/BoxDrawingRenderer.ts +0 -585
- package/src/PublicEntrypointBoundary.test.ts +0 -20
- package/src/WebHostApp.test.ts +0 -222
- package/src/WebHostApp.ts +0 -269
- package/src/WebHostSceneManifest.test.ts +0 -38
- package/src/WebHostSceneManifest.ts +0 -156
- package/src/WebHostSceneRuntime.test.ts +0 -1982
- package/src/WebHostSceneRuntime.ts +0 -1142
- package/src/WebHostSurfaceTransport.test.ts +0 -362
- package/src/WebHostSurfaceTransport.ts +0 -691
- package/src/WebHostTerminalStyle.test.ts +0 -123
- package/src/WebHostTerminalStyle.ts +0 -471
- package/src/WebHostTestFixtures.ts +0 -10
- package/src/WebSocketSceneBridge.test.ts +0 -198
- package/src/WebSocketSceneBridge.ts +0 -233
- package/src/browser.ts +0 -59
- package/src/wasi/BrowserWASIBridge.test.ts +0 -168
- package/src/wasi/BrowserWASIBridge.ts +0 -167
- package/src/wasi/SharedInputQueue.test.ts +0 -146
- package/src/wasi/SharedInputQueue.ts +0 -199
- package/src/wasi/StdIOPipe.ts +0 -72
- package/src/wasi/WasiPollScheduler.test.ts +0 -176
- package/src/wasi/WasiPollScheduler.ts +0 -305
- package/src/wasi/WasmSceneRuntime.ts +0 -205
- package/src/wasi/WasmSceneWorker.ts +0 -182
- package/testing.ts +0 -1
- package/tsconfig.json +0 -29
- package/wasi-worker.ts +0 -1
- package/wasi.ts +0 -4
- package/websocket.ts +0 -1
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
WebSocketSceneBridge,
|
|
5
|
-
webSocketSceneURL,
|
|
6
|
-
type WebSocketSceneSocket,
|
|
7
|
-
} from "./WebSocketSceneBridge.ts";
|
|
8
|
-
|
|
9
|
-
const encoder = new TextEncoder();
|
|
10
|
-
const decoder = new TextDecoder();
|
|
11
|
-
|
|
12
|
-
class FakeWebSocket implements WebSocketSceneSocket {
|
|
13
|
-
binaryType: BinaryType = "blob";
|
|
14
|
-
readyState = 0;
|
|
15
|
-
readonly sent: Uint8Array[] = [];
|
|
16
|
-
closeCode?: number;
|
|
17
|
-
closeReason?: string;
|
|
18
|
-
|
|
19
|
-
private readonly listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
20
|
-
|
|
21
|
-
send(
|
|
22
|
-
data: string | ArrayBufferLike | Blob | ArrayBufferView
|
|
23
|
-
): void {
|
|
24
|
-
if (typeof data === "string") {
|
|
25
|
-
this.sent.push(encoder.encode(data));
|
|
26
|
-
} else if (data instanceof Uint8Array) {
|
|
27
|
-
this.sent.push(new Uint8Array(data));
|
|
28
|
-
} else if (data instanceof ArrayBuffer) {
|
|
29
|
-
this.sent.push(new Uint8Array(data));
|
|
30
|
-
} else if (ArrayBuffer.isView(data)) {
|
|
31
|
-
this.sent.push(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
32
|
-
} else {
|
|
33
|
-
throw new Error("fake socket does not support Blob sends");
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
close(
|
|
38
|
-
code?: number,
|
|
39
|
-
reason?: string
|
|
40
|
-
): void {
|
|
41
|
-
this.closeCode = code;
|
|
42
|
-
this.closeReason = reason;
|
|
43
|
-
this.readyState = 3;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
addEventListener(
|
|
47
|
-
type: string,
|
|
48
|
-
listener: (event: unknown) => void
|
|
49
|
-
): void {
|
|
50
|
-
const listeners = this.listeners.get(type) ?? new Set();
|
|
51
|
-
listeners.add(listener);
|
|
52
|
-
this.listeners.set(type, listeners);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
removeEventListener(
|
|
56
|
-
type: string,
|
|
57
|
-
listener: (event: unknown) => void
|
|
58
|
-
): void {
|
|
59
|
-
this.listeners.get(type)?.delete(listener);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
open(): void {
|
|
63
|
-
this.readyState = 1;
|
|
64
|
-
this.emit("open", {});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
message(
|
|
68
|
-
data: unknown
|
|
69
|
-
): void {
|
|
70
|
-
this.emit("message", { data });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private emit(
|
|
74
|
-
type: string,
|
|
75
|
-
event: unknown
|
|
76
|
-
): void {
|
|
77
|
-
for (const listener of this.listeners.get(type) ?? []) {
|
|
78
|
-
listener(event);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
test("websocket scene URLs use the embedded host path and token", () => {
|
|
84
|
-
expect(String(webSocketSceneURL({
|
|
85
|
-
sceneId: "main",
|
|
86
|
-
token: "test-token",
|
|
87
|
-
baseURL: "http://127.0.0.1:9123/",
|
|
88
|
-
}))).toBe("ws://127.0.0.1:9123/ws/scene/main?token=test-token");
|
|
89
|
-
|
|
90
|
-
expect(String(webSocketSceneURL({
|
|
91
|
-
sceneId: "main",
|
|
92
|
-
token: "secure-token",
|
|
93
|
-
baseURL: "https://localhost:9443/app/",
|
|
94
|
-
}))).toBe("wss://localhost:9443/app/ws/scene/main?token=secure-token");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("bridge decodes websocket output and sends queued input when the socket opens", async () => {
|
|
98
|
-
const socket = new FakeWebSocket();
|
|
99
|
-
const bridge = new WebSocketSceneBridge({
|
|
100
|
-
sceneId: "main",
|
|
101
|
-
token: "test-token",
|
|
102
|
-
baseURL: "http://127.0.0.1:9123/",
|
|
103
|
-
webSocketFactory: () => socket,
|
|
104
|
-
});
|
|
105
|
-
const frames: unknown[] = [];
|
|
106
|
-
const text: string[] = [];
|
|
107
|
-
const clipboard: string[] = [];
|
|
108
|
-
const runtimeIssues: unknown[] = [];
|
|
109
|
-
const frameDiagnostics: unknown[] = [];
|
|
110
|
-
|
|
111
|
-
bridge.bindOutput({
|
|
112
|
-
presentSurface: (frame) => frames.push(frame),
|
|
113
|
-
writeClipboard: (value) => clipboard.push(value),
|
|
114
|
-
notifyRuntimeIssue: (issue) => runtimeIssues.push(issue),
|
|
115
|
-
recordFrameDiagnostic: (diagnostic) => frameDiagnostics.push(diagnostic),
|
|
116
|
-
writeOutput: (chunk) => text.push(chunk),
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
bridge.resize(100, 32, 9, 18);
|
|
120
|
-
expect(socket.sent).toHaveLength(0);
|
|
121
|
-
|
|
122
|
-
socket.open();
|
|
123
|
-
expect(decoder.decode(socket.sent[0])).toBe("\u001Eresize:100:32:9:18\n");
|
|
124
|
-
|
|
125
|
-
socket.message(encoder.encode(
|
|
126
|
-
'\u001Esurface:{"version":2,"width":2,"height":1,"styles":[null],"rows":[[]],'
|
|
127
|
-
+ '"accessibilityTree":[{"id":"root","rect":[0,0,2,1],"role":"group"}]}\n'
|
|
128
|
-
+ '\u001Eclipboard:{"text":"copied text"}\n'
|
|
129
|
-
+ '\u001EruntimeIssue:{"severity":"warning","code":"toolbar.unhostedItems",'
|
|
130
|
-
+ '"message":"Toolbar item was not rendered",'
|
|
131
|
-
+ '"description":"SwiftTUI runtime warning [toolbar.unhostedItems] Toolbar item was not rendered"}\n'
|
|
132
|
-
+ '\u001EframeDiagnostic:{"format":"swift-tui-frame-diagnostics-v1",'
|
|
133
|
-
+ '"header":["frame","total_ms"],"fields":["7","14.20"]}\n'
|
|
134
|
-
+ "legacy output\n"
|
|
135
|
-
));
|
|
136
|
-
await Promise.resolve();
|
|
137
|
-
|
|
138
|
-
expect(frames).toHaveLength(1);
|
|
139
|
-
expect(frames[0]).toMatchObject({
|
|
140
|
-
version: 2,
|
|
141
|
-
width: 2,
|
|
142
|
-
accessibilityTree: [{ id: "root", role: "group" }],
|
|
143
|
-
});
|
|
144
|
-
expect(clipboard).toEqual(["copied text"]);
|
|
145
|
-
expect(runtimeIssues).toEqual([
|
|
146
|
-
{
|
|
147
|
-
severity: "warning",
|
|
148
|
-
code: "toolbar.unhostedItems",
|
|
149
|
-
message: "Toolbar item was not rendered",
|
|
150
|
-
description: "SwiftTUI runtime warning [toolbar.unhostedItems] Toolbar item was not rendered",
|
|
151
|
-
},
|
|
152
|
-
]);
|
|
153
|
-
expect(frameDiagnostics).toEqual([
|
|
154
|
-
{
|
|
155
|
-
format: "swift-tui-frame-diagnostics-v1",
|
|
156
|
-
header: ["frame", "total_ms"],
|
|
157
|
-
fields: ["7", "14.20"],
|
|
158
|
-
},
|
|
159
|
-
]);
|
|
160
|
-
expect(text).toEqual(["legacy output\n"]);
|
|
161
|
-
|
|
162
|
-
bridge.sendInput(encoder.encode("\u001Ekey:return:0\n"));
|
|
163
|
-
expect(decoder.decode(socket.sent.at(-1))).toBe("\u001Ekey:return:0\n");
|
|
164
|
-
|
|
165
|
-
bridge.dispose();
|
|
166
|
-
expect(socket.closeCode).toBe(1000);
|
|
167
|
-
expect(socket.closeReason).toBe("WebHost scene disposed");
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("bridge buffers output until a runtime binds a sink", async () => {
|
|
171
|
-
const socket = new FakeWebSocket();
|
|
172
|
-
const bridge = new WebSocketSceneBridge({
|
|
173
|
-
sceneId: "main",
|
|
174
|
-
token: "test-token",
|
|
175
|
-
baseURL: "http://127.0.0.1:9123/",
|
|
176
|
-
webSocketFactory: () => socket,
|
|
177
|
-
});
|
|
178
|
-
const frames: unknown[] = [];
|
|
179
|
-
|
|
180
|
-
socket.message(encoder.encode(
|
|
181
|
-
'\u001Esurface:{"version":1,"width":3,"height":1,"styles":[null],"rows":[[]]}\n'
|
|
182
|
-
));
|
|
183
|
-
await Promise.resolve();
|
|
184
|
-
|
|
185
|
-
bridge.bindOutput({
|
|
186
|
-
presentSurface: (frame) => frames.push(frame),
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
expect(frames).toEqual([
|
|
190
|
-
{
|
|
191
|
-
version: 1,
|
|
192
|
-
width: 3,
|
|
193
|
-
height: 1,
|
|
194
|
-
styles: [null],
|
|
195
|
-
rows: [[]],
|
|
196
|
-
},
|
|
197
|
-
]);
|
|
198
|
-
});
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
WebHostOutputDecoder,
|
|
3
|
-
encodeRenderStyleControlMessage,
|
|
4
|
-
encodeResizeControlMessage,
|
|
5
|
-
type WebHostOutputRecord,
|
|
6
|
-
type WebHostOutputSink,
|
|
7
|
-
} from "./WebHostSurfaceTransport.ts";
|
|
8
|
-
import type { WebHostTerminalStyle } from "./WebHostTerminalStyle.ts";
|
|
9
|
-
import type { WebHostSceneBridge } from "./WebHostSceneRuntime.ts";
|
|
10
|
-
|
|
11
|
-
export interface WebSocketSceneBridgeOptions {
|
|
12
|
-
sceneId: string;
|
|
13
|
-
token: string;
|
|
14
|
-
baseURL?: string | URL;
|
|
15
|
-
webSocketURL?: string | URL;
|
|
16
|
-
webSocketFactory?: WebSocketSceneBridgeFactory;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type WebSocketSceneBridgeFactory = (url: string | URL) => WebSocketSceneSocket;
|
|
20
|
-
|
|
21
|
-
export interface WebSocketSceneSocket {
|
|
22
|
-
binaryType: BinaryType;
|
|
23
|
-
readonly readyState: number;
|
|
24
|
-
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
|
|
25
|
-
close(code?: number, reason?: string): void;
|
|
26
|
-
addEventListener(type: "open", listener: (event: Event) => void): void;
|
|
27
|
-
addEventListener(type: "message", listener: (event: MessageEvent) => void): void;
|
|
28
|
-
addEventListener(type: "close", listener: (event: CloseEvent) => void): void;
|
|
29
|
-
addEventListener(type: "error", listener: (event: Event) => void): void;
|
|
30
|
-
removeEventListener(type: "open", listener: (event: Event) => void): void;
|
|
31
|
-
removeEventListener(type: "message", listener: (event: MessageEvent) => void): void;
|
|
32
|
-
removeEventListener(type: "close", listener: (event: CloseEvent) => void): void;
|
|
33
|
-
removeEventListener(type: "error", listener: (event: Event) => void): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const socketOpenState = 1;
|
|
37
|
-
const textEncoder = new TextEncoder();
|
|
38
|
-
|
|
39
|
-
export class WebSocketSceneBridge implements WebHostSceneBridge {
|
|
40
|
-
readonly url: URL;
|
|
41
|
-
|
|
42
|
-
private readonly socket: WebSocketSceneSocket;
|
|
43
|
-
private readonly decoder = new WebHostOutputDecoder();
|
|
44
|
-
private readonly queuedInput: Uint8Array[] = [];
|
|
45
|
-
private readonly queuedOutput: WebHostOutputRecord[] = [];
|
|
46
|
-
private sink?: WebHostOutputSink;
|
|
47
|
-
private disposed = false;
|
|
48
|
-
|
|
49
|
-
private readonly handleOpen = () => {
|
|
50
|
-
this.flushQueuedInput();
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
private readonly handleMessage = (event: MessageEvent) => {
|
|
54
|
-
void this.receive(event.data);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
private readonly handleClose = () => {
|
|
58
|
-
for (const record of this.decoder.flush()) {
|
|
59
|
-
this.deliver(record);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
private readonly handleError = () => {};
|
|
64
|
-
|
|
65
|
-
constructor(options: WebSocketSceneBridgeOptions) {
|
|
66
|
-
this.url = webSocketSceneURL(options);
|
|
67
|
-
this.socket = (options.webSocketFactory ?? defaultWebSocketFactory)(this.url);
|
|
68
|
-
this.socket.binaryType = "arraybuffer";
|
|
69
|
-
this.socket.addEventListener("open", this.handleOpen);
|
|
70
|
-
this.socket.addEventListener("message", this.handleMessage);
|
|
71
|
-
this.socket.addEventListener("close", this.handleClose);
|
|
72
|
-
this.socket.addEventListener("error", this.handleError);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
bindOutput(
|
|
76
|
-
sink: WebHostOutputSink
|
|
77
|
-
): void {
|
|
78
|
-
this.sink = sink;
|
|
79
|
-
while (this.queuedOutput.length > 0) {
|
|
80
|
-
this.deliver(this.queuedOutput.shift()!);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
resize(
|
|
85
|
-
columns: number,
|
|
86
|
-
rows: number,
|
|
87
|
-
cellWidth?: number,
|
|
88
|
-
cellHeight?: number
|
|
89
|
-
): void {
|
|
90
|
-
this.sendInput(encodeResizeControlMessage(columns, rows, cellWidth, cellHeight));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
updateRenderStyle(
|
|
94
|
-
style: WebHostTerminalStyle
|
|
95
|
-
): void {
|
|
96
|
-
this.sendInput(encodeRenderStyleControlMessage(style));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
sendInput(
|
|
100
|
-
chunk: Uint8Array
|
|
101
|
-
): void {
|
|
102
|
-
if (this.disposed) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const copy = new Uint8Array(chunk);
|
|
107
|
-
if (this.socket.readyState === socketOpenState) {
|
|
108
|
-
this.socket.send(copy);
|
|
109
|
-
} else {
|
|
110
|
-
this.queuedInput.push(copy);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
dispose(): void {
|
|
115
|
-
if (this.disposed) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
this.disposed = true;
|
|
119
|
-
this.socket.removeEventListener("open", this.handleOpen);
|
|
120
|
-
this.socket.removeEventListener("message", this.handleMessage);
|
|
121
|
-
this.socket.removeEventListener("close", this.handleClose);
|
|
122
|
-
this.socket.removeEventListener("error", this.handleError);
|
|
123
|
-
this.queuedInput.length = 0;
|
|
124
|
-
this.queuedOutput.length = 0;
|
|
125
|
-
this.socket.close(1000, "WebHost scene disposed");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private async receive(
|
|
129
|
-
message: unknown
|
|
130
|
-
): Promise<void> {
|
|
131
|
-
if (this.disposed) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const bytes = await bytesFromWebSocketMessage(message);
|
|
136
|
-
if (!bytes) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
for (const record of this.decoder.feed(bytes)) {
|
|
141
|
-
this.deliver(record);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private deliver(
|
|
146
|
-
record: WebHostOutputRecord
|
|
147
|
-
): void {
|
|
148
|
-
const sink = this.sink;
|
|
149
|
-
if (!sink) {
|
|
150
|
-
this.queuedOutput.push(record);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
switch (record.type) {
|
|
155
|
-
case "surface":
|
|
156
|
-
sink.presentSurface(record.frame);
|
|
157
|
-
break;
|
|
158
|
-
case "clipboard":
|
|
159
|
-
void sink.writeClipboard?.(record.text);
|
|
160
|
-
break;
|
|
161
|
-
case "runtimeIssue":
|
|
162
|
-
sink.notifyRuntimeIssue?.(record.issue);
|
|
163
|
-
break;
|
|
164
|
-
case "frameDiagnostic":
|
|
165
|
-
sink.recordFrameDiagnostic?.(record.diagnostic);
|
|
166
|
-
break;
|
|
167
|
-
case "text":
|
|
168
|
-
sink.writeOutput?.(record.text);
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private flushQueuedInput(): void {
|
|
174
|
-
if (this.disposed || this.socket.readyState !== socketOpenState) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
while (this.queuedInput.length > 0) {
|
|
178
|
-
this.socket.send(this.queuedInput.shift()!);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function webSocketSceneURL(
|
|
184
|
-
options: Pick<WebSocketSceneBridgeOptions, "baseURL" | "webSocketURL" | "sceneId" | "token">
|
|
185
|
-
): URL {
|
|
186
|
-
if (options.webSocketURL) {
|
|
187
|
-
const explicit = new URL(String(options.webSocketURL), currentPageURL());
|
|
188
|
-
explicit.searchParams.set("token", options.token);
|
|
189
|
-
return explicit;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const url = new URL(String(options.baseURL ?? currentPageURL()), currentPageURL());
|
|
193
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
194
|
-
const basePath = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
195
|
-
url.pathname = `${basePath}/ws/scene/${encodeURIComponent(options.sceneId)}`;
|
|
196
|
-
url.search = "";
|
|
197
|
-
url.searchParams.set("token", options.token);
|
|
198
|
-
return url;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function bytesFromWebSocketMessage(
|
|
202
|
-
message: unknown
|
|
203
|
-
): Promise<Uint8Array | undefined> {
|
|
204
|
-
if (typeof message === "string") {
|
|
205
|
-
return textEncoder.encode(message);
|
|
206
|
-
}
|
|
207
|
-
if (message instanceof Uint8Array) {
|
|
208
|
-
return message;
|
|
209
|
-
}
|
|
210
|
-
if (message instanceof ArrayBuffer) {
|
|
211
|
-
return new Uint8Array(message);
|
|
212
|
-
}
|
|
213
|
-
if (ArrayBuffer.isView(message)) {
|
|
214
|
-
return new Uint8Array(message.buffer, message.byteOffset, message.byteLength);
|
|
215
|
-
}
|
|
216
|
-
if (typeof Blob !== "undefined" && message instanceof Blob) {
|
|
217
|
-
return new Uint8Array(await message.arrayBuffer());
|
|
218
|
-
}
|
|
219
|
-
return undefined;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function defaultWebSocketFactory(
|
|
223
|
-
url: string | URL
|
|
224
|
-
): WebSocketSceneSocket {
|
|
225
|
-
if (typeof WebSocket === "undefined") {
|
|
226
|
-
throw new Error("WebSocket is not available");
|
|
227
|
-
}
|
|
228
|
-
return new WebSocket(url) as WebSocketSceneSocket;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function currentPageURL(): string {
|
|
232
|
-
return globalThis.location?.href ?? "http://127.0.0.1/";
|
|
233
|
-
}
|
package/src/browser.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { createWebHostApp, type WebHostTerminalStyle } from "./WebHostApp.ts";
|
|
2
|
-
|
|
3
|
-
declare global {
|
|
4
|
-
interface Window {
|
|
5
|
-
__WEBTUI__?: {
|
|
6
|
-
manifestUrl?: string;
|
|
7
|
-
initialSceneId?: string;
|
|
8
|
-
style?: WebHostTerminalStyle;
|
|
9
|
-
embeddedHost?: {
|
|
10
|
-
token?: string;
|
|
11
|
-
webSocketBaseURL?: string;
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
__WEBTUI_APP__?: Awaited<ReturnType<typeof createWebHostApp>>;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function bootstrap(): Promise<void> {
|
|
19
|
-
const mount = document.getElementById("webhost-root");
|
|
20
|
-
if (!mount) {
|
|
21
|
-
throw new Error("webhost root element not found");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const config = window.__WEBTUI__ ?? {};
|
|
25
|
-
const pageURL = new URL(globalThis.location?.href ?? import.meta.url);
|
|
26
|
-
const embeddedToken = config.embeddedHost?.token ?? pageURL.searchParams.get("token") ?? undefined;
|
|
27
|
-
const manifestUrl = tokenizedURL(
|
|
28
|
-
config.manifestUrl ?? new URL("./scene-manifest.json", pageURL),
|
|
29
|
-
embeddedToken
|
|
30
|
-
);
|
|
31
|
-
const controller = await createWebHostApp({
|
|
32
|
-
mount,
|
|
33
|
-
manifestUrl,
|
|
34
|
-
initialSceneId: config.initialSceneId,
|
|
35
|
-
style: config.style,
|
|
36
|
-
embeddedHost: embeddedToken
|
|
37
|
-
? {
|
|
38
|
-
token: embeddedToken,
|
|
39
|
-
webSocketBaseURL: config.embeddedHost?.webSocketBaseURL ?? new URL("./", pageURL).href,
|
|
40
|
-
}
|
|
41
|
-
: undefined,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
window.__WEBTUI_APP__ = controller;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
void bootstrap();
|
|
48
|
-
|
|
49
|
-
function tokenizedURL(
|
|
50
|
-
value: string | URL,
|
|
51
|
-
token: string | undefined
|
|
52
|
-
): string | URL {
|
|
53
|
-
if (!token) {
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
56
|
-
const url = new URL(String(value), globalThis.location?.href ?? import.meta.url);
|
|
57
|
-
url.searchParams.set("token", token);
|
|
58
|
-
return url;
|
|
59
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
BrowserWASIBridge,
|
|
5
|
-
encodeRenderStyleControlMessage,
|
|
6
|
-
encodeResizeControlMessage,
|
|
7
|
-
} from "./BrowserWASIBridge.ts";
|
|
8
|
-
import {
|
|
9
|
-
decodeWebHostTerminalRenderStyleBase64,
|
|
10
|
-
encodeWebHostTerminalRenderStyleBase64,
|
|
11
|
-
} from "../WebHostTerminalStyle.ts";
|
|
12
|
-
|
|
13
|
-
test("bridge seeds initial render style and emits runtime style updates", async () => {
|
|
14
|
-
const style = {
|
|
15
|
-
theme: {
|
|
16
|
-
foreground: "#ededed",
|
|
17
|
-
background: "#111111",
|
|
18
|
-
tint: "#56b6c2",
|
|
19
|
-
separator: "#4c566a",
|
|
20
|
-
selection: "#2e3440",
|
|
21
|
-
placeholder: "#8c92ac",
|
|
22
|
-
link: "#5ba3ff",
|
|
23
|
-
fill: "#2b303b",
|
|
24
|
-
windowBackground: "#15181e",
|
|
25
|
-
success: "#61c67b",
|
|
26
|
-
warning: "#ebb33c",
|
|
27
|
-
danger: "#e05757",
|
|
28
|
-
info: "#56b6c2",
|
|
29
|
-
muted: "#8c92ac",
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const bridge = new BrowserWASIBridge({
|
|
34
|
-
sceneId: "main",
|
|
35
|
-
columns: 80,
|
|
36
|
-
rows: 24,
|
|
37
|
-
renderStyle: style,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
expect(
|
|
41
|
-
decodeWebHostTerminalRenderStyleBase64(bridge.environment.TUIGUI_RENDER_STYLE ?? "")
|
|
42
|
-
?.appearance.backgroundColor
|
|
43
|
-
).toBe("#111111");
|
|
44
|
-
expect(
|
|
45
|
-
bridge.environment.TUIGUI_RENDER_STYLE
|
|
46
|
-
).toBe(encodeWebHostTerminalRenderStyleBase64(style));
|
|
47
|
-
expect(bridge.environment.TUIGUI_TRANSPORT).toBe("surface");
|
|
48
|
-
expect(bridge.environment.TUIGUI_SURFACE_DELTA).toBe("1");
|
|
49
|
-
|
|
50
|
-
bridge.updateRenderStyle(style);
|
|
51
|
-
const input = await bridge.stdin.read();
|
|
52
|
-
expect(Array.from(input ?? [])).toEqual(
|
|
53
|
-
Array.from(encodeRenderStyleControlMessage(style))
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("bridge allows callers to disable surface delta support", () => {
|
|
58
|
-
const bridge = new BrowserWASIBridge({
|
|
59
|
-
sceneId: "main",
|
|
60
|
-
columns: 80,
|
|
61
|
-
rows: 24,
|
|
62
|
-
environment: {
|
|
63
|
-
TUIGUI_SURFACE_DELTA: "0",
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(bridge.environment.TUIGUI_SURFACE_DELTA).toBe("0");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("bridge resize updates environment, emits control input, and notifies listeners", async () => {
|
|
71
|
-
const bridge = new BrowserWASIBridge({
|
|
72
|
-
sceneId: "main",
|
|
73
|
-
columns: 80,
|
|
74
|
-
rows: 24,
|
|
75
|
-
});
|
|
76
|
-
const seen: Array<[number, number, number | undefined, number | undefined]> = [];
|
|
77
|
-
const unsubscribe = bridge.subscribeResize((columns, rows, cellWidth, cellHeight) => {
|
|
78
|
-
seen.push([columns, rows, cellWidth, cellHeight]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
expect(seen).toEqual([[80, 24, undefined, undefined]]);
|
|
82
|
-
|
|
83
|
-
bridge.resize(132, 41, 9, 18);
|
|
84
|
-
|
|
85
|
-
expect(bridge.environment.TUIGUI_COLUMNS).toBe("132");
|
|
86
|
-
expect(bridge.environment.TUIGUI_ROWS).toBe("41");
|
|
87
|
-
expect(seen).toEqual([
|
|
88
|
-
[80, 24, undefined, undefined],
|
|
89
|
-
[132, 41, 9, 18],
|
|
90
|
-
]);
|
|
91
|
-
|
|
92
|
-
const input = await bridge.stdin.read();
|
|
93
|
-
expect(Array.from(input ?? [])).toEqual(Array.from(encodeResizeControlMessage(132, 41, 9, 18)));
|
|
94
|
-
|
|
95
|
-
unsubscribe();
|
|
96
|
-
bridge.resize(90, 30);
|
|
97
|
-
expect(seen).toEqual([
|
|
98
|
-
[80, 24, undefined, undefined],
|
|
99
|
-
[132, 41, 9, 18],
|
|
100
|
-
]);
|
|
101
|
-
|
|
102
|
-
const replayed: Array<[number, number, number | undefined, number | undefined]> = [];
|
|
103
|
-
bridge.subscribeResize((columns, rows, cellWidth, cellHeight) => {
|
|
104
|
-
replayed.push([columns, rows, cellWidth, cellHeight]);
|
|
105
|
-
})();
|
|
106
|
-
expect(replayed).toEqual([[90, 30, undefined, undefined]]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("bridge delivers typed clipboard output to sinks", () => {
|
|
110
|
-
const bridge = new BrowserWASIBridge({
|
|
111
|
-
sceneId: "main",
|
|
112
|
-
columns: 80,
|
|
113
|
-
rows: 24,
|
|
114
|
-
});
|
|
115
|
-
const clipboard: string[] = [];
|
|
116
|
-
|
|
117
|
-
bridge.bindOutput({
|
|
118
|
-
presentSurface: () => {},
|
|
119
|
-
writeClipboard: (text) => clipboard.push(text),
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
bridge.stdout.write(new TextEncoder().encode('\u001Eclipboard:{"text":"copied text"}\n'));
|
|
123
|
-
|
|
124
|
-
expect(clipboard).toEqual(["copied text"]);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("bridge delivers typed runtime issues and frame diagnostics to sinks", () => {
|
|
128
|
-
const bridge = new BrowserWASIBridge({
|
|
129
|
-
sceneId: "main",
|
|
130
|
-
columns: 80,
|
|
131
|
-
rows: 24,
|
|
132
|
-
});
|
|
133
|
-
const runtimeIssues: unknown[] = [];
|
|
134
|
-
const frameDiagnostics: unknown[] = [];
|
|
135
|
-
const text: string[] = [];
|
|
136
|
-
|
|
137
|
-
bridge.bindOutput({
|
|
138
|
-
presentSurface: () => {},
|
|
139
|
-
notifyRuntimeIssue: (issue) => runtimeIssues.push(issue),
|
|
140
|
-
recordFrameDiagnostic: (diagnostic) => frameDiagnostics.push(diagnostic),
|
|
141
|
-
writeOutput: (chunk) => text.push(chunk),
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
bridge.stdout.write(new TextEncoder().encode(
|
|
145
|
-
'\u001EruntimeIssue:{"severity":"warning","code":"toolbar.unhostedItems",'
|
|
146
|
-
+ '"message":"Toolbar item was not rendered",'
|
|
147
|
-
+ '"description":"SwiftTUI runtime warning [toolbar.unhostedItems] Toolbar item was not rendered"}\n'
|
|
148
|
-
+ '\u001EframeDiagnostic:{"format":"swift-tui-frame-diagnostics-v1",'
|
|
149
|
-
+ '"header":["frame","total_ms"],"fields":["7","14.20"]}\n'
|
|
150
|
-
));
|
|
151
|
-
|
|
152
|
-
expect(runtimeIssues).toEqual([
|
|
153
|
-
{
|
|
154
|
-
severity: "warning",
|
|
155
|
-
code: "toolbar.unhostedItems",
|
|
156
|
-
message: "Toolbar item was not rendered",
|
|
157
|
-
description: "SwiftTUI runtime warning [toolbar.unhostedItems] Toolbar item was not rendered",
|
|
158
|
-
},
|
|
159
|
-
]);
|
|
160
|
-
expect(frameDiagnostics).toEqual([
|
|
161
|
-
{
|
|
162
|
-
format: "swift-tui-frame-diagnostics-v1",
|
|
163
|
-
header: ["frame", "total_ms"],
|
|
164
|
-
fields: ["7", "14.20"],
|
|
165
|
-
},
|
|
166
|
-
]);
|
|
167
|
-
expect(text).toEqual([]);
|
|
168
|
-
});
|