@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.
Files changed (92) hide show
  1. package/README.md +24 -10
  2. package/dist/index.d.ts +9 -0
  3. package/dist/index.js +9 -0
  4. package/dist/manifest.d.ts +2 -0
  5. package/dist/manifest.js +2 -0
  6. package/dist/src/AccessibilityTree.js +156 -0
  7. package/dist/src/AccessibilityTree.js.map +1 -0
  8. package/dist/src/BoxDrawingRenderer.js +1106 -0
  9. package/dist/src/BoxDrawingRenderer.js.map +1 -0
  10. package/dist/src/WebHostApp.d.ts +41 -0
  11. package/dist/src/WebHostApp.js +135 -0
  12. package/dist/src/WebHostApp.js.map +1 -0
  13. package/dist/src/WebHostSceneManifest.d.ts +18 -0
  14. package/dist/src/WebHostSceneManifest.js +70 -0
  15. package/dist/src/WebHostSceneManifest.js.map +1 -0
  16. package/dist/src/WebHostSceneRuntime.d.ts +112 -0
  17. package/dist/src/WebHostSceneRuntime.js +651 -0
  18. package/dist/src/WebHostSceneRuntime.js.map +1 -0
  19. package/dist/src/WebHostSurfaceTransport.d.ts +166 -0
  20. package/dist/src/WebHostSurfaceTransport.js +252 -0
  21. package/dist/src/WebHostSurfaceTransport.js.map +1 -0
  22. package/dist/src/WebHostTerminalStyle.d.ts +92 -0
  23. package/dist/src/WebHostTerminalStyle.js +277 -0
  24. package/dist/src/WebHostTerminalStyle.js.map +1 -0
  25. package/dist/src/WebHostTestFixtures.d.ts +5 -0
  26. package/dist/src/WebHostTestFixtures.js +9 -0
  27. package/dist/src/WebHostTestFixtures.js.map +1 -0
  28. package/dist/src/WebSocketSceneBridge.d.ts +53 -0
  29. package/dist/src/WebSocketSceneBridge.js +124 -0
  30. package/dist/src/WebSocketSceneBridge.js.map +1 -0
  31. package/dist/src/wasi/BrowserWASIBridge.d.ts +33 -0
  32. package/dist/src/wasi/BrowserWASIBridge.js +97 -0
  33. package/dist/src/wasi/BrowserWASIBridge.js.map +1 -0
  34. package/dist/src/wasi/SharedInputQueue.d.ts +31 -0
  35. package/dist/src/wasi/SharedInputQueue.js +102 -0
  36. package/dist/src/wasi/SharedInputQueue.js.map +1 -0
  37. package/dist/src/wasi/StdIOPipe.d.ts +15 -0
  38. package/dist/src/wasi/StdIOPipe.js +56 -0
  39. package/dist/src/wasi/StdIOPipe.js.map +1 -0
  40. package/dist/src/wasi/WasiPollScheduler.js +114 -0
  41. package/dist/src/wasi/WasiPollScheduler.js.map +1 -0
  42. package/dist/src/wasi/WasmSceneRuntime.d.ts +23 -0
  43. package/dist/src/wasi/WasmSceneRuntime.js +119 -0
  44. package/dist/src/wasi/WasmSceneRuntime.js.map +1 -0
  45. package/dist/src/wasi/WasmSceneWorker.d.ts +27 -0
  46. package/dist/src/wasi/WasmSceneWorker.js +109 -0
  47. package/dist/src/wasi/WasmSceneWorker.js.map +1 -0
  48. package/dist/testing.d.ts +2 -0
  49. package/dist/testing.js +2 -0
  50. package/dist/wasi-worker.d.ts +2 -0
  51. package/dist/wasi-worker.js +2 -0
  52. package/dist/wasi.d.ts +6 -0
  53. package/dist/wasi.js +6 -0
  54. package/dist/websocket.d.ts +2 -0
  55. package/dist/websocket.js +2 -0
  56. package/package.json +49 -18
  57. package/AGENTS.md +0 -52
  58. package/cli.ts +0 -168
  59. package/index.html +0 -50
  60. package/index.ts +0 -8
  61. package/manifest.ts +0 -1
  62. package/src/AccessibilityTree.ts +0 -262
  63. package/src/BoxDrawingRenderer.ts +0 -585
  64. package/src/PublicEntrypointBoundary.test.ts +0 -20
  65. package/src/WebHostApp.test.ts +0 -222
  66. package/src/WebHostApp.ts +0 -269
  67. package/src/WebHostSceneManifest.test.ts +0 -38
  68. package/src/WebHostSceneManifest.ts +0 -156
  69. package/src/WebHostSceneRuntime.test.ts +0 -1982
  70. package/src/WebHostSceneRuntime.ts +0 -1142
  71. package/src/WebHostSurfaceTransport.test.ts +0 -362
  72. package/src/WebHostSurfaceTransport.ts +0 -691
  73. package/src/WebHostTerminalStyle.test.ts +0 -123
  74. package/src/WebHostTerminalStyle.ts +0 -471
  75. package/src/WebHostTestFixtures.ts +0 -10
  76. package/src/WebSocketSceneBridge.test.ts +0 -198
  77. package/src/WebSocketSceneBridge.ts +0 -233
  78. package/src/browser.ts +0 -59
  79. package/src/wasi/BrowserWASIBridge.test.ts +0 -168
  80. package/src/wasi/BrowserWASIBridge.ts +0 -167
  81. package/src/wasi/SharedInputQueue.test.ts +0 -146
  82. package/src/wasi/SharedInputQueue.ts +0 -199
  83. package/src/wasi/StdIOPipe.ts +0 -72
  84. package/src/wasi/WasiPollScheduler.test.ts +0 -176
  85. package/src/wasi/WasiPollScheduler.ts +0 -305
  86. package/src/wasi/WasmSceneRuntime.ts +0 -205
  87. package/src/wasi/WasmSceneWorker.ts +0 -182
  88. package/testing.ts +0 -1
  89. package/tsconfig.json +0 -29
  90. package/wasi-worker.ts +0 -1
  91. package/wasi.ts +0 -4
  92. package/websocket.ts +0 -1
@@ -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
- }