@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
package/src/WebHostApp.test.ts
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { createWebHostApp, type WebHostAppOptions } from "./WebHostApp.ts";
|
|
4
|
-
import type { WebSocketSceneSocket } from "./WebSocketSceneBridge.ts";
|
|
5
|
-
import type {
|
|
6
|
-
ResolvedWebHostTerminalStyle,
|
|
7
|
-
WebHostTerminalStyle,
|
|
8
|
-
} from "./WebHostTerminalStyle.ts";
|
|
9
|
-
import type { WebHostSceneRuntimeOptions } from "./WebHostSceneRuntime.ts";
|
|
10
|
-
|
|
11
|
-
class FakeRuntime {
|
|
12
|
-
readonly descriptorId: string;
|
|
13
|
-
mountCount = 0;
|
|
14
|
-
visible = false;
|
|
15
|
-
styleUpdates: Array<WebHostTerminalStyle | ResolvedWebHostTerminalStyle> = [];
|
|
16
|
-
disposed = false;
|
|
17
|
-
|
|
18
|
-
constructor(descriptorId: string) {
|
|
19
|
-
this.descriptorId = descriptorId;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async mount(): Promise<void> {
|
|
23
|
-
this.mountCount += 1;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
setVisible(visible: boolean): void {
|
|
27
|
-
this.visible = visible;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
setStyle(
|
|
31
|
-
style: WebHostTerminalStyle | ResolvedWebHostTerminalStyle
|
|
32
|
-
): void {
|
|
33
|
-
this.styleUpdates.push(style);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
resize(_columns: number, _rows: number): void {}
|
|
37
|
-
|
|
38
|
-
writeOutput(_text: string): void {}
|
|
39
|
-
|
|
40
|
-
sendInput(_chunk: Uint8Array): void {}
|
|
41
|
-
|
|
42
|
-
dispose(): void {
|
|
43
|
-
this.disposed = true;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
test("app controller switches scenes and propagates active styles", async () => {
|
|
48
|
-
const runtimes = new Map<string, FakeRuntime>();
|
|
49
|
-
const seenRuntimeOptions = new Map<string, WebHostSceneRuntimeOptions>();
|
|
50
|
-
const mount = makeElement("div");
|
|
51
|
-
const options: WebHostAppOptions = {
|
|
52
|
-
mount: mount as unknown as HTMLElement,
|
|
53
|
-
manifest: {
|
|
54
|
-
defaultSceneId: "dashboard",
|
|
55
|
-
scenes: [
|
|
56
|
-
{ id: "dashboard", title: "Dashboard", isDefault: true },
|
|
57
|
-
{ id: "controls", title: "Controls", isDefault: false },
|
|
58
|
-
],
|
|
59
|
-
},
|
|
60
|
-
style: {
|
|
61
|
-
theme: {
|
|
62
|
-
foreground: "#111111",
|
|
63
|
-
background: "#f0f0f0",
|
|
64
|
-
tint: "#0057b8",
|
|
65
|
-
separator: "#cccccc",
|
|
66
|
-
selection: "#ddeeff",
|
|
67
|
-
placeholder: "#666666",
|
|
68
|
-
link: "#0057b8",
|
|
69
|
-
fill: "#f7f7f7",
|
|
70
|
-
windowBackground: "#ffffff",
|
|
71
|
-
success: "#1a7f37",
|
|
72
|
-
warning: "#9a6700",
|
|
73
|
-
danger: "#cf222e",
|
|
74
|
-
info: "#0969da",
|
|
75
|
-
muted: "#57606a",
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
createElement: (tagName: string) => makeElement(tagName) as unknown as HTMLElement,
|
|
79
|
-
sceneRuntimeFactory: (runtimeOptions: WebHostSceneRuntimeOptions) => {
|
|
80
|
-
const runtime = new FakeRuntime(runtimeOptions.descriptor.id);
|
|
81
|
-
runtimes.set(runtimeOptions.descriptor.id, runtime);
|
|
82
|
-
seenRuntimeOptions.set(runtimeOptions.descriptor.id, runtimeOptions);
|
|
83
|
-
return runtime as unknown as never;
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const controller = await createWebHostApp(options);
|
|
88
|
-
const dashboardRuntime = runtimes.get("dashboard");
|
|
89
|
-
const dashboardOptions = seenRuntimeOptions.get("dashboard");
|
|
90
|
-
|
|
91
|
-
expect(controller.selectedSceneId).toBe("dashboard");
|
|
92
|
-
expect(dashboardOptions?.style.theme?.background).toBe("#f0f0f0");
|
|
93
|
-
expect(dashboardRuntime?.visible).toBe(true);
|
|
94
|
-
expect(dashboardRuntime?.mountCount).toBe(1);
|
|
95
|
-
|
|
96
|
-
await controller.switchScene("controls");
|
|
97
|
-
const controlsRuntime = runtimes.get("controls");
|
|
98
|
-
|
|
99
|
-
expect(controller.selectedSceneId).toBe("controls");
|
|
100
|
-
expect(dashboardRuntime?.visible).toBe(false);
|
|
101
|
-
expect(controlsRuntime?.visible).toBe(true);
|
|
102
|
-
expect(controlsRuntime?.mountCount).toBe(1);
|
|
103
|
-
|
|
104
|
-
await controller.switchScene("dashboard");
|
|
105
|
-
expect(runtimes.get("dashboard")).toBe(dashboardRuntime);
|
|
106
|
-
expect(dashboardRuntime?.mountCount).toBe(1);
|
|
107
|
-
|
|
108
|
-
controller.setStyle({
|
|
109
|
-
cursorBlink: true,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
expect(dashboardRuntime?.styleUpdates.at(-1)?.cursorBlink).toBe(true);
|
|
113
|
-
expect(controlsRuntime?.styleUpdates.at(-1)?.cursorBlink).toBe(true);
|
|
114
|
-
|
|
115
|
-
await controller.dispose();
|
|
116
|
-
expect(dashboardRuntime?.disposed).toBe(true);
|
|
117
|
-
expect(controlsRuntime?.disposed).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("app controller uses the embedded WebSocket bridge when configured", async () => {
|
|
121
|
-
const socket = new FakeSocket();
|
|
122
|
-
let socketURL = "";
|
|
123
|
-
let runtimeOptions: WebHostSceneRuntimeOptions | undefined;
|
|
124
|
-
const mount = makeElement("div");
|
|
125
|
-
|
|
126
|
-
const controller = await createWebHostApp({
|
|
127
|
-
mount: mount as unknown as HTMLElement,
|
|
128
|
-
manifest: {
|
|
129
|
-
defaultSceneId: "main",
|
|
130
|
-
scenes: [{ id: "main", title: "Main", isDefault: true }],
|
|
131
|
-
},
|
|
132
|
-
embeddedHost: {
|
|
133
|
-
token: "test-token",
|
|
134
|
-
webSocketBaseURL: "http://127.0.0.1:9123/",
|
|
135
|
-
webSocketFactory: (url) => {
|
|
136
|
-
socketURL = String(url);
|
|
137
|
-
return socket;
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
createElement: (tagName: string) => makeElement(tagName) as unknown as HTMLElement,
|
|
141
|
-
sceneRuntimeFactory: (options: WebHostSceneRuntimeOptions) => {
|
|
142
|
-
runtimeOptions = options;
|
|
143
|
-
return new FakeRuntime(options.descriptor.id) as unknown as never;
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
expect(socketURL).toBe("ws://127.0.0.1:9123/ws/scene/main?token=test-token");
|
|
148
|
-
|
|
149
|
-
socket.open();
|
|
150
|
-
runtimeOptions?.onInput(new TextEncoder().encode("input-record"));
|
|
151
|
-
expect(new TextDecoder().decode(socket.sent[0])).toBe("input-record");
|
|
152
|
-
|
|
153
|
-
await controller.dispose();
|
|
154
|
-
expect(socket.closed).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
function makeElement(
|
|
158
|
-
tagName: string
|
|
159
|
-
): Record<string, unknown> {
|
|
160
|
-
return {
|
|
161
|
-
tagName,
|
|
162
|
-
className: "",
|
|
163
|
-
dataset: {},
|
|
164
|
-
hidden: false,
|
|
165
|
-
style: {},
|
|
166
|
-
replaceChildren: () => {},
|
|
167
|
-
appendChild: () => {},
|
|
168
|
-
remove: () => {},
|
|
169
|
-
hasAttribute: () => false,
|
|
170
|
-
setAttribute: () => {},
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
class FakeSocket implements WebSocketSceneSocket {
|
|
175
|
-
binaryType: BinaryType = "blob";
|
|
176
|
-
readyState = 0;
|
|
177
|
-
readonly sent: Uint8Array[] = [];
|
|
178
|
-
closed = false;
|
|
179
|
-
|
|
180
|
-
private readonly listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
181
|
-
|
|
182
|
-
send(
|
|
183
|
-
data: string | ArrayBufferLike | Blob | ArrayBufferView
|
|
184
|
-
): void {
|
|
185
|
-
if (typeof data === "string") {
|
|
186
|
-
this.sent.push(new TextEncoder().encode(data));
|
|
187
|
-
} else if (data instanceof Uint8Array) {
|
|
188
|
-
this.sent.push(new Uint8Array(data));
|
|
189
|
-
} else if (data instanceof ArrayBuffer) {
|
|
190
|
-
this.sent.push(new Uint8Array(data));
|
|
191
|
-
} else if (ArrayBuffer.isView(data)) {
|
|
192
|
-
this.sent.push(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
close(): void {
|
|
197
|
-
this.closed = true;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
addEventListener(
|
|
201
|
-
type: string,
|
|
202
|
-
listener: (event: unknown) => void
|
|
203
|
-
): void {
|
|
204
|
-
const listeners = this.listeners.get(type) ?? new Set();
|
|
205
|
-
listeners.add(listener);
|
|
206
|
-
this.listeners.set(type, listeners);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
removeEventListener(
|
|
210
|
-
type: string,
|
|
211
|
-
listener: (event: unknown) => void
|
|
212
|
-
): void {
|
|
213
|
-
this.listeners.get(type)?.delete(listener);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
open(): void {
|
|
217
|
-
this.readyState = 1;
|
|
218
|
-
for (const listener of this.listeners.get("open") ?? []) {
|
|
219
|
-
listener({});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
package/src/WebHostApp.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { BrowserWASIBridge } from "./wasi/BrowserWASIBridge.ts";
|
|
2
|
-
import {
|
|
3
|
-
WebSocketSceneBridge,
|
|
4
|
-
type WebSocketSceneBridgeOptions,
|
|
5
|
-
} from "./WebSocketSceneBridge.ts";
|
|
6
|
-
import {
|
|
7
|
-
loadWebHostSceneManifest,
|
|
8
|
-
normalizeWebHostSceneManifest,
|
|
9
|
-
type WebHostSceneDescriptor,
|
|
10
|
-
type WebHostSceneManifest,
|
|
11
|
-
type WebHostSceneManifestSource,
|
|
12
|
-
} from "./WebHostSceneManifest.ts";
|
|
13
|
-
import {
|
|
14
|
-
mergeWebHostTerminalStyle,
|
|
15
|
-
normalizeWebHostTerminalStyle,
|
|
16
|
-
type ResolvedWebHostTerminalStyle,
|
|
17
|
-
type WebHostTerminalStyle,
|
|
18
|
-
} from "./WebHostTerminalStyle.ts";
|
|
19
|
-
import {
|
|
20
|
-
WebHostSceneRuntime,
|
|
21
|
-
type WebHostSceneBridge,
|
|
22
|
-
type WebHostSceneRuntimeOptions,
|
|
23
|
-
} from "./WebHostSceneRuntime.ts";
|
|
24
|
-
|
|
25
|
-
export interface WebHostEmbeddedHostConfig {
|
|
26
|
-
token: string;
|
|
27
|
-
webSocketBaseURL?: string | URL;
|
|
28
|
-
webSocketFactory?: WebSocketSceneBridgeOptions["webSocketFactory"];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface WebHostBridgeFactoryOptions {
|
|
32
|
-
sceneId: string;
|
|
33
|
-
descriptor: WebHostSceneDescriptor;
|
|
34
|
-
style: WebHostTerminalStyle;
|
|
35
|
-
environment?: Record<string, string>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type WebHostBridgeFactory = (options: WebHostBridgeFactoryOptions) => WebHostSceneBridge;
|
|
39
|
-
|
|
40
|
-
export interface WebHostAppOptions {
|
|
41
|
-
mount: HTMLElement;
|
|
42
|
-
manifest?: WebHostSceneManifestSource;
|
|
43
|
-
manifestUrl?: string | URL;
|
|
44
|
-
initialSceneId?: string;
|
|
45
|
-
style?: WebHostTerminalStyle;
|
|
46
|
-
environment?: Record<string, string>;
|
|
47
|
-
embeddedHost?: WebHostEmbeddedHostConfig;
|
|
48
|
-
bridgeFactory?: WebHostBridgeFactory;
|
|
49
|
-
createElement?: (tagName: string) => HTMLElement;
|
|
50
|
-
sceneRuntimeFactory?: (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface WebHostAppController {
|
|
54
|
-
scenes: WebHostSceneDescriptor[];
|
|
55
|
-
selectedSceneId: string;
|
|
56
|
-
switchScene(id: string): Promise<void>;
|
|
57
|
-
setStyle(style: WebHostTerminalStyle): void;
|
|
58
|
-
dispose(): Promise<void>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
type RuntimeFactory = (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;
|
|
62
|
-
|
|
63
|
-
export async function createWebHostApp(
|
|
64
|
-
options: WebHostAppOptions
|
|
65
|
-
): Promise<WebHostAppController> {
|
|
66
|
-
const manifest = await resolveManifest(options);
|
|
67
|
-
const controller = new InternalWebHostAppController({
|
|
68
|
-
mount: options.mount,
|
|
69
|
-
manifest,
|
|
70
|
-
style: options.style,
|
|
71
|
-
environment: options.environment,
|
|
72
|
-
embeddedHost: options.embeddedHost,
|
|
73
|
-
bridgeFactory: options.bridgeFactory,
|
|
74
|
-
initialSceneId: options.initialSceneId,
|
|
75
|
-
createElement: options.createElement,
|
|
76
|
-
sceneRuntimeFactory: options.sceneRuntimeFactory ?? ((runtimeOptions) => new WebHostSceneRuntime(runtimeOptions)),
|
|
77
|
-
});
|
|
78
|
-
await controller.initialize();
|
|
79
|
-
return controller;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
class InternalWebHostAppController implements WebHostAppController {
|
|
83
|
-
readonly scenes: WebHostSceneDescriptor[];
|
|
84
|
-
selectedSceneId: string;
|
|
85
|
-
|
|
86
|
-
private readonly mount: HTMLElement;
|
|
87
|
-
private readonly sceneRoot: HTMLElement;
|
|
88
|
-
private style: ResolvedWebHostTerminalStyle;
|
|
89
|
-
private readonly environment?: Record<string, string>;
|
|
90
|
-
private readonly embeddedHost?: WebHostEmbeddedHostConfig;
|
|
91
|
-
private readonly bridgeFactory?: WebHostBridgeFactory;
|
|
92
|
-
private readonly sceneRuntimeFactory: RuntimeFactory;
|
|
93
|
-
private readonly runtimes = new Map<string, WebHostSceneRuntime>();
|
|
94
|
-
private readonly bridges = new Map<string, WebHostSceneBridge>();
|
|
95
|
-
|
|
96
|
-
constructor(options: {
|
|
97
|
-
mount: HTMLElement;
|
|
98
|
-
manifest: WebHostSceneManifest;
|
|
99
|
-
style?: WebHostTerminalStyle;
|
|
100
|
-
environment?: Record<string, string>;
|
|
101
|
-
embeddedHost?: WebHostEmbeddedHostConfig;
|
|
102
|
-
bridgeFactory?: WebHostBridgeFactory;
|
|
103
|
-
initialSceneId?: string;
|
|
104
|
-
createElement?: (tagName: string) => HTMLElement;
|
|
105
|
-
sceneRuntimeFactory: RuntimeFactory;
|
|
106
|
-
}) {
|
|
107
|
-
this.mount = options.mount;
|
|
108
|
-
this.style = normalizeWebHostTerminalStyle(options.style ?? {});
|
|
109
|
-
this.environment = options.environment;
|
|
110
|
-
this.embeddedHost = options.embeddedHost;
|
|
111
|
-
this.bridgeFactory = options.bridgeFactory;
|
|
112
|
-
this.sceneRuntimeFactory = options.sceneRuntimeFactory;
|
|
113
|
-
this.scenes = options.manifest.scenes;
|
|
114
|
-
this.selectedSceneId =
|
|
115
|
-
options.initialSceneId &&
|
|
116
|
-
options.manifest.scenes.some((scene) => scene.id === options.initialSceneId)
|
|
117
|
-
? options.initialSceneId
|
|
118
|
-
: options.manifest.scenes.find((scene) => scene.id === options.manifest.defaultSceneId)?.id ??
|
|
119
|
-
options.manifest.defaultSceneId;
|
|
120
|
-
|
|
121
|
-
this.sceneRoot = (options.createElement ?? defaultCreateElement)("div");
|
|
122
|
-
this.sceneRoot.className = "webhost-scene-root";
|
|
123
|
-
this.mount.replaceChildren(this.sceneRoot);
|
|
124
|
-
this.applyHostFrameStyle();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async initialize(): Promise<void> {
|
|
128
|
-
await this.ensureRuntime(this.selectedSceneId);
|
|
129
|
-
await this.switchScene(this.selectedSceneId);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async switchScene(
|
|
133
|
-
id: string
|
|
134
|
-
): Promise<void> {
|
|
135
|
-
const descriptor = this.scenes.find((scene) => scene.id === id);
|
|
136
|
-
if (!descriptor) {
|
|
137
|
-
throw new Error(`Unknown scene: ${id}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
for (const [sceneId, runtime] of this.runtimes) {
|
|
141
|
-
runtime.setVisible(sceneId === id);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const runtime = await this.ensureRuntime(id);
|
|
145
|
-
runtime.setVisible(true);
|
|
146
|
-
this.selectedSceneId = id;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
setStyle(
|
|
150
|
-
style: WebHostTerminalStyle
|
|
151
|
-
): void {
|
|
152
|
-
const merged = mergeWebHostTerminalStyle(this.style, style);
|
|
153
|
-
this.style = merged;
|
|
154
|
-
|
|
155
|
-
for (const runtime of this.runtimes.values()) {
|
|
156
|
-
runtime.setStyle(this.style);
|
|
157
|
-
}
|
|
158
|
-
this.applyHostFrameStyle();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async dispose(): Promise<void> {
|
|
162
|
-
for (const runtime of this.runtimes.values()) {
|
|
163
|
-
runtime.dispose();
|
|
164
|
-
}
|
|
165
|
-
for (const bridge of this.bridges.values()) {
|
|
166
|
-
bridge.dispose();
|
|
167
|
-
}
|
|
168
|
-
this.runtimes.clear();
|
|
169
|
-
this.bridges.clear();
|
|
170
|
-
this.mount.replaceChildren();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private async ensureRuntime(
|
|
174
|
-
id: string
|
|
175
|
-
): Promise<WebHostSceneRuntime> {
|
|
176
|
-
const existing = this.runtimes.get(id);
|
|
177
|
-
if (existing) {
|
|
178
|
-
return existing;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const descriptor = this.scenes.find((scene) => scene.id === id);
|
|
182
|
-
if (!descriptor) {
|
|
183
|
-
throw new Error(`Unknown scene: ${id}`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const bridge = this.makeBridge(id, descriptor);
|
|
187
|
-
const runtime = this.sceneRuntimeFactory({
|
|
188
|
-
mount: this.sceneRoot,
|
|
189
|
-
descriptor,
|
|
190
|
-
style: this.style,
|
|
191
|
-
bridge,
|
|
192
|
-
onInput: (chunk) => bridge.sendInput(chunk),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
this.bridges.set(id, bridge);
|
|
196
|
-
this.runtimes.set(id, runtime);
|
|
197
|
-
await runtime.mount();
|
|
198
|
-
runtime.setVisible(id === this.selectedSceneId);
|
|
199
|
-
return runtime;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private makeBridge(
|
|
203
|
-
sceneId: string,
|
|
204
|
-
descriptor: WebHostSceneDescriptor
|
|
205
|
-
): WebHostSceneBridge {
|
|
206
|
-
if (this.bridgeFactory) {
|
|
207
|
-
return this.bridgeFactory({
|
|
208
|
-
sceneId,
|
|
209
|
-
descriptor,
|
|
210
|
-
style: this.style,
|
|
211
|
-
environment: this.environment,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (this.embeddedHost) {
|
|
216
|
-
return new WebSocketSceneBridge({
|
|
217
|
-
sceneId,
|
|
218
|
-
token: this.embeddedHost.token,
|
|
219
|
-
baseURL: this.embeddedHost.webSocketBaseURL,
|
|
220
|
-
webSocketFactory: this.embeddedHost.webSocketFactory,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return new BrowserWASIBridge({
|
|
225
|
-
sceneId,
|
|
226
|
-
columns: 80,
|
|
227
|
-
rows: 24,
|
|
228
|
-
environment: this.environment,
|
|
229
|
-
renderStyle: this.style,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
private applyHostFrameStyle(): void {
|
|
234
|
-
this.mount.style.background = "linear-gradient(180deg, #0f172a 0%, #111827 100%)";
|
|
235
|
-
this.mount.style.minHeight = "100%";
|
|
236
|
-
this.mount.style.display = "block";
|
|
237
|
-
this.mount.style.padding = "1rem";
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function defaultCreateElement(
|
|
242
|
-
tagName: string
|
|
243
|
-
): HTMLElement {
|
|
244
|
-
if (typeof document === "undefined") {
|
|
245
|
-
throw new Error("document is not available");
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return document.createElement(tagName);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function resolveManifest(
|
|
252
|
-
options: WebHostAppOptions
|
|
253
|
-
): Promise<WebHostSceneManifest> {
|
|
254
|
-
if (options.manifest) {
|
|
255
|
-
return loadWebHostSceneManifest(options.manifest);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (options.manifestUrl) {
|
|
259
|
-
return loadWebHostSceneManifest(options.manifestUrl);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return normalizeWebHostSceneManifest([
|
|
263
|
-
{
|
|
264
|
-
id: "main",
|
|
265
|
-
title: "Main",
|
|
266
|
-
isDefault: true,
|
|
267
|
-
},
|
|
268
|
-
]);
|
|
269
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
normalizeWebHostSceneManifest,
|
|
5
|
-
webTUISceneManifestToJSON,
|
|
6
|
-
} from "./WebHostSceneManifest.ts";
|
|
7
|
-
|
|
8
|
-
test("scene manifests preserve declaration order and default scene", () => {
|
|
9
|
-
const manifest = normalizeWebHostSceneManifest([
|
|
10
|
-
{ id: "dashboard", title: "Dashboard", isDefault: true },
|
|
11
|
-
{ id: "controls", title: "Controls", isDefault: false },
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
expect(manifest.defaultSceneId).toBe("dashboard");
|
|
15
|
-
expect(manifest.scenes.map((scene) => scene.id)).toEqual([
|
|
16
|
-
"dashboard",
|
|
17
|
-
"controls",
|
|
18
|
-
]);
|
|
19
|
-
expect(webTUISceneManifestToJSON(manifest)).toBe(
|
|
20
|
-
JSON.stringify({
|
|
21
|
-
defaultSceneId: "dashboard",
|
|
22
|
-
scenes: [
|
|
23
|
-
{ id: "dashboard", title: "Dashboard", isDefault: true },
|
|
24
|
-
{ id: "controls", title: "Controls", isDefault: false },
|
|
25
|
-
],
|
|
26
|
-
})
|
|
27
|
-
);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("scene manifests infer a default scene when one is not marked", () => {
|
|
31
|
-
const manifest = normalizeWebHostSceneManifest([
|
|
32
|
-
{ id: "primary", title: "Primary", isDefault: false },
|
|
33
|
-
{ id: "secondary", title: "Secondary", isDefault: false },
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
expect(manifest.defaultSceneId).toBe("primary");
|
|
37
|
-
expect(manifest.scenes[0]?.isDefault).toBe(true);
|
|
38
|
-
});
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
export interface WebHostSceneDescriptor {
|
|
2
|
-
id: string;
|
|
3
|
-
title?: string;
|
|
4
|
-
isDefault: boolean;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface WebHostSceneManifest {
|
|
8
|
-
defaultSceneId: string;
|
|
9
|
-
scenes: WebHostSceneDescriptor[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type WebHostSceneManifestSource =
|
|
13
|
-
| WebHostSceneManifest
|
|
14
|
-
| WebHostSceneDescriptor[]
|
|
15
|
-
| string
|
|
16
|
-
| URL
|
|
17
|
-
| Request
|
|
18
|
-
| Response;
|
|
19
|
-
|
|
20
|
-
export function normalizeWebHostSceneManifest(
|
|
21
|
-
source: unknown
|
|
22
|
-
): WebHostSceneManifest {
|
|
23
|
-
const scenes = normalizeSceneDescriptors(source);
|
|
24
|
-
if (scenes.length === 0) {
|
|
25
|
-
throw new Error("scene manifest must contain at least one scene");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const defaultScene = scenes.find((scene) => scene.isDefault) ?? scenes[0];
|
|
29
|
-
return {
|
|
30
|
-
defaultSceneId: defaultScene.id,
|
|
31
|
-
scenes: scenes.map((scene, index) => ({
|
|
32
|
-
...scene,
|
|
33
|
-
isDefault: scene.id === defaultScene.id || (index === 0 && !scenes.some((entry) => entry.isDefault)),
|
|
34
|
-
})),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function webTUISceneManifestToJSON(
|
|
39
|
-
manifest: WebHostSceneManifest
|
|
40
|
-
): string {
|
|
41
|
-
return JSON.stringify({
|
|
42
|
-
defaultSceneId: manifest.defaultSceneId,
|
|
43
|
-
scenes: manifest.scenes.map((scene) => ({
|
|
44
|
-
id: scene.id,
|
|
45
|
-
...(scene.title ? { title: scene.title } : {}),
|
|
46
|
-
isDefault: scene.isDefault,
|
|
47
|
-
})),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function webTUISceneManifestFromDescriptors(
|
|
52
|
-
descriptors: WebHostSceneDescriptor[]
|
|
53
|
-
): WebHostSceneManifest {
|
|
54
|
-
return normalizeWebHostSceneManifest(descriptors);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function loadWebHostSceneManifest(
|
|
58
|
-
source: WebHostSceneManifestSource
|
|
59
|
-
): Promise<WebHostSceneManifest> {
|
|
60
|
-
if (Array.isArray(source) || isSceneManifest(source)) {
|
|
61
|
-
return normalizeWebHostSceneManifest(source);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (source instanceof URL) {
|
|
65
|
-
return loadWebHostSceneManifestFromResponse(await fetch(source));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (source instanceof Request) {
|
|
69
|
-
return loadWebHostSceneManifestFromResponse(await fetch(source));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (source instanceof Response) {
|
|
73
|
-
return loadWebHostSceneManifestFromResponse(source);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (typeof source === "string") {
|
|
77
|
-
const trimmed = source.trim();
|
|
78
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
79
|
-
return normalizeWebHostSceneManifest(JSON.parse(trimmed));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return loadWebHostSceneManifest(new URL(source, import.meta.url));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return normalizeWebHostSceneManifest(source);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function normalizeSceneDescriptors(
|
|
89
|
-
source: unknown
|
|
90
|
-
): WebHostSceneDescriptor[] {
|
|
91
|
-
if (Array.isArray(source)) {
|
|
92
|
-
return source.map(normalizeDescriptor);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (isSceneManifest(source)) {
|
|
96
|
-
return source.scenes.map(normalizeDescriptor);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (isObject(source) && Array.isArray((source as { scenes?: unknown }).scenes)) {
|
|
100
|
-
return ((source as { scenes: unknown[] }).scenes ?? []).map(normalizeDescriptor);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
throw new Error("scene manifest must be an array or an object with scenes");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function normalizeDescriptor(
|
|
107
|
-
value: unknown,
|
|
108
|
-
index?: number
|
|
109
|
-
): WebHostSceneDescriptor {
|
|
110
|
-
if (!isObject(value)) {
|
|
111
|
-
throw new Error(`scene descriptor at index ${index ?? 0} must be an object`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const id = String((value as { id?: unknown }).id ?? "").trim();
|
|
115
|
-
if (!id) {
|
|
116
|
-
throw new Error(`scene descriptor at index ${index ?? 0} is missing an id`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const titleValue = (value as { title?: unknown }).title;
|
|
120
|
-
const isDefaultValue = Boolean((value as { isDefault?: unknown }).isDefault);
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
id,
|
|
124
|
-
title:
|
|
125
|
-
typeof titleValue === "string" && titleValue.trim().length > 0
|
|
126
|
-
? titleValue.trim()
|
|
127
|
-
: undefined,
|
|
128
|
-
isDefault: isDefaultValue,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function isSceneManifest(
|
|
133
|
-
value: unknown
|
|
134
|
-
): value is WebHostSceneManifest {
|
|
135
|
-
return (
|
|
136
|
-
isObject(value) &&
|
|
137
|
-
typeof (value as { defaultSceneId?: unknown }).defaultSceneId === "string" &&
|
|
138
|
-
Array.isArray((value as { scenes?: unknown }).scenes)
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function isObject(
|
|
143
|
-
value: unknown
|
|
144
|
-
): value is Record<string, unknown> {
|
|
145
|
-
return typeof value === "object" && value !== null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function loadWebHostSceneManifestFromResponse(
|
|
149
|
-
response: Response
|
|
150
|
-
): Promise<WebHostSceneManifest> {
|
|
151
|
-
if (!response.ok) {
|
|
152
|
-
throw new Error(`failed to load scene manifest: ${response.status} ${response.statusText}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return normalizeWebHostSceneManifest(await response.json());
|
|
156
|
-
}
|