@wildwinter/app-shell 0.53.0 → 0.55.0
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 +42 -2
- package/dist/extension-copy.cjs +11 -0
- package/dist/extension-copy.cjs.map +1 -1
- package/dist/extension-copy.js +11 -0
- package/dist/extension-copy.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/live-link.cjs +150 -0
- package/dist/live-link.cjs.map +1 -0
- package/dist/live-link.d.cts +71 -0
- package/dist/live-link.d.ts +71 -0
- package/dist/live-link.js +134 -0
- package/dist/live-link.js.map +1 -0
- package/dist/value-rows.cjs +89 -2
- package/dist/value-rows.cjs.map +1 -1
- package/dist/value-rows.d.cts +11 -1
- package/dist/value-rows.d.ts +11 -1
- package/dist/value-rows.js +90 -3
- package/dist/value-rows.js.map +1 -1
- package/package.json +15 -4
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { isLocalOrigin } from './chunk-QVACRWG7.js';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
|
|
4
|
+
function createLiveLinkServer(opts) {
|
|
5
|
+
const { port } = opts;
|
|
6
|
+
let wss = null;
|
|
7
|
+
let client = null;
|
|
8
|
+
let connected = null;
|
|
9
|
+
let clientBuild = null;
|
|
10
|
+
const status = () => {
|
|
11
|
+
if (!wss) return { state: "off" };
|
|
12
|
+
if (!client || !connected) return { state: "listening", port };
|
|
13
|
+
return {
|
|
14
|
+
...protocol.describe(),
|
|
15
|
+
state: "connected",
|
|
16
|
+
port,
|
|
17
|
+
build: connected.build,
|
|
18
|
+
...connected.project !== void 0 ? { project: connected.project } : {}
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const push = () => opts.onStatus(status());
|
|
22
|
+
const pushStatus = () => {
|
|
23
|
+
if (connected) push();
|
|
24
|
+
};
|
|
25
|
+
const protocol = opts.protocol({ pushStatus });
|
|
26
|
+
const forget = () => {
|
|
27
|
+
connected = null;
|
|
28
|
+
clientBuild = null;
|
|
29
|
+
protocol.reset();
|
|
30
|
+
};
|
|
31
|
+
const handleMessage = (raw) => {
|
|
32
|
+
let msg;
|
|
33
|
+
try {
|
|
34
|
+
msg = JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (msg === null || typeof msg !== "object" || Array.isArray(msg)) return;
|
|
39
|
+
const m = msg;
|
|
40
|
+
if (m["t"] !== "hello") {
|
|
41
|
+
if (connected) protocol.message(m);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
clientBuild = typeof m["build"] === "string" ? m["build"] : null;
|
|
45
|
+
const current = opts.currentBuildHash();
|
|
46
|
+
const build = current === null ? "unknown" : clientBuild === current ? "match" : "stale";
|
|
47
|
+
const project = typeof m["project"] === "string" ? m["project"] : void 0;
|
|
48
|
+
connected = { build, ...project !== void 0 ? { project } : {} };
|
|
49
|
+
protocol.hello({ build, ...project !== void 0 ? { project } : {}, msg: m });
|
|
50
|
+
push();
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
isOn: () => wss !== null,
|
|
54
|
+
status,
|
|
55
|
+
pushStatus,
|
|
56
|
+
pushBundle(build, data) {
|
|
57
|
+
if (!client || !connected) return;
|
|
58
|
+
if (clientBuild === build) return;
|
|
59
|
+
try {
|
|
60
|
+
client.send(JSON.stringify({ t: "bundle", v: 1, build, data }));
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
if (connected.build !== "stale") {
|
|
64
|
+
connected = { ...connected, build: "stale" };
|
|
65
|
+
push();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
start() {
|
|
69
|
+
if (wss) return;
|
|
70
|
+
try {
|
|
71
|
+
wss = new WebSocketServer({
|
|
72
|
+
host: "127.0.0.1",
|
|
73
|
+
port,
|
|
74
|
+
verifyClient: (info) => isLocalOrigin(info.origin)
|
|
75
|
+
}, () => push());
|
|
76
|
+
} catch (e) {
|
|
77
|
+
opts.onStatus({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
78
|
+
wss = null;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
wss.on("error", (e) => {
|
|
82
|
+
opts.onStatus({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
83
|
+
if (!client) {
|
|
84
|
+
const s = wss;
|
|
85
|
+
wss = null;
|
|
86
|
+
try {
|
|
87
|
+
s?.close();
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
wss.on("connection", (ws) => {
|
|
93
|
+
if (client) {
|
|
94
|
+
try {
|
|
95
|
+
client.close();
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
client = ws;
|
|
100
|
+
forget();
|
|
101
|
+
ws.on("message", (data) => handleMessage(data.toString()));
|
|
102
|
+
ws.on("close", () => {
|
|
103
|
+
if (client === ws) {
|
|
104
|
+
client = null;
|
|
105
|
+
forget();
|
|
106
|
+
push();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
ws.on("error", () => {
|
|
110
|
+
});
|
|
111
|
+
push();
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
stop() {
|
|
115
|
+
try {
|
|
116
|
+
client?.close();
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
client = null;
|
|
120
|
+
forget();
|
|
121
|
+
const s = wss;
|
|
122
|
+
wss = null;
|
|
123
|
+
try {
|
|
124
|
+
s?.close();
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
push();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { createLiveLinkServer };
|
|
133
|
+
//# sourceMappingURL=live-link.js.map
|
|
134
|
+
//# sourceMappingURL=live-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/live-link.ts"],"names":[],"mappings":"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","file":"live-link.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The live link server: the loopback WebSocket server, in the main process,\n// that a running game connects to. Patterpad's debug-link.ts and Storyletter's\n// live-link.ts were this file twice (the family parity review of 2026-10, item\n// 14), so the parts that were the same are here once and each app plugs in its\n// own protocol:\n//\n// - loopback only, and refused to any Origin that isn't this machine\n// (isLocalOrigin), so a web page can't take the slot;\n// - one game at a time: a new connection replaces the old;\n// - nothing is honoured before a hello, which names the build the game runs,\n// judged against the project's (match / stale / unknown);\n// - the status off -> listening -> connected, or error, pushed to the app;\n// - pushBundle, gated on the build the game reported, and the build shown as\n// stale until the game re-hellos with the one it was sent;\n// - a bind failure (the port is taken) leaves the server off, not dead \"on\".\n//\n// What the game sends after its hello (Patterpad's frames, Storyletter's\n// boards and traces), and what the app adds to the connected status, is the\n// protocol's. Main-process code: `ws` is an optional peer the app supplies.\n// The chip that shows the status is link-status.ts.\n// ---------------------------------------------------------------------------\n\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport { isLocalOrigin } from \"./util.js\";\n\n/** How the game's build compares with the project's. */\nexport type LiveLinkBuild = \"match\" | \"stale\" | \"unknown\";\n\n/** One parsed message from the game: a JSON object with its `t`. */\nexport type LiveLinkMessage = Record<string, unknown>;\n\n/** The server's status, with the protocol's own fields on `connected`. */\nexport type LiveLinkServerStatus<X extends object = object> =\n | { state: \"off\" }\n | { state: \"error\"; message: string }\n | { state: \"listening\"; port: number }\n | ({ state: \"connected\"; port: number; project?: string; build: LiveLinkBuild } & X);\n\n/** A hello, as the server read it. */\nexport interface LiveLinkHello {\n /** The game's build judged against the project's. */\n build: LiveLinkBuild;\n /** What the game said it is, if it did. */\n project?: string;\n /** The whole message, for the protocol's own fields (flows, boxes). */\n msg: LiveLinkMessage;\n}\n\n/** An app's half of the link. */\nexport interface LiveLinkProtocol<X extends object = object> {\n /** A hello landed: a run begins, or a pushed bundle took. The status is\n * pushed after it returns. */\n hello(hello: LiveLinkHello): void;\n /** Any other message, once the game has said hello. Call `pushStatus` when\n * it changes what the status shows. */\n message(msg: LiveLinkMessage): void;\n /** The game went away, was replaced, or the server stopped: forget the run. */\n reset(): void;\n /** The protocol's fields on a connected status. */\n describe(): X;\n}\n\nexport interface LiveLinkServerOptions<X extends object = object> {\n /** Each app has its own, so both editors can listen on one machine. */\n port: number;\n /** The project's current compiled hash, read at each hello; null with no\n * project open, which makes the build \"unknown\". */\n currentBuildHash: () => string | null;\n /** Make the protocol. `pushStatus` sends the status again while a game is\n * connected (before that, nothing the protocol adds is shown). */\n protocol: (link: { pushStatus(): void }) => LiveLinkProtocol<X>;\n /** The latest status, for the app's windows and menu. */\n onStatus: (status: LiveLinkServerStatus<X>) => void;\n}\n\nexport interface LiveLinkServer<X extends object = object> {\n start(): void;\n stop(): void;\n isOn(): boolean;\n status(): LiveLinkServerStatus<X>;\n /** Send the status again while a game is connected: something the protocol\n * shows changed outside a message (the app followed another flow). */\n pushStatus(): void;\n /** Push a freshly compiled bundle to the connected game. Nothing happens when\n * no game has said hello, or when it already runs this build. The game\n * applies it and re-hellos with the new build; until then it shows stale. */\n pushBundle(build: string, data: string): void;\n}\n\nexport function createLiveLinkServer<X extends object = object>(opts: LiveLinkServerOptions<X>): LiveLinkServer<X> {\n const { port } = opts;\n let wss: WebSocketServer | null = null;\n let client: WebSocket | null = null;\n // Set by a hello: nothing else is honoured until it is.\n let connected: { project?: string; build: LiveLinkBuild } | null = null;\n let clientBuild: string | null = null; // the build the game reported: gates pushBundle\n\n const status = (): LiveLinkServerStatus<X> => {\n if (!wss) return { state: \"off\" };\n if (!client || !connected) return { state: \"listening\", port };\n return {\n ...protocol.describe(),\n state: \"connected\", port, build: connected.build,\n ...(connected.project !== undefined ? { project: connected.project } : {}),\n };\n };\n const push = (): void => opts.onStatus(status());\n const pushStatus = (): void => { if (connected) push(); };\n const protocol = opts.protocol({ pushStatus });\n\n const forget = (): void => { connected = null; clientBuild = null; protocol.reset(); };\n\n const handleMessage = (raw: string): void => {\n let msg: unknown;\n try { msg = JSON.parse(raw); } catch { return; }\n if (msg === null || typeof msg !== \"object\" || Array.isArray(msg)) return;\n const m = msg as LiveLinkMessage;\n if (m[\"t\"] !== \"hello\") {\n if (connected) protocol.message(m);\n return;\n }\n clientBuild = typeof m[\"build\"] === \"string\" ? m[\"build\"] : null;\n const current = opts.currentBuildHash();\n const build: LiveLinkBuild = current === null ? \"unknown\" : clientBuild === current ? \"match\" : \"stale\";\n const project = typeof m[\"project\"] === \"string\" ? m[\"project\"] : undefined;\n connected = { build, ...(project !== undefined ? { project } : {}) };\n protocol.hello({ build, ...(project !== undefined ? { project } : {}), msg: m });\n push();\n };\n\n return {\n isOn: () => wss !== null,\n status,\n pushStatus,\n pushBundle(build: string, data: string): void {\n if (!client || !connected) return; // nothing connected, or no hello yet\n if (clientBuild === build) return; // the game already runs this build\n try { client.send(JSON.stringify({ t: \"bundle\", v: 1, build, data })); } catch { /* socket went away */ }\n // Until the game re-hellos with the new build it is on the old one, and\n // the chip says so rather than keep the match it had at the hello.\n if (connected.build !== \"stale\") { connected = { ...connected, build: \"stale\" }; push(); }\n },\n start(): void {\n if (wss) return;\n try {\n // Loopback only: only processes on this machine can reach it. And not\n // a web page on it: any site can open a WebSocket to localhost, and the\n // browser stamps the upgrade with the page's Origin.\n wss = new WebSocketServer({\n host: \"127.0.0.1\", port,\n verifyClient: (info: { origin?: string }) => isLocalOrigin(info.origin),\n }, () => push()); // \"listening\" once actually bound\n } catch (e) {\n opts.onStatus({ state: \"error\", message: e instanceof Error ? e.message : String(e) });\n wss = null;\n return;\n }\n wss.on(\"error\", (e) => {\n opts.onStatus({ state: \"error\", message: e instanceof Error ? e.message : String(e) });\n // A bind failure leaves no server behind: the next click starts again\n // rather than finding a dead one \"on\".\n if (!client) { const s = wss; wss = null; try { s?.close(); } catch { /* already closing */ } }\n });\n wss.on(\"connection\", (ws) => {\n // One game at a time: a new connection replaces the old.\n if (client) { try { client.close(); } catch { /* gone */ } }\n client = ws; forget();\n ws.on(\"message\", (data) => handleMessage(data.toString()));\n ws.on(\"close\", () => { if (client === ws) { client = null; forget(); push(); } });\n ws.on(\"error\", () => { /* a flaky client must not take the editor down */ });\n push();\n });\n },\n stop(): void {\n try { client?.close(); } catch { /* gone */ }\n client = null; forget();\n const s = wss; wss = null;\n try { s?.close(); } catch { /* already closing */ }\n push();\n },\n };\n}\n"]}
|
package/dist/value-rows.cjs
CHANGED
|
@@ -837,13 +837,20 @@ var nameOf = (decl) => decl.label ?? decl.name;
|
|
|
837
837
|
var asDecl = (d, name) => ({ ...d, name: d.name ?? name });
|
|
838
838
|
function ownProblems(decl, value, adapter) {
|
|
839
839
|
if (value === void 0) return "";
|
|
840
|
-
|
|
841
|
-
|
|
840
|
+
return extensions.valueProblems(decl, value, contextOf(adapter)).filter((p) => p.path === "" && (p.severity === "error" || p.code === "pick-ambiguous") && p.code !== "not-a-value" && p.code !== "not-a-stage").map((p) => `${p.message.replace(/^the value/, "The value")}.`).join(" ");
|
|
841
|
+
}
|
|
842
|
+
function contextOf(adapter) {
|
|
843
|
+
if (!adapter) return {};
|
|
844
|
+
return {
|
|
845
|
+
exists: (kind, id) => nodeLabel(adapter, kind, id) !== void 0,
|
|
846
|
+
...adapter.list ? { list: (name) => adapter.list(name) } : {}
|
|
847
|
+
};
|
|
842
848
|
}
|
|
843
849
|
function valueRow(initial) {
|
|
844
850
|
const decl = initial.decl;
|
|
845
851
|
if (SCALARS.has(decl.type)) return scalarRow(initial);
|
|
846
852
|
if (decl.type === "reference") return referenceRow(initial);
|
|
853
|
+
if (decl.type === "pick") return pickRow(initial);
|
|
847
854
|
return structuredRow(initial);
|
|
848
855
|
}
|
|
849
856
|
function scalarRow(initial) {
|
|
@@ -923,6 +930,86 @@ function referenceRow(initial) {
|
|
|
923
930
|
draw(initial);
|
|
924
931
|
return { el: row, update: draw };
|
|
925
932
|
}
|
|
933
|
+
function pickRow(initial) {
|
|
934
|
+
let opts = initial;
|
|
935
|
+
if (!opts.adapter && (opts.decl.from ?? []).some((s) => !("words" in s))) {
|
|
936
|
+
const inner = scalarRow({ ...opts, decl: { ...opts.decl, type: "text" } });
|
|
937
|
+
return { el: inner.el, update: (o) => {
|
|
938
|
+
opts = o;
|
|
939
|
+
inner.update({ ...o, decl: { ...o.decl, type: "text" } });
|
|
940
|
+
} };
|
|
941
|
+
}
|
|
942
|
+
const row = el("div", "shell-fieldrow shell-valuerow-pick");
|
|
943
|
+
const caption = el("span", "shell-fieldrow-label");
|
|
944
|
+
const body = el("div", "shell-fieldrow-body");
|
|
945
|
+
const sel = el("select", "field shell-fieldrow-select");
|
|
946
|
+
const help = el("p", "shell-fieldrow-purpose");
|
|
947
|
+
const problem = el("p", "shell-fieldrow-problem");
|
|
948
|
+
problem.setAttribute("role", "status");
|
|
949
|
+
body.append(sel, help, problem);
|
|
950
|
+
row.append(caption, body);
|
|
951
|
+
sel.addEventListener("change", () => {
|
|
952
|
+
const v = sel.value === "" ? void 0 : sel.value;
|
|
953
|
+
opts = { ...opts, value: v };
|
|
954
|
+
draw(opts);
|
|
955
|
+
opts.onCommit?.(v);
|
|
956
|
+
});
|
|
957
|
+
const groupName = (s) => "to" in s ? s.to === "cast" ? "Cast" : s.to.charAt(0).toUpperCase() + s.to.slice(1) : "list" in s ? s.list : void 0;
|
|
958
|
+
const draw = (o) => {
|
|
959
|
+
opts = o;
|
|
960
|
+
row.className = `shell-fieldrow shell-valuerow-pick${o.className ? ` ${o.className}` : ""}`;
|
|
961
|
+
const label = o.label ?? nameOf(o.decl);
|
|
962
|
+
if (typeof label === "string") caption.textContent = label;
|
|
963
|
+
else caption.replaceChildren(label);
|
|
964
|
+
if (o.controlClassName) sel.className = `shell-fieldrow-select ${o.controlClassName}`;
|
|
965
|
+
const adapter = o.adapter;
|
|
966
|
+
const choices = extensions.pickChoices(o.decl, {
|
|
967
|
+
...adapter ? { nodes: (kind) => adapter.nodes(kind) } : {},
|
|
968
|
+
...adapter?.list ? { list: (name) => adapter.list(name) } : {}
|
|
969
|
+
});
|
|
970
|
+
const held = typeof o.value === "string" ? o.value : "";
|
|
971
|
+
sel.replaceChildren();
|
|
972
|
+
const option = (v, text) => {
|
|
973
|
+
const opt = el("option", void 0, text);
|
|
974
|
+
opt.value = v;
|
|
975
|
+
return opt;
|
|
976
|
+
};
|
|
977
|
+
sel.append(option("", "auto"));
|
|
978
|
+
const offered = /* @__PURE__ */ new Set();
|
|
979
|
+
let group;
|
|
980
|
+
for (const c of choices) {
|
|
981
|
+
if (offered.has(c.value)) continue;
|
|
982
|
+
offered.add(c.value);
|
|
983
|
+
const name = groupName(c.source);
|
|
984
|
+
if (name === void 0) {
|
|
985
|
+
group = void 0;
|
|
986
|
+
sel.append(option(c.value, c.label));
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (group?.source !== c.source) {
|
|
990
|
+
const og = el("optgroup");
|
|
991
|
+
og.setAttribute("label", name);
|
|
992
|
+
sel.append(og);
|
|
993
|
+
group = { source: c.source, el: og };
|
|
994
|
+
}
|
|
995
|
+
group.el.append(option(c.value, c.label));
|
|
996
|
+
}
|
|
997
|
+
if (held !== "" && !offered.has(held)) {
|
|
998
|
+
const named = (o.decl.from ?? []).map((s) => "to" in s && adapter ? adapter.label?.(s.to, held) : void 0).find((n) => n !== void 0);
|
|
999
|
+
sel.append(option(held, named ?? `${held} (missing)`));
|
|
1000
|
+
}
|
|
1001
|
+
sel.value = held;
|
|
1002
|
+
sel.disabled = o.onCommit === void 0 || o.disabled === true;
|
|
1003
|
+
help.textContent = o.decl.help ?? o.decl.purpose ?? "";
|
|
1004
|
+
help.hidden = help.textContent === "";
|
|
1005
|
+
const words = o.replaceProblem ?? [ownProblems(o.decl, o.value, o.adapter), o.problem ?? ""].filter((w) => w !== "").join(" ");
|
|
1006
|
+
problem.textContent = words;
|
|
1007
|
+
problem.hidden = words === "";
|
|
1008
|
+
row.classList.toggle("has-problem", words !== "");
|
|
1009
|
+
};
|
|
1010
|
+
draw(initial);
|
|
1011
|
+
return { el: row, update: draw };
|
|
1012
|
+
}
|
|
926
1013
|
function structuredRow(initial) {
|
|
927
1014
|
let opts = initial;
|
|
928
1015
|
const row = el("div", "shell-fieldrow shell-valuerow");
|