@textjs/app 0.0.0-stage → 1.0.3
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 +34 -2
- package/bin/text.js +29 -0
- package/bin/text.ts +13 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli/build.d.ts +39 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +157 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +73 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/dev.d.ts +12 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +49 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +26 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +192 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +31 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +111 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +14 -0
- package/dist/cli/start.d.ts.map +1 -0
- package/dist/cli/start.js +183 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/ui.d.ts +35 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +73 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +93 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +143 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +26 -0
- package/dist/context.js.map +1 -0
- package/dist/dev-errors.d.ts +68 -0
- package/dist/dev-errors.d.ts.map +1 -0
- package/dist/dev-errors.js +121 -0
- package/dist/dev-errors.js.map +1 -0
- package/dist/devtools.d.ts +64 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +186 -0
- package/dist/devtools.js.map +1 -0
- package/dist/frame-mirror.d.ts +19 -0
- package/dist/frame-mirror.d.ts.map +1 -0
- package/dist/frame-mirror.js +141 -0
- package/dist/frame-mirror.js.map +1 -0
- package/dist/frame-protocol.d.ts +175 -0
- package/dist/frame-protocol.d.ts.map +1 -0
- package/dist/frame-protocol.js +369 -0
- package/dist/frame-protocol.js.map +1 -0
- package/dist/history.d.ts +54 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +152 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +32 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +54 -0
- package/dist/link.js.map +1 -0
- package/dist/live-player.d.ts +83 -0
- package/dist/live-player.d.ts.map +1 -0
- package/dist/live-player.js +413 -0
- package/dist/live-player.js.map +1 -0
- package/dist/live-protocol.d.ts +132 -0
- package/dist/live-protocol.d.ts.map +1 -0
- package/dist/live-protocol.js +278 -0
- package/dist/live-protocol.js.map +1 -0
- package/dist/live-stub.d.ts +10 -0
- package/dist/live-stub.d.ts.map +1 -0
- package/dist/live-stub.js +32 -0
- package/dist/live-stub.js.map +1 -0
- package/dist/mcp/cli.d.ts +22 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +177 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/frame.d.ts +58 -0
- package/dist/mcp/frame.d.ts.map +1 -0
- package/dist/mcp/frame.js +169 -0
- package/dist/mcp/frame.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/inspect.d.ts +95 -0
- package/dist/mcp/inspect.d.ts.map +1 -0
- package/dist/mcp/inspect.js +386 -0
- package/dist/mcp/inspect.js.map +1 -0
- package/dist/mcp/keys.d.ts +20 -0
- package/dist/mcp/keys.d.ts.map +1 -0
- package/dist/mcp/keys.js +87 -0
- package/dist/mcp/keys.js.map +1 -0
- package/dist/mcp/logs.d.ts +63 -0
- package/dist/mcp/logs.d.ts.map +1 -0
- package/dist/mcp/logs.js +217 -0
- package/dist/mcp/logs.js.map +1 -0
- package/dist/mcp/server.d.ts +64 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +230 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +742 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/navigation-errors.d.ts +28 -0
- package/dist/navigation-errors.d.ts.map +1 -0
- package/dist/navigation-errors.js +43 -0
- package/dist/navigation-errors.js.map +1 -0
- package/dist/navigation.d.ts +21 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +33 -0
- package/dist/navigation.js.map +1 -0
- package/dist/router.d.ts +76 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.d.ts +101 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +286 -0
- package/dist/routing.js.map +1 -0
- package/dist/server/adapters/build-step.d.ts +34 -0
- package/dist/server/adapters/build-step.d.ts.map +1 -0
- package/dist/server/adapters/build-step.js +112 -0
- package/dist/server/adapters/build-step.js.map +1 -0
- package/dist/server/adapters/bun.d.ts +8 -0
- package/dist/server/adapters/bun.d.ts.map +1 -0
- package/dist/server/adapters/bun.js +89 -0
- package/dist/server/adapters/bun.js.map +1 -0
- package/dist/server/adapters/bundle.d.ts +21 -0
- package/dist/server/adapters/bundle.d.ts.map +1 -0
- package/dist/server/adapters/bundle.js +86 -0
- package/dist/server/adapters/bundle.js.map +1 -0
- package/dist/server/adapters/cloudflare.d.ts +29 -0
- package/dist/server/adapters/cloudflare.d.ts.map +1 -0
- package/dist/server/adapters/cloudflare.js +105 -0
- package/dist/server/adapters/cloudflare.js.map +1 -0
- package/dist/server/adapters/index.d.ts +10 -0
- package/dist/server/adapters/index.d.ts.map +1 -0
- package/dist/server/adapters/index.js +24 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/runtime-node.d.ts +16 -0
- package/dist/server/adapters/runtime-node.d.ts.map +1 -0
- package/dist/server/adapters/runtime-node.js +29 -0
- package/dist/server/adapters/runtime-node.js.map +1 -0
- package/dist/server/adapters/runtime.d.ts +42 -0
- package/dist/server/adapters/runtime.d.ts.map +1 -0
- package/dist/server/adapters/runtime.js +115 -0
- package/dist/server/adapters/runtime.js.map +1 -0
- package/dist/server/adapters/shared.d.ts +66 -0
- package/dist/server/adapters/shared.d.ts.map +1 -0
- package/dist/server/adapters/shared.js +124 -0
- package/dist/server/adapters/shared.js.map +1 -0
- package/dist/server/adapters/static.d.ts +9 -0
- package/dist/server/adapters/static.d.ts.map +1 -0
- package/dist/server/adapters/static.js +70 -0
- package/dist/server/adapters/static.js.map +1 -0
- package/dist/server/adapters/vercel.d.ts +43 -0
- package/dist/server/adapters/vercel.d.ts.map +1 -0
- package/dist/server/adapters/vercel.js +119 -0
- package/dist/server/adapters/vercel.js.map +1 -0
- package/dist/server/bun-jsx.d.ts +18 -0
- package/dist/server/bun-jsx.d.ts.map +1 -0
- package/dist/server/bun-jsx.js +56 -0
- package/dist/server/bun-jsx.js.map +1 -0
- package/dist/server/cache-route.d.ts +38 -0
- package/dist/server/cache-route.d.ts.map +1 -0
- package/dist/server/cache-route.js +109 -0
- package/dist/server/cache-route.js.map +1 -0
- package/dist/server/cache.d.ts +131 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +409 -0
- package/dist/server/cache.js.map +1 -0
- package/dist/server/frames.d.ts +45 -0
- package/dist/server/frames.d.ts.map +1 -0
- package/dist/server/frames.js +294 -0
- package/dist/server/frames.js.map +1 -0
- package/dist/server/http.d.ts +80 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +453 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/jsx-dev-runtime.d.ts +5 -0
- package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/server/jsx-dev-runtime.js +6 -0
- package/dist/server/jsx-dev-runtime.js.map +1 -0
- package/dist/server/jsx-runtime.d.ts +10 -0
- package/dist/server/jsx-runtime.d.ts.map +1 -0
- package/dist/server/jsx-runtime.js +11 -0
- package/dist/server/jsx-runtime.js.map +1 -0
- package/dist/server/live-app.d.ts +87 -0
- package/dist/server/live-app.d.ts.map +1 -0
- package/dist/server/live-app.js +307 -0
- package/dist/server/live-app.js.map +1 -0
- package/dist/server/live-build.d.ts +70 -0
- package/dist/server/live-build.d.ts.map +1 -0
- package/dist/server/live-build.js +222 -0
- package/dist/server/live-build.js.map +1 -0
- package/dist/server/live-dev.d.ts +62 -0
- package/dist/server/live-dev.d.ts.map +1 -0
- package/dist/server/live-dev.js +313 -0
- package/dist/server/live-dev.js.map +1 -0
- package/dist/server/live-manifest.d.ts +23 -0
- package/dist/server/live-manifest.d.ts.map +1 -0
- package/dist/server/live-manifest.js +7 -0
- package/dist/server/live-manifest.js.map +1 -0
- package/dist/server/live-prod.d.ts +30 -0
- package/dist/server/live-prod.d.ts.map +1 -0
- package/dist/server/live-prod.js +97 -0
- package/dist/server/live-prod.js.map +1 -0
- package/dist/server/live-routes.d.ts +72 -0
- package/dist/server/live-routes.d.ts.map +1 -0
- package/dist/server/live-routes.js +274 -0
- package/dist/server/live-routes.js.map +1 -0
- package/dist/server/live-web.d.ts +16 -0
- package/dist/server/live-web.d.ts.map +1 -0
- package/dist/server/live-web.js +159 -0
- package/dist/server/live-web.js.map +1 -0
- package/dist/server/live.d.ts +138 -0
- package/dist/server/live.d.ts.map +1 -0
- package/dist/server/live.js +542 -0
- package/dist/server/live.js.map +1 -0
- package/dist/server/middleware-node.d.ts +48 -0
- package/dist/server/middleware-node.d.ts.map +1 -0
- package/dist/server/middleware-node.js +167 -0
- package/dist/server/middleware-node.js.map +1 -0
- package/dist/server/middleware-runtime.d.ts +97 -0
- package/dist/server/middleware-runtime.d.ts.map +1 -0
- package/dist/server/middleware-runtime.js +447 -0
- package/dist/server/middleware-runtime.js.map +1 -0
- package/dist/server/middleware.d.ts +145 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +365 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/ppr.d.ts +22 -0
- package/dist/server/ppr.d.ts.map +1 -0
- package/dist/server/ppr.js +58 -0
- package/dist/server/ppr.js.map +1 -0
- package/dist/server/scan.d.ts +48 -0
- package/dist/server/scan.d.ts.map +1 -0
- package/dist/server/scan.js +161 -0
- package/dist/server/scan.js.map +1 -0
- package/dist/server/server-components.d.ts +65 -0
- package/dist/server/server-components.d.ts.map +1 -0
- package/dist/server/server-components.js +215 -0
- package/dist/server/server-components.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +5 -0
- package/dist/terminal.js.map +1 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +5 -0
- package/dist/testing.js.map +1 -0
- package/dist/vite-plugin.d.ts +51 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +403 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +130 -4
- package/src/cli/build.ts +182 -0
- package/src/cli/config.ts +72 -0
- package/src/cli/dev.ts +54 -0
- package/src/cli/index.ts +185 -0
- package/src/cli/run.ts +124 -0
- package/src/cli/start.ts +194 -0
- package/src/cli/ui.ts +93 -0
- package/src/client.tsx +122 -0
- package/src/config.ts +155 -0
- package/src/context.ts +69 -0
- package/src/dev-errors.ts +167 -0
- package/src/devtools.tsx +419 -0
- package/src/frame-mirror.ts +145 -0
- package/src/frame-protocol.ts +435 -0
- package/src/history.ts +201 -0
- package/src/index.ts +9 -0
- package/src/link.tsx +108 -0
- package/src/live-player.ts +449 -0
- package/src/live-protocol.ts +317 -0
- package/src/live-stub.tsx +32 -0
- package/src/mcp/cli.ts +188 -0
- package/src/mcp/frame.ts +224 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/inspect.ts +445 -0
- package/src/mcp/keys.ts +86 -0
- package/src/mcp/logs.ts +225 -0
- package/src/mcp/server.ts +595 -0
- package/src/mcp/session.ts +821 -0
- package/src/navigation-errors.ts +60 -0
- package/src/navigation.ts +44 -0
- package/src/router.tsx +497 -0
- package/src/routing.ts +344 -0
- package/src/server/adapters/build-step.ts +135 -0
- package/src/server/adapters/bun.ts +92 -0
- package/src/server/adapters/bundle.ts +97 -0
- package/src/server/adapters/cloudflare.ts +111 -0
- package/src/server/adapters/index.ts +29 -0
- package/src/server/adapters/runtime-node.ts +34 -0
- package/src/server/adapters/runtime.ts +145 -0
- package/src/server/adapters/shared.ts +159 -0
- package/src/server/adapters/static.ts +68 -0
- package/src/server/adapters/vercel.ts +149 -0
- package/src/server/bun-jsx.ts +65 -0
- package/src/server/cache-route.ts +139 -0
- package/src/server/cache.ts +498 -0
- package/src/server/frames.ts +326 -0
- package/src/server/http.ts +467 -0
- package/src/server/jsx-dev-runtime.ts +9 -0
- package/src/server/jsx-runtime.ts +13 -0
- package/src/server/live-app.tsx +374 -0
- package/src/server/live-build.ts +258 -0
- package/src/server/live-dev.ts +357 -0
- package/src/server/live-manifest.ts +25 -0
- package/src/server/live-prod.ts +120 -0
- package/src/server/live-routes.ts +298 -0
- package/src/server/live-web.ts +170 -0
- package/src/server/live.ts +551 -0
- package/src/server/middleware-node.ts +168 -0
- package/src/server/middleware-runtime.ts +478 -0
- package/src/server/middleware.ts +451 -0
- package/src/server/ppr.ts +62 -0
- package/src/server/scan.ts +190 -0
- package/src/server/server-components.ts +224 -0
- package/src/terminal.ts +5 -0
- package/src/testing.ts +5 -0
- package/src/vite-plugin.ts +413 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of the dev frame mirror: while an editor is watching
|
|
3
|
+
* (`textjs:mirror { active: true }`), encode each emitted frame as a row delta
|
|
4
|
+
* and send it over the Vite HMR socket; apply forwarded input. Idle otherwise.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { extractStyledFrame } from "@textjs/core";
|
|
8
|
+
import type { ConsoleRoot } from "@textjs/react";
|
|
9
|
+
import { FrameEncoder, MAX_TITLE, HMR_FRAME_EVENT, HMR_HELLO_EVENT, HMR_INPUT_EVENT, HMR_MIRROR_EVENT, parseInputMessage, type FrameMeta, type InputMessage } from "./frame-protocol";
|
|
10
|
+
import type { RouterHistory } from "./history";
|
|
11
|
+
|
|
12
|
+
export interface MirrorHotContext {
|
|
13
|
+
on(event: string, callback: (payload: any) => void): void;
|
|
14
|
+
off?(event: string, callback: (payload: any) => void): void;
|
|
15
|
+
send(event: string, data?: unknown): void;
|
|
16
|
+
/** `import.meta.hot.dispose`: the mirror disconnects when the module is replaced. */
|
|
17
|
+
dispose?(callback: (data: any) => void): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sourceId(): string {
|
|
21
|
+
return Math.random().toString(36).slice(2, 10);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Apply a forwarded input message to a root. */
|
|
25
|
+
export function applyInputMessage(root: ConsoleRoot, msg: InputMessage, history?: RouterHistory): void {
|
|
26
|
+
switch (msg.type) {
|
|
27
|
+
case "key":
|
|
28
|
+
root.input.handleKeyDown({ ...msg, preventDefault() {} });
|
|
29
|
+
break;
|
|
30
|
+
case "paste":
|
|
31
|
+
root.input.handlePaste(msg.text);
|
|
32
|
+
break;
|
|
33
|
+
case "wheel":
|
|
34
|
+
root.input.handleWheel({ ...msg, ctrlKey: false, metaKey: false, shiftKey: false, altKey: false });
|
|
35
|
+
break;
|
|
36
|
+
case "navigate":
|
|
37
|
+
history?.push(msg.href);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function connectFrameMirror(root: ConsoleRoot, hot: MirrorHotContext, history?: RouterHistory): () => void {
|
|
43
|
+
const source = sourceId();
|
|
44
|
+
const encoder = new FrameEncoder();
|
|
45
|
+
let unsubscribe: (() => void) | null = null;
|
|
46
|
+
let hiddenTimer: ReturnType<typeof setInterval> | null = null;
|
|
47
|
+
let sendTimer: ReturnType<typeof setTimeout> | null = null;
|
|
48
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
49
|
+
let disposed = false;
|
|
50
|
+
|
|
51
|
+
const meta = (): FrameMeta => ({
|
|
52
|
+
path: history?.getLocation().pathname,
|
|
53
|
+
title: typeof document !== "undefined" ? document.title.slice(0, MAX_TITLE) : undefined,
|
|
54
|
+
focused: root.renderer.focused,
|
|
55
|
+
});
|
|
56
|
+
const send = (): void => {
|
|
57
|
+
// Nobody is watching (stopped, or a deferred send outlived the subscription).
|
|
58
|
+
if (!unsubscribe) return;
|
|
59
|
+
const msg = encoder.encode(extractStyledFrame(root.renderer.buffer), { source, meta: meta() });
|
|
60
|
+
if (msg) hot.send(HMR_FRAME_EVENT, msg);
|
|
61
|
+
};
|
|
62
|
+
// Background tabs get no animation frames, so the renderer defers work; flush directly instead.
|
|
63
|
+
const isHidden = (): boolean => typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
64
|
+
const flushIfHidden = (): void => {
|
|
65
|
+
if (isHidden()) root.flushSyncRender();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const start = (): void => {
|
|
69
|
+
if (unsubscribe || disposed) return;
|
|
70
|
+
encoder.reset();
|
|
71
|
+
unsubscribe = root.onFrame((f) => {
|
|
72
|
+
if (!f.skipped) send();
|
|
73
|
+
});
|
|
74
|
+
send();
|
|
75
|
+
hiddenTimer = setInterval(flushIfHidden, 100);
|
|
76
|
+
};
|
|
77
|
+
const stop = (): void => {
|
|
78
|
+
unsubscribe?.();
|
|
79
|
+
unsubscribe = null;
|
|
80
|
+
if (hiddenTimer !== null) clearInterval(hiddenTimer);
|
|
81
|
+
hiddenTimer = null;
|
|
82
|
+
if (sendTimer !== null) clearTimeout(sendTimer);
|
|
83
|
+
sendTimer = null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onMirror = (payload: { active?: boolean }): void => (payload?.active ? start() : stop());
|
|
87
|
+
const onInput = (payload: unknown): void => {
|
|
88
|
+
const msg = parseInputMessage(payload);
|
|
89
|
+
if (!msg) return;
|
|
90
|
+
applyInputMessage(root, msg, history);
|
|
91
|
+
if (flushTimer === null) {
|
|
92
|
+
flushTimer = setTimeout(() => {
|
|
93
|
+
flushTimer = null;
|
|
94
|
+
flushIfHidden();
|
|
95
|
+
}, 0);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const hello = (): void => {
|
|
99
|
+
const buf = root.renderer.buffer;
|
|
100
|
+
hot.send(HMR_HELLO_EVENT, { source, grid: { width: buf.width, height: buf.height }, path: history?.getLocation().pathname });
|
|
101
|
+
};
|
|
102
|
+
// Keep the server's status (editor status bars) current without streaming frames.
|
|
103
|
+
let helloTimer: ReturnType<typeof setTimeout> | null = null;
|
|
104
|
+
const helloSoon = (): void => {
|
|
105
|
+
if (helloTimer !== null) clearTimeout(helloTimer);
|
|
106
|
+
helloTimer = setTimeout(() => {
|
|
107
|
+
helloTimer = null;
|
|
108
|
+
hello();
|
|
109
|
+
}, 250);
|
|
110
|
+
};
|
|
111
|
+
const unlisten = history?.listen(() => {
|
|
112
|
+
if (!unsubscribe) helloSoon();
|
|
113
|
+
else if (sendTimer === null) {
|
|
114
|
+
sendTimer = setTimeout(() => {
|
|
115
|
+
sendTimer = null;
|
|
116
|
+
send();
|
|
117
|
+
}, 0);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const unlistenViewport = root.renderer.viewport.subscribe(() => {
|
|
121
|
+
if (!unsubscribe) helloSoon();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
hot.on(HMR_MIRROR_EVENT, onMirror);
|
|
125
|
+
hot.on(HMR_INPUT_EVENT, onInput);
|
|
126
|
+
hot.on("vite:ws:connect", hello);
|
|
127
|
+
hello();
|
|
128
|
+
|
|
129
|
+
const dispose = (): void => {
|
|
130
|
+
if (disposed) return;
|
|
131
|
+
disposed = true;
|
|
132
|
+
stop();
|
|
133
|
+
unlisten?.();
|
|
134
|
+
unlistenViewport();
|
|
135
|
+
if (helloTimer !== null) clearTimeout(helloTimer);
|
|
136
|
+
helloTimer = null;
|
|
137
|
+
if (flushTimer !== null) clearTimeout(flushTimer);
|
|
138
|
+
flushTimer = null;
|
|
139
|
+
hot.off?.(HMR_MIRROR_EVENT, onMirror);
|
|
140
|
+
hot.off?.(HMR_INPUT_EVENT, onInput);
|
|
141
|
+
hot.off?.("vite:ws:connect", hello);
|
|
142
|
+
};
|
|
143
|
+
hot.dispose?.(dispose);
|
|
144
|
+
return dispose;
|
|
145
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@textjs/app/frame-protocol` — the wire format `text dev` uses to mirror a
|
|
3
|
+
* running app's console frames to editors (the VS Code Live Preview) and to
|
|
4
|
+
* forward keyboard input back.
|
|
5
|
+
*
|
|
6
|
+
* Browser tab ──(Vite HMR socket: `textjs:frame`)──▶ dev server ──(SSE `/__textjs/frames`)──▶ editor
|
|
7
|
+
* Editor ──(POST `/__textjs/input`)──▶ dev server ──(HMR: `textjs:input`)──▶ browser tab
|
|
8
|
+
*
|
|
9
|
+
* Frames are row deltas: only rows whose spans changed since the previous
|
|
10
|
+
* message are sent, each with a small per-message style table. Pure and
|
|
11
|
+
* dependency-free so the editor bundle can share it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { StyledFrame, StyledSpan } from "@textjs/core";
|
|
15
|
+
|
|
16
|
+
export const FRAME_PROTOCOL_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
/** Server-sent events stream of {@link WireFrame} messages (dev only, loopback only). */
|
|
19
|
+
export const FRAMES_PATH = "/__textjs/frames";
|
|
20
|
+
/** `POST` an {@link InputMessage} as JSON. */
|
|
21
|
+
export const INPUT_PATH = "/__textjs/input";
|
|
22
|
+
/** `GET` a {@link DevStatus} JSON document. */
|
|
23
|
+
export const STATUS_PATH = "/__textjs/status";
|
|
24
|
+
|
|
25
|
+
/** Browser → server: an encoded frame. */
|
|
26
|
+
export const HMR_FRAME_EVENT = "textjs:frame";
|
|
27
|
+
/** Server → browser: `{ active: boolean }`, whether anyone is watching. */
|
|
28
|
+
export const HMR_MIRROR_EVENT = "textjs:mirror";
|
|
29
|
+
/** Server → browser: an {@link InputMessage}. */
|
|
30
|
+
export const HMR_INPUT_EVENT = "textjs:input";
|
|
31
|
+
/** Browser → server: sent on boot and reconnect; the server replies with {@link HMR_MIRROR_EVENT}. */
|
|
32
|
+
export const HMR_HELLO_EVENT = "textjs:hello";
|
|
33
|
+
/** Required on `POST /__textjs/input` (with a JSON body) so cross-site simple requests are rejected. */
|
|
34
|
+
export const MIRROR_HEADER = "x-textjs-mirror";
|
|
35
|
+
|
|
36
|
+
export const FLAG_BOLD = 1;
|
|
37
|
+
export const FLAG_DIM = 2;
|
|
38
|
+
export const FLAG_ITALIC = 4;
|
|
39
|
+
export const FLAG_UNDERLINE = 8;
|
|
40
|
+
export const FLAG_STRIKETHROUGH = 16;
|
|
41
|
+
export const FLAG_INVERSE = 32;
|
|
42
|
+
export const FLAG_ALLOW_LINK = 64;
|
|
43
|
+
|
|
44
|
+
/** `[fgCss, bgCss, flags]` — `null` colors use the theme default. */
|
|
45
|
+
export type WireStyle = [fg: string | null, bg: string | null, flags: number];
|
|
46
|
+
/** `[text, styleIndex]`, or `[text, styleIndex, width]` when the column width differs from `text.length`. */
|
|
47
|
+
export type WireSpan = [text: string, style: number] | [text: string, style: number, width: number];
|
|
48
|
+
|
|
49
|
+
export interface FrameMeta {
|
|
50
|
+
/** Current pathname of the app. */
|
|
51
|
+
path?: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
/** Whether the page has keyboard focus. */
|
|
54
|
+
focused?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WireFrame {
|
|
58
|
+
v: typeof FRAME_PROTOCOL_VERSION;
|
|
59
|
+
/** Monotonic per-source sequence number. */
|
|
60
|
+
seq: number;
|
|
61
|
+
/** Source identifier (one per browser tab). */
|
|
62
|
+
source?: string;
|
|
63
|
+
width: number;
|
|
64
|
+
height: number;
|
|
65
|
+
/** `true` when `rows` covers every row (receivers drop previous state). */
|
|
66
|
+
full: boolean;
|
|
67
|
+
styles: WireStyle[];
|
|
68
|
+
/** `[y, spans]` for each changed row. */
|
|
69
|
+
rows: Array<[y: number, spans: WireSpan[]]>;
|
|
70
|
+
meta?: FrameMeta;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DevStatus {
|
|
74
|
+
name: "textjs";
|
|
75
|
+
version: string;
|
|
76
|
+
/** Absolute project root. */
|
|
77
|
+
root: string;
|
|
78
|
+
/** Browser tabs currently connected to the HMR socket. */
|
|
79
|
+
clients: number;
|
|
80
|
+
/** Browser tabs running the app (announced via `textjs:hello`). */
|
|
81
|
+
sources: number;
|
|
82
|
+
/** Editors subscribed to the frame stream. */
|
|
83
|
+
watchers: number;
|
|
84
|
+
/** Size of the most recent frame, if any. */
|
|
85
|
+
grid: { width: number; height: number } | null;
|
|
86
|
+
path: string | null;
|
|
87
|
+
/** The app's `basePath` (`""` when none): app paths are served under it. */
|
|
88
|
+
basePath?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type InputMessage =
|
|
92
|
+
| {
|
|
93
|
+
type: "key";
|
|
94
|
+
key: string;
|
|
95
|
+
code?: string;
|
|
96
|
+
ctrlKey: boolean;
|
|
97
|
+
metaKey: boolean;
|
|
98
|
+
shiftKey: boolean;
|
|
99
|
+
altKey: boolean;
|
|
100
|
+
repeat?: boolean;
|
|
101
|
+
}
|
|
102
|
+
| { type: "paste"; text: string }
|
|
103
|
+
| { type: "wheel"; deltaX: number; deltaY: number; deltaMode: number }
|
|
104
|
+
| { type: "navigate"; href: string };
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Spans
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
export function spanFlags(s: StyledSpan): number {
|
|
111
|
+
return (
|
|
112
|
+
(s.bold ? FLAG_BOLD : 0) |
|
|
113
|
+
(s.dim ? FLAG_DIM : 0) |
|
|
114
|
+
(s.italic ? FLAG_ITALIC : 0) |
|
|
115
|
+
(s.underline ? FLAG_UNDERLINE : 0) |
|
|
116
|
+
(s.strikethrough ? FLAG_STRIKETHROUGH : 0) |
|
|
117
|
+
(s.inverse ? FLAG_INVERSE : 0) |
|
|
118
|
+
(s.allowLink ? FLAG_ALLOW_LINK : 0)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A {@link StyledSpan} rebuilt from wire data. Packed colors are not transmitted (they read `0`). */
|
|
123
|
+
export function toStyledSpan(text: string, width: number, style: WireStyle): StyledSpan {
|
|
124
|
+
const [fgCss, bgCss, f] = style;
|
|
125
|
+
return {
|
|
126
|
+
text,
|
|
127
|
+
width,
|
|
128
|
+
fg: 0,
|
|
129
|
+
bg: 0,
|
|
130
|
+
attrs: f,
|
|
131
|
+
fgCss,
|
|
132
|
+
bgCss,
|
|
133
|
+
bold: (f & FLAG_BOLD) !== 0,
|
|
134
|
+
dim: (f & FLAG_DIM) !== 0,
|
|
135
|
+
italic: (f & FLAG_ITALIC) !== 0,
|
|
136
|
+
underline: (f & FLAG_UNDERLINE) !== 0,
|
|
137
|
+
strikethrough: (f & FLAG_STRIKETHROUGH) !== 0,
|
|
138
|
+
inverse: (f & FLAG_INVERSE) !== 0,
|
|
139
|
+
allowLink: (f & FLAG_ALLOW_LINK) !== 0,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function rowKey(spans: readonly StyledSpan[]): string {
|
|
144
|
+
let k = "";
|
|
145
|
+
for (const s of spans) k += `${s.fgCss ?? ""}\u0001${s.bgCss ?? ""}\u0001${spanFlags(s)}\u0001${s.width}\u0001${s.text}\u0002`;
|
|
146
|
+
return k;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Encoder / decoder
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
export interface EncodeOptions {
|
|
154
|
+
meta?: FrameMeta;
|
|
155
|
+
source?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Stateful encoder: the first call (and the first after `reset()` or a resize)
|
|
160
|
+
* emits a full frame, later calls emit only changed rows. Returns `null` when
|
|
161
|
+
* nothing changed and no meta changed.
|
|
162
|
+
*/
|
|
163
|
+
export class FrameEncoder {
|
|
164
|
+
private keys: string[] = [];
|
|
165
|
+
private width = -1;
|
|
166
|
+
private height = -1;
|
|
167
|
+
private seq = 0;
|
|
168
|
+
private metaKey = "";
|
|
169
|
+
|
|
170
|
+
reset(): void {
|
|
171
|
+
this.keys = [];
|
|
172
|
+
this.width = -1;
|
|
173
|
+
this.height = -1;
|
|
174
|
+
this.metaKey = "";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
encode(frame: StyledFrame, options: EncodeOptions = {}): WireFrame | null {
|
|
178
|
+
const full = frame.width !== this.width || frame.height !== this.height;
|
|
179
|
+
if (full) {
|
|
180
|
+
this.width = frame.width;
|
|
181
|
+
this.height = frame.height;
|
|
182
|
+
this.keys = new Array<string>(frame.height).fill("\u0000");
|
|
183
|
+
}
|
|
184
|
+
const styles: WireStyle[] = [];
|
|
185
|
+
const styleIds = new Map<string, number>();
|
|
186
|
+
const rows: WireFrame["rows"] = [];
|
|
187
|
+
for (let y = 0; y < frame.height; y++) {
|
|
188
|
+
const spans = frame.rows[y] ?? [];
|
|
189
|
+
const key = rowKey(spans);
|
|
190
|
+
if (!full && key === this.keys[y]) continue;
|
|
191
|
+
this.keys[y] = key;
|
|
192
|
+
const wire: WireSpan[] = [];
|
|
193
|
+
for (const s of spans) {
|
|
194
|
+
const flags = spanFlags(s);
|
|
195
|
+
const sk = `${s.fgCss ?? ""}\u0001${s.bgCss ?? ""}\u0001${flags}`;
|
|
196
|
+
let id = styleIds.get(sk);
|
|
197
|
+
if (id === undefined) {
|
|
198
|
+
id = styles.length;
|
|
199
|
+
styles.push([s.fgCss, s.bgCss, flags]);
|
|
200
|
+
styleIds.set(sk, id);
|
|
201
|
+
}
|
|
202
|
+
wire.push(s.width === s.text.length ? [s.text, id] : [s.text, id, s.width]);
|
|
203
|
+
}
|
|
204
|
+
rows.push([y, wire]);
|
|
205
|
+
}
|
|
206
|
+
const metaKey = options.meta ? JSON.stringify(options.meta) : "";
|
|
207
|
+
const metaChanged = metaKey !== this.metaKey;
|
|
208
|
+
this.metaKey = metaKey;
|
|
209
|
+
if (rows.length === 0 && !metaChanged) return null;
|
|
210
|
+
const out: WireFrame = { v: FRAME_PROTOCOL_VERSION, seq: ++this.seq, width: frame.width, height: frame.height, full, styles, rows };
|
|
211
|
+
if (options.source !== undefined) out.source = options.source;
|
|
212
|
+
if (options.meta) out.meta = options.meta;
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Encode a complete frame without delta state (for late subscribers). */
|
|
218
|
+
export function encodeFullFrame(frame: StyledFrame, options: EncodeOptions & { seq?: number } = {}): WireFrame {
|
|
219
|
+
const out = new FrameEncoder().encode(frame, options) as WireFrame;
|
|
220
|
+
if (options.seq !== undefined) out.seq = options.seq;
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Applies {@link WireFrame} messages to rebuild the current {@link StyledFrame}. */
|
|
225
|
+
export class FrameDecoder {
|
|
226
|
+
frame: StyledFrame | null = null;
|
|
227
|
+
meta: FrameMeta = {};
|
|
228
|
+
seq = 0;
|
|
229
|
+
|
|
230
|
+
/** Returns the updated frame, or `null` when a delta arrives before any full frame. */
|
|
231
|
+
apply(msg: WireFrame): StyledFrame | null {
|
|
232
|
+
if (msg.v !== FRAME_PROTOCOL_VERSION) throw new Error(`Unsupported frame protocol v${String(msg.v)} (expected v${FRAME_PROTOCOL_VERSION}).`);
|
|
233
|
+
let rows: StyledSpan[][];
|
|
234
|
+
if (msg.full || this.frame === null || this.frame.width !== msg.width || this.frame.height !== msg.height) {
|
|
235
|
+
if (!msg.full) return null;
|
|
236
|
+
rows = Array.from({ length: msg.height }, () => []);
|
|
237
|
+
} else rows = this.frame.rows.slice();
|
|
238
|
+
for (const [y, spans] of msg.rows) {
|
|
239
|
+
if (y < 0 || y >= msg.height) continue;
|
|
240
|
+
rows[y] = spans.map(([text, id, width]) => toStyledSpan(text, width ?? text.length, msg.styles[id] ?? [null, null, 0]));
|
|
241
|
+
}
|
|
242
|
+
this.frame = { width: msg.width, height: msg.height, rows };
|
|
243
|
+
this.seq = msg.seq;
|
|
244
|
+
if (msg.meta) this.meta = { ...this.meta, ...msg.meta };
|
|
245
|
+
return this.frame;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
reset(): void {
|
|
249
|
+
this.frame = null;
|
|
250
|
+
this.meta = {};
|
|
251
|
+
this.seq = 0;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Whether frames from a tab with `next` meta should become the mirrored
|
|
257
|
+
* ("active") source instead of the current one. A focused tab is never
|
|
258
|
+
* displaced by an unfocused one (background tabs that animate keep sending
|
|
259
|
+
* frames), so the preview and forwarded keystrokes stay on the tab in use.
|
|
260
|
+
*/
|
|
261
|
+
export function shouldSwitchSource(current: FrameMeta | null, next: FrameMeta): boolean {
|
|
262
|
+
return current === null || next.focused === true || current.focused !== true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Plain text of a frame (trailing whitespace trimmed), for tests and copy. */
|
|
266
|
+
export function frameToText(frame: StyledFrame): string {
|
|
267
|
+
return frame.rows
|
|
268
|
+
.map((r) => r.map((s) => s.text).join("").replace(/\s+$/, ""))
|
|
269
|
+
.join("\n")
|
|
270
|
+
.replace(/\n+$/, "");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Validation
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
const MAX_TEXT = 64 * 1024;
|
|
278
|
+
|
|
279
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
280
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Validate an untrusted {@link InputMessage}. Returns a clean copy or `null`. */
|
|
284
|
+
export function parseInputMessage(value: unknown): InputMessage | null {
|
|
285
|
+
if (!isObj(value)) return null;
|
|
286
|
+
const b = (k: string): boolean => value[k] === true;
|
|
287
|
+
switch (value.type) {
|
|
288
|
+
case "key": {
|
|
289
|
+
const key = value.key;
|
|
290
|
+
if (typeof key !== "string" || key.length === 0 || key.length > 32) return null;
|
|
291
|
+
const code = typeof value.code === "string" && value.code.length <= 32 ? value.code : undefined;
|
|
292
|
+
const msg: InputMessage = { type: "key", key, ctrlKey: b("ctrlKey"), metaKey: b("metaKey"), shiftKey: b("shiftKey"), altKey: b("altKey") };
|
|
293
|
+
if (code !== undefined) msg.code = code;
|
|
294
|
+
if (b("repeat")) msg.repeat = true;
|
|
295
|
+
return msg;
|
|
296
|
+
}
|
|
297
|
+
case "paste":
|
|
298
|
+
return typeof value.text === "string" && value.text.length <= MAX_TEXT ? { type: "paste", text: value.text } : null;
|
|
299
|
+
case "wheel": {
|
|
300
|
+
const n = (k: string): number => (typeof value[k] === "number" && Number.isFinite(value[k]) ? Math.max(-10_000, Math.min(10_000, value[k] as number)) : 0);
|
|
301
|
+
const mode = value.deltaMode === 1 || value.deltaMode === 2 ? value.deltaMode : 0;
|
|
302
|
+
return { type: "wheel", deltaX: n("deltaX"), deltaY: n("deltaY"), deltaMode: mode };
|
|
303
|
+
}
|
|
304
|
+
case "navigate": {
|
|
305
|
+
const href = value.href;
|
|
306
|
+
// Same-app paths only: never let a forwarded message send the tab elsewhere.
|
|
307
|
+
if (typeof href !== "string" || href.length > 2048 || !href.startsWith("/") || href.startsWith("//") || href.includes("\\")) return null;
|
|
308
|
+
return { type: "navigate", href };
|
|
309
|
+
}
|
|
310
|
+
default:
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
316
|
+
|
|
317
|
+
function isCssColor(v: unknown): boolean {
|
|
318
|
+
return v === null || (typeof v === "string" && HEX_COLOR.test(v));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Largest grid side, style table and total span text accepted in one frame. */
|
|
322
|
+
export const MAX_GRID = 1000;
|
|
323
|
+
export const MAX_STYLES = 4096;
|
|
324
|
+
export const MAX_FRAME_TEXT = 1024 * 1024;
|
|
325
|
+
export const MAX_TITLE = 512;
|
|
326
|
+
export const MAX_PATH = 2048;
|
|
327
|
+
|
|
328
|
+
const isInt = (v: unknown, min: number, max: number): v is number => Number.isInteger(v) && (v as number) >= min && (v as number) <= max;
|
|
329
|
+
|
|
330
|
+
/** A same-app pathname: starts with one `/`, no backslashes, bounded. */
|
|
331
|
+
export function isAppPath(v: unknown): v is string {
|
|
332
|
+
return typeof v === "string" && v.length <= MAX_PATH && v.startsWith("/") && !v.startsWith("//") && !v.includes("\\");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** `""` or `/segment[/segment…]` without a trailing slash. */
|
|
336
|
+
export function isBasePath(v: unknown): v is string {
|
|
337
|
+
return v === "" || (isAppPath(v) && v.length <= 256 && !v.endsWith("/") && !/[?#]/.test(v));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Validate untrusted {@link FrameMeta}; returns a copy of the known fields or `null`. */
|
|
341
|
+
export function parseFrameMeta(value: unknown): FrameMeta | null {
|
|
342
|
+
if (!isObj(value)) return null;
|
|
343
|
+
const meta: FrameMeta = {};
|
|
344
|
+
if (value.path !== undefined) {
|
|
345
|
+
if (!isAppPath(value.path)) return null;
|
|
346
|
+
meta.path = value.path;
|
|
347
|
+
}
|
|
348
|
+
if (value.title !== undefined) {
|
|
349
|
+
if (typeof value.title !== "string") return null;
|
|
350
|
+
meta.title = value.title.slice(0, MAX_TITLE);
|
|
351
|
+
}
|
|
352
|
+
if (value.focused !== undefined) {
|
|
353
|
+
if (typeof value.focused !== "boolean") return null;
|
|
354
|
+
meta.focused = value.focused;
|
|
355
|
+
}
|
|
356
|
+
return meta;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Validate a frame source id (one per browser tab). */
|
|
360
|
+
export function isSourceId(v: unknown): v is string {
|
|
361
|
+
return typeof v === "string" && v.length >= 1 && v.length <= 64;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Validate an untrusted {@link WireFrame} (structure, bounds and hex-only
|
|
366
|
+
* colors) and return a clean copy, or `null`. Rows must lie inside the grid,
|
|
367
|
+
* spans must have a positive width that fits it and reference a style, and
|
|
368
|
+
* `meta` keeps only `path`, `title` and `focused`.
|
|
369
|
+
*/
|
|
370
|
+
export function parseWireFrame(value: unknown): WireFrame | null {
|
|
371
|
+
if (!isObj(value)) return null;
|
|
372
|
+
if (value.v !== FRAME_PROTOCOL_VERSION || !isInt(value.seq, 0, Number.MAX_SAFE_INTEGER) || typeof value.full !== "boolean") return null;
|
|
373
|
+
const { width, height } = value;
|
|
374
|
+
if (!isInt(width, 1, MAX_GRID) || !isInt(height, 1, MAX_GRID)) return null;
|
|
375
|
+
if (value.source !== undefined && !isSourceId(value.source)) return null;
|
|
376
|
+
if (!Array.isArray(value.styles) || !Array.isArray(value.rows)) return null;
|
|
377
|
+
if (value.styles.length > MAX_STYLES || value.rows.length > height) return null;
|
|
378
|
+
const styles: WireStyle[] = [];
|
|
379
|
+
for (const s of value.styles as unknown[]) {
|
|
380
|
+
if (!Array.isArray(s) || s.length !== 3) return null;
|
|
381
|
+
if (!isCssColor(s[0]) || !isCssColor(s[1]) || !isInt(s[2], 0, 0xffff)) return null;
|
|
382
|
+
styles.push([s[0] as string | null, s[1] as string | null, s[2] as number]);
|
|
383
|
+
}
|
|
384
|
+
let text = 0;
|
|
385
|
+
const rows: WireFrame["rows"] = [];
|
|
386
|
+
for (const r of value.rows as unknown[]) {
|
|
387
|
+
if (!Array.isArray(r) || r.length !== 2 || !isInt(r[0], 0, height - 1) || !Array.isArray(r[1])) return null;
|
|
388
|
+
const spans = r[1] as unknown[];
|
|
389
|
+
if (spans.length > width) return null;
|
|
390
|
+
const out: WireSpan[] = [];
|
|
391
|
+
for (const sp of spans) {
|
|
392
|
+
if (!Array.isArray(sp) || (sp.length !== 2 && sp.length !== 3)) return null;
|
|
393
|
+
const [t, id, w] = sp as [unknown, unknown, unknown];
|
|
394
|
+
if (typeof t !== "string" || !isInt(id, 0, styles.length - 1)) return null;
|
|
395
|
+
if (sp.length === 3 ? !isInt(w, 1, width) : t.length < 1 || t.length > width) return null;
|
|
396
|
+
text += t.length;
|
|
397
|
+
if (text > MAX_FRAME_TEXT) return null;
|
|
398
|
+
out.push(sp.length === 3 ? [t, id, w as number] : [t, id]);
|
|
399
|
+
}
|
|
400
|
+
rows.push([r[0] as number, out]);
|
|
401
|
+
}
|
|
402
|
+
const frame: WireFrame = { v: FRAME_PROTOCOL_VERSION, seq: value.seq as number, width, height, full: value.full, styles, rows };
|
|
403
|
+
if (value.source !== undefined) frame.source = value.source as string;
|
|
404
|
+
if (value.meta !== undefined) {
|
|
405
|
+
const meta = parseFrameMeta(value.meta);
|
|
406
|
+
if (!meta) return null;
|
|
407
|
+
frame.meta = meta;
|
|
408
|
+
}
|
|
409
|
+
return frame;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Whether `value` is a valid {@link WireFrame} (see {@link parseWireFrame}). */
|
|
413
|
+
export function isWireFrame(value: unknown): value is WireFrame {
|
|
414
|
+
return parseWireFrame(value) !== null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Validate an untrusted {@link DevStatus} (from `/__textjs/status` or the stream). */
|
|
418
|
+
export function parseDevStatus(value: unknown): DevStatus | null {
|
|
419
|
+
if (!isObj(value) || value.name !== "textjs") return null;
|
|
420
|
+
if (typeof value.version !== "string" || value.version.length > 64 || typeof value.root !== "string" || value.root.length > 4096) return null;
|
|
421
|
+
const count = (v: unknown): number => (isInt(v, 0, 1_000_000) ? v : 0);
|
|
422
|
+
let grid: DevStatus["grid"] = null;
|
|
423
|
+
if (isObj(value.grid) && isInt(value.grid.width, 1, MAX_GRID) && isInt(value.grid.height, 1, MAX_GRID)) grid = { width: value.grid.width, height: value.grid.height };
|
|
424
|
+
return {
|
|
425
|
+
name: "textjs",
|
|
426
|
+
version: value.version,
|
|
427
|
+
root: value.root,
|
|
428
|
+
clients: count(value.clients),
|
|
429
|
+
sources: count(value.sources),
|
|
430
|
+
watchers: count(value.watchers),
|
|
431
|
+
grid,
|
|
432
|
+
path: isAppPath(value.path) ? value.path : null,
|
|
433
|
+
basePath: isBasePath(value.basePath) ? value.basePath : "",
|
|
434
|
+
};
|
|
435
|
+
}
|