@jigs.design/server 0.2.0 → 0.3.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 +19 -13
- package/dist/chat.d.ts +2 -2
- package/dist/chat.js +46 -34
- package/dist/compile-cache.js +60 -0
- package/dist/compile.d.ts +1 -1
- package/dist/compile.js +167 -35
- package/dist/{server.d.ts → core.d.ts} +7 -3
- package/dist/{server.js → core.js} +309 -395
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +3 -3
- package/dist/events.js +129 -0
- package/dist/handler.d.ts +4 -0
- package/dist/handler.js +344 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -2
- package/dist/model-server.js +154 -0
- package/dist/session.d.ts +1 -2
- package/dist/session.js +116 -52
- package/dist/types.d.ts +277 -51
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @jigs.design/server
|
|
2
2
|
|
|
3
|
-
`createJigsServer` turns a host into one fetch handler. That handler streams checked programs, runs queries and mutations, and serves the sandbox frame. You give it a `run` function per procedure, a `context` for each request
|
|
3
|
+
`createJigsServer` turns a host into one fetch handler. That handler streams checked programs, runs queries and mutations, and serves the sandbox frame. You give it a `run` function per procedure, and a `context` for each request; `.withModel` adds a `model` function that calls your LLM. Without it, the server serves calls, pages, the push stream and a session that opens a document with no ask, and nothing that asks a model.
|
|
4
4
|
|
|
5
5
|
Import it from server code only.
|
|
6
6
|
|
|
@@ -21,8 +21,9 @@ export const jigs = createJigsServer({
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
|
-
model: aiSdkModel("anthropic/claude-sonnet-5.5"),
|
|
25
24
|
instructions: "You are Acme's order assistant. Today is 2026-10-18.",
|
|
25
|
+
}).withModel({
|
|
26
|
+
model: aiSdkModel("anthropic/claude-sonnet-5.5"),
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
// Mount it as GET, POST and OPTIONS on one route, e.g. /api/jigs.
|
|
@@ -31,26 +32,31 @@ export const handler = jigs.handler;
|
|
|
31
32
|
|
|
32
33
|
The server renders the prompt from the host, calls the model, checks each streamed prefix and compiles it to a program. `instructions`, such as who the assistant is and today's date, reach every prompt: document-mode sessions, chat and repairs. When the checker finds errors, it runs repair rounds. A call carries its procedure's `kind`, and the server refuses one that does not match the host. A follow-up turn may answer with SEARCH/REPLACE edits instead of a whole document.
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
UIs need not come from the model. `await jigs.page(name, props)` returns a page your product ships as a jig on the host, compiled at startup or, for new props, at its first call; `await jigs.page(id, undefined, { ctx })` also answers a page a user saved, which `resolvePage` finds for that `ctx`. The handler serves both as `POST { type: "page" }`, gated by `context` and `pagePolicy`, and compiled pages go through an in-process cache bounded by size. A session can open either as its document, `{ name, props }`.
|
|
36
|
+
|
|
37
|
+
With an `events` option, a `GET` that accepts `text/event-stream` opens a push stream, on which your server names the queries and pages to refetch.
|
|
35
38
|
|
|
36
39
|
## Exports
|
|
37
40
|
|
|
38
|
-
| Export | What it is
|
|
39
|
-
| -------------------------- |
|
|
40
|
-
| `createJigsServer` | The server: `handler`, `
|
|
41
|
-
| `defineRun` | Types `run` functions outside a server, to share them between two servers
|
|
42
|
-
| `JigsError` | Throw from `context` or `run` with a code the browser can read
|
|
43
|
-
| `JigsCompileError` | What a strict `jigs.compile` rejects with, carrying the `diagnostics`
|
|
44
|
-
| `chatBlockText` | Writes a chat block back as a fenced block, for the model's history
|
|
45
|
-
| `@jigs.design/server/node` | `nodeHandler`, for Node's `http` and Express
|
|
41
|
+
| Export | What it is |
|
|
42
|
+
| -------------------------- | ------------------------------------------------------------------------------ |
|
|
43
|
+
| `createJigsServer` | The server: `handler`, `page`, `compile`, `withModel` for `session` and `chat` |
|
|
44
|
+
| `defineRun` | Types `run` functions outside a server, to share them between two servers |
|
|
45
|
+
| `JigsError` | Throw from `context` or `run` with a code the browser can read |
|
|
46
|
+
| `JigsCompileError` | What a strict `jigs.compile` rejects with, carrying the `diagnostics` |
|
|
47
|
+
| `chatBlockText` | Writes a chat block back as a fenced block, for the model's history |
|
|
48
|
+
| `@jigs.design/server/node` | `nodeHandler`, for Node's `http` and Express |
|
|
46
49
|
|
|
47
50
|
It re-exports the shared types from `@jigs.design/protocol`.
|
|
48
51
|
|
|
49
52
|
## Source
|
|
50
53
|
|
|
51
|
-
- `src/
|
|
54
|
+
- `src/core.ts`: `createJigsServer`, the model-free core: calls, pages, compiling, the frame and prompts.
|
|
55
|
+
- `src/compile-cache.ts`: the process's compile cache, bounded by the size of its programs.
|
|
56
|
+
- `src/handler.ts`: the fetch handler: refusals, body parsing, routing and the event stream.
|
|
57
|
+
- `src/model-server.ts`: `withModel`: `session`, `chat` and the handler that runs turns.
|
|
52
58
|
- `src/session.ts`: one turn: prompt, model, checking, repair and edits.
|
|
53
|
-
- `src/compile.ts`: compiling a document against the host's catalog and
|
|
59
|
+
- `src/compile.ts`: compiling a document against the host's catalog and tags, a page against every define jig, and the checks on a page's props.
|
|
54
60
|
- `src/inputs.ts`, `src/literal.ts`: literal procedure inputs checked against their schemas.
|
|
55
61
|
- `src/chat.ts`: chat mode, where a Markdown reply carries fenced `jigs` blocks.
|
|
56
62
|
- `src/invalidation.ts`: which queries a mutation refreshes.
|
package/dist/chat.d.ts
CHANGED
|
@@ -2,11 +2,11 @@ import "./design.js";
|
|
|
2
2
|
import { ChatBlockSource } from "./types.js";
|
|
3
3
|
import "./session.js";
|
|
4
4
|
import "@jigs.design/compiler";
|
|
5
|
-
import { ChatBlock, Diagnostic, Program } from "@jigs.design/protocol";
|
|
5
|
+
import { ChatBlock, Diagnostic, PageRef, Program } from "@jigs.design/protocol";
|
|
6
6
|
//#region src/chat.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* A block as the model wrote it, for the model's history: a fenced `jigs`
|
|
9
|
-
* block with its id, and its source without the pragma line.
|
|
9
|
+
* block with its name as the fence id, and its source without the pragma line.
|
|
10
10
|
*/
|
|
11
11
|
export declare function chatBlockText(block: ChatBlockSource): string;
|
|
12
12
|
//#endregion
|
package/dist/chat.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MARKDOWN_ELEMENTS } from "./compile.js";
|
|
2
1
|
import { belowPrefix, pragmaLine, splitPragma, withPragma } from "./pragma.js";
|
|
2
|
+
import { MARKDOWN_ELEMENTS } from "./compile.js";
|
|
3
3
|
import { reorderDeclarations } from "./rewrite.js";
|
|
4
4
|
import { applyChecked, editResult, runSession } from "./session.js";
|
|
5
5
|
import { applyEdits, isEditReply, parseEdits } from "@jigs.design/compiler";
|
|
@@ -87,11 +87,11 @@ const AUTO_ID = /^jigs-(\d+)$/;
|
|
|
87
87
|
const VALID_ID = /^[a-z][a-z0-9-]*$/;
|
|
88
88
|
/**
|
|
89
89
|
* A block as the model wrote it, for the model's history: a fenced `jigs`
|
|
90
|
-
* block with its id, and its source without the pragma line.
|
|
90
|
+
* block with its name as the fence id, and its source without the pragma line.
|
|
91
91
|
*/
|
|
92
92
|
function chatBlockText(block) {
|
|
93
93
|
const body = splitPragma(block.source).body.replace(/\n+$/, "");
|
|
94
|
-
return `\`\`\`jigs ${block.
|
|
94
|
+
return `\`\`\`jigs ${block.name}\n${body}\n\`\`\``;
|
|
95
95
|
}
|
|
96
96
|
/** The block's id from the fence's info string, made kebab-case. */
|
|
97
97
|
function slug(info) {
|
|
@@ -133,18 +133,18 @@ function depthChange(line) {
|
|
|
133
133
|
* its program changes.
|
|
134
134
|
*/
|
|
135
135
|
function createReplyParser(deps, history) {
|
|
136
|
-
/** Latest body (no pragma) by
|
|
136
|
+
/** Latest body (no pragma) by name. */
|
|
137
137
|
const latest = /* @__PURE__ */ new Map();
|
|
138
138
|
/**
|
|
139
|
-
* The body as this reply wrote it, by
|
|
139
|
+
* The body as this reply wrote it, by name, for a block whose `{@body}`
|
|
140
140
|
* lines the server moved: an edit later in the same reply was written
|
|
141
141
|
* against it, not against the moved source.
|
|
142
142
|
*/
|
|
143
143
|
const asWritten = /* @__PURE__ */ new Map();
|
|
144
144
|
/** The version `edits` apply to: the latest, or the one the reply wrote. */
|
|
145
|
-
const baseFor = (
|
|
146
|
-
const base = latest.get(
|
|
147
|
-
const written = asWritten.get(
|
|
145
|
+
const baseFor = (name, edits) => {
|
|
146
|
+
const base = latest.get(name);
|
|
147
|
+
const written = asWritten.get(name);
|
|
148
148
|
return base !== void 0 && written !== void 0 && !applyEdits(base, edits).ok && applyEdits(written, edits).ok ? written : base;
|
|
149
149
|
};
|
|
150
150
|
const versions = /* @__PURE__ */ new Map();
|
|
@@ -156,10 +156,10 @@ function createReplyParser(deps, history) {
|
|
|
156
156
|
let opened = 0;
|
|
157
157
|
let repairsLeft = MAX_BLOCK_REPAIRS;
|
|
158
158
|
for (const entry of history) {
|
|
159
|
-
if (!VALID_ID.test(entry.
|
|
160
|
-
latest.set(entry.
|
|
161
|
-
versions.set(entry.
|
|
162
|
-
const auto = AUTO_ID.exec(entry.
|
|
159
|
+
if (!VALID_ID.test(entry.name)) continue;
|
|
160
|
+
latest.set(entry.name, splitPragma(entry.source).body);
|
|
161
|
+
versions.set(entry.name, (versions.get(entry.name) ?? 0) + 1);
|
|
162
|
+
const auto = AUTO_ID.exec(entry.name);
|
|
163
163
|
if (auto !== null) autoId = Math.max(autoId, Number(auto[1]));
|
|
164
164
|
}
|
|
165
165
|
const empty = deps.compile("");
|
|
@@ -209,14 +209,15 @@ function createReplyParser(deps, history) {
|
|
|
209
209
|
const blockOf = (open, body, compiled, status, error) => {
|
|
210
210
|
const block = {
|
|
211
211
|
diagnostics: belowPrefix(compiled.diagnostics, pragmaLine(deps.catalogVersion)),
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
key: `${open.name}@${open.version}`,
|
|
213
|
+
name: open.name,
|
|
214
214
|
program: compiled.program,
|
|
215
215
|
source: withPragma(body, deps.catalogVersion),
|
|
216
216
|
status,
|
|
217
217
|
version: open.version
|
|
218
218
|
};
|
|
219
219
|
if (error !== void 0) block.error = error;
|
|
220
|
+
if (compiled.ref !== null) block.ref = compiled.ref;
|
|
220
221
|
return block;
|
|
221
222
|
};
|
|
222
223
|
const emit = (block) => {
|
|
@@ -236,34 +237,35 @@ function createReplyParser(deps, history) {
|
|
|
236
237
|
advice: belowPrefix(advice, pragmaLine(deps.catalogVersion)),
|
|
237
238
|
diagnostics: edits?.applied === false ? [] : block.diagnostics,
|
|
238
239
|
fenced: !open.implicit,
|
|
239
|
-
id: block.id,
|
|
240
240
|
key: block.key,
|
|
241
|
+
name: block.name,
|
|
241
242
|
repairs: [],
|
|
242
243
|
status: block.status === "repairing" ? "repairing" : "done"
|
|
243
244
|
};
|
|
244
245
|
if (edits !== void 0) result.edits = edits;
|
|
245
246
|
if (block.error !== void 0) result.error = block.error;
|
|
246
247
|
if (rewrite !== void 0) result.rewrites = [rewrite];
|
|
248
|
+
if (block.ref !== void 0) result.ref = block.ref;
|
|
247
249
|
results.set(block.key, result);
|
|
248
250
|
emit(block);
|
|
249
251
|
};
|
|
250
252
|
const open = (info, indent, implicit = false) => {
|
|
251
253
|
const named = slug(info ?? "");
|
|
252
|
-
const
|
|
253
|
-
const version = (versions.get(
|
|
254
|
-
versions.set(
|
|
254
|
+
const name = named === "" ? `jigs-${++autoId}` : named;
|
|
255
|
+
const version = (versions.get(name) ?? 0) + 1;
|
|
256
|
+
versions.set(name, version);
|
|
255
257
|
opened++;
|
|
256
258
|
const block = {
|
|
257
259
|
baseShown: false,
|
|
258
260
|
depth: 0,
|
|
259
261
|
dirty: false,
|
|
260
262
|
editsShown: 0,
|
|
261
|
-
id,
|
|
262
263
|
implicit,
|
|
263
264
|
indent,
|
|
264
265
|
lastKey: JSON.stringify(empty.program),
|
|
265
266
|
lastSentAt: Number.NEGATIVE_INFINITY,
|
|
266
267
|
lines: [],
|
|
268
|
+
name,
|
|
267
269
|
over: opened > MAX_BLOCKS,
|
|
268
270
|
partial: "",
|
|
269
271
|
version,
|
|
@@ -286,7 +288,7 @@ function createReplyParser(deps, history) {
|
|
|
286
288
|
const editing = isEditReply(body);
|
|
287
289
|
if (editing === true) {
|
|
288
290
|
const edits = parseEdits(body);
|
|
289
|
-
const base = baseFor(block.
|
|
291
|
+
const base = baseFor(block.name, edits);
|
|
290
292
|
if (base === void 0) return;
|
|
291
293
|
if (!block.baseShown) {
|
|
292
294
|
block.baseShown = true;
|
|
@@ -328,11 +330,11 @@ function createReplyParser(deps, history) {
|
|
|
328
330
|
let edits;
|
|
329
331
|
if (isEditReply(written) === true) {
|
|
330
332
|
const parsed = parseEdits(written);
|
|
331
|
-
const base = baseFor(block.
|
|
333
|
+
const base = baseFor(block.name, parsed);
|
|
332
334
|
if (base === void 0) {
|
|
333
335
|
settle(block, blockOf(block, "", empty, "done", {
|
|
334
336
|
code: "NOT_FOUND",
|
|
335
|
-
message: `There is no earlier jig "${block.
|
|
337
|
+
message: `There is no earlier jig "${block.name}" to change.`
|
|
336
338
|
}), {
|
|
337
339
|
applied: false,
|
|
338
340
|
blocks: parsed.length
|
|
@@ -364,12 +366,12 @@ function createReplyParser(deps, history) {
|
|
|
364
366
|
}
|
|
365
367
|
let compiled = deps.compile(body);
|
|
366
368
|
const reordered = reorderDeclarations(body, compiled, deps.compile);
|
|
367
|
-
if (reordered === null) asWritten.delete(block.
|
|
369
|
+
if (reordered === null) asWritten.delete(block.name);
|
|
368
370
|
else {
|
|
369
|
-
asWritten.set(block.
|
|
371
|
+
asWritten.set(block.name, body);
|
|
370
372
|
({body, compiled} = reordered);
|
|
371
373
|
}
|
|
372
|
-
latest.set(block.
|
|
374
|
+
latest.set(block.name, body);
|
|
373
375
|
const advice = compiled.advise("chat");
|
|
374
376
|
const chosen = advice.filter((entry) => deps.designRepair.has(entry.code));
|
|
375
377
|
if ((needsRepair(compiled.diagnostics) || chosen.length > 0) && repairable()) {
|
|
@@ -544,9 +546,9 @@ function createReplyParser(deps, history) {
|
|
|
544
546
|
end,
|
|
545
547
|
plan: (block) => plans.get(block.key),
|
|
546
548
|
push,
|
|
547
|
-
remember: (
|
|
548
|
-
asWritten.delete(
|
|
549
|
-
latest.set(
|
|
549
|
+
remember: (name, body) => {
|
|
550
|
+
asWritten.delete(name);
|
|
551
|
+
latest.set(name, body);
|
|
550
552
|
},
|
|
551
553
|
result: (key) => results.get(key),
|
|
552
554
|
results: () => [...results.values()].map((result) => ({ ...result }))
|
|
@@ -576,7 +578,7 @@ async function* repairBlock(deps, block) {
|
|
|
576
578
|
}
|
|
577
579
|
const result = deps.parser.result(block.key);
|
|
578
580
|
const base = plan.kind === "edit" ? plan.base : plan.body;
|
|
579
|
-
const ask = `Write the "${block.
|
|
581
|
+
const ask = `Write the "${block.name}" UI.`;
|
|
580
582
|
const messages = [
|
|
581
583
|
{
|
|
582
584
|
content: deps.prompt === void 0 ? ask : `${deps.prompt}\n\n${ask}`,
|
|
@@ -587,7 +589,7 @@ async function* repairBlock(deps, block) {
|
|
|
587
589
|
role: "assistant"
|
|
588
590
|
},
|
|
589
591
|
{
|
|
590
|
-
content: plan.kind === "edit" ? editRepairText(plan.failure, plan.base) : repairText(plan.diagnostics, plan.advice),
|
|
592
|
+
content: plan.kind === "edit" ? editRepairText(plan.failure, plan.base) : repairText(plan.diagnostics, plan.advice, "answer"),
|
|
591
593
|
role: "user"
|
|
592
594
|
}
|
|
593
595
|
];
|
|
@@ -606,17 +608,27 @@ async function* repairBlock(deps, block) {
|
|
|
606
608
|
await forward?.(turn);
|
|
607
609
|
}
|
|
608
610
|
};
|
|
609
|
-
for await (const event of runSession(session,
|
|
611
|
+
for await (const event of runSession(session, {
|
|
612
|
+
document: base,
|
|
613
|
+
messages
|
|
614
|
+
}, deps.options, deps.call)) if (event.type === "program") program = event.program;
|
|
610
615
|
else if (event.type === "done") {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
616
|
+
const body = splitPragma(event.source).body;
|
|
617
|
+
deps.parser.remember(block.name, body);
|
|
618
|
+
const repaired = {
|
|
614
619
|
...block,
|
|
615
620
|
diagnostics: event.diagnostics,
|
|
616
621
|
program: program ?? block.program,
|
|
617
622
|
source: event.source,
|
|
618
623
|
status: "done"
|
|
619
624
|
};
|
|
625
|
+
const { ref } = deps.session.checker.compile(body);
|
|
626
|
+
if (ref !== null) repaired.ref = ref;
|
|
627
|
+
if (result !== void 0) {
|
|
628
|
+
result.status = "done";
|
|
629
|
+
if (ref !== null) result.ref = ref;
|
|
630
|
+
}
|
|
631
|
+
yield repaired;
|
|
620
632
|
return;
|
|
621
633
|
} else failure = event.error;
|
|
622
634
|
if (result !== void 0) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/compile-cache.ts
|
|
2
|
+
/** The default bound: 32 MB of programs, as JSON. */
|
|
3
|
+
const DEFAULT_MAX_BYTES = 33554432;
|
|
4
|
+
/**
|
|
5
|
+
* A cache bounded by the size of its programs, least recently used out
|
|
6
|
+
* first. A program larger than the whole bound is not kept.
|
|
7
|
+
*/
|
|
8
|
+
function memoryCompileCache(maxBytes = DEFAULT_MAX_BYTES) {
|
|
9
|
+
const entries = /* @__PURE__ */ new Map();
|
|
10
|
+
let bytes = 0;
|
|
11
|
+
const drop = (key) => {
|
|
12
|
+
const entry = entries.get(key);
|
|
13
|
+
if (entry !== void 0) {
|
|
14
|
+
entries.delete(key);
|
|
15
|
+
bytes -= entry.size;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
get bytes() {
|
|
20
|
+
return bytes;
|
|
21
|
+
},
|
|
22
|
+
get: (key) => {
|
|
23
|
+
const entry = entries.get(key);
|
|
24
|
+
if (entry === void 0) return;
|
|
25
|
+
entries.delete(key);
|
|
26
|
+
entries.set(key, entry);
|
|
27
|
+
return entry.value;
|
|
28
|
+
},
|
|
29
|
+
set: (key, value) => {
|
|
30
|
+
drop(key);
|
|
31
|
+
const size = JSON.stringify(value.program).length;
|
|
32
|
+
if (size > maxBytes) return;
|
|
33
|
+
entries.set(key, {
|
|
34
|
+
size,
|
|
35
|
+
value
|
|
36
|
+
});
|
|
37
|
+
bytes += size;
|
|
38
|
+
for (const oldest of entries.keys()) {
|
|
39
|
+
if (bytes <= maxBytes) break;
|
|
40
|
+
drop(oldest);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
get size() {
|
|
44
|
+
return entries.size;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The one cache every server in this process shares. Keys start with a
|
|
50
|
+
* hash of what the server compiles against, so two hosts never read each
|
|
51
|
+
* other's entries.
|
|
52
|
+
*/
|
|
53
|
+
const processCompileCache = memoryCompileCache();
|
|
54
|
+
/** SHA-256 of `text`, as hex: collisions here would serve the wrong page. */
|
|
55
|
+
async function sha256(text) {
|
|
56
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
57
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { memoryCompileCache, processCompileCache, sha256 };
|
package/dist/compile.d.ts
CHANGED
package/dist/compile.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { designChecks } from "./design.js";
|
|
2
|
-
import { JigsCompileError, fatalDiagnostics } from "./errors.js";
|
|
2
|
+
import { JigsCompileError, JigsError, fatalDiagnostics } from "./errors.js";
|
|
3
3
|
import { inputDiagnostics } from "./inputs.js";
|
|
4
4
|
import { outputDiagnostics } from "./outputs.js";
|
|
5
|
+
import { splitPragma, versionDiagnostics } from "./pragma.js";
|
|
5
6
|
import { compileJigs, compileLibrary, spanIn } from "@jigs.design/compiler";
|
|
6
7
|
import { componentSchema } from "@jigs.design/host";
|
|
7
8
|
import { PROGRAM_FORMAT } from "@jigs.design/protocol";
|
|
@@ -110,68 +111,162 @@ function tagsIn(source) {
|
|
|
110
111
|
for (const match of source.matchAll(TAG)) if (match[1] !== void 0) tags.add(match[1]);
|
|
111
112
|
return tags;
|
|
112
113
|
}
|
|
114
|
+
/** The longest canonical props, as JSON, a page accepts. */
|
|
115
|
+
const MAX_PAGE_PROPS = 4096;
|
|
116
|
+
/** A JavaScript identifier, as the compiler requires of a define prop. */
|
|
117
|
+
const BINDING_NAME = /^[A-Za-z_$][\w$]*$/;
|
|
118
|
+
/** What the compiler reads as a number literal; anything else is an expression. */
|
|
119
|
+
const NUMBER_LITERAL = /^-?\d+(?:\.\d+)?$/;
|
|
120
|
+
const SCALAR_KINDS = [
|
|
121
|
+
"boolean",
|
|
122
|
+
"number",
|
|
123
|
+
"string"
|
|
124
|
+
];
|
|
125
|
+
/**
|
|
126
|
+
* Whether `value` is a literal of `kind`. Props reach the server from
|
|
127
|
+
* untyped callers too, so a value may be none of the three.
|
|
128
|
+
*/
|
|
129
|
+
function isOfKind(value, kind) {
|
|
130
|
+
return typeof value === kind;
|
|
131
|
+
}
|
|
132
|
+
/** `props` with keys sorted, so equal props make equal bodies. */
|
|
133
|
+
function sortedProps(props) {
|
|
134
|
+
return Object.fromEntries(Object.entries(props).toSorted(([a], [b]) => a < b ? -1 : 1));
|
|
135
|
+
}
|
|
136
|
+
/** Each prop a literal in braces, keys sorted, so equal props make equal bodies. */
|
|
137
|
+
function oneTag(name, props) {
|
|
138
|
+
const attrs = Object.entries(sortedProps(props)).map(([key, value]) => `${key}={${JSON.stringify(value)}}`);
|
|
139
|
+
return attrs.length === 0 ? `<${name}/>` : `<${name} ${attrs.join(" ")}/>`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Why `props` cannot be a page's props for `define`, or null when they can.
|
|
143
|
+
* Each value must be one the compiler reads as a literal, so the compile
|
|
144
|
+
* that follows types it.
|
|
145
|
+
*/
|
|
146
|
+
function pagePropsProblem(define, props) {
|
|
147
|
+
const declared = new Map(define.props.map((prop) => [prop.name, prop]));
|
|
148
|
+
for (const [key, value] of Object.entries(props)) {
|
|
149
|
+
if (key === "name" || key === "key" || !BINDING_NAME.test(key)) return `"${key}" cannot be a prop name`;
|
|
150
|
+
const prop = declared.get(key);
|
|
151
|
+
if (prop === void 0) {
|
|
152
|
+
const known = [...declared.keys()];
|
|
153
|
+
return `<${define.name}> has no prop "${key}"${known.length === 0 ? "" : `; its props are ${known.join(", ")}`}`;
|
|
154
|
+
}
|
|
155
|
+
const kind = SCALAR_KINDS.find((candidate) => isOfKind(value, candidate));
|
|
156
|
+
if (kind === void 0) return `"${key}" must be a string, a number or a boolean`;
|
|
157
|
+
if (prop.kind !== null && prop.kind !== kind) return `"${key}" must be a ${prop.kind}`;
|
|
158
|
+
if (kind === "number" && !NUMBER_LITERAL.test(JSON.stringify(value))) return `"${key}" must be a finite number written without an exponent`;
|
|
159
|
+
if (kind === "string" && /["\\\r\n\u2028\u2029]/.test(String(value))) return `"${key}" must not contain a quote, a backslash or a line break`;
|
|
160
|
+
}
|
|
161
|
+
const missing = define.props.find((prop) => prop.required && !Object.hasOwn(props, prop.name));
|
|
162
|
+
if (missing !== void 0) return `<${define.name}> needs the prop "${missing.name}"`;
|
|
163
|
+
if (JSON.stringify(sortedProps(props)).length > MAX_PAGE_PROPS) return `The props are longer than ${MAX_PAGE_PROPS} characters`;
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The page a tree names, when it is one call to a library jig that is a
|
|
168
|
+
* page, with no children and literal props only.
|
|
169
|
+
*/
|
|
170
|
+
function refOf(tree, pages) {
|
|
171
|
+
const nodes = tree.filter((node) => node.type !== "text" || node.value.trim() !== "");
|
|
172
|
+
const [node] = nodes;
|
|
173
|
+
if (nodes.length !== 1 || node?.type !== "call" || node.origin !== "library" || node.key !== null || node.children.length > 0 || !pages.has(node.name)) return null;
|
|
174
|
+
const props = {};
|
|
175
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
176
|
+
if (value.kind !== "literal") return null;
|
|
177
|
+
props[key] = value.value;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
name: node.name,
|
|
181
|
+
props: sortedProps(props)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
113
184
|
/**
|
|
114
185
|
* The host, ready to check sources: its catalog in the compiler's form and
|
|
115
|
-
* its
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
186
|
+
* its define jigs compiled into two libraries, one of the tags the model
|
|
187
|
+
* may write and one of every define jig, for pages. Throws when a jig does
|
|
188
|
+
* not compile, carries a pragma for a newer language or catalog, calls a
|
|
189
|
+
* procedure the host does not have, or is a tag that uses a page-only jig,
|
|
190
|
+
* so a bad jig stops the server at startup instead of reaching the model.
|
|
191
|
+
* Throws too when a situation's example has a diagnostic worse than info,
|
|
192
|
+
* so the prompt never shows the model Jigs the checker refuses.
|
|
120
193
|
*/
|
|
121
194
|
function createChecker(host) {
|
|
122
195
|
const catalog = compileCatalog(host.components);
|
|
123
|
-
const
|
|
196
|
+
const defines = [];
|
|
197
|
+
for (const entry of host.jigs) {
|
|
198
|
+
const split = splitPragma(entry.source);
|
|
199
|
+
const fatal = fatalDiagnostics(versionDiagnostics(split.versions, host.components.version));
|
|
200
|
+
if (fatal.length > 0) throw new JigsCompileError(`Jig "${entry.name}"`, fatal);
|
|
201
|
+
if (entry.kind === "define") defines.push({
|
|
202
|
+
body: split.body,
|
|
203
|
+
name: entry.name,
|
|
204
|
+
tag: entry.tag
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const tagLibrary = compileLibrary(defines.filter((entry) => entry.tag).map((entry) => ({
|
|
124
208
|
name: entry.name,
|
|
125
|
-
source: entry.
|
|
209
|
+
source: entry.body
|
|
126
210
|
})), catalog);
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
211
|
+
const fullLibrary = compileLibrary(defines.map((entry) => ({
|
|
212
|
+
name: entry.name,
|
|
213
|
+
source: entry.body
|
|
214
|
+
})), catalog);
|
|
215
|
+
const allFacts = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const entry of defines) {
|
|
217
|
+
const uses = [...fullLibrary.get(entry.name)?.procedures ?? []];
|
|
218
|
+
const problem = [...procedureDiagnostics(host, entry.body, uses), ...inputDiagnostics(host, entry.body)][0];
|
|
219
|
+
if (problem !== void 0) throw new Error(`Jig "${entry.name}" failed to compile: line ${problem.span.line}: ${problem.message}`);
|
|
220
|
+
allFacts.set(entry.name, {
|
|
221
|
+
tags: tagsIn(entry.body),
|
|
134
222
|
uses
|
|
135
223
|
});
|
|
136
224
|
}
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
225
|
+
const tagFacts = new Map([...allFacts].filter(([name]) => tagLibrary.has(name)));
|
|
226
|
+
const pages = new Set(host.jigs.filter((entry) => entry.page).map((entry) => entry.name));
|
|
227
|
+
const tags = /* @__PURE__ */ new Set([...host.components.components.map((component) => component.name), ...tagLibrary.keys()]);
|
|
228
|
+
/** The tags a source draws, through every jig it uses, and the procedures those jigs call. */
|
|
229
|
+
const reach = (body, facts) => {
|
|
230
|
+
const drawn = new Set(MARKDOWN_ELEMENTS);
|
|
140
231
|
const uses = [];
|
|
141
232
|
const pending = [...tagsIn(body)];
|
|
142
233
|
for (let tag = pending.pop(); tag !== void 0; tag = pending.pop()) {
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
147
|
-
uses.push(...
|
|
148
|
-
pending.push(...
|
|
234
|
+
if (drawn.has(tag)) continue;
|
|
235
|
+
drawn.add(tag);
|
|
236
|
+
const found = facts.get(tag);
|
|
237
|
+
if (found !== void 0) {
|
|
238
|
+
uses.push(...found.uses);
|
|
239
|
+
pending.push(...found.tags);
|
|
149
240
|
}
|
|
150
241
|
}
|
|
151
242
|
return {
|
|
152
|
-
tags,
|
|
243
|
+
tags: drawn,
|
|
153
244
|
uses
|
|
154
245
|
};
|
|
155
246
|
};
|
|
156
|
-
const
|
|
247
|
+
const compileWith = (body, library, facts) => {
|
|
157
248
|
const result = compileJigs(body, {
|
|
158
249
|
catalog,
|
|
159
250
|
library
|
|
160
251
|
});
|
|
161
252
|
const own = result.procedures;
|
|
162
|
-
const reached = reach(result.source);
|
|
253
|
+
const reached = reach(result.source, facts);
|
|
254
|
+
const diagnostics = [
|
|
255
|
+
...result.diagnostics,
|
|
256
|
+
...procedureDiagnostics(host, body, own),
|
|
257
|
+
...outputDiagnostics(host, body, result.reads),
|
|
258
|
+
...inputDiagnostics(host, body)
|
|
259
|
+
].toSorted((a, b) => a.span.line - b.span.line);
|
|
260
|
+
let ref = diagnostics.some((diagnostic) => diagnostic.severity === "error") ? null : refOf(result.program.tree, pages);
|
|
261
|
+
const define = ref === null ? void 0 : fullLibrary.get(ref.name);
|
|
262
|
+
if (ref !== null && (define === void 0 || pagePropsProblem(define, ref.props) !== null)) ref = null;
|
|
163
263
|
return {
|
|
164
264
|
advise: (placement) => designChecks({
|
|
165
265
|
catalog: host.components,
|
|
166
266
|
compiled: result,
|
|
167
267
|
placement
|
|
168
268
|
}),
|
|
169
|
-
diagnostics
|
|
170
|
-
...result.diagnostics,
|
|
171
|
-
...procedureDiagnostics(host, body, own),
|
|
172
|
-
...outputDiagnostics(host, body, result.reads),
|
|
173
|
-
...inputDiagnostics(host, body)
|
|
174
|
-
].toSorted((a, b) => a.span.line - b.span.line),
|
|
269
|
+
diagnostics,
|
|
175
270
|
program: {
|
|
176
271
|
code: result.output.code,
|
|
177
272
|
components: Object.fromEntries(host.components.components.filter((component) => reached.tags.has(component.name)).map((component) => [component.name, componentSchema(component)])),
|
|
@@ -179,15 +274,52 @@ function createChecker(host) {
|
|
|
179
274
|
imageHosts: host.imageHosts,
|
|
180
275
|
procedures: procedureTable(host, [...own, ...reached.uses]),
|
|
181
276
|
prose: result.output.constants
|
|
182
|
-
}
|
|
277
|
+
},
|
|
278
|
+
ref
|
|
183
279
|
};
|
|
184
280
|
};
|
|
281
|
+
const compile = (body) => compileWith(body, tagLibrary, tagFacts);
|
|
282
|
+
const definePage = (name) => {
|
|
283
|
+
const define = fullLibrary.get(name);
|
|
284
|
+
if (define === void 0 || !pages.has(name)) throw new Error(`"${name}" is not a define page on this host`);
|
|
285
|
+
return define;
|
|
286
|
+
};
|
|
287
|
+
const pageBody = (name, props) => {
|
|
288
|
+
const problem = pagePropsProblem(definePage(name), props);
|
|
289
|
+
if (problem !== null) throw new JigsError("BAD_INPUT", problem);
|
|
290
|
+
return oneTag(name, props);
|
|
291
|
+
};
|
|
292
|
+
const pageDocument = (name, props) => {
|
|
293
|
+
const use = pageBody(name, props);
|
|
294
|
+
const reached = /* @__PURE__ */ new Set();
|
|
295
|
+
const pending = [...allFacts.get(name)?.tags ?? []];
|
|
296
|
+
for (let tag = pending.pop(); tag !== void 0; tag = pending.pop()) {
|
|
297
|
+
const facts = allFacts.get(tag);
|
|
298
|
+
if (reached.has(tag) || facts === void 0 || tagLibrary.has(tag)) continue;
|
|
299
|
+
reached.add(tag);
|
|
300
|
+
pending.push(...facts.tags);
|
|
301
|
+
}
|
|
302
|
+
const bodies = defines.filter((entry) => entry.name !== name && reached.has(entry.name)).map((entry) => entry.body.trim());
|
|
303
|
+
const own = defines.find((entry) => entry.name === name)?.body ?? "";
|
|
304
|
+
return [
|
|
305
|
+
...bodies,
|
|
306
|
+
own.trim(),
|
|
307
|
+
use
|
|
308
|
+
].join("\n\n");
|
|
309
|
+
};
|
|
185
310
|
for (const entry of host.situations) {
|
|
186
311
|
if (entry.example === void 0) continue;
|
|
187
312
|
const fatal = fatalDiagnostics(compile(entry.example).diagnostics);
|
|
188
313
|
if (fatal.length > 0) throw new JigsCompileError(`Situation "${entry.name}" example`, fatal);
|
|
189
314
|
}
|
|
190
|
-
return {
|
|
315
|
+
return {
|
|
316
|
+
compile,
|
|
317
|
+
needsProps: (name) => definePage(name).props.some((prop) => prop.required),
|
|
318
|
+
page: (name, props) => compileWith(pageBody(name, props), fullLibrary, allFacts),
|
|
319
|
+
pageBody,
|
|
320
|
+
pageDocument,
|
|
321
|
+
tags
|
|
322
|
+
};
|
|
191
323
|
}
|
|
192
324
|
/** Props the parser sets on Markdown elements, with the values it uses. */
|
|
193
325
|
const MARKDOWN_PROPS = [{
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import "./compile.js";
|
|
1
2
|
import { ContextFunction, JigsServer, JigsServerOptions, RunFunctions } from "./types.js";
|
|
2
|
-
import
|
|
3
|
-
|
|
3
|
+
import "./session.js";
|
|
4
|
+
import "./handler.js";
|
|
5
|
+
import { Catalog, Host, JigDefinition, PageName, Procedures } from "@jigs.design/host";
|
|
6
|
+
import { JigsErrorData } from "@jigs.design/protocol";
|
|
7
|
+
//#region src/core.d.ts
|
|
4
8
|
/**
|
|
5
9
|
* Types a `context` and `run` pair outside `createJigsServer`, so two
|
|
6
10
|
* servers (one per platform) can share them. `TContext` comes from `context`
|
|
@@ -13,5 +17,5 @@ export declare function defineRun<const TProcedures extends Procedures, TContext
|
|
|
13
17
|
context: ContextFunction<TContext>;
|
|
14
18
|
run: RunFunctions<TProcedures, TContext>;
|
|
15
19
|
};
|
|
16
|
-
export declare function createJigsServer<const TProcedures extends Procedures, const TCatalog extends Catalog, TContext = any,
|
|
20
|
+
export declare function createJigsServer<const TProcedures extends Procedures, const TCatalog extends Catalog, TContext = any, const TJigs extends ReadonlyArray<JigDefinition> = readonly []>(options: JigsServerOptions<TProcedures, TCatalog, TContext, TJigs>): JigsServer<TContext, PageName<TJigs>>;
|
|
17
21
|
//#endregion
|