@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
package/README.md
CHANGED
|
@@ -219,10 +219,12 @@ const outcome = await history.applyUndo(); // { ok, label } or { ok: false, er
|
|
|
219
219
|
|
|
220
220
|
**`@wildwinter/app-shell/value-rows`**: a row for a value of any declared type,
|
|
221
221
|
for an extension's fields. The seven scalars are the field row; a `reference`
|
|
222
|
-
picks one of the host's nodes, a `
|
|
222
|
+
picks one of the host's nodes, a `pick` one choice from its sources ("auto"
|
|
223
|
+
first, then its fixed words, the host's nodes of a kind, and the extension's
|
|
224
|
+
project lists, through the adapter's optional `list(name)`), a `record` is its fields, a `map` is keyed by a
|
|
223
225
|
host's nodes or by text, a `list` adds, moves and removes items (a new item of
|
|
224
226
|
an id list gets a fresh id). The checks are `@wildwinter/toolkit/extensions`'
|
|
225
|
-
(an optional peer, 0.2.
|
|
227
|
+
(an optional peer, 0.2.5 or later), so the editor and validation agree.
|
|
226
228
|
|
|
227
229
|
```ts
|
|
228
230
|
import { valueRows } from "@wildwinter/app-shell/value-rows";
|
|
@@ -242,6 +244,44 @@ host.append(rows.row(`${nodeId}:${decl.name}`, { decl, value, adapter: { nodes:
|
|
|
242
244
|
`valueProblemSentence`, `fieldPhrase`), Patterpad's rules made the family's,
|
|
243
245
|
with each app's words for its items, kinds and where a value is changed
|
|
244
246
|
passed in.
|
|
247
|
+
- **v0.54:** the `pick` row, the adapter's `list(name)` for a pick's
|
|
248
|
+
project lists, and the words for a pick's problems (`not-a-choice`,
|
|
249
|
+
`pick-ambiguous`, `pick-clash`). Needs toolkit 0.2.5.
|
|
250
|
+
|
|
251
|
+
## v0.55 (one live link server)
|
|
252
|
+
|
|
253
|
+
**`@wildwinter/app-shell/live-link`** (main process): the loopback WebSocket
|
|
254
|
+
server a running game connects to, once for the family. Patterpad's and
|
|
255
|
+
Storyletter's were copies; each now plugs its own protocol into this one.
|
|
256
|
+
`ws` (8 or later) is an optional peer the app supplies.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { createLiveLinkServer } from "@wildwinter/app-shell/live-link";
|
|
260
|
+
|
|
261
|
+
const server = createLiveLinkServer<MyFields>({
|
|
262
|
+
port: 4471,
|
|
263
|
+
currentBuildHash: () => projectHash(),
|
|
264
|
+
onStatus: (s) => sendToWindows(s),
|
|
265
|
+
protocol: ({ pushStatus }) => ({
|
|
266
|
+
hello: ({ build, project, msg }) => { /* a run begins: read msg's own fields */ },
|
|
267
|
+
message: (msg) => { /* anything after the hello; pushStatus() if it changes the status */ },
|
|
268
|
+
reset: () => { /* the game went away */ },
|
|
269
|
+
describe: () => ({ /* your fields on a connected status */ }),
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
server.start();
|
|
273
|
+
server.pushBundle(hash, json); // after a save
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- Loopback only, and a web page's Origin is refused (`isLocalOrigin`).
|
|
277
|
+
- One game at a time: a new connection replaces the old.
|
|
278
|
+
- Nothing reaches the protocol before a hello, which names the build: `match`,
|
|
279
|
+
`stale`, or `unknown` (no project open).
|
|
280
|
+
- `pushBundle` sends `{ t: "bundle", v: 1, build, data }` only to a game that
|
|
281
|
+
has said hello and runs another build, and shows the build stale until the
|
|
282
|
+
game re-hellos with the new one.
|
|
283
|
+
- A taken port reports an error and leaves the server off. The status is the
|
|
284
|
+
link-status chip's.
|
|
245
285
|
|
|
246
286
|
## Roadmap
|
|
247
287
|
|
package/dist/extension-copy.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/extension-copy.ts
|
|
4
4
|
var quoted = (s) => `\u201C${s}\u201D`;
|
|
5
|
+
var curly = (s) => s.replace(/"([^"]*)"/g, (_, x) => quoted(x));
|
|
5
6
|
var cap = (s) => `${s.charAt(0).toUpperCase()}${s.slice(1)}`;
|
|
6
7
|
var article = (s) => /^[aeiou]/i.test(s) ? "an" : "a";
|
|
7
8
|
function fieldPhrase(fields, path) {
|
|
@@ -35,6 +36,16 @@ function valueProblemSentence(code, message, w) {
|
|
|
35
36
|
case "not-a-value":
|
|
36
37
|
if (/not among its flags$/.test(message)) return { text: `${on} has a flag that isn't one of its choices.`, next: `Pick from the choices ${place}.` };
|
|
37
38
|
return { text: `${on} is ${quoted(value ?? "")}, which isn't one of its choices.`, next: `Pick one ${place}.` };
|
|
39
|
+
case "not-a-choice":
|
|
40
|
+
return { text: `${on} is ${quoted(value ?? "")}, which isn't one of its choices.`, next: `Pick one ${place}.` };
|
|
41
|
+
case "pick-ambiguous": {
|
|
42
|
+
const m = / is "([^"]*)", which is (.+)$/.exec(message);
|
|
43
|
+
return m ? { text: `${on} is ${quoted(m[1])}, which is ${curly(m[2])}, so it's ambiguous.`, next: "Rename one of them." } : void 0;
|
|
44
|
+
}
|
|
45
|
+
case "pick-clash": {
|
|
46
|
+
const m = /^"([^"]*)" is (.+), so a value naming it is ambiguous$/.exec(message);
|
|
47
|
+
return m ? { text: `${quoted(m[1])} is ${curly(m[2])}, so ${field} naming it is ambiguous.`, next: "Rename one of them." } : void 0;
|
|
48
|
+
}
|
|
38
49
|
case "not-a-stage":
|
|
39
50
|
return { text: `${on} is ${quoted(value ?? "")}, which isn't one of its stages.`, next: `Pick one ${place}.` };
|
|
40
51
|
case "renamed-stage": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/extension-copy.ts"],"names":[],"mappings":"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","file":"extension-copy.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// An extension's problems, worded for the writer, in both apps.\n//\n// The command lines word an extension's problems for the file: \"entries.card.c_7k2.cue.sound: \"cue.sound\" is\n// required and not set\". The problems bar says the same for the writer: the extension by its name, the item as\n// the writer knows it, and the field by its label: \"Probe: The bell needs a sound in cue.\" Patterpad wrote these\n// rules first (its problem-copy.ts); they are the family's, here, so the two apps read alike.\n//\n// What is the app's is passed in: how it names the item (a line by its words, a card by its title), the words\n// for its node kinds, and where the writer goes to change a value (\"in the inspector\", \"on its page\"). The rules\n// key on the toolkit's code for the problem (`@wildwinter/toolkit/extensions`' Problem.code), which each app's\n// compiler carries on the issue; a code they do not know stands as the message it came with.\n//\n// No app state, no Electron, no DOM: words only.\n// ---------------------------------------------------------------------------\n\n/** A field declaration as the wording reads it: its name, its label, and a record's or an item's own fields. */\nexport interface FieldWordsDecl {\n name: string;\n label?: string;\n fields?: readonly FieldWordsDecl[];\n of?: { fields?: readonly FieldWordsDecl[] };\n}\n\n/** What the app knows to word one problem about a value. */\nexport interface ValueProblemWords {\n /** The extension's name, which leads the line (\"Probe\"). */\n extension: string;\n /** The item, as the writer knows it (\"“What'll it be, stranger?”\", \"the card “bell”\"). */\n node: string;\n /** The field, by its label (`fieldPhrase`). */\n field: string;\n /** Where a value is changed, finishing \"Pick one ...\" (\"in the inspector\", \"on its page\"). */\n place: string;\n /** A node kind in the app's words (\"game event\", \"hand template\"). */\n kindWord(kind: string): string;\n}\n\nexport const quoted = (s: string): string => `“${s}”`;\nconst cap = (s: string): string => `${s.charAt(0).toUpperCase()}${s.slice(1)}`;\nconst article = (s: string): string => (/^[aeiou]/i.test(s) ? \"an\" : \"a\");\n\n/** A field inside a node's entry, by its label: \"Note\", \"sound in cue\", \"item 2 of words\". */\nexport function fieldPhrase(fields: readonly FieldWordsDecl[], path: string): string {\n const tokens = path.split(/\\.|(?=\\[)/).filter((t) => t !== \"\");\n const top = fields.find((f) => f.name === tokens[0]);\n const topLabel = top?.label ?? tokens[0] ?? \"\";\n if (tokens.length <= 1) return topLabel;\n const last = tokens[tokens.length - 1]!;\n const index = /^\\[(\\d+)\\]$/.exec(last);\n if (index) return `item ${Number(index[1]) + 1} of ${topLabel}`;\n const parts = [...(top?.fields ?? []), ...(top?.of?.fields ?? [])];\n return `${parts.find((f) => f.name === last)?.label ?? last} in ${topLabel}`;\n}\n\n/** The sentence and its next step for one value a check refused, by the toolkit's code; undefined for a code\n * the rules do not know. `message` is the check's own sentence, which some sentences take their values from. */\nexport function valueProblemSentence(code: string | undefined, message: string, w: ValueProblemWords): { text: string; next?: string } | undefined {\n const { field, node, extension: name, place } = w;\n const on = `${field} on ${node}`;\n const value = /^\"[^\"]*\" is \"([^\"]*)\"/.exec(message)?.[1];\n switch (code) {\n case \"required\": return { text: `${node} needs ${article(field)} ${field}.` };\n case \"wrong-type\": return { text: `${on} isn't the kind of value ${name} expects.`, next: `Set it again ${place}.` };\n case \"below-min\": { const m = / is ([^,]+), below the least it may be, (.+)$/.exec(message); return m ? { text: `${on} is ${m[1]}, but ${name} takes nothing below ${m[2]}.` } : undefined; }\n case \"above-max\": { const m = / is ([^,]+), above the most it may be, (.+)$/.exec(message); return m ? { text: `${on} is ${m[1]}, but ${name} takes nothing above ${m[2]}.` } : undefined; }\n case \"not-a-value\":\n if (/not among its flags$/.test(message)) return { text: `${on} has a flag that isn't one of its choices.`, next: `Pick from the choices ${place}.` };\n return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its choices.`, next: `Pick one ${place}.` };\n case \"not-a-stage\": return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its stages.`, next: `Pick one ${place}.` };\n case \"renamed-stage\": {\n const m = / is \"([^\"]*)\", a stage this extension has since renamed \"([^\"]*)\"/.exec(message);\n return m ? { text: `${on} is ${quoted(m[1]!)}, a stage ${name} has since renamed ${quoted(m[2]!)}.`, next: `Pick ${quoted(m[2]!)} to update it.` } : undefined;\n }\n case \"no-such-node\": {\n const m = / (?:names|is keyed by) (\\w+) \"([^\"]*)\", which does not exist/.exec(message);\n return m ? { text: `${on} names the ${w.kindWord(m[1]!)} ${quoted(m[2]!)}, which isn't in the project.`, next: \"Choose another, or clear it.\" } : undefined;\n }\n case \"missing-id\": return { text: `An item in ${field} on ${node} has no id.`, next: `Give it one ${place}.` };\n case \"duplicate-id\": { const id = / repeats the id \"([^\"]*)\"/.exec(message)?.[1]; return { text: `Two items in ${field} on ${node} share the id ${quoted(id ?? \"\")}.`, next: \"Give each its own.\" }; }\n case \"unknown-field\": case \"unknown-key\":\n return { text: `${node} has a value for ${quoted(field)}, which ${name} doesn't declare any more.`, next: \"It's kept, but your game won't get it.\" };\n default: return undefined;\n }\n}\n\n/** A problem's line: the extension's name, then the sentence and its next step. */\nexport function extensionLine(extension: string, c: { text: string; next?: string } | undefined): string | undefined {\n return c ? `${extension}: ${cap(c.text)}${c.next ? ` ${c.next}` : \"\"}` : undefined;\n}\n\n/** What the app knows to word an entry problem (`entryProblemLine`). */\nexport interface EntryProblemWords {\n extension: string;\n /** The extension's declarations for a node kind. */\n fields(kind: string): readonly FieldWordsDecl[];\n /** A node as the writer knows it, by kind and id. */\n node(kind: string, id: string): string;\n place: string;\n kindWord(kind: string): string;\n}\n\n/**\n * The writer's line for a problem in a node's extension values, read from the command line's\n * \"entries.<kind>.<id>[.<path>]: <what is wrong>\" and the toolkit's code (`orphan` and `unresolved` included);\n * undefined for a message not of that shape, or a code the rules do not know.\n */\nexport function entryProblemLine(message: string, code: string | undefined, w: EntryProblemWords): string | undefined {\n const entry = /^entries\\.(\\w+)\\.([^.:\\s[]+)(\\S*?): (.*)$/.exec(message);\n if (!entry) return undefined;\n const [, kind, id, rest, said] = entry as unknown as [string, string, string, string, string];\n if (code === \"orphan\") {\n return extensionLine(w.extension, { text: `There are values for ${article(w.kindWord(kind))} ${w.kindWord(kind)} that isn't in the project any more.`, next: \"They're kept, but your game won't get them.\" });\n }\n if (code === \"unresolved\") return extensionLine(w.extension, { text: \"A merge left a clash in this data that hasn't been resolved.\", next: \"Resolve it before you publish.\" });\n const field = fieldPhrase(w.fields(kind), rest.replace(/^\\./, \"\"));\n return extensionLine(w.extension, valueProblemSentence(code, said, {\n extension: w.extension, node: w.node(kind, id), field, place: w.place, kindWord: w.kindWord,\n }));\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/extension-copy.ts"],"names":[],"mappings":"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","file":"extension-copy.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// An extension's problems, worded for the writer, in both apps.\n//\n// The command lines word an extension's problems for the file: \"entries.card.c_7k2.cue.sound: \"cue.sound\" is\n// required and not set\". The problems bar says the same for the writer: the extension by its name, the item as\n// the writer knows it, and the field by its label: \"Probe: The bell needs a sound in cue.\" Patterpad wrote these\n// rules first (its problem-copy.ts); they are the family's, here, so the two apps read alike.\n//\n// What is the app's is passed in: how it names the item (a line by its words, a card by its title), the words\n// for its node kinds, and where the writer goes to change a value (\"in the inspector\", \"on its page\"). The rules\n// key on the toolkit's code for the problem (`@wildwinter/toolkit/extensions`' Problem.code), which each app's\n// compiler carries on the issue; a code they do not know stands as the message it came with.\n//\n// No app state, no Electron, no DOM: words only.\n// ---------------------------------------------------------------------------\n\n/** A field declaration as the wording reads it: its name, its label, and a record's or an item's own fields. */\nexport interface FieldWordsDecl {\n name: string;\n label?: string;\n fields?: readonly FieldWordsDecl[];\n of?: { fields?: readonly FieldWordsDecl[] };\n}\n\n/** What the app knows to word one problem about a value. */\nexport interface ValueProblemWords {\n /** The extension's name, which leads the line (\"Probe\"). */\n extension: string;\n /** The item, as the writer knows it (\"“What'll it be, stranger?”\", \"the card “bell”\"). */\n node: string;\n /** The field, by its label (`fieldPhrase`). */\n field: string;\n /** Where a value is changed, finishing \"Pick one ...\" (\"in the inspector\", \"on its page\"). */\n place: string;\n /** A node kind in the app's words (\"game event\", \"hand template\"). */\n kindWord(kind: string): string;\n}\n\nexport const quoted = (s: string): string => `“${s}”`;\n/** A check's own quoted names in the family's quotes. */\nconst curly = (s: string): string => s.replace(/\"([^\"]*)\"/g, (_, x: string) => quoted(x));\nconst cap = (s: string): string => `${s.charAt(0).toUpperCase()}${s.slice(1)}`;\nconst article = (s: string): string => (/^[aeiou]/i.test(s) ? \"an\" : \"a\");\n\n/** A field inside a node's entry, by its label: \"Note\", \"sound in cue\", \"item 2 of words\". */\nexport function fieldPhrase(fields: readonly FieldWordsDecl[], path: string): string {\n const tokens = path.split(/\\.|(?=\\[)/).filter((t) => t !== \"\");\n const top = fields.find((f) => f.name === tokens[0]);\n const topLabel = top?.label ?? tokens[0] ?? \"\";\n if (tokens.length <= 1) return topLabel;\n const last = tokens[tokens.length - 1]!;\n const index = /^\\[(\\d+)\\]$/.exec(last);\n if (index) return `item ${Number(index[1]) + 1} of ${topLabel}`;\n const parts = [...(top?.fields ?? []), ...(top?.of?.fields ?? [])];\n return `${parts.find((f) => f.name === last)?.label ?? last} in ${topLabel}`;\n}\n\n/** The sentence and its next step for one value a check refused, by the toolkit's code; undefined for a code\n * the rules do not know. `message` is the check's own sentence, which some sentences take their values from. */\nexport function valueProblemSentence(code: string | undefined, message: string, w: ValueProblemWords): { text: string; next?: string } | undefined {\n const { field, node, extension: name, place } = w;\n const on = `${field} on ${node}`;\n const value = /^\"[^\"]*\" is \"([^\"]*)\"/.exec(message)?.[1];\n switch (code) {\n case \"required\": return { text: `${node} needs ${article(field)} ${field}.` };\n case \"wrong-type\": return { text: `${on} isn't the kind of value ${name} expects.`, next: `Set it again ${place}.` };\n case \"below-min\": { const m = / is ([^,]+), below the least it may be, (.+)$/.exec(message); return m ? { text: `${on} is ${m[1]}, but ${name} takes nothing below ${m[2]}.` } : undefined; }\n case \"above-max\": { const m = / is ([^,]+), above the most it may be, (.+)$/.exec(message); return m ? { text: `${on} is ${m[1]}, but ${name} takes nothing above ${m[2]}.` } : undefined; }\n case \"not-a-value\":\n if (/not among its flags$/.test(message)) return { text: `${on} has a flag that isn't one of its choices.`, next: `Pick from the choices ${place}.` };\n return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its choices.`, next: `Pick one ${place}.` };\n case \"not-a-choice\": return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its choices.`, next: `Pick one ${place}.` };\n case \"pick-ambiguous\": {\n const m = / is \"([^\"]*)\", which is (.+)$/.exec(message);\n return m ? { text: `${on} is ${quoted(m[1]!)}, which is ${curly(m[2]!)}, so it's ambiguous.`, next: \"Rename one of them.\" } : undefined;\n }\n case \"pick-clash\": {\n const m = /^\"([^\"]*)\" is (.+), so a value naming it is ambiguous$/.exec(message);\n return m ? { text: `${quoted(m[1]!)} is ${curly(m[2]!)}, so ${field} naming it is ambiguous.`, next: \"Rename one of them.\" } : undefined;\n }\n case \"not-a-stage\": return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its stages.`, next: `Pick one ${place}.` };\n case \"renamed-stage\": {\n const m = / is \"([^\"]*)\", a stage this extension has since renamed \"([^\"]*)\"/.exec(message);\n return m ? { text: `${on} is ${quoted(m[1]!)}, a stage ${name} has since renamed ${quoted(m[2]!)}.`, next: `Pick ${quoted(m[2]!)} to update it.` } : undefined;\n }\n case \"no-such-node\": {\n const m = / (?:names|is keyed by) (\\w+) \"([^\"]*)\", which does not exist/.exec(message);\n return m ? { text: `${on} names the ${w.kindWord(m[1]!)} ${quoted(m[2]!)}, which isn't in the project.`, next: \"Choose another, or clear it.\" } : undefined;\n }\n case \"missing-id\": return { text: `An item in ${field} on ${node} has no id.`, next: `Give it one ${place}.` };\n case \"duplicate-id\": { const id = / repeats the id \"([^\"]*)\"/.exec(message)?.[1]; return { text: `Two items in ${field} on ${node} share the id ${quoted(id ?? \"\")}.`, next: \"Give each its own.\" }; }\n case \"unknown-field\": case \"unknown-key\":\n return { text: `${node} has a value for ${quoted(field)}, which ${name} doesn't declare any more.`, next: \"It's kept, but your game won't get it.\" };\n default: return undefined;\n }\n}\n\n/** A problem's line: the extension's name, then the sentence and its next step. */\nexport function extensionLine(extension: string, c: { text: string; next?: string } | undefined): string | undefined {\n return c ? `${extension}: ${cap(c.text)}${c.next ? ` ${c.next}` : \"\"}` : undefined;\n}\n\n/** What the app knows to word an entry problem (`entryProblemLine`). */\nexport interface EntryProblemWords {\n extension: string;\n /** The extension's declarations for a node kind. */\n fields(kind: string): readonly FieldWordsDecl[];\n /** A node as the writer knows it, by kind and id. */\n node(kind: string, id: string): string;\n place: string;\n kindWord(kind: string): string;\n}\n\n/**\n * The writer's line for a problem in a node's extension values, read from the command line's\n * \"entries.<kind>.<id>[.<path>]: <what is wrong>\" and the toolkit's code (`orphan` and `unresolved` included);\n * undefined for a message not of that shape, or a code the rules do not know.\n */\nexport function entryProblemLine(message: string, code: string | undefined, w: EntryProblemWords): string | undefined {\n const entry = /^entries\\.(\\w+)\\.([^.:\\s[]+)(\\S*?): (.*)$/.exec(message);\n if (!entry) return undefined;\n const [, kind, id, rest, said] = entry as unknown as [string, string, string, string, string];\n if (code === \"orphan\") {\n return extensionLine(w.extension, { text: `There are values for ${article(w.kindWord(kind))} ${w.kindWord(kind)} that isn't in the project any more.`, next: \"They're kept, but your game won't get them.\" });\n }\n if (code === \"unresolved\") return extensionLine(w.extension, { text: \"A merge left a clash in this data that hasn't been resolved.\", next: \"Resolve it before you publish.\" });\n const field = fieldPhrase(w.fields(kind), rest.replace(/^\\./, \"\"));\n return extensionLine(w.extension, valueProblemSentence(code, said, {\n extension: w.extension, node: w.node(kind, id), field, place: w.place, kindWord: w.kindWord,\n }));\n}\n"]}
|
package/dist/extension-copy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/extension-copy.ts
|
|
2
2
|
var quoted = (s) => `\u201C${s}\u201D`;
|
|
3
|
+
var curly = (s) => s.replace(/"([^"]*)"/g, (_, x) => quoted(x));
|
|
3
4
|
var cap = (s) => `${s.charAt(0).toUpperCase()}${s.slice(1)}`;
|
|
4
5
|
var article = (s) => /^[aeiou]/i.test(s) ? "an" : "a";
|
|
5
6
|
function fieldPhrase(fields, path) {
|
|
@@ -33,6 +34,16 @@ function valueProblemSentence(code, message, w) {
|
|
|
33
34
|
case "not-a-value":
|
|
34
35
|
if (/not among its flags$/.test(message)) return { text: `${on} has a flag that isn't one of its choices.`, next: `Pick from the choices ${place}.` };
|
|
35
36
|
return { text: `${on} is ${quoted(value ?? "")}, which isn't one of its choices.`, next: `Pick one ${place}.` };
|
|
37
|
+
case "not-a-choice":
|
|
38
|
+
return { text: `${on} is ${quoted(value ?? "")}, which isn't one of its choices.`, next: `Pick one ${place}.` };
|
|
39
|
+
case "pick-ambiguous": {
|
|
40
|
+
const m = / is "([^"]*)", which is (.+)$/.exec(message);
|
|
41
|
+
return m ? { text: `${on} is ${quoted(m[1])}, which is ${curly(m[2])}, so it's ambiguous.`, next: "Rename one of them." } : void 0;
|
|
42
|
+
}
|
|
43
|
+
case "pick-clash": {
|
|
44
|
+
const m = /^"([^"]*)" is (.+), so a value naming it is ambiguous$/.exec(message);
|
|
45
|
+
return m ? { text: `${quoted(m[1])} is ${curly(m[2])}, so ${field} naming it is ambiguous.`, next: "Rename one of them." } : void 0;
|
|
46
|
+
}
|
|
36
47
|
case "not-a-stage":
|
|
37
48
|
return { text: `${on} is ${quoted(value ?? "")}, which isn't one of its stages.`, next: `Pick one ${place}.` };
|
|
38
49
|
case "renamed-stage": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/extension-copy.ts"],"names":[],"mappings":"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","file":"extension-copy.js","sourcesContent":["// ---------------------------------------------------------------------------\n// An extension's problems, worded for the writer, in both apps.\n//\n// The command lines word an extension's problems for the file: \"entries.card.c_7k2.cue.sound: \"cue.sound\" is\n// required and not set\". The problems bar says the same for the writer: the extension by its name, the item as\n// the writer knows it, and the field by its label: \"Probe: The bell needs a sound in cue.\" Patterpad wrote these\n// rules first (its problem-copy.ts); they are the family's, here, so the two apps read alike.\n//\n// What is the app's is passed in: how it names the item (a line by its words, a card by its title), the words\n// for its node kinds, and where the writer goes to change a value (\"in the inspector\", \"on its page\"). The rules\n// key on the toolkit's code for the problem (`@wildwinter/toolkit/extensions`' Problem.code), which each app's\n// compiler carries on the issue; a code they do not know stands as the message it came with.\n//\n// No app state, no Electron, no DOM: words only.\n// ---------------------------------------------------------------------------\n\n/** A field declaration as the wording reads it: its name, its label, and a record's or an item's own fields. */\nexport interface FieldWordsDecl {\n name: string;\n label?: string;\n fields?: readonly FieldWordsDecl[];\n of?: { fields?: readonly FieldWordsDecl[] };\n}\n\n/** What the app knows to word one problem about a value. */\nexport interface ValueProblemWords {\n /** The extension's name, which leads the line (\"Probe\"). */\n extension: string;\n /** The item, as the writer knows it (\"“What'll it be, stranger?”\", \"the card “bell”\"). */\n node: string;\n /** The field, by its label (`fieldPhrase`). */\n field: string;\n /** Where a value is changed, finishing \"Pick one ...\" (\"in the inspector\", \"on its page\"). */\n place: string;\n /** A node kind in the app's words (\"game event\", \"hand template\"). */\n kindWord(kind: string): string;\n}\n\nexport const quoted = (s: string): string => `“${s}”`;\nconst cap = (s: string): string => `${s.charAt(0).toUpperCase()}${s.slice(1)}`;\nconst article = (s: string): string => (/^[aeiou]/i.test(s) ? \"an\" : \"a\");\n\n/** A field inside a node's entry, by its label: \"Note\", \"sound in cue\", \"item 2 of words\". */\nexport function fieldPhrase(fields: readonly FieldWordsDecl[], path: string): string {\n const tokens = path.split(/\\.|(?=\\[)/).filter((t) => t !== \"\");\n const top = fields.find((f) => f.name === tokens[0]);\n const topLabel = top?.label ?? tokens[0] ?? \"\";\n if (tokens.length <= 1) return topLabel;\n const last = tokens[tokens.length - 1]!;\n const index = /^\\[(\\d+)\\]$/.exec(last);\n if (index) return `item ${Number(index[1]) + 1} of ${topLabel}`;\n const parts = [...(top?.fields ?? []), ...(top?.of?.fields ?? [])];\n return `${parts.find((f) => f.name === last)?.label ?? last} in ${topLabel}`;\n}\n\n/** The sentence and its next step for one value a check refused, by the toolkit's code; undefined for a code\n * the rules do not know. `message` is the check's own sentence, which some sentences take their values from. */\nexport function valueProblemSentence(code: string | undefined, message: string, w: ValueProblemWords): { text: string; next?: string } | undefined {\n const { field, node, extension: name, place } = w;\n const on = `${field} on ${node}`;\n const value = /^\"[^\"]*\" is \"([^\"]*)\"/.exec(message)?.[1];\n switch (code) {\n case \"required\": return { text: `${node} needs ${article(field)} ${field}.` };\n case \"wrong-type\": return { text: `${on} isn't the kind of value ${name} expects.`, next: `Set it again ${place}.` };\n case \"below-min\": { const m = / is ([^,]+), below the least it may be, (.+)$/.exec(message); return m ? { text: `${on} is ${m[1]}, but ${name} takes nothing below ${m[2]}.` } : undefined; }\n case \"above-max\": { const m = / is ([^,]+), above the most it may be, (.+)$/.exec(message); return m ? { text: `${on} is ${m[1]}, but ${name} takes nothing above ${m[2]}.` } : undefined; }\n case \"not-a-value\":\n if (/not among its flags$/.test(message)) return { text: `${on} has a flag that isn't one of its choices.`, next: `Pick from the choices ${place}.` };\n return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its choices.`, next: `Pick one ${place}.` };\n case \"not-a-stage\": return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its stages.`, next: `Pick one ${place}.` };\n case \"renamed-stage\": {\n const m = / is \"([^\"]*)\", a stage this extension has since renamed \"([^\"]*)\"/.exec(message);\n return m ? { text: `${on} is ${quoted(m[1]!)}, a stage ${name} has since renamed ${quoted(m[2]!)}.`, next: `Pick ${quoted(m[2]!)} to update it.` } : undefined;\n }\n case \"no-such-node\": {\n const m = / (?:names|is keyed by) (\\w+) \"([^\"]*)\", which does not exist/.exec(message);\n return m ? { text: `${on} names the ${w.kindWord(m[1]!)} ${quoted(m[2]!)}, which isn't in the project.`, next: \"Choose another, or clear it.\" } : undefined;\n }\n case \"missing-id\": return { text: `An item in ${field} on ${node} has no id.`, next: `Give it one ${place}.` };\n case \"duplicate-id\": { const id = / repeats the id \"([^\"]*)\"/.exec(message)?.[1]; return { text: `Two items in ${field} on ${node} share the id ${quoted(id ?? \"\")}.`, next: \"Give each its own.\" }; }\n case \"unknown-field\": case \"unknown-key\":\n return { text: `${node} has a value for ${quoted(field)}, which ${name} doesn't declare any more.`, next: \"It's kept, but your game won't get it.\" };\n default: return undefined;\n }\n}\n\n/** A problem's line: the extension's name, then the sentence and its next step. */\nexport function extensionLine(extension: string, c: { text: string; next?: string } | undefined): string | undefined {\n return c ? `${extension}: ${cap(c.text)}${c.next ? ` ${c.next}` : \"\"}` : undefined;\n}\n\n/** What the app knows to word an entry problem (`entryProblemLine`). */\nexport interface EntryProblemWords {\n extension: string;\n /** The extension's declarations for a node kind. */\n fields(kind: string): readonly FieldWordsDecl[];\n /** A node as the writer knows it, by kind and id. */\n node(kind: string, id: string): string;\n place: string;\n kindWord(kind: string): string;\n}\n\n/**\n * The writer's line for a problem in a node's extension values, read from the command line's\n * \"entries.<kind>.<id>[.<path>]: <what is wrong>\" and the toolkit's code (`orphan` and `unresolved` included);\n * undefined for a message not of that shape, or a code the rules do not know.\n */\nexport function entryProblemLine(message: string, code: string | undefined, w: EntryProblemWords): string | undefined {\n const entry = /^entries\\.(\\w+)\\.([^.:\\s[]+)(\\S*?): (.*)$/.exec(message);\n if (!entry) return undefined;\n const [, kind, id, rest, said] = entry as unknown as [string, string, string, string, string];\n if (code === \"orphan\") {\n return extensionLine(w.extension, { text: `There are values for ${article(w.kindWord(kind))} ${w.kindWord(kind)} that isn't in the project any more.`, next: \"They're kept, but your game won't get them.\" });\n }\n if (code === \"unresolved\") return extensionLine(w.extension, { text: \"A merge left a clash in this data that hasn't been resolved.\", next: \"Resolve it before you publish.\" });\n const field = fieldPhrase(w.fields(kind), rest.replace(/^\\./, \"\"));\n return extensionLine(w.extension, valueProblemSentence(code, said, {\n extension: w.extension, node: w.node(kind, id), field, place: w.place, kindWord: w.kindWord,\n }));\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/extension-copy.ts"],"names":[],"mappings":"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","file":"extension-copy.js","sourcesContent":["// ---------------------------------------------------------------------------\n// An extension's problems, worded for the writer, in both apps.\n//\n// The command lines word an extension's problems for the file: \"entries.card.c_7k2.cue.sound: \"cue.sound\" is\n// required and not set\". The problems bar says the same for the writer: the extension by its name, the item as\n// the writer knows it, and the field by its label: \"Probe: The bell needs a sound in cue.\" Patterpad wrote these\n// rules first (its problem-copy.ts); they are the family's, here, so the two apps read alike.\n//\n// What is the app's is passed in: how it names the item (a line by its words, a card by its title), the words\n// for its node kinds, and where the writer goes to change a value (\"in the inspector\", \"on its page\"). The rules\n// key on the toolkit's code for the problem (`@wildwinter/toolkit/extensions`' Problem.code), which each app's\n// compiler carries on the issue; a code they do not know stands as the message it came with.\n//\n// No app state, no Electron, no DOM: words only.\n// ---------------------------------------------------------------------------\n\n/** A field declaration as the wording reads it: its name, its label, and a record's or an item's own fields. */\nexport interface FieldWordsDecl {\n name: string;\n label?: string;\n fields?: readonly FieldWordsDecl[];\n of?: { fields?: readonly FieldWordsDecl[] };\n}\n\n/** What the app knows to word one problem about a value. */\nexport interface ValueProblemWords {\n /** The extension's name, which leads the line (\"Probe\"). */\n extension: string;\n /** The item, as the writer knows it (\"“What'll it be, stranger?”\", \"the card “bell”\"). */\n node: string;\n /** The field, by its label (`fieldPhrase`). */\n field: string;\n /** Where a value is changed, finishing \"Pick one ...\" (\"in the inspector\", \"on its page\"). */\n place: string;\n /** A node kind in the app's words (\"game event\", \"hand template\"). */\n kindWord(kind: string): string;\n}\n\nexport const quoted = (s: string): string => `“${s}”`;\n/** A check's own quoted names in the family's quotes. */\nconst curly = (s: string): string => s.replace(/\"([^\"]*)\"/g, (_, x: string) => quoted(x));\nconst cap = (s: string): string => `${s.charAt(0).toUpperCase()}${s.slice(1)}`;\nconst article = (s: string): string => (/^[aeiou]/i.test(s) ? \"an\" : \"a\");\n\n/** A field inside a node's entry, by its label: \"Note\", \"sound in cue\", \"item 2 of words\". */\nexport function fieldPhrase(fields: readonly FieldWordsDecl[], path: string): string {\n const tokens = path.split(/\\.|(?=\\[)/).filter((t) => t !== \"\");\n const top = fields.find((f) => f.name === tokens[0]);\n const topLabel = top?.label ?? tokens[0] ?? \"\";\n if (tokens.length <= 1) return topLabel;\n const last = tokens[tokens.length - 1]!;\n const index = /^\\[(\\d+)\\]$/.exec(last);\n if (index) return `item ${Number(index[1]) + 1} of ${topLabel}`;\n const parts = [...(top?.fields ?? []), ...(top?.of?.fields ?? [])];\n return `${parts.find((f) => f.name === last)?.label ?? last} in ${topLabel}`;\n}\n\n/** The sentence and its next step for one value a check refused, by the toolkit's code; undefined for a code\n * the rules do not know. `message` is the check's own sentence, which some sentences take their values from. */\nexport function valueProblemSentence(code: string | undefined, message: string, w: ValueProblemWords): { text: string; next?: string } | undefined {\n const { field, node, extension: name, place } = w;\n const on = `${field} on ${node}`;\n const value = /^\"[^\"]*\" is \"([^\"]*)\"/.exec(message)?.[1];\n switch (code) {\n case \"required\": return { text: `${node} needs ${article(field)} ${field}.` };\n case \"wrong-type\": return { text: `${on} isn't the kind of value ${name} expects.`, next: `Set it again ${place}.` };\n case \"below-min\": { const m = / is ([^,]+), below the least it may be, (.+)$/.exec(message); return m ? { text: `${on} is ${m[1]}, but ${name} takes nothing below ${m[2]}.` } : undefined; }\n case \"above-max\": { const m = / is ([^,]+), above the most it may be, (.+)$/.exec(message); return m ? { text: `${on} is ${m[1]}, but ${name} takes nothing above ${m[2]}.` } : undefined; }\n case \"not-a-value\":\n if (/not among its flags$/.test(message)) return { text: `${on} has a flag that isn't one of its choices.`, next: `Pick from the choices ${place}.` };\n return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its choices.`, next: `Pick one ${place}.` };\n case \"not-a-choice\": return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its choices.`, next: `Pick one ${place}.` };\n case \"pick-ambiguous\": {\n const m = / is \"([^\"]*)\", which is (.+)$/.exec(message);\n return m ? { text: `${on} is ${quoted(m[1]!)}, which is ${curly(m[2]!)}, so it's ambiguous.`, next: \"Rename one of them.\" } : undefined;\n }\n case \"pick-clash\": {\n const m = /^\"([^\"]*)\" is (.+), so a value naming it is ambiguous$/.exec(message);\n return m ? { text: `${quoted(m[1]!)} is ${curly(m[2]!)}, so ${field} naming it is ambiguous.`, next: \"Rename one of them.\" } : undefined;\n }\n case \"not-a-stage\": return { text: `${on} is ${quoted(value ?? \"\")}, which isn't one of its stages.`, next: `Pick one ${place}.` };\n case \"renamed-stage\": {\n const m = / is \"([^\"]*)\", a stage this extension has since renamed \"([^\"]*)\"/.exec(message);\n return m ? { text: `${on} is ${quoted(m[1]!)}, a stage ${name} has since renamed ${quoted(m[2]!)}.`, next: `Pick ${quoted(m[2]!)} to update it.` } : undefined;\n }\n case \"no-such-node\": {\n const m = / (?:names|is keyed by) (\\w+) \"([^\"]*)\", which does not exist/.exec(message);\n return m ? { text: `${on} names the ${w.kindWord(m[1]!)} ${quoted(m[2]!)}, which isn't in the project.`, next: \"Choose another, or clear it.\" } : undefined;\n }\n case \"missing-id\": return { text: `An item in ${field} on ${node} has no id.`, next: `Give it one ${place}.` };\n case \"duplicate-id\": { const id = / repeats the id \"([^\"]*)\"/.exec(message)?.[1]; return { text: `Two items in ${field} on ${node} share the id ${quoted(id ?? \"\")}.`, next: \"Give each its own.\" }; }\n case \"unknown-field\": case \"unknown-key\":\n return { text: `${node} has a value for ${quoted(field)}, which ${name} doesn't declare any more.`, next: \"It's kept, but your game won't get it.\" };\n default: return undefined;\n }\n}\n\n/** A problem's line: the extension's name, then the sentence and its next step. */\nexport function extensionLine(extension: string, c: { text: string; next?: string } | undefined): string | undefined {\n return c ? `${extension}: ${cap(c.text)}${c.next ? ` ${c.next}` : \"\"}` : undefined;\n}\n\n/** What the app knows to word an entry problem (`entryProblemLine`). */\nexport interface EntryProblemWords {\n extension: string;\n /** The extension's declarations for a node kind. */\n fields(kind: string): readonly FieldWordsDecl[];\n /** A node as the writer knows it, by kind and id. */\n node(kind: string, id: string): string;\n place: string;\n kindWord(kind: string): string;\n}\n\n/**\n * The writer's line for a problem in a node's extension values, read from the command line's\n * \"entries.<kind>.<id>[.<path>]: <what is wrong>\" and the toolkit's code (`orphan` and `unresolved` included);\n * undefined for a message not of that shape, or a code the rules do not know.\n */\nexport function entryProblemLine(message: string, code: string | undefined, w: EntryProblemWords): string | undefined {\n const entry = /^entries\\.(\\w+)\\.([^.:\\s[]+)(\\S*?): (.*)$/.exec(message);\n if (!entry) return undefined;\n const [, kind, id, rest, said] = entry as unknown as [string, string, string, string, string];\n if (code === \"orphan\") {\n return extensionLine(w.extension, { text: `There are values for ${article(w.kindWord(kind))} ${w.kindWord(kind)} that isn't in the project any more.`, next: \"They're kept, but your game won't get them.\" });\n }\n if (code === \"unresolved\") return extensionLine(w.extension, { text: \"A merge left a clash in this data that hasn't been resolved.\", next: \"Resolve it before you publish.\" });\n const field = fieldPhrase(w.fields(kind), rest.replace(/^\\./, \"\"));\n return extensionLine(w.extension, valueProblemSentence(code, said, {\n extension: w.extension, node: w.node(kind, id), field, place: w.place, kindWord: w.kindWord,\n }));\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { mountWelcome } from './chunk-XSS7KM6F.js';
|
|
2
2
|
export { feedUpdaterDownloadProgress, showUpdaterDialog } from './chunk-C245WRPF.js';
|
|
3
|
+
export { EDIT_MENU, FILE_MENU, GO_MENU, PANE_MENU, PLAY_MENU, PUBLISH_MENU, REVIEW_MENU, VIEW_MENU } from './chunk-T25WXWPB.js';
|
|
3
4
|
export { defaultProblemCopy, describeProblem, problemLine, problemName } from './chunk-4OAORWEM.js';
|
|
4
5
|
export { PROPERTY_NAME_HINT, PROPERTY_TYPES, RESERVED_PROPERTY_NAMES, bindPropertyName, bindPropertyRef, defaultControl, dupGuard, expandableRow, firstIllegalPropertyName, focusNewRow, isCaseOnlyPropertyName, isValidPropertyName, mountPropertyList, mountSettingsDialog, propertyNameProblem, propertyNameify, propertyRefProblem, revalidatePropertyRefs, revealRow, revealRowWhenReady } from './chunk-5BKGDTYD.js';
|
|
5
6
|
export { defaultLabel, fallbackText, fieldRow, fieldRows, fieldValueProblem, rungStrip, stageGateNote } from './chunk-XJM733E6.js';
|
|
@@ -10,7 +11,6 @@ export { kitTile, openKitGallery } from './chunk-62KWJPRP.js';
|
|
|
10
11
|
export { linkAddress, linkStatusClass, linkStatusTip, mountLinkStatus } from './chunk-UQTUCOGV.js';
|
|
11
12
|
import { isEditableTarget } from './chunk-QVACRWG7.js';
|
|
12
13
|
export { copyWithFeedback, debounce, formatCount, isEditableTarget, isLocalOrigin, plural, relativeTime } from './chunk-QVACRWG7.js';
|
|
13
|
-
export { EDIT_MENU, FILE_MENU, GO_MENU, PANE_MENU, PLAY_MENU, PUBLISH_MENU, REVIEW_MENU, VIEW_MENU } from './chunk-T25WXWPB.js';
|
|
14
14
|
import { dialogFrame } from './chunk-VXEFIL43.js';
|
|
15
15
|
export { dialogFrame } from './chunk-VXEFIL43.js';
|
|
16
16
|
export { createFloating } from './chunk-PFERSFRB.js';
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ws = require('ws');
|
|
4
|
+
|
|
5
|
+
// src/live-link.ts
|
|
6
|
+
|
|
7
|
+
// src/util.ts
|
|
8
|
+
function isLocalOrigin(origin) {
|
|
9
|
+
if (origin === void 0 || origin === "") return true;
|
|
10
|
+
let host;
|
|
11
|
+
try {
|
|
12
|
+
host = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(origin) ? origin : `http://${origin}`).hostname;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/live-link.ts
|
|
20
|
+
function createLiveLinkServer(opts) {
|
|
21
|
+
const { port } = opts;
|
|
22
|
+
let wss = null;
|
|
23
|
+
let client = null;
|
|
24
|
+
let connected = null;
|
|
25
|
+
let clientBuild = null;
|
|
26
|
+
const status = () => {
|
|
27
|
+
if (!wss) return { state: "off" };
|
|
28
|
+
if (!client || !connected) return { state: "listening", port };
|
|
29
|
+
return {
|
|
30
|
+
...protocol.describe(),
|
|
31
|
+
state: "connected",
|
|
32
|
+
port,
|
|
33
|
+
build: connected.build,
|
|
34
|
+
...connected.project !== void 0 ? { project: connected.project } : {}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const push = () => opts.onStatus(status());
|
|
38
|
+
const pushStatus = () => {
|
|
39
|
+
if (connected) push();
|
|
40
|
+
};
|
|
41
|
+
const protocol = opts.protocol({ pushStatus });
|
|
42
|
+
const forget = () => {
|
|
43
|
+
connected = null;
|
|
44
|
+
clientBuild = null;
|
|
45
|
+
protocol.reset();
|
|
46
|
+
};
|
|
47
|
+
const handleMessage = (raw) => {
|
|
48
|
+
let msg;
|
|
49
|
+
try {
|
|
50
|
+
msg = JSON.parse(raw);
|
|
51
|
+
} catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (msg === null || typeof msg !== "object" || Array.isArray(msg)) return;
|
|
55
|
+
const m = msg;
|
|
56
|
+
if (m["t"] !== "hello") {
|
|
57
|
+
if (connected) protocol.message(m);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
clientBuild = typeof m["build"] === "string" ? m["build"] : null;
|
|
61
|
+
const current = opts.currentBuildHash();
|
|
62
|
+
const build = current === null ? "unknown" : clientBuild === current ? "match" : "stale";
|
|
63
|
+
const project = typeof m["project"] === "string" ? m["project"] : void 0;
|
|
64
|
+
connected = { build, ...project !== void 0 ? { project } : {} };
|
|
65
|
+
protocol.hello({ build, ...project !== void 0 ? { project } : {}, msg: m });
|
|
66
|
+
push();
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
isOn: () => wss !== null,
|
|
70
|
+
status,
|
|
71
|
+
pushStatus,
|
|
72
|
+
pushBundle(build, data) {
|
|
73
|
+
if (!client || !connected) return;
|
|
74
|
+
if (clientBuild === build) return;
|
|
75
|
+
try {
|
|
76
|
+
client.send(JSON.stringify({ t: "bundle", v: 1, build, data }));
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
if (connected.build !== "stale") {
|
|
80
|
+
connected = { ...connected, build: "stale" };
|
|
81
|
+
push();
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
start() {
|
|
85
|
+
if (wss) return;
|
|
86
|
+
try {
|
|
87
|
+
wss = new ws.WebSocketServer({
|
|
88
|
+
host: "127.0.0.1",
|
|
89
|
+
port,
|
|
90
|
+
verifyClient: (info) => isLocalOrigin(info.origin)
|
|
91
|
+
}, () => push());
|
|
92
|
+
} catch (e) {
|
|
93
|
+
opts.onStatus({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
94
|
+
wss = null;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
wss.on("error", (e) => {
|
|
98
|
+
opts.onStatus({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
99
|
+
if (!client) {
|
|
100
|
+
const s = wss;
|
|
101
|
+
wss = null;
|
|
102
|
+
try {
|
|
103
|
+
s?.close();
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
wss.on("connection", (ws) => {
|
|
109
|
+
if (client) {
|
|
110
|
+
try {
|
|
111
|
+
client.close();
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
client = ws;
|
|
116
|
+
forget();
|
|
117
|
+
ws.on("message", (data) => handleMessage(data.toString()));
|
|
118
|
+
ws.on("close", () => {
|
|
119
|
+
if (client === ws) {
|
|
120
|
+
client = null;
|
|
121
|
+
forget();
|
|
122
|
+
push();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
ws.on("error", () => {
|
|
126
|
+
});
|
|
127
|
+
push();
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
stop() {
|
|
131
|
+
try {
|
|
132
|
+
client?.close();
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
client = null;
|
|
136
|
+
forget();
|
|
137
|
+
const s = wss;
|
|
138
|
+
wss = null;
|
|
139
|
+
try {
|
|
140
|
+
s?.close();
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
push();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
exports.createLiveLinkServer = createLiveLinkServer;
|
|
149
|
+
//# sourceMappingURL=live-link.cjs.map
|
|
150
|
+
//# sourceMappingURL=live-link.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util.ts","../src/live-link.ts"],"names":["WebSocketServer"],"mappings":"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","file":"live-link.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The small idioms both apps wrote by hand and never gave a name: the plural\n// suffix (about forty sites), the debounce (eight copies), \"is the focus in a\n// field\" (four copies that disagreed about <select>), the count with grouping\n// separators, clipboard copy with the `.copied` revert (three timings), and\n// relative time (two wordings), and which connection to a live link is this\n// machine's own (isLocalOrigin, two copies). One answer each, here.\n//\n// DOM-free except where the idiom is about the DOM (isEditableTarget,\n// copyWithFeedback), so main-process code can import the rest.\n// ---------------------------------------------------------------------------\n\n/** \"1 link\" / \"3 links\". The plural form defaults to `noun + \"s\"`; pass the\n * irregular one (\"entry\", \"entries\") when that is wrong. */\nexport function plural(n: number, noun: string, pluralForm = `${noun}s`): string {\n return `${n} ${n === 1 ? noun : pluralForm}`;\n}\n\nexport interface Debounced<A extends unknown[]> {\n (...args: A): void;\n /** Drop the pending call. */\n cancel(): void;\n /** Run the pending call now, if there is one. */\n flush(): void;\n}\n\n/** Trailing-edge debounce: the call runs `ms` after the LAST call. */\nexport function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): Debounced<A> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let pending: A | undefined;\n const run = (): void => {\n timer = undefined;\n if (pending === undefined) return;\n const args = pending;\n pending = undefined;\n fn(...args);\n };\n const debounced = ((...args: A): void => {\n pending = args;\n if (timer !== undefined) clearTimeout(timer);\n timer = setTimeout(run, ms);\n }) as Debounced<A>;\n debounced.cancel = () => { if (timer !== undefined) clearTimeout(timer); timer = undefined; pending = undefined; };\n debounced.flush = () => { if (timer !== undefined) clearTimeout(timer); run(); };\n return debounced;\n}\n\n/**\n * Is the keyboard's focus in a field? The one agreed answer: an input, a\n * textarea, a select, or anything contenteditable (the attribute is read as\n * well as `isContentEditable`, which jsdom does not implement). A window-level\n * shortcut checks this before acting, so a bare letter typed into a field\n * stays a letter.\n */\nexport function isEditableTarget(t: EventTarget | null | undefined): boolean {\n if (!(t instanceof Element)) return false;\n if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) return true;\n if (t instanceof HTMLElement && t.isContentEditable === true) return true;\n const attr = t.getAttribute(\"contenteditable\");\n return attr !== null && attr !== \"false\";\n}\n\n/** A count for reading: grouping separators in the user's locale. */\nexport function formatCount(n: number): string {\n return n.toLocaleString();\n}\n\nconst copyTimers = new WeakMap<Element, ReturnType<typeof setTimeout>>();\n\n/**\n * Copy `text` to the clipboard and light `el` with `.copied` for `ms` (the\n * family's 1000). A second click restarts the timer rather than stacking a\n * second revert under the first. Resolves when the clipboard write has.\n */\nexport function copyWithFeedback(el: Element, text: string, ms = 1000): Promise<void> {\n const write = typeof navigator !== \"undefined\" && navigator.clipboard\n ? navigator.clipboard.writeText(text).catch(() => {})\n : Promise.resolve();\n el.classList.add(\"copied\");\n const prior = copyTimers.get(el);\n if (prior !== undefined) clearTimeout(prior);\n copyTimers.set(el, setTimeout(() => { el.classList.remove(\"copied\"); copyTimers.delete(el); }, ms));\n return write;\n}\n\n/**\n * How long ago, in one wording: \"just now\", \"4 minutes ago\", \"an hour ago\",\n * \"3 hours ago\", \"yesterday\", \"5 days ago\", and past a week the date itself.\n * Coarse on purpose: the question a reader has is whether the thing is from\n * this sitting, not how many seconds ago it was. A future date is \"just now\".\n */\nexport function relativeTime(date: Date | number | string, now: number = Date.now()): string {\n const then = date instanceof Date ? date.getTime() : new Date(date).getTime();\n if (!Number.isFinite(then)) return \"\";\n const mins = Math.floor((now - then) / 60000);\n if (mins < 1) return \"just now\";\n if (mins < 60) return `${plural(mins, \"minute\")} ago`;\n const hours = Math.floor(mins / 60);\n if (hours === 1) return \"an hour ago\";\n if (hours < 24) return `${hours} hours ago`;\n const days = Math.floor(hours / 24);\n if (days === 1) return \"yesterday\";\n if (days < 7) return `${days} days ago`;\n return new Date(then).toLocaleDateString(undefined, { day: \"numeric\", month: \"short\", year: \"numeric\" });\n}\n\n/**\n * Whether a WebSocket upgrade's Origin is this machine, so a game may take a\n * live link's slot (Patterpad's Live Link and Storyletter's, which listen on\n * loopback). A web page in any browser can open a socket to localhost, and the\n * browser stamps the upgrade with the page's Origin, so the Origin is the check:\n *\n * - no Origin: Unity's ClientWebSocket, Godot and Node send none;\n * - a loopback host, literally `localhost`, `127.0.0.1` or `[::1]`, with or\n * without a scheme and port: Unreal's libwebsockets sends the bare address\n * (`127.0.0.1`), and a browser game on a local dev server sends\n * `http://localhost:5173`;\n * - anything else is refused: another site, `null` (a file:// page sends it,\n * but so does a sandboxed frame on any site), and a name that only resolves\n * to loopback (`127.0.0.1.nip.io`), since the Origin names the domain, not\n * the address.\n */\nexport function isLocalOrigin(origin: string | undefined): boolean {\n if (origin === undefined || origin === \"\") return true;\n let host: string;\n try {\n // A browser sends scheme://host[:port]; Unreal sends a bare host, which URL would misread as a scheme.\n host = new URL(/^[a-z][a-z0-9+.-]*:\\/\\//i.test(origin) ? origin : `http://${origin}`).hostname;\n } catch { return false; }\n return host === \"localhost\" || host === \"127.0.0.1\" || host === \"[::1]\";\n}\n","// ---------------------------------------------------------------------------\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"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** How the game's build compares with the project's. */
|
|
2
|
+
type LiveLinkBuild = "match" | "stale" | "unknown";
|
|
3
|
+
/** One parsed message from the game: a JSON object with its `t`. */
|
|
4
|
+
type LiveLinkMessage = Record<string, unknown>;
|
|
5
|
+
/** The server's status, with the protocol's own fields on `connected`. */
|
|
6
|
+
type LiveLinkServerStatus<X extends object = object> = {
|
|
7
|
+
state: "off";
|
|
8
|
+
} | {
|
|
9
|
+
state: "error";
|
|
10
|
+
message: string;
|
|
11
|
+
} | {
|
|
12
|
+
state: "listening";
|
|
13
|
+
port: number;
|
|
14
|
+
} | ({
|
|
15
|
+
state: "connected";
|
|
16
|
+
port: number;
|
|
17
|
+
project?: string;
|
|
18
|
+
build: LiveLinkBuild;
|
|
19
|
+
} & X);
|
|
20
|
+
/** A hello, as the server read it. */
|
|
21
|
+
interface LiveLinkHello {
|
|
22
|
+
/** The game's build judged against the project's. */
|
|
23
|
+
build: LiveLinkBuild;
|
|
24
|
+
/** What the game said it is, if it did. */
|
|
25
|
+
project?: string;
|
|
26
|
+
/** The whole message, for the protocol's own fields (flows, boxes). */
|
|
27
|
+
msg: LiveLinkMessage;
|
|
28
|
+
}
|
|
29
|
+
/** An app's half of the link. */
|
|
30
|
+
interface LiveLinkProtocol<X extends object = object> {
|
|
31
|
+
/** A hello landed: a run begins, or a pushed bundle took. The status is
|
|
32
|
+
* pushed after it returns. */
|
|
33
|
+
hello(hello: LiveLinkHello): void;
|
|
34
|
+
/** Any other message, once the game has said hello. Call `pushStatus` when
|
|
35
|
+
* it changes what the status shows. */
|
|
36
|
+
message(msg: LiveLinkMessage): void;
|
|
37
|
+
/** The game went away, was replaced, or the server stopped: forget the run. */
|
|
38
|
+
reset(): void;
|
|
39
|
+
/** The protocol's fields on a connected status. */
|
|
40
|
+
describe(): X;
|
|
41
|
+
}
|
|
42
|
+
interface LiveLinkServerOptions<X extends object = object> {
|
|
43
|
+
/** Each app has its own, so both editors can listen on one machine. */
|
|
44
|
+
port: number;
|
|
45
|
+
/** The project's current compiled hash, read at each hello; null with no
|
|
46
|
+
* project open, which makes the build "unknown". */
|
|
47
|
+
currentBuildHash: () => string | null;
|
|
48
|
+
/** Make the protocol. `pushStatus` sends the status again while a game is
|
|
49
|
+
* connected (before that, nothing the protocol adds is shown). */
|
|
50
|
+
protocol: (link: {
|
|
51
|
+
pushStatus(): void;
|
|
52
|
+
}) => LiveLinkProtocol<X>;
|
|
53
|
+
/** The latest status, for the app's windows and menu. */
|
|
54
|
+
onStatus: (status: LiveLinkServerStatus<X>) => void;
|
|
55
|
+
}
|
|
56
|
+
interface LiveLinkServer<X extends object = object> {
|
|
57
|
+
start(): void;
|
|
58
|
+
stop(): void;
|
|
59
|
+
isOn(): boolean;
|
|
60
|
+
status(): LiveLinkServerStatus<X>;
|
|
61
|
+
/** Send the status again while a game is connected: something the protocol
|
|
62
|
+
* shows changed outside a message (the app followed another flow). */
|
|
63
|
+
pushStatus(): void;
|
|
64
|
+
/** Push a freshly compiled bundle to the connected game. Nothing happens when
|
|
65
|
+
* no game has said hello, or when it already runs this build. The game
|
|
66
|
+
* applies it and re-hellos with the new build; until then it shows stale. */
|
|
67
|
+
pushBundle(build: string, data: string): void;
|
|
68
|
+
}
|
|
69
|
+
declare function createLiveLinkServer<X extends object = object>(opts: LiveLinkServerOptions<X>): LiveLinkServer<X>;
|
|
70
|
+
|
|
71
|
+
export { type LiveLinkBuild, type LiveLinkHello, type LiveLinkMessage, type LiveLinkProtocol, type LiveLinkServer, type LiveLinkServerOptions, type LiveLinkServerStatus, createLiveLinkServer };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** How the game's build compares with the project's. */
|
|
2
|
+
type LiveLinkBuild = "match" | "stale" | "unknown";
|
|
3
|
+
/** One parsed message from the game: a JSON object with its `t`. */
|
|
4
|
+
type LiveLinkMessage = Record<string, unknown>;
|
|
5
|
+
/** The server's status, with the protocol's own fields on `connected`. */
|
|
6
|
+
type LiveLinkServerStatus<X extends object = object> = {
|
|
7
|
+
state: "off";
|
|
8
|
+
} | {
|
|
9
|
+
state: "error";
|
|
10
|
+
message: string;
|
|
11
|
+
} | {
|
|
12
|
+
state: "listening";
|
|
13
|
+
port: number;
|
|
14
|
+
} | ({
|
|
15
|
+
state: "connected";
|
|
16
|
+
port: number;
|
|
17
|
+
project?: string;
|
|
18
|
+
build: LiveLinkBuild;
|
|
19
|
+
} & X);
|
|
20
|
+
/** A hello, as the server read it. */
|
|
21
|
+
interface LiveLinkHello {
|
|
22
|
+
/** The game's build judged against the project's. */
|
|
23
|
+
build: LiveLinkBuild;
|
|
24
|
+
/** What the game said it is, if it did. */
|
|
25
|
+
project?: string;
|
|
26
|
+
/** The whole message, for the protocol's own fields (flows, boxes). */
|
|
27
|
+
msg: LiveLinkMessage;
|
|
28
|
+
}
|
|
29
|
+
/** An app's half of the link. */
|
|
30
|
+
interface LiveLinkProtocol<X extends object = object> {
|
|
31
|
+
/** A hello landed: a run begins, or a pushed bundle took. The status is
|
|
32
|
+
* pushed after it returns. */
|
|
33
|
+
hello(hello: LiveLinkHello): void;
|
|
34
|
+
/** Any other message, once the game has said hello. Call `pushStatus` when
|
|
35
|
+
* it changes what the status shows. */
|
|
36
|
+
message(msg: LiveLinkMessage): void;
|
|
37
|
+
/** The game went away, was replaced, or the server stopped: forget the run. */
|
|
38
|
+
reset(): void;
|
|
39
|
+
/** The protocol's fields on a connected status. */
|
|
40
|
+
describe(): X;
|
|
41
|
+
}
|
|
42
|
+
interface LiveLinkServerOptions<X extends object = object> {
|
|
43
|
+
/** Each app has its own, so both editors can listen on one machine. */
|
|
44
|
+
port: number;
|
|
45
|
+
/** The project's current compiled hash, read at each hello; null with no
|
|
46
|
+
* project open, which makes the build "unknown". */
|
|
47
|
+
currentBuildHash: () => string | null;
|
|
48
|
+
/** Make the protocol. `pushStatus` sends the status again while a game is
|
|
49
|
+
* connected (before that, nothing the protocol adds is shown). */
|
|
50
|
+
protocol: (link: {
|
|
51
|
+
pushStatus(): void;
|
|
52
|
+
}) => LiveLinkProtocol<X>;
|
|
53
|
+
/** The latest status, for the app's windows and menu. */
|
|
54
|
+
onStatus: (status: LiveLinkServerStatus<X>) => void;
|
|
55
|
+
}
|
|
56
|
+
interface LiveLinkServer<X extends object = object> {
|
|
57
|
+
start(): void;
|
|
58
|
+
stop(): void;
|
|
59
|
+
isOn(): boolean;
|
|
60
|
+
status(): LiveLinkServerStatus<X>;
|
|
61
|
+
/** Send the status again while a game is connected: something the protocol
|
|
62
|
+
* shows changed outside a message (the app followed another flow). */
|
|
63
|
+
pushStatus(): void;
|
|
64
|
+
/** Push a freshly compiled bundle to the connected game. Nothing happens when
|
|
65
|
+
* no game has said hello, or when it already runs this build. The game
|
|
66
|
+
* applies it and re-hellos with the new build; until then it shows stale. */
|
|
67
|
+
pushBundle(build: string, data: string): void;
|
|
68
|
+
}
|
|
69
|
+
declare function createLiveLinkServer<X extends object = object>(opts: LiveLinkServerOptions<X>): LiveLinkServer<X>;
|
|
70
|
+
|
|
71
|
+
export { type LiveLinkBuild, type LiveLinkHello, type LiveLinkMessage, type LiveLinkProtocol, type LiveLinkServer, type LiveLinkServerOptions, type LiveLinkServerStatus, createLiveLinkServer };
|