@jigs.design/server 0.0.0-stage → 0.1.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.
@@ -0,0 +1,120 @@
1
+ import { branches, objectFields } from "./inputs.js";
2
+ import { ELEMENT, columnAt, lineAt, nearest } from "@jigs.design/compiler";
3
+ import { dataSchema } from "@jigs.design/host";
4
+ import { z } from "zod";
5
+ //#region src/outputs.ts
6
+ const jsonSchemas = /* @__PURE__ */ new WeakMap();
7
+ /** The JSON Schema of an output schema, as output; null when it has none. */
8
+ function jsonSchemaOf(schema) {
9
+ const cached = jsonSchemas.get(schema);
10
+ if (cached !== void 0) return cached;
11
+ let converted;
12
+ try {
13
+ converted = z.toJSONSchema(schema, { io: "output" });
14
+ } catch {
15
+ converted = null;
16
+ }
17
+ jsonSchemas.set(schema, converted);
18
+ return converted;
19
+ }
20
+ /** `node`, or what its `$ref` names in `root`'s definitions. */
21
+ function resolve(node, root) {
22
+ if (node.$ref === void 0) return node;
23
+ if (node.$ref === "#") return root;
24
+ const name = /^#\/\$defs\/(.+)$/.exec(node.$ref)?.[1];
25
+ return name === void 0 ? null : root.$defs?.[name] ?? null;
26
+ }
27
+ /** What each element of the arrays `nodes` describe is; null when one of them is not such an array. */
28
+ function elements(nodes, root) {
29
+ const next = [];
30
+ for (const node of nodes) for (const raw of branches(node)) {
31
+ const branch = resolve(raw, root);
32
+ if (branch === null || branch.oneOf !== void 0 || branch.allOf !== void 0) return null;
33
+ if (branch.anyOf !== void 0 || branch.type === "null") continue;
34
+ const { items } = branch;
35
+ if (items === void 0 || items === true || items === false || Array.isArray(items)) return null;
36
+ next.push(items);
37
+ }
38
+ return next.length === 0 ? null : next;
39
+ }
40
+ /**
41
+ * The first field of `path` that the data `root` describes cannot have.
42
+ * An `ELEMENT` step goes into an array's items. Null when every field is
43
+ * there, or when the walk reaches something it cannot check: a value with
44
+ * no schema, an open object, an array or a string (whose fields are
45
+ * methods), or a combinator other than `anyOf`.
46
+ */
47
+ function missingField(root, path) {
48
+ let nodes = [root];
49
+ for (const [index, name] of path.entries()) {
50
+ if (name === ELEMENT) {
51
+ const items = elements(nodes, root);
52
+ if (items === null) return null;
53
+ nodes = items;
54
+ continue;
55
+ }
56
+ const next = [];
57
+ const fields = /* @__PURE__ */ new Set();
58
+ for (const node of nodes) for (const raw of branches(node)) {
59
+ const branch = resolve(raw, root);
60
+ if (branch === null || branch.oneOf !== void 0 || branch.allOf !== void 0) return null;
61
+ if (branch.anyOf !== void 0 || branch.type === "null") continue;
62
+ const object = objectFields(branch);
63
+ if (object === null) return null;
64
+ const field = object.fields.get(name);
65
+ if (field !== void 0) next.push(field);
66
+ else if (object.open) return null;
67
+ else for (const key of object.fields.keys()) fields.add(key);
68
+ }
69
+ if (next.length === 0) return fields.size === 0 ? null : {
70
+ fields: [...fields],
71
+ index
72
+ };
73
+ nodes = next;
74
+ }
75
+ return null;
76
+ }
77
+ /** How the read up to `path[index]` is written, for a message. */
78
+ function writtenRead(read, index) {
79
+ if (read.item === void 0) return `\`${[read.data, ...read.path.slice(0, index)].join(".")}\``;
80
+ const { name } = read.item;
81
+ const fields = read.path.slice(read.item.steps, index);
82
+ const list = `\`${read.item.list}\``;
83
+ return fields.length === 0 ? `\`${name}\` (an item of ${list})` : `\`${[name, ...fields].join(".")}\` (\`${name}\` is an item of ${list})`;
84
+ }
85
+ /** The longest field list a message spells out. */
86
+ const MAX_LISTED_FIELDS = 12;
87
+ /**
88
+ * Reads of a procedure's data, such as `orders.data.totl`, through a field
89
+ * its output schema does not have. Each is an error: the read is undefined,
90
+ * and a read through it throws.
91
+ */
92
+ function outputDiagnostics(host, source, reads) {
93
+ const diagnostics = [];
94
+ for (const read of reads) {
95
+ const procedure = Object.hasOwn(host.procedures, read.procedure) ? host.procedures[read.procedure] : void 0;
96
+ const data = procedure === void 0 ? void 0 : dataSchema(procedure);
97
+ const output = data === void 0 ? null : jsonSchemaOf(data);
98
+ const missing = output === null ? null : missingField(output, read.path);
99
+ const place = missing === null ? void 0 : read.places[missing.index];
100
+ if (missing === null || place === void 0 || place === null) continue;
101
+ const name = read.path[missing.index] ?? "";
102
+ const written = writtenRead(read, missing.index);
103
+ const near = nearest(name, missing.fields, 1)[0];
104
+ const hint = near !== void 0 ? `; did you mean ${near}?` : missing.fields.length <= MAX_LISTED_FIELDS ? `; it has ${missing.fields.join(", ")}` : "";
105
+ diagnostics.push({
106
+ code: "procedure_output",
107
+ message: `${written} has no field "${name}"${hint}`,
108
+ severity: "error",
109
+ span: place.start === void 0 || place.end === void 0 ? { line: place.line } : {
110
+ column: columnAt(source, place.start),
111
+ end: place.end,
112
+ line: lineAt(source, place.start),
113
+ start: place.start
114
+ }
115
+ });
116
+ }
117
+ return diagnostics;
118
+ }
119
+ //#endregion
120
+ export { outputDiagnostics };
package/dist/pragma.js ADDED
@@ -0,0 +1,80 @@
1
+ const PRAGMA = /^\{@pickle\s+(\d+)(?:\s+catalog=(\d+))?\}[ \t]*(?:\r?\n|$)/;
2
+ /** Splits a leading `{@pickle 0 catalog=1}` line from the body. */
3
+ function splitPragma(source) {
4
+ const match = PRAGMA.exec(source);
5
+ if (match === null) return {
6
+ body: source,
7
+ versions: null
8
+ };
9
+ const catalog = match[2] === void 0 ? null : Number(match[2]);
10
+ return {
11
+ body: source.slice(match[0].length),
12
+ versions: {
13
+ catalog,
14
+ language: Number(match[1])
15
+ }
16
+ };
17
+ }
18
+ /**
19
+ * What a stored spec's pragma says about where it came from. Nothing is
20
+ * migrated yet: the catalog has no deprecation entries until the v2
21
+ * compiler, so a mismatch is only reported.
22
+ */
23
+ function versionDiagnostics(versions, catalogVersion) {
24
+ if (versions === null) return [];
25
+ const diagnostics = [];
26
+ if (versions.language > 0) diagnostics.push({
27
+ code: "language_version",
28
+ message: `This spec was written for language version ${versions.language}; this server checks version 0`,
29
+ severity: "warning",
30
+ span: { line: 1 }
31
+ });
32
+ if (versions.catalog !== null && versions.catalog !== catalogVersion) diagnostics.push({
33
+ code: "catalog_version",
34
+ message: versions.catalog > catalogVersion ? `This spec was written for catalog version ${versions.catalog}, newer than this server's ${catalogVersion}; components or props added since are dropped` : `This spec was written for catalog version ${versions.catalog} and was checked against version ${catalogVersion}`,
35
+ severity: versions.catalog > catalogVersion ? "warning" : "info",
36
+ span: { line: 1 }
37
+ });
38
+ return diagnostics;
39
+ }
40
+ /** The pragma line `withPragma` puts above a body, with its line break. */
41
+ function pragmaLine(catalogVersion) {
42
+ return `{@pickle 0 catalog=${catalogVersion}}\n`;
43
+ }
44
+ /** `body` with a pragma naming the versions it was checked against. */
45
+ function withPragma(body, catalogVersion) {
46
+ return `${pragmaLine(catalogVersion)}${body}`;
47
+ }
48
+ /**
49
+ * `items` with their spans moved past `prefix`, the text a source has above
50
+ * the body they were reported on: lines by its line breaks, offsets by its
51
+ * length. `prefix` ends in a line break, so columns stay.
52
+ */
53
+ function belowPrefix(items, prefix) {
54
+ const lines = prefix.split("\n").length - 1;
55
+ const shift = (span) => span.start === void 0 || span.end === void 0 ? {
56
+ ...span,
57
+ line: span.line + lines
58
+ } : {
59
+ ...span,
60
+ end: span.end + prefix.length,
61
+ line: span.line + lines,
62
+ start: span.start + prefix.length
63
+ };
64
+ return items.map((item) => {
65
+ const moved = {
66
+ ...item,
67
+ span: shift(item.span)
68
+ };
69
+ if (item.fix !== void 0) moved.fix = {
70
+ edits: item.fix.edits.map((edit) => ({
71
+ span: shift(edit.span),
72
+ text: edit.text
73
+ })),
74
+ title: item.fix.title
75
+ };
76
+ return moved;
77
+ });
78
+ }
79
+ //#endregion
80
+ export { belowPrefix, pragmaLine, splitPragma, versionDiagnostics, withPragma };
@@ -0,0 +1,30 @@
1
+ import { hoistDeclarations } from "@jigs.design/compiler";
2
+ import { needsRepair } from "@jigs.design/prompt";
3
+ //#region src/rewrite.ts
4
+ /**
5
+ * When the only errors in `body` are markup that reads names `{@body}` lines
6
+ * below it declare, `body` with its top-level `{@body}` lines moved to the
7
+ * top, compiled, instead of a repair round. A finished document already runs
8
+ * every declaration before it renders, so the program is the same; the
9
+ * diagnostic is about the page while it streams in, which the stored source
10
+ * fixes for the next turn. Null when anything else is wrong, or the moved
11
+ * source still needs a repair.
12
+ */
13
+ function reorderDeclarations(body, compiled, compile) {
14
+ const errors = compiled.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
15
+ if (errors.length === 0 || errors.some((diagnostic) => diagnostic.code !== "use_before_declare")) return null;
16
+ const hoisted = hoistDeclarations(body);
17
+ if (hoisted === null) return null;
18
+ const recompiled = compile(hoisted.text);
19
+ if (needsRepair(recompiled.diagnostics)) return null;
20
+ return {
21
+ body: hoisted.text,
22
+ compiled: recompiled,
23
+ rewrite: {
24
+ code: "use_before_declare",
25
+ moved: hoisted.moved
26
+ }
27
+ };
28
+ }
29
+ //#endregion
30
+ export { reorderDeclarations };
@@ -0,0 +1,17 @@
1
+ import { ContextFunction, PickleServer, PickleServerOptions, RunFunctions } from "./types.js";
2
+ import { Catalog, Procedures } from "@jigs.design/host";
3
+ //#region src/server.d.ts
4
+ /**
5
+ * Types a `context` and `run` pair outside `createPickleServer`, so two
6
+ * servers (one per platform) can share them. `TContext` comes from `context`
7
+ * alone, as in `createPickleServer`.
8
+ */
9
+ export declare function defineRun<const TProcedures extends Procedures, TContext = any>(procedures: TProcedures, handlers: {
10
+ context: ContextFunction<TContext>;
11
+ run: RunFunctions<NoInfer<TProcedures>, NoInfer<TContext>>;
12
+ }): {
13
+ context: ContextFunction<TContext>;
14
+ run: RunFunctions<TProcedures, TContext>;
15
+ };
16
+ export declare function createPickleServer<const TProcedures extends Procedures, const TCatalog extends Catalog, TContext = any, TOptions = undefined, const TSpecName extends string = never>(options: PickleServerOptions<TProcedures, TCatalog, TContext, TOptions, TSpecName>): PickleServer<TContext, TSpecName>;
17
+ //#endregion