@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
@@ -0,0 +1,119 @@
1
+ import { WebHostSceneRuntime } from "../WebHostSceneRuntime.js";
2
+ import { SharedInputQueueWriter, createSharedInputQueue } from "./SharedInputQueue.js";
3
+ //#region src/wasi/WasmSceneRuntime.ts
4
+ const workerModuleURL = new URL("./wasm-scene-worker.js", import.meta.url);
5
+ function createWasmSceneRuntimeFactory(wasmURL, factoryOptions = {}) {
6
+ return (options) => {
7
+ const runtime = new WasmSceneRuntime(options, wasmURL, factoryOptions);
8
+ factoryOptions.onRuntimeCreated?.(runtime);
9
+ return runtime;
10
+ };
11
+ }
12
+ var WasmSceneRuntime = class extends WebHostSceneRuntime {
13
+ bridge;
14
+ wasmURL;
15
+ onSceneResize;
16
+ workerModuleURL;
17
+ inputQueue;
18
+ inputWriter;
19
+ detachBridgeInputListener;
20
+ detachResizeListener;
21
+ worker;
22
+ didMount = false;
23
+ constructor(options, wasmURL, factoryOptions) {
24
+ let inputQueue;
25
+ let inputWriter;
26
+ try {
27
+ inputQueue = createSharedInputQueue();
28
+ inputWriter = new SharedInputQueueWriter(inputQueue);
29
+ } catch (error) {
30
+ console.error("[SwiftTUIWeb] failed to create shared stdin queue", error);
31
+ }
32
+ super({
33
+ ...options,
34
+ onInput: (chunk) => {
35
+ try {
36
+ inputWriter?.write(chunk);
37
+ } catch (error) {
38
+ console.error("[SwiftTUIWeb] failed to enqueue terminal input", error);
39
+ }
40
+ }
41
+ });
42
+ this.bridge = options.bridge;
43
+ this.wasmURL = wasmURL;
44
+ this.onSceneResize = factoryOptions.onSceneResize;
45
+ this.workerModuleURL = factoryOptions.workerModuleURL ?? workerModuleURL;
46
+ this.inputQueue = inputQueue;
47
+ this.inputWriter = inputWriter;
48
+ }
49
+ async mount() {
50
+ await super.mount();
51
+ if (this.didMount) return;
52
+ this.didMount = true;
53
+ this.detachBridgeInputListener = this.bridge?.stdin.subscribe((chunk) => {
54
+ this.inputWriter?.write(chunk);
55
+ });
56
+ this.detachResizeListener = this.bridge?.subscribeResize((columns, rows, cellWidth, cellHeight) => {
57
+ this.onSceneResize?.({
58
+ sceneId: this.descriptor.id,
59
+ columns,
60
+ rows,
61
+ cellWidth,
62
+ cellHeight
63
+ });
64
+ });
65
+ const initialColumns = Number(this.bridge?.environment.TUIGUI_COLUMNS ?? "0") || 0;
66
+ const initialRows = Number(this.bridge?.environment.TUIGUI_ROWS ?? "0") || 0;
67
+ if (!this.bridge && initialColumns > 0 && initialRows > 0) this.onSceneResize?.({
68
+ sceneId: this.descriptor.id,
69
+ columns: initialColumns,
70
+ rows: initialRows
71
+ });
72
+ if (!this.inputQueue || !this.inputWriter || !this.bridge) {
73
+ this.writeOutput("\r\nSwiftTUI WASI browser runtime requires SharedArrayBuffer-backed stdin. Serve the app with COOP/COEP headers.\r\n");
74
+ return;
75
+ }
76
+ this.worker = new Worker(this.workerModuleURL, { type: "module" });
77
+ this.worker.addEventListener("message", (event) => {
78
+ this.handleWorkerMessage(event.data);
79
+ });
80
+ this.worker.addEventListener("error", (event) => {
81
+ this.bridge?.stderr.write(`\nSwiftTUI WASI worker failed: ${event.message || "unknown worker error"}\n`);
82
+ });
83
+ const environment = { ...this.bridge.environment };
84
+ const message = {
85
+ type: "start",
86
+ wasmURL: this.wasmURL.href,
87
+ environment,
88
+ inputQueue: this.inputQueue
89
+ };
90
+ this.worker.postMessage(message);
91
+ }
92
+ dispose() {
93
+ this.detachBridgeInputListener?.();
94
+ this.detachResizeListener?.();
95
+ this.inputWriter?.close();
96
+ this.worker?.terminate();
97
+ super.dispose();
98
+ }
99
+ handleWorkerMessage(message) {
100
+ switch (message.type) {
101
+ case "stdout":
102
+ this.bridge?.stdout.write(message.chunk);
103
+ break;
104
+ case "stderr":
105
+ this.bridge?.stderr.write(message.chunk);
106
+ break;
107
+ case "exit":
108
+ if (message.code !== 0) this.bridge?.stderr.write(`\nSwiftTUI WASI app exited with code ${message.code}.\n`);
109
+ break;
110
+ case "error":
111
+ this.bridge?.stderr.write(`\nFailed to start SwiftTUI WASI app: ${message.message}\n`);
112
+ break;
113
+ }
114
+ }
115
+ };
116
+ //#endregion
117
+ export { createWasmSceneRuntimeFactory };
118
+
119
+ //# sourceMappingURL=WasmSceneRuntime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WasmSceneRuntime.js","names":[],"sources":["../../../src/wasi/WasmSceneRuntime.ts"],"sourcesContent":["import {\n WebHostSceneRuntime,\n type WebHostSceneRuntimeOptions,\n} from \"../WebHostSceneRuntime.ts\";\nimport {\n encodeResizeControlMessage,\n type BrowserWASIBridge,\n} from \"./BrowserWASIBridge.ts\";\n\nimport {\n SharedInputQueueWriter,\n createSharedInputQueue,\n type SharedInputQueueBuffers,\n} from \"./SharedInputQueue.ts\";\n\nconst workerModuleURL = new URL(\"./wasm-scene-worker.js\", import.meta.url);\n\ninterface WorkerStartMessage {\n type: \"start\";\n wasmURL: string;\n environment: Record<string, string>;\n inputQueue: SharedInputQueueBuffers;\n}\n\ninterface WorkerOutputMessage {\n type: \"stdout\" | \"stderr\";\n chunk: Uint8Array;\n}\n\ninterface WorkerExitMessage {\n type: \"exit\";\n code: number;\n}\n\ninterface WorkerErrorMessage {\n type: \"error\";\n message: string;\n}\n\ntype WorkerMessage = WorkerOutputMessage | WorkerExitMessage | WorkerErrorMessage;\n\nexport interface WasmSceneResizeEvent {\n sceneId: string;\n columns: number;\n rows: number;\n cellWidth?: number;\n cellHeight?: number;\n}\n\nexport interface WasmSceneRuntimeHandle {\n readonly descriptor: WebHostSceneRuntime[\"descriptor\"];\n sendInput(chunk: Uint8Array): void;\n}\n\nexport interface WasmSceneRuntimeFactoryOptions {\n onSceneResize?(event: WasmSceneResizeEvent): void;\n onRuntimeCreated?(runtime: WasmSceneRuntimeHandle): void;\n workerModuleURL?: string | URL;\n}\n\nexport function createWasmSceneRuntimeFactory(\n wasmURL: URL,\n factoryOptions: WasmSceneRuntimeFactoryOptions = {}\n): (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime {\n return (options) => {\n const runtime = new WasmSceneRuntime(options, wasmURL, factoryOptions);\n factoryOptions.onRuntimeCreated?.(runtime);\n return runtime;\n };\n}\n\nclass WasmSceneRuntime extends WebHostSceneRuntime {\n private readonly bridge?: BrowserWASIBridge;\n private readonly wasmURL: URL;\n private readonly onSceneResize?: (event: WasmSceneResizeEvent) => void;\n private readonly workerModuleURL: string | URL;\n private readonly inputQueue?: SharedInputQueueBuffers;\n private readonly inputWriter?: SharedInputQueueWriter;\n\n private detachBridgeInputListener?: () => void;\n private detachResizeListener?: () => void;\n private worker?: Worker;\n private didMount = false;\n\n constructor(\n options: WebHostSceneRuntimeOptions,\n wasmURL: URL,\n factoryOptions: WasmSceneRuntimeFactoryOptions\n ) {\n let inputQueue: SharedInputQueueBuffers | undefined;\n let inputWriter: SharedInputQueueWriter | undefined;\n\n try {\n inputQueue = createSharedInputQueue();\n inputWriter = new SharedInputQueueWriter(inputQueue);\n } catch (error) {\n console.error(\"[SwiftTUIWeb] failed to create shared stdin queue\", error);\n }\n\n super({\n ...options,\n onInput: (chunk) => {\n try {\n inputWriter?.write(chunk);\n } catch (error) {\n console.error(\"[SwiftTUIWeb] failed to enqueue terminal input\", error);\n }\n },\n });\n\n this.bridge = options.bridge;\n this.wasmURL = wasmURL;\n this.onSceneResize = factoryOptions.onSceneResize;\n this.workerModuleURL = factoryOptions.workerModuleURL ?? workerModuleURL;\n this.inputQueue = inputQueue;\n this.inputWriter = inputWriter;\n }\n\n override async mount(): Promise<void> {\n await super.mount();\n if (this.didMount) {\n return;\n }\n\n this.didMount = true;\n this.detachBridgeInputListener = this.bridge?.stdin.subscribe((chunk) => {\n this.inputWriter?.write(chunk);\n });\n this.detachResizeListener = this.bridge?.subscribeResize((columns, rows, cellWidth, cellHeight) => {\n this.onSceneResize?.({\n sceneId: this.descriptor.id,\n columns,\n rows,\n cellWidth,\n cellHeight,\n });\n });\n\n const initialColumns = Number(this.bridge?.environment.TUIGUI_COLUMNS ?? \"0\") || 0;\n const initialRows = Number(this.bridge?.environment.TUIGUI_ROWS ?? \"0\") || 0;\n if (!this.bridge && initialColumns > 0 && initialRows > 0) {\n this.onSceneResize?.({\n sceneId: this.descriptor.id,\n columns: initialColumns,\n rows: initialRows,\n });\n }\n\n if (!this.inputQueue || !this.inputWriter || !this.bridge) {\n this.writeOutput(\n \"\\r\\nSwiftTUI WASI browser runtime requires SharedArrayBuffer-backed stdin. Serve the app with COOP/COEP headers.\\r\\n\"\n );\n return;\n }\n\n this.worker = new Worker(this.workerModuleURL, { type: \"module\" });\n this.worker.addEventListener(\"message\", (event: MessageEvent<WorkerMessage>) => {\n this.handleWorkerMessage(event.data);\n });\n this.worker.addEventListener(\"error\", (event) => {\n this.bridge?.stderr.write(\n `\\nSwiftTUI WASI worker failed: ${event.message || \"unknown worker error\"}\\n`\n );\n });\n\n const environment = { ...this.bridge.environment };\n\n const message: WorkerStartMessage = {\n type: \"start\",\n wasmURL: this.wasmURL.href,\n environment,\n inputQueue: this.inputQueue,\n };\n this.worker.postMessage(message);\n }\n\n override dispose(): void {\n this.detachBridgeInputListener?.();\n this.detachResizeListener?.();\n this.inputWriter?.close();\n this.worker?.terminate();\n super.dispose();\n }\n\n private handleWorkerMessage(\n message: WorkerMessage\n ): void {\n switch (message.type) {\n case \"stdout\":\n this.bridge?.stdout.write(message.chunk);\n break;\n case \"stderr\":\n this.bridge?.stderr.write(message.chunk);\n break;\n case \"exit\":\n if (message.code !== 0) {\n this.bridge?.stderr.write(`\\nSwiftTUI WASI app exited with code ${message.code}.\\n`);\n }\n break;\n case \"error\":\n this.bridge?.stderr.write(`\\nFailed to start SwiftTUI WASI app: ${message.message}\\n`);\n break;\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { SharedInputQueueBuffers } from "./SharedInputQueue.js";
2
+
3
+ //#region src/wasi/WasmSceneWorker.d.ts
4
+ interface StartWasmSceneWorkerMessage {
5
+ type: "start";
6
+ wasmURL: string;
7
+ environment: Record<string, string>;
8
+ inputQueue: SharedInputQueueBuffers;
9
+ }
10
+ interface OutputWasmSceneWorkerMessage {
11
+ type: "stdout" | "stderr";
12
+ chunk: Uint8Array;
13
+ }
14
+ interface ExitWasmSceneWorkerMessage {
15
+ type: "exit";
16
+ code: number;
17
+ }
18
+ interface ErrorWasmSceneWorkerMessage {
19
+ type: "error";
20
+ message: string;
21
+ }
22
+ type WasmSceneWorkerMessage = StartWasmSceneWorkerMessage;
23
+ type WasmSceneWorkerResponse = OutputWasmSceneWorkerMessage | ExitWasmSceneWorkerMessage | ErrorWasmSceneWorkerMessage;
24
+ declare function startWasmSceneWorker(): void;
25
+ //#endregion
26
+ export { ErrorWasmSceneWorkerMessage, ExitWasmSceneWorkerMessage, OutputWasmSceneWorkerMessage, StartWasmSceneWorkerMessage, WasmSceneWorkerMessage, WasmSceneWorkerResponse, startWasmSceneWorker };
27
+ //# sourceMappingURL=WasmSceneWorker.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { SharedInputQueueReader } from "./SharedInputQueue.js";
2
+ import { WasiPollScheduler } from "./WasiPollScheduler.js";
3
+ import { ConsoleStdout, Fd, WASI, wasi } from "@bjorn3/browser_wasi_shim";
4
+ //#region src/wasi/WasmSceneWorker.ts
5
+ function startWasmSceneWorker() {
6
+ globalThis.addEventListener("message", (event) => {
7
+ if (event.data.type !== "start") return;
8
+ startWasmScene(event.data);
9
+ });
10
+ }
11
+ var BlockingInputFileDescriptor = class extends Fd {
12
+ reader;
13
+ fdstat = (() => {
14
+ const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0);
15
+ fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_READ);
16
+ return fdstat;
17
+ })();
18
+ constructor(inputQueue) {
19
+ super();
20
+ this.reader = new SharedInputQueueReader(inputQueue);
21
+ }
22
+ fd_fdstat_get() {
23
+ return {
24
+ ret: wasi.ERRNO_SUCCESS,
25
+ fdstat: this.fdstat
26
+ };
27
+ }
28
+ fd_filestat_get() {
29
+ return {
30
+ ret: wasi.ERRNO_SUCCESS,
31
+ filestat: new wasi.Filestat(0n, wasi.FILETYPE_CHARACTER_DEVICE, 0n)
32
+ };
33
+ }
34
+ fd_read(size) {
35
+ const chunk = this.reader.readAvailable(size);
36
+ if (chunk) return {
37
+ ret: wasi.ERRNO_SUCCESS,
38
+ data: chunk
39
+ };
40
+ if (this.reader.isClosed()) return {
41
+ ret: wasi.ERRNO_SUCCESS,
42
+ data: new Uint8Array()
43
+ };
44
+ return {
45
+ ret: wasi.ERRNO_AGAIN,
46
+ data: new Uint8Array()
47
+ };
48
+ }
49
+ availableBytes() {
50
+ return this.reader.availableBytes();
51
+ }
52
+ waitForReadable(timeoutMilliseconds) {
53
+ return this.reader.waitForReadable(timeoutMilliseconds);
54
+ }
55
+ isClosed() {
56
+ return this.reader.isClosed();
57
+ }
58
+ };
59
+ function installWasiPollScheduler(wasiBridge, stdin) {
60
+ const originalPoll = wasiBridge.wasiImport.poll_oneoff;
61
+ if (typeof originalPoll !== "function") return;
62
+ const scheduler = new WasiPollScheduler({
63
+ memory: () => wasiBridge.inst?.exports.memory,
64
+ stdin,
65
+ fallbackPoll: (inPtr, outPtr, nsubscriptions, neventsPtr) => originalPoll(inPtr, outPtr, nsubscriptions, neventsPtr)
66
+ });
67
+ wasiBridge.wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => scheduler.pollOneOff(inPtr, outPtr, nsubscriptions, neventsPtr);
68
+ }
69
+ async function startWasmScene(message) {
70
+ try {
71
+ const stdin = new BlockingInputFileDescriptor(message.inputQueue);
72
+ const wasiBridge = new WASI(["app.wasm"], Object.entries(message.environment).map(([key, value]) => `${key}=${value}`), [
73
+ stdin,
74
+ new ConsoleStdout((chunk) => {
75
+ postWorkerMessage({
76
+ type: "stdout",
77
+ chunk
78
+ });
79
+ }),
80
+ new ConsoleStdout((chunk) => {
81
+ postWorkerMessage({
82
+ type: "stderr",
83
+ chunk
84
+ });
85
+ })
86
+ ]);
87
+ installWasiPollScheduler(wasiBridge, stdin);
88
+ const response = await fetch(message.wasmURL);
89
+ if (!response.ok) throw new Error(`failed to load ${message.wasmURL}: ${response.status} ${response.statusText}`);
90
+ const module = await WebAssembly.compile(await response.arrayBuffer());
91
+ const instance = await WebAssembly.instantiate(module, { wasi_snapshot_preview1: wasiBridge.wasiImport });
92
+ postWorkerMessage({
93
+ type: "exit",
94
+ code: wasiBridge.start(instance)
95
+ });
96
+ } catch (error) {
97
+ postWorkerMessage({
98
+ type: "error",
99
+ message: error instanceof Error ? error.message : String(error)
100
+ });
101
+ }
102
+ }
103
+ function postWorkerMessage(message) {
104
+ globalThis.postMessage(message);
105
+ }
106
+ //#endregion
107
+ export { startWasmSceneWorker };
108
+
109
+ //# sourceMappingURL=WasmSceneWorker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WasmSceneWorker.js","names":[],"sources":["../../../src/wasi/WasmSceneWorker.ts"],"sourcesContent":["import { ConsoleStdout, Fd, WASI, wasi } from \"@bjorn3/browser_wasi_shim\";\nimport {\n SharedInputQueueReader,\n type SharedInputQueueBuffers,\n} from \"./SharedInputQueue.ts\";\nimport { WasiPollScheduler } from \"./WasiPollScheduler.ts\";\n\nexport interface StartWasmSceneWorkerMessage {\n type: \"start\";\n wasmURL: string;\n environment: Record<string, string>;\n inputQueue: SharedInputQueueBuffers;\n}\n\nexport interface OutputWasmSceneWorkerMessage {\n type: \"stdout\" | \"stderr\";\n chunk: Uint8Array;\n}\n\nexport interface ExitWasmSceneWorkerMessage {\n type: \"exit\";\n code: number;\n}\n\nexport interface ErrorWasmSceneWorkerMessage {\n type: \"error\";\n message: string;\n}\n\nexport type WasmSceneWorkerMessage = StartWasmSceneWorkerMessage;\nexport type WasmSceneWorkerResponse =\n | OutputWasmSceneWorkerMessage\n | ExitWasmSceneWorkerMessage\n | ErrorWasmSceneWorkerMessage;\n\nexport function startWasmSceneWorker(): void {\n globalThis.addEventListener(\"message\", (event: MessageEvent<WasmSceneWorkerMessage>) => {\n if (event.data.type !== \"start\") {\n return;\n }\n\n void startWasmScene(event.data);\n });\n}\n\nclass BlockingInputFileDescriptor extends Fd {\n private readonly reader: SharedInputQueueReader;\n private readonly fdstat = (() => {\n const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0);\n fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_READ);\n return fdstat;\n })();\n\n constructor(inputQueue: SharedInputQueueBuffers) {\n super();\n this.reader = new SharedInputQueueReader(inputQueue);\n }\n\n override fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat } {\n return {\n ret: wasi.ERRNO_SUCCESS,\n fdstat: this.fdstat,\n };\n }\n\n override fd_filestat_get(): { ret: number; filestat: wasi.Filestat } {\n return {\n ret: wasi.ERRNO_SUCCESS,\n filestat: new wasi.Filestat(0n, wasi.FILETYPE_CHARACTER_DEVICE, 0n),\n };\n }\n\n override fd_read(size: number): { ret: number; data: Uint8Array } {\n const chunk = this.reader.readAvailable(size);\n if (chunk) {\n return {\n ret: wasi.ERRNO_SUCCESS,\n data: chunk,\n };\n }\n\n if (this.reader.isClosed()) {\n return {\n ret: wasi.ERRNO_SUCCESS,\n data: new Uint8Array(),\n };\n }\n\n return {\n ret: wasi.ERRNO_AGAIN,\n data: new Uint8Array(),\n };\n }\n\n availableBytes(): number {\n return this.reader.availableBytes();\n }\n\n waitForReadable(\n timeoutMilliseconds?: number\n ) {\n return this.reader.waitForReadable(timeoutMilliseconds);\n }\n\n isClosed(): boolean {\n return this.reader.isClosed();\n }\n}\n\nfunction installWasiPollScheduler(\n wasiBridge: WASI,\n stdin: BlockingInputFileDescriptor\n): void {\n const originalPoll = wasiBridge.wasiImport.poll_oneoff;\n if (typeof originalPoll !== \"function\") {\n return;\n }\n\n const scheduler = new WasiPollScheduler({\n memory: () => wasiBridge.inst?.exports.memory as WebAssembly.Memory | undefined,\n stdin,\n fallbackPoll: (inPtr, outPtr, nsubscriptions, neventsPtr) =>\n originalPoll(inPtr, outPtr, nsubscriptions, neventsPtr),\n });\n wasiBridge.wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) =>\n scheduler.pollOneOff(inPtr, outPtr, nsubscriptions, neventsPtr);\n}\n\nasync function startWasmScene(\n message: StartWasmSceneWorkerMessage\n): Promise<void> {\n try {\n const stdin = new BlockingInputFileDescriptor(message.inputQueue);\n const wasiBridge = new WASI(\n [\"app.wasm\"],\n Object.entries(message.environment).map(([key, value]) => `${key}=${value}`),\n [\n stdin,\n new ConsoleStdout((chunk) => {\n postWorkerMessage({\n type: \"stdout\",\n chunk,\n });\n }),\n new ConsoleStdout((chunk) => {\n postWorkerMessage({\n type: \"stderr\",\n chunk,\n });\n }),\n ]\n );\n installWasiPollScheduler(wasiBridge, stdin);\n\n const response = await fetch(message.wasmURL);\n if (!response.ok) {\n throw new Error(`failed to load ${message.wasmURL}: ${response.status} ${response.statusText}`);\n }\n\n const module = await WebAssembly.compile(await response.arrayBuffer());\n const instance = await WebAssembly.instantiate(module, {\n wasi_snapshot_preview1: wasiBridge.wasiImport,\n });\n\n const exitCode = wasiBridge.start(instance as WebAssembly.Instance);\n postWorkerMessage({\n type: \"exit\",\n code: exitCode,\n });\n } catch (error) {\n postWorkerMessage({\n type: \"error\",\n message: error instanceof Error ? error.message : String(error),\n });\n }\n}\n\nfunction postWorkerMessage(\n message: WasmSceneWorkerResponse\n): void {\n globalThis.postMessage(message);\n}\n"],"mappings":";;;;AAmCA,SAAgB,uBAA6B;CAC3C,WAAW,iBAAiB,YAAY,UAAgD;EACtF,IAAI,MAAM,KAAK,SAAS,SACtB;EAGF,eAAoB,MAAM,IAAI;CAChC,CAAC;AACH;AAEA,IAAM,8BAAN,cAA0C,GAAG;CAC3C;CACA,gBAAiC;EAC/B,MAAM,SAAS,IAAI,KAAK,OAAO,KAAK,2BAA2B,CAAC;EAChE,OAAO,iBAAiB,OAAO,KAAK,cAAc;EAClD,OAAO;CACT,EAAA,CAAG;CAEH,YAAY,YAAqC;EAC/C,MAAM;EACN,KAAK,SAAS,IAAI,uBAAuB,UAAU;CACrD;CAEA,gBAA+D;EAC7D,OAAO;GACL,KAAK,KAAK;GACV,QAAQ,KAAK;EACf;CACF;CAEA,kBAAqE;EACnE,OAAO;GACL,KAAK,KAAK;GACV,UAAU,IAAI,KAAK,SAAS,IAAI,KAAK,2BAA2B,EAAE;EACpE;CACF;CAEA,QAAiB,MAAiD;EAChE,MAAM,QAAQ,KAAK,OAAO,cAAc,IAAI;EAC5C,IAAI,OACF,OAAO;GACL,KAAK,KAAK;GACV,MAAM;EACR;EAGF,IAAI,KAAK,OAAO,SAAS,GACvB,OAAO;GACL,KAAK,KAAK;GACV,MAAM,IAAI,WAAW;EACvB;EAGF,OAAO;GACL,KAAK,KAAK;GACV,MAAM,IAAI,WAAW;EACvB;CACF;CAEA,iBAAyB;EACvB,OAAO,KAAK,OAAO,eAAe;CACpC;CAEA,gBACE,qBACA;EACA,OAAO,KAAK,OAAO,gBAAgB,mBAAmB;CACxD;CAEA,WAAoB;EAClB,OAAO,KAAK,OAAO,SAAS;CAC9B;AACF;AAEA,SAAS,yBACP,YACA,OACM;CACN,MAAM,eAAe,WAAW,WAAW;CAC3C,IAAI,OAAO,iBAAiB,YAC1B;CAGF,MAAM,YAAY,IAAI,kBAAkB;EACtC,cAAc,WAAW,MAAM,QAAQ;EACvC;EACA,eAAe,OAAO,QAAQ,gBAAgB,eAC5C,aAAa,OAAO,QAAQ,gBAAgB,UAAU;CAC1D,CAAC;CACD,WAAW,WAAW,eAAe,OAAO,QAAQ,gBAAgB,eAClE,UAAU,WAAW,OAAO,QAAQ,gBAAgB,UAAU;AAClE;AAEA,eAAe,eACb,SACe;CACf,IAAI;EACF,MAAM,QAAQ,IAAI,4BAA4B,QAAQ,UAAU;EAChE,MAAM,aAAa,IAAI,KACrB,CAAC,UAAU,GACX,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,GAAG,IAAI,GAAG,OAAO,GAC3E;GACE;GACA,IAAI,eAAe,UAAU;IAC3B,kBAAkB;KAChB,MAAM;KACN;IACF,CAAC;GACH,CAAC;GACD,IAAI,eAAe,UAAU;IAC3B,kBAAkB;KAChB,MAAM;KACN;IACF,CAAC;GACH,CAAC;EACH,CACF;EACA,yBAAyB,YAAY,KAAK;EAE1C,MAAM,WAAW,MAAM,MAAM,QAAQ,OAAO;EAC5C,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,kBAAkB,QAAQ,QAAQ,IAAI,SAAS,OAAO,GAAG,SAAS,YAAY;EAGhG,MAAM,SAAS,MAAM,YAAY,QAAQ,MAAM,SAAS,YAAY,CAAC;EACrE,MAAM,WAAW,MAAM,YAAY,YAAY,QAAQ,EACrD,wBAAwB,WAAW,WACrC,CAAC;EAGD,kBAAkB;GAChB,MAAM;GACN,MAHe,WAAW,MAAM,QAGnB;EACf,CAAC;CACH,SAAS,OAAO;EACd,kBAAkB;GAChB,MAAM;GACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAChE,CAAC;CACH;AACF;AAEA,SAAS,kBACP,SACM;CACN,WAAW,YAAY,OAAO;AAChC"}
@@ -0,0 +1,2 @@
1
+ import { transportFixture } from "./src/WebHostTestFixtures.js";
2
+ export { transportFixture };
@@ -0,0 +1,2 @@
1
+ import { transportFixture } from "./src/WebHostTestFixtures.js";
2
+ export { transportFixture };
@@ -0,0 +1,2 @@
1
+ import { ErrorWasmSceneWorkerMessage, ExitWasmSceneWorkerMessage, OutputWasmSceneWorkerMessage, StartWasmSceneWorkerMessage, WasmSceneWorkerMessage, WasmSceneWorkerResponse, startWasmSceneWorker } from "./src/wasi/WasmSceneWorker.js";
2
+ export { ErrorWasmSceneWorkerMessage, ExitWasmSceneWorkerMessage, OutputWasmSceneWorkerMessage, StartWasmSceneWorkerMessage, WasmSceneWorkerMessage, WasmSceneWorkerResponse, startWasmSceneWorker };
@@ -0,0 +1,2 @@
1
+ import { startWasmSceneWorker } from "./src/wasi/WasmSceneWorker.js";
2
+ export { startWasmSceneWorker };
package/dist/wasi.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
2
+ import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
3
+ import { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink } from "./src/wasi/BrowserWASIBridge.js";
4
+ import { SharedInputQueueBuffers, SharedInputQueueReader, SharedInputQueueWriter, SharedInputReadiness, createSharedInputQueue, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity } from "./src/wasi/SharedInputQueue.js";
5
+ import { WasmSceneResizeEvent, WasmSceneRuntimeFactoryOptions, WasmSceneRuntimeHandle, createWasmSceneRuntimeFactory } from "./src/wasi/WasmSceneRuntime.js";
6
+ export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, SharedInputQueueBuffers, SharedInputQueueReader, SharedInputQueueWriter, SharedInputReadiness, StdIOPipe, WasmSceneResizeEvent, WasmSceneRuntimeFactoryOptions, WasmSceneRuntimeHandle, createSharedInputQueue, createWasmSceneRuntimeFactory, encodeRenderStyleControlMessage, encodeResizeControlMessage, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity };
package/dist/wasi.js ADDED
@@ -0,0 +1,6 @@
1
+ import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
2
+ import { encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
3
+ import { BrowserWASIBridge } from "./src/wasi/BrowserWASIBridge.js";
4
+ import { SharedInputQueueReader, SharedInputQueueWriter, createSharedInputQueue, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity } from "./src/wasi/SharedInputQueue.js";
5
+ import { createWasmSceneRuntimeFactory } from "./src/wasi/WasmSceneRuntime.js";
6
+ export { BrowserWASIBridge, SharedInputQueueReader, SharedInputQueueWriter, StdIOPipe, createSharedInputQueue, createWasmSceneRuntimeFactory, encodeRenderStyleControlMessage, encodeResizeControlMessage, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity };
@@ -0,0 +1,2 @@
1
+ import { WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
2
+ export { WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, webSocketSceneURL };
@@ -0,0 +1,2 @@
1
+ import { WebSocketSceneBridge, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
2
+ export { WebSocketSceneBridge, webSocketSceneURL };
package/package.json CHANGED
@@ -1,33 +1,64 @@
1
1
  {
2
2
  "name": "@swifttui/web",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "license": "MIT",
5
- "module": "index.ts",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "files": [
14
+ "dist",
15
+ "style.css"
16
+ ],
6
17
  "exports": {
7
- ".": "./index.ts",
8
- "./manifest": "./manifest.ts",
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./manifest": {
23
+ "types": "./dist/manifest.d.ts",
24
+ "import": "./dist/manifest.js"
25
+ },
26
+ "./testing": {
27
+ "types": "./dist/testing.d.ts",
28
+ "import": "./dist/testing.js"
29
+ },
30
+ "./wasi": {
31
+ "types": "./dist/wasi.d.ts",
32
+ "import": "./dist/wasi.js"
33
+ },
34
+ "./wasi-worker": {
35
+ "types": "./dist/wasi-worker.d.ts",
36
+ "import": "./dist/wasi-worker.js"
37
+ },
38
+ "./websocket": {
39
+ "types": "./dist/websocket.d.ts",
40
+ "import": "./dist/websocket.js"
41
+ },
9
42
  "./style.css": "./style.css",
10
- "./testing": "./testing.ts",
11
- "./wasi": "./wasi.ts",
12
- "./wasi-worker": "./wasi-worker.ts",
13
- "./websocket": "./websocket.ts"
43
+ "./package.json": "./package.json"
14
44
  },
15
- "type": "module",
16
45
  "scripts": {
17
- "build:manifest": "bun run --cwd ../build build:manifest -- --dist ../web/dist",
18
- "build:wasm": "bun run --cwd ../build build:wasm -- --dist ../web/dist",
19
- "build:web": "bun run cli.ts build:web",
20
- "build": "bun run cli.ts build",
21
- "dev": "bun run cli.ts dev",
22
- "test": "bun test"
46
+ "build": "tsdown",
47
+ "build:manifest": "bun run --cwd ../build build:manifest -- --dist ../web/dist-demo",
48
+ "build:wasm": "bun run --cwd ../build build:wasm -- --dist ../web/dist-demo",
49
+ "build:web": "bun run cli.ts build:web --dist dist-demo",
50
+ "build:app": "bun run cli.ts build --dist dist-demo",
51
+ "dev": "bun run cli.ts dev --dist dist-demo",
52
+ "test": "bun test",
53
+ "prepublishOnly": "tsdown"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
23
57
  },
24
58
  "dependencies": {
25
59
  "@bjorn3/browser_wasi_shim": "^0.4.2"
26
60
  },
27
61
  "devDependencies": {
28
62
  "@types/bun": "1.3.13"
29
- },
30
- "peerDependencies": {
31
- "typescript": "^5"
32
63
  }
33
64
  }
package/AGENTS.md DELETED
@@ -1,52 +0,0 @@
1
- # AGENTS.md
2
-
3
- Guidance for agentic assistants working in **`@swifttui/web`**. Keep this
4
- concise; [`README.md`](README.md) is the full reference.
5
-
6
- ## What this package is
7
-
8
- The **browser runtime** for SwiftTUI apps. It owns the browser-safe runtime
9
- APIs: scene-manifest loading, canvas rendering, ARIA mounting, WebSocket scene
10
- bridges, and WASI scene bridges. Build/packaging tooling lives in the sibling
11
- [`@swifttui/build`](../build) workspace package — keep that split.
12
-
13
- It lives in the repo's Bun workspace (`packages/web` in `swift-tui-web`). Run
14
- `bun install` from the repo root or any package dir; one root `bun.lock` is
15
- maintained.
16
-
17
- ## Toolchains
18
-
19
- - **Bun** for dev, bundling, and the test runner.
20
- - **`swiftly`** Swift 6.3.1 for any Swift the build path triggers
21
- (`swiftly run swift --version`). Do not use bare `swift`/`xcrun swift`.
22
-
23
- ## Commands
24
-
25
- ```bash
26
- bun test # this package's tests (or `bun run test`)
27
- bun run build:web # bundle index.html + browser entrypoint
28
- bun run build -- --app <Exe> # full build (manifest + wasm + web)
29
- bun run dev # watch/dev
30
- ```
31
-
32
- `build:manifest` and `build:wasm` delegate to `@swifttui/build` and default to
33
- `--configuration release`. The org-level gate for this repo is `bun run ci`
34
- (install --frozen-lockfile + test + build:web), run from the `swift-tui-web` root.
35
-
36
- ## Architecture notes
37
-
38
- - Transport is SwiftTUI's **`web-surface` WASI transport**: the Swift runner
39
- emits raster-surface records on stdout and the host draws rects/text to a
40
- canvas. **No terminal emulator** — does not use `ghostty-web`/`ghostty-vt.wasm`.
41
- `BrowserWASIBridge` sets `TUIGUI_TRANSPORT=surface`.
42
- - Entry points: `createWebHostApp` (`.`), `createWasmSceneRuntimeFactory`
43
- (`./wasi`), `startWasmSceneWorker` (`./wasi-worker`). Subpath exports are
44
- declared in `package.json` — keep `exports` in sync when adding modules.
45
- - Scene switching is controller-managed and retains existing scene runtimes.
46
- - Terminal styling is host-owned via `WebHostTerminalStyle` (one active
47
- palette/theme pair); the library ships no built-in mode switcher.
48
-
49
- ## Conventions
50
-
51
- `AGENTS.md` is the real file; `CLAUDE.md` is a symlink to it. Edit `AGENTS.md`.
52
- Tests are colocated as `*.test.ts` (browser-only specs use `*.browser.ts`).
package/cli.ts DELETED
@@ -1,168 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import { mkdir } from "node:fs/promises";
4
- import { join, resolve } from "node:path";
5
- import {
6
- buildAppWasm,
7
- buildSwiftTUIWebApp,
8
- generateSceneManifest,
9
- type WasmBuildConfiguration,
10
- } from "../build/index.ts";
11
-
12
- void runCli(process.argv.slice(2));
13
-
14
- async function runCli(argv: string[]): Promise<void> {
15
- const command = argv[0] ?? "build";
16
- const flags = parseFlags(argv.slice(1));
17
- const packagePath = resolve(flags["package-path"] ?? "../../");
18
- const distPath = resolve(flags["dist"] ?? "./dist");
19
- const appExecutable = flags.app ?? flags.product ?? flags["app-product"] ?? "";
20
- const wasmConfiguration = parseWasmBuildConfiguration(flags.configuration ?? "release");
21
-
22
- switch (command) {
23
- case "build:manifest":
24
- assertAppExecutable(appExecutable);
25
- await generateSceneManifest({
26
- packagePath,
27
- outputPath: join(distPath, "scene-manifest.json"),
28
- appExecutable,
29
- });
30
- return;
31
- case "build:wasm":
32
- assertAppExecutable(appExecutable);
33
- await buildAppWasm({
34
- configuration: wasmConfiguration,
35
- packagePath,
36
- outputDirectory: distPath,
37
- product: appExecutable,
38
- });
39
- return;
40
- case "build:web":
41
- await bunBuildWeb({
42
- outputDirectory: distPath,
43
- });
44
- return;
45
- case "build":
46
- assertAppExecutable(appExecutable);
47
- await buildSwiftTUIWebApp({
48
- configuration: wasmConfiguration,
49
- packagePath,
50
- outputDirectory: distPath,
51
- product: appExecutable,
52
- });
53
- await bunBuildWeb({
54
- outputDirectory: distPath,
55
- });
56
- return;
57
- case "dev":
58
- await bunServe(packagePath, distPath);
59
- return;
60
- default:
61
- throw new Error(`unknown command: ${command}`);
62
- }
63
- }
64
-
65
- async function bunBuildWeb(options: {
66
- outputDirectory: string;
67
- }): Promise<void> {
68
- await mkdir(options.outputDirectory, { recursive: true });
69
- const proc = Bun.spawn({
70
- cmd: [
71
- "bun",
72
- "build",
73
- "./index.html",
74
- "--outdir",
75
- options.outputDirectory,
76
- ],
77
- stdout: "pipe",
78
- stderr: "pipe",
79
- });
80
- const [stdout, stderr, exitCode] = await Promise.all([
81
- new Response(proc.stdout).text(),
82
- new Response(proc.stderr).text(),
83
- proc.exited,
84
- ]);
85
-
86
- if (exitCode !== 0) {
87
- throw new Error([stdout, stderr].filter(Boolean).join("\n").trim() || "web build failed");
88
- }
89
- }
90
-
91
- async function bunServe(
92
- packagePath: string,
93
- distPath: string
94
- ): Promise<void> {
95
- const server = Bun.serve({
96
- port: Number(process.env.PORT ?? "3000"),
97
- development: {
98
- hmr: true,
99
- console: true,
100
- },
101
- fetch(request) {
102
- const url = new URL(request.url);
103
- if (url.pathname === "/") {
104
- return new Response(Bun.file(join(distPath, "index.html")));
105
- }
106
- if (url.pathname === "/scene-manifest.json") {
107
- return new Response(Bun.file(join(distPath, "scene-manifest.json")));
108
- }
109
- if (url.pathname.startsWith("/assets/")) {
110
- return new Response(Bun.file(join(distPath, url.pathname.slice(1))));
111
- }
112
- if (url.pathname.startsWith("/src/")) {
113
- return new Response(Bun.file(join(packagePath, url.pathname.slice(1))));
114
- }
115
- return new Response("not found", { status: 404 });
116
- },
117
- });
118
-
119
- console.log(`WebHost dev server running at ${server.url.href}`);
120
- await new Promise<void>(() => {});
121
- }
122
-
123
- function parseFlags(
124
- argv: string[]
125
- ): Record<string, string> {
126
- const flags: Record<string, string> = {};
127
- for (let index = 0; index < argv.length; index += 1) {
128
- const value = argv[index];
129
- if (!value.startsWith("--")) {
130
- continue;
131
- }
132
- const equalsIndex = value.indexOf("=");
133
- if (equalsIndex !== -1) {
134
- flags[value.slice(2, equalsIndex)] = value.slice(equalsIndex + 1);
135
- continue;
136
- }
137
- const name = value.slice(2);
138
- const next = argv[index + 1];
139
- if (next && !next.startsWith("--")) {
140
- flags[name] = next;
141
- index += 1;
142
- } else {
143
- flags[name] = "true";
144
- }
145
- }
146
- return flags;
147
- }
148
-
149
- function parseWasmBuildConfiguration(
150
- value: string
151
- ): WasmBuildConfiguration {
152
- switch (value) {
153
- case "debug":
154
- return "debug";
155
- case "release":
156
- return "release";
157
- default:
158
- throw new Error(`unsupported wasm build configuration: ${value}`);
159
- }
160
- }
161
-
162
- function assertAppExecutable(
163
- value: string
164
- ): asserts value is string {
165
- if (!value) {
166
- throw new Error("missing --app or --product flag");
167
- }
168
- }