@uicast/core 0.0.1-beta.1 → 0.0.1
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/CHANGELOG.md +3 -6
- package/README.md +120 -1
- package/dist/build-elements-by-key.d.ts +13 -0
- package/dist/build-elements-by-key.js +39 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +4 -0
- package/dist/create-component-definition.d.ts +26 -0
- package/dist/create-component-definition.js +74 -0
- package/dist/entry-error.d.ts +57 -0
- package/dist/entry-error.js +85 -0
- package/dist/entry-shape.d.ts +7 -0
- package/dist/entry-shape.js +88 -0
- package/dist/expr/evaluate.d.ts +5 -0
- package/dist/expr/evaluate.js +54 -0
- package/dist/expr/extract-deps.d.ts +4 -0
- package/dist/expr/extract-deps.js +30 -0
- package/dist/expr/plan-step-waves.d.ts +8 -0
- package/dist/expr/plan-step-waves.js +45 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/internal.d.ts +12 -0
- package/dist/internal.js +13 -0
- package/dist/json-schema.d.ts +58 -0
- package/dist/json-schema.js +73 -0
- package/dist/prompt/format.d.ts +5 -0
- package/dist/prompt/format.js +22 -0
- package/dist/prompt/get-common-instructions-partial-prompt.d.ts +23 -0
- package/dist/prompt/get-common-instructions-partial-prompt.js +16 -0
- package/dist/prompt/get-components-partial-prompt.d.ts +27 -0
- package/dist/prompt/get-components-partial-prompt.js +133 -0
- package/dist/prompt/get-edit-request-prompt.d.ts +23 -0
- package/dist/prompt/get-edit-request-prompt.js +15 -0
- package/dist/prompt/get-error-recovery-prompt.d.ts +35 -0
- package/dist/prompt/get-error-recovery-prompt.js +26 -0
- package/dist/prompt/get-expressions-partial-prompt.d.ts +23 -0
- package/dist/prompt/get-expressions-partial-prompt.js +17 -0
- package/dist/prompt/get-functions-partial-prompt.d.ts +22 -0
- package/dist/prompt/get-functions-partial-prompt.js +58 -0
- package/dist/prompt/get-scope-partial-prompt.d.ts +24 -0
- package/dist/prompt/get-scope-partial-prompt.js +30 -0
- package/dist/prompt/index.d.ts +7 -0
- package/dist/prompt/index.js +7 -0
- package/dist/prompt/json-schema-to-ts.d.ts +6 -0
- package/dist/prompt/json-schema-to-ts.js +146 -0
- package/dist/prompt/md/EXPRESSIONS.json +1 -0
- package/dist/prompt/md/INSTRUCTIONS.json +1 -0
- package/dist/prompt/shared-types.d.ts +5 -0
- package/dist/prompt/shared-types.js +82 -0
- package/dist/scope/create-proxy-scope.d.ts +55 -0
- package/dist/scope/create-proxy-scope.js +295 -0
- package/dist/scope/parse-scope.d.ts +2 -0
- package/dist/scope/parse-scope.js +11 -0
- package/dist/scope/parse-set-address.d.ts +16 -0
- package/dist/scope/parse-set-address.js +52 -0
- package/dist/stream-json-lines.d.ts +14 -0
- package/dist/stream-json-lines.js +59 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.js +19 -0
- package/dist/url-policy.d.ts +45 -0
- package/dist/url-policy.js +252 -0
- package/package.json +27 -18
- package/src/def/create-component-definition.ts +0 -21
- package/src/entry-error.ts +0 -104
- package/src/expr/analyze.ts +0 -190
- package/src/expr/ast-utils.ts +0 -28
- package/src/expr/evaluate.ts +0 -74
- package/src/expr/extract-deps.ts +0 -32
- package/src/expr/globals.ts +0 -118
- package/src/expr/safer-eval.ts +0 -233
- package/src/expr/validate.ts +0 -149
- package/src/index.ts +0 -28
- package/src/prompt/get-common-instructions-partial-prompt.ts +0 -20
- package/src/prompt/get-components-partial-prompt.ts +0 -153
- package/src/prompt/get-edit-request-prompt.ts +0 -37
- package/src/prompt/get-error-recovery-prompt.ts +0 -37
- package/src/prompt/get-expressions-partial-prompt.ts +0 -33
- package/src/prompt/get-functions-partial-prompt.ts +0 -62
- package/src/prompt/get-scope-partial-prompt.ts +0 -57
- package/src/prompt/index.ts +0 -32
- package/src/prompt/md/EXPRESSIONS.json +0 -1
- package/src/prompt/md/EXPRESSIONS.md +0 -29
- package/src/prompt/md/INSTRUCTIONS.json +0 -1
- package/src/prompt/md/INSTRUCTIONS.md +0 -172
- package/src/prompt-utils/json-schema-to-ts.ts +0 -225
- package/src/scope/create-proxy-scope.ts +0 -131
- package/src/scope/parse-scope.ts +0 -36
- package/src/stream/stream-json-lines.ts +0 -56
- package/src/types.ts +0 -124
- package/src/utils/utils.ts +0 -64
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { EntryError } from "../entry-error.js";
|
|
2
|
+
import { PROTOTYPE_KEYS, ROW_FIELDS } from "./parse-set-address.js";
|
|
3
|
+
function createEmitter() {
|
|
4
|
+
const events = new Map();
|
|
5
|
+
let emits = 0;
|
|
6
|
+
return {
|
|
7
|
+
get emits() {
|
|
8
|
+
return emits;
|
|
9
|
+
},
|
|
10
|
+
on(field, handler) {
|
|
11
|
+
const handlers = events.get(field);
|
|
12
|
+
if (handlers)
|
|
13
|
+
handlers.add(handler);
|
|
14
|
+
else
|
|
15
|
+
events.set(field, new Set([handler]));
|
|
16
|
+
return () => events.get(field)?.delete(handler);
|
|
17
|
+
},
|
|
18
|
+
emit(field) {
|
|
19
|
+
emits++;
|
|
20
|
+
for (const fn of events.get(field) ?? [])
|
|
21
|
+
fn();
|
|
22
|
+
for (const fn of events.get("*") ?? [])
|
|
23
|
+
fn();
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const isObject = (value) => value !== null && typeof value === "object";
|
|
28
|
+
// Every scope's emitter is `createEmitter`'s, so the count is there.
|
|
29
|
+
const countEmits = (scope) => scope.$$emitter.emits;
|
|
30
|
+
function assertField(field) {
|
|
31
|
+
if (PROTOTYPE_KEYS.has(field)) {
|
|
32
|
+
throw new EntryError(`Cannot set "${field}": it reaches the prototype chain.`, {
|
|
33
|
+
reason: "guardrail-violation",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (field.includes(".")) {
|
|
37
|
+
throw new EntryError(`Cannot set "${field}": a scope field has no dots. Write the whole field.`, {
|
|
38
|
+
reason: "guardrail-violation",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates a reactive scope: a shallow proxy over `bag`. A write to a field emits it, so every element reading that
|
|
44
|
+
* field re-renders.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const userCtx = createProxyScope({ name: "Hopper", plan: "pro" });
|
|
48
|
+
* const init: InitFn = ({ scopes }) => { scopes.userCtx = userCtx; }; // read as scopes.userCtx.name
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* userCtx.plan = "free"; // emits "plan"
|
|
52
|
+
* userCtx.$$set("plan", "pro", { default: true }); // no-op: "plan" is set
|
|
53
|
+
*/
|
|
54
|
+
function createProxyScope(bag = {}) {
|
|
55
|
+
const emitter = createEmitter();
|
|
56
|
+
const fields = bag;
|
|
57
|
+
const write = (field, value, options) => {
|
|
58
|
+
assertField(field);
|
|
59
|
+
const oldValue = fields[field];
|
|
60
|
+
if (options?.default && oldValue !== undefined)
|
|
61
|
+
return;
|
|
62
|
+
if (Object.is(oldValue, value))
|
|
63
|
+
return;
|
|
64
|
+
fields[field] = value;
|
|
65
|
+
emitter.emit(field);
|
|
66
|
+
};
|
|
67
|
+
return new Proxy(bag, {
|
|
68
|
+
get(target, prop, receiver) {
|
|
69
|
+
if (prop === "$$emitter")
|
|
70
|
+
return emitter;
|
|
71
|
+
if (prop === "$$set")
|
|
72
|
+
return write;
|
|
73
|
+
return Reflect.get(target, prop, receiver);
|
|
74
|
+
},
|
|
75
|
+
set(target, prop, value) {
|
|
76
|
+
if (typeof prop !== "string")
|
|
77
|
+
return Reflect.set(target, prop, value);
|
|
78
|
+
write(prop, value);
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Deeper than any result the evaluator lets out.
|
|
84
|
+
const MAX_DATA_DEPTH = 256;
|
|
85
|
+
// `value` with each part `swap` changes replaced, down to `depth` levels, copying what holds it; the same value when
|
|
86
|
+
// nothing changes.
|
|
87
|
+
function rebuild(value, swap, depth) {
|
|
88
|
+
const swapped = swap(value);
|
|
89
|
+
if (swapped !== value || depth === 0 || !isObject(value))
|
|
90
|
+
return swapped;
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
let out = null;
|
|
93
|
+
for (let i = 0; i < value.length; i++) {
|
|
94
|
+
const next = rebuild(value[i], swap, depth - 1);
|
|
95
|
+
if (next === value[i])
|
|
96
|
+
continue;
|
|
97
|
+
out ??= [...value];
|
|
98
|
+
out[i] = next;
|
|
99
|
+
}
|
|
100
|
+
return out ?? value;
|
|
101
|
+
}
|
|
102
|
+
let out = null;
|
|
103
|
+
for (const [key, child] of Object.entries(value)) {
|
|
104
|
+
const next = rebuild(child, swap, depth - 1);
|
|
105
|
+
if (next === child)
|
|
106
|
+
continue;
|
|
107
|
+
out ??= { ...value };
|
|
108
|
+
out[key] = next;
|
|
109
|
+
}
|
|
110
|
+
return out ?? value;
|
|
111
|
+
}
|
|
112
|
+
// How many levels below `value` the item first appears, or -1. Level by level, each object once, so a cycle ends.
|
|
113
|
+
function levelOf(value, item) {
|
|
114
|
+
if (value === item)
|
|
115
|
+
return 0;
|
|
116
|
+
const seen = new WeakSet();
|
|
117
|
+
let level = [value];
|
|
118
|
+
for (let depth = 1; level.length > 0; depth++) {
|
|
119
|
+
const next = [];
|
|
120
|
+
for (const node of level) {
|
|
121
|
+
if (!isObject(node) || seen.has(node))
|
|
122
|
+
continue;
|
|
123
|
+
seen.add(node);
|
|
124
|
+
for (const child of Array.isArray(node) ? node : Object.values(node)) {
|
|
125
|
+
if (child === item)
|
|
126
|
+
return depth;
|
|
127
|
+
if (isObject(child))
|
|
128
|
+
next.push(child);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
level = next;
|
|
132
|
+
}
|
|
133
|
+
return -1;
|
|
134
|
+
}
|
|
135
|
+
// Follows `path` down own keys, then replaces the item wherever it first appears below.
|
|
136
|
+
function replaceAt(value, path, from, to) {
|
|
137
|
+
if (path.length === 0) {
|
|
138
|
+
const depth = levelOf(value, from);
|
|
139
|
+
return depth === -1 ? value : rebuild(value, (part) => (part === from ? to : part), depth);
|
|
140
|
+
}
|
|
141
|
+
const [key, ...rest] = path;
|
|
142
|
+
if (!isObject(value) || !Object.hasOwn(value, key))
|
|
143
|
+
return value;
|
|
144
|
+
const next = replaceAt(value[key], rest, from, to);
|
|
145
|
+
if (next === value[key])
|
|
146
|
+
return value;
|
|
147
|
+
return Array.isArray(value) ? Object.assign([...value], { [key]: next }) : { ...value, [key]: next };
|
|
148
|
+
}
|
|
149
|
+
const forwardTargetsByProxy = new WeakMap();
|
|
150
|
+
// A row window's current item.
|
|
151
|
+
const itemOfWindow = new WeakMap();
|
|
152
|
+
// Every row window in `value` swapped for the item it shows now: a result holds the data, not a live view that a
|
|
153
|
+
// later write changes behind the memo.
|
|
154
|
+
const unwrapRows = (value) => rebuild(value, (part) => (isObject(part) ? (itemOfWindow.get(part)?.() ?? part) : part), MAX_DATA_DEPTH);
|
|
155
|
+
// The fields a row's write replaces: those its list's `each` reads. Empty for a root-kind scope.
|
|
156
|
+
const getForwardTargets = (proxy) => forwardTargetsByProxy.get(proxy)?.() ?? [];
|
|
157
|
+
const ENGINE_DESCRIPTOR = { writable: false, enumerable: false, configurable: true };
|
|
158
|
+
// A write never changes the item: it puts a copy in the fields `each` reads. A field of an outer row is that row's
|
|
159
|
+
// write, so a nested edit climbs to the outermost array.
|
|
160
|
+
function createRowScope() {
|
|
161
|
+
const emitter = createEmitter();
|
|
162
|
+
let element;
|
|
163
|
+
let visible = {};
|
|
164
|
+
let reads = [];
|
|
165
|
+
let detached = false;
|
|
166
|
+
// "scopes.root.data.items" → root's `data` field, then `items` below it. A bare scope names no field.
|
|
167
|
+
const sources = () => {
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const read of reads) {
|
|
170
|
+
const [name, field, ...path] = read.split(".").slice(1);
|
|
171
|
+
const scope = Object.hasOwn(visible, name) ? visible[name] : undefined;
|
|
172
|
+
if (scope && field !== undefined && Object.hasOwn(scope, field))
|
|
173
|
+
out.push({ scope, field, path });
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
};
|
|
177
|
+
const write = (field, value, options) => {
|
|
178
|
+
assertField(field);
|
|
179
|
+
if (!isObject(element) || Array.isArray(element)) {
|
|
180
|
+
const kind = element === null ? "null" : Array.isArray(element) ? "an array" : `a ${typeof element}`;
|
|
181
|
+
throw new EntryError(`Cannot set "${field}": this row holds ${kind}, not an object. Replace it through the array it came from.`, { reason: "guardrail-violation" });
|
|
182
|
+
}
|
|
183
|
+
const oldValue = element[field];
|
|
184
|
+
if (options?.default && oldValue !== undefined)
|
|
185
|
+
return;
|
|
186
|
+
if (Object.is(oldValue, value))
|
|
187
|
+
return;
|
|
188
|
+
const copy = { ...element, [field]: value };
|
|
189
|
+
// Two sources may share a field (`data.a`, `data.b`), so each builds on the last.
|
|
190
|
+
const next = new Map();
|
|
191
|
+
for (const { scope, field: name, path } of detached ? [] : sources()) {
|
|
192
|
+
const pending = next.get(scope);
|
|
193
|
+
const current = pending?.has(name) ? pending.get(name) : scope[name];
|
|
194
|
+
const replaced = replaceAt(current, path, element, copy);
|
|
195
|
+
if (replaced !== current)
|
|
196
|
+
next.set(scope, (pending ?? new Map()).set(name, replaced));
|
|
197
|
+
}
|
|
198
|
+
if (next.size === 0) {
|
|
199
|
+
throw new EntryError(`Cannot set "${field}": this item is in no field "each" reads — the row was removed, or "each" built a new object. Edit the array it came from.`, { reason: "unknown-reference" });
|
|
200
|
+
}
|
|
201
|
+
for (const [scope, fields] of next)
|
|
202
|
+
for (const [name, replaced] of fields)
|
|
203
|
+
scope.$$set(name, replaced);
|
|
204
|
+
element = copy;
|
|
205
|
+
emitter.emit(field);
|
|
206
|
+
};
|
|
207
|
+
const proxy = new Proxy({}, {
|
|
208
|
+
get(_, prop) {
|
|
209
|
+
if (prop === "$$emitter")
|
|
210
|
+
return emitter;
|
|
211
|
+
if (prop === "$$set")
|
|
212
|
+
return write;
|
|
213
|
+
if (typeof prop !== "string")
|
|
214
|
+
return undefined;
|
|
215
|
+
return isObject(element) ? element[prop] : undefined;
|
|
216
|
+
},
|
|
217
|
+
has(_, prop) {
|
|
218
|
+
return typeof prop === "string" && isObject(element) && Object.hasOwn(element, prop);
|
|
219
|
+
},
|
|
220
|
+
ownKeys() {
|
|
221
|
+
return isObject(element) ? Reflect.ownKeys(element) : [];
|
|
222
|
+
},
|
|
223
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
224
|
+
if (typeof prop !== "string" || !isObject(element))
|
|
225
|
+
return undefined;
|
|
226
|
+
const desc = Object.getOwnPropertyDescriptor(element, prop);
|
|
227
|
+
// The proxy target has no such property, so it must be reported configurable.
|
|
228
|
+
return desc ? { ...desc, configurable: true } : undefined;
|
|
229
|
+
},
|
|
230
|
+
set(_, prop, value) {
|
|
231
|
+
if (typeof prop !== "string")
|
|
232
|
+
return false;
|
|
233
|
+
write(prop, value);
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
forwardTargetsByProxy.set(proxy, () => sources().map(({ scope, field }) => ({ scope, field })));
|
|
238
|
+
itemOfWindow.set(proxy, () => element);
|
|
239
|
+
return {
|
|
240
|
+
proxy,
|
|
241
|
+
retarget(nextElement) {
|
|
242
|
+
const moved = nextElement !== element;
|
|
243
|
+
element = nextElement;
|
|
244
|
+
return moved;
|
|
245
|
+
},
|
|
246
|
+
see(scopes, eachReads) {
|
|
247
|
+
visible = scopes;
|
|
248
|
+
reads = eachReads;
|
|
249
|
+
},
|
|
250
|
+
detach() {
|
|
251
|
+
detached = true;
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// `scopes.$<as>`: the runtime's read-only `index`, `id` and `value`, then whatever the row's steps set.
|
|
256
|
+
function createRowState() {
|
|
257
|
+
const scope = createProxyScope(Object.create(null));
|
|
258
|
+
let runtime = {};
|
|
259
|
+
const write = (field, value, options) => {
|
|
260
|
+
if (ROW_FIELDS.has(field)) {
|
|
261
|
+
throw new EntryError(`Cannot set "${field}": the runtime owns it.`, { reason: "guardrail-violation" });
|
|
262
|
+
}
|
|
263
|
+
scope.$$set(field, value, options);
|
|
264
|
+
};
|
|
265
|
+
const proxy = new Proxy(scope, {
|
|
266
|
+
get(target, prop, receiver) {
|
|
267
|
+
if (prop === "$$set")
|
|
268
|
+
return write;
|
|
269
|
+
if (typeof prop === "string" && ROW_FIELDS.has(prop))
|
|
270
|
+
return runtime[prop];
|
|
271
|
+
return Reflect.get(target, prop, receiver);
|
|
272
|
+
},
|
|
273
|
+
has(target, prop) {
|
|
274
|
+
return (typeof prop === "string" && ROW_FIELDS.has(prop)) || Reflect.has(target, prop);
|
|
275
|
+
},
|
|
276
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
277
|
+
if (typeof prop === "string" && ROW_FIELDS.has(prop))
|
|
278
|
+
return { ...ENGINE_DESCRIPTOR, value: runtime[prop] };
|
|
279
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
280
|
+
},
|
|
281
|
+
set(_, prop, value) {
|
|
282
|
+
if (typeof prop !== "string")
|
|
283
|
+
return false;
|
|
284
|
+
write(prop, value);
|
|
285
|
+
return true;
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
proxy,
|
|
290
|
+
retarget(element, index, id) {
|
|
291
|
+
runtime = { index, id, value: isObject(element) ? undefined : element };
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
export { createProxyScope, createRowScope, createRowState, countEmits, getForwardTargets, unwrapRows, };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const PREFIX = "scopes.";
|
|
2
|
+
// "scopes.root.user.name" → ["root", "user.name"]
|
|
3
|
+
export const parseScope = (key) => {
|
|
4
|
+
const dot = key.indexOf(".", PREFIX.length);
|
|
5
|
+
return [key.slice(PREFIX.length, dot), key.slice(dot + 1)];
|
|
6
|
+
};
|
|
7
|
+
// "scopes.root.user.name" → "scopes.root.user"; a bare scope reads every field: "scopes.root.*".
|
|
8
|
+
export const depKey = (path) => {
|
|
9
|
+
const [scope, field] = path.slice(PREFIX.length).split(".");
|
|
10
|
+
return field ? `scopes.${scope}.${field}` : `scopes.${scope}.*`;
|
|
11
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EntryError } from "../entry-error.js";
|
|
2
|
+
import type { ComponentEntry } from "../types.js";
|
|
3
|
+
export declare const PROTOTYPE_KEYS: Set<string>;
|
|
4
|
+
export declare const ROW_FIELDS: Set<string>;
|
|
5
|
+
type SetAddressFault = {
|
|
6
|
+
kind: "shape" | "reserved" | "prototype";
|
|
7
|
+
segment?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function findSetAddressFault(address: string): SetAddressFault | null;
|
|
10
|
+
export declare function setAddressError(address: string, fault: SetAddressFault, elementKey?: string): EntryError;
|
|
11
|
+
export declare function parseSetAddress(address: string, elementKey?: string): {
|
|
12
|
+
scope: string;
|
|
13
|
+
field: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function entrySetAddressError(entry: Pick<ComponentEntry, "key" | "seed" | "callbacks">): EntryError | null;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { EntryError } from "../entry-error.js";
|
|
2
|
+
const ADDRESS = /^scopes\.([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/;
|
|
3
|
+
export const PROTOTYPE_KEYS = new Set([
|
|
4
|
+
"__proto__",
|
|
5
|
+
"constructor",
|
|
6
|
+
"prototype",
|
|
7
|
+
"__defineGetter__",
|
|
8
|
+
"__defineSetter__",
|
|
9
|
+
"__lookupGetter__",
|
|
10
|
+
"__lookupSetter__",
|
|
11
|
+
]);
|
|
12
|
+
// A row's `$<as>` scope: readable, never written.
|
|
13
|
+
export const ROW_FIELDS = new Set(["index", "id", "value"]);
|
|
14
|
+
export function findSetAddressFault(address) {
|
|
15
|
+
const match = ADDRESS.exec(address);
|
|
16
|
+
if (!match)
|
|
17
|
+
return { kind: "shape" };
|
|
18
|
+
const [, scope, field] = match;
|
|
19
|
+
const prototype = [scope, field].find((segment) => PROTOTYPE_KEYS.has(segment));
|
|
20
|
+
if (prototype)
|
|
21
|
+
return { kind: "prototype", segment: prototype };
|
|
22
|
+
return scope.startsWith("$") && ROW_FIELDS.has(field) ? { kind: "reserved", segment: field } : null;
|
|
23
|
+
}
|
|
24
|
+
// The message names the fix — the recovery prompt forwards it verbatim.
|
|
25
|
+
export function setAddressError(address, fault, elementKey) {
|
|
26
|
+
const messages = {
|
|
27
|
+
shape: `"set": "${address}" is not an address. A set names one field, "scopes.<scope>.<field>"; to change part of a field, write the whole field.`,
|
|
28
|
+
reserved: `"set": "${address}" writes "${fault.segment}", which the runtime owns (a row's index, id and value are read-only).`,
|
|
29
|
+
prototype: `"set": "${address}" writes through "${fault.segment}", which reaches the prototype chain.`,
|
|
30
|
+
};
|
|
31
|
+
return new EntryError(messages[fault.kind], { reason: "guardrail-violation", elementKey });
|
|
32
|
+
}
|
|
33
|
+
export function parseSetAddress(address, elementKey) {
|
|
34
|
+
const fault = findSetAddressFault(address);
|
|
35
|
+
if (fault)
|
|
36
|
+
throw setAddressError(address, fault, elementKey);
|
|
37
|
+
const [, scope, field] = ADDRESS.exec(address);
|
|
38
|
+
return { scope, field };
|
|
39
|
+
}
|
|
40
|
+
// The first bad `set` in the entry's seed and callbacks, as the error it raises.
|
|
41
|
+
export function entrySetAddressError(entry) {
|
|
42
|
+
for (const steps of [entry.seed ?? [], ...Object.values(entry.callbacks ?? {})]) {
|
|
43
|
+
for (const { set } of steps) {
|
|
44
|
+
if (set === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
const fault = findSetAddressFault(set);
|
|
47
|
+
if (fault)
|
|
48
|
+
return setAddressError(set, fault, entry.key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yields each line of a byte or text stream that parses as JSON. Skips blank and non-JSON lines: code fences, prose,
|
|
3
|
+
* a cut-off last line.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* for await (const value of streamJsonLines(result.textStream)) {
|
|
7
|
+
* if (isComponentEntry(value)) await saveEntry(value);
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* if (!res.body) throw new Error(`Generation failed (${res.status})`);
|
|
12
|
+
* const entries = streamJsonLines<ComponentEntry>(res.body);
|
|
13
|
+
*/
|
|
14
|
+
export declare function streamJsonLines<T = unknown>(source: ReadableStream<Uint8Array | string> | AsyncIterable<string | Uint8Array>): AsyncGenerator<T>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yields each line of a byte or text stream that parses as JSON. Skips blank and non-JSON lines: code fences, prose,
|
|
3
|
+
* a cut-off last line.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* for await (const value of streamJsonLines(result.textStream)) {
|
|
7
|
+
* if (isComponentEntry(value)) await saveEntry(value);
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* if (!res.body) throw new Error(`Generation failed (${res.status})`);
|
|
12
|
+
* const entries = streamJsonLines<ComponentEntry>(res.body);
|
|
13
|
+
*/
|
|
14
|
+
export async function* streamJsonLines(source) {
|
|
15
|
+
const decoder = new TextDecoder();
|
|
16
|
+
let buffer = "";
|
|
17
|
+
for await (const chunk of toAsyncIterable(source)) {
|
|
18
|
+
buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
19
|
+
let nl = buffer.indexOf("\n");
|
|
20
|
+
while (nl !== -1) {
|
|
21
|
+
yield* parseLine(buffer.slice(0, nl));
|
|
22
|
+
buffer = buffer.slice(nl + 1);
|
|
23
|
+
nl = buffer.indexOf("\n");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
yield* parseLine(buffer + decoder.decode());
|
|
27
|
+
}
|
|
28
|
+
function* parseLine(raw) {
|
|
29
|
+
const line = raw.trim();
|
|
30
|
+
if (!line)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
yield JSON.parse(line);
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
function toAsyncIterable(source) {
|
|
38
|
+
if (Symbol.asyncIterator in source) {
|
|
39
|
+
return source;
|
|
40
|
+
}
|
|
41
|
+
// Not every browser's ReadableStream is async-iterable.
|
|
42
|
+
const stream = source;
|
|
43
|
+
return {
|
|
44
|
+
async *[Symbol.asyncIterator]() {
|
|
45
|
+
const reader = stream.getReader();
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
const { done, value } = await reader.read();
|
|
49
|
+
if (done)
|
|
50
|
+
return;
|
|
51
|
+
yield value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
reader.releaseLock();
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@uicast/expr";
|
|
2
|
+
type Expression = string;
|
|
3
|
+
type ScopePath = string;
|
|
4
|
+
/**
|
|
5
|
+
* One value: `literal` is used as is, `expr` is an expression evaluated against the scopes.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const props: ValueSource = { expr: "({ text: scopes.root.title })" };
|
|
9
|
+
* const fixed: ValueSource = { literal: { variant: "outline" } };
|
|
10
|
+
*/
|
|
11
|
+
export type ValueSource = {
|
|
12
|
+
expr: Expression;
|
|
13
|
+
} | {
|
|
14
|
+
literal: unknown;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A `seed` step: a value source and the field it writes.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const step: ValueSourceAssignment = { set: "scopes.root.users", expr: "getUsers()" };
|
|
21
|
+
*/
|
|
22
|
+
export type ValueSourceAssignment = {
|
|
23
|
+
/** The field to write: `scopes.<scope>.<field>`, never deeper. */
|
|
24
|
+
set: ScopePath;
|
|
25
|
+
} & ValueSource;
|
|
26
|
+
/**
|
|
27
|
+
* A callback step: a value source, optionally written to a field, confirmed first or debounced.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const toggle: CallbackValueSourceAssignment = { set: "scopes.root.open", expr: "!currentValue" };
|
|
31
|
+
* const remove: CallbackValueSourceAssignment = { confirm: "Delete it?", expr: "deleteRow({ id: scopes.row.id })" };
|
|
32
|
+
*/
|
|
33
|
+
export type CallbackValueSourceAssignment = {
|
|
34
|
+
/** The field to write: `scopes.<scope>.<field>`. Absent: the step runs for its effect only. */
|
|
35
|
+
set?: ScopePath;
|
|
36
|
+
/** A question asked before this step, e.g. `"Delete this row?"`. Declined, this step and the rest are skipped. */
|
|
37
|
+
confirm?: string;
|
|
38
|
+
/** This step and the rest wait for 300 ms of quiet; only the latest call runs. */
|
|
39
|
+
debounce?: boolean;
|
|
40
|
+
} & ValueSource;
|
|
41
|
+
/**
|
|
42
|
+
* One line of a document: the JSON for one element. List fields are optional, so a `ComponentEntry[]` holds lists
|
|
43
|
+
* too; `each` marks a list.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const entry: ComponentEntry = {
|
|
47
|
+
* key: "open-count", component: "Typography",
|
|
48
|
+
* props: { expr: "({ text: 'Open: ' + scopes.root.open })" },
|
|
49
|
+
* };
|
|
50
|
+
*/
|
|
51
|
+
export interface ComponentEntry {
|
|
52
|
+
/** Unique id. Parents name children by it; re-emitting it replaces the element and its subtree. */
|
|
53
|
+
key: string;
|
|
54
|
+
/** The component name, matched exactly against the catalog. */
|
|
55
|
+
component: string;
|
|
56
|
+
/** The props object, e.g. `{ expr: "({ text: scopes.root.title })" }`. Reactive. */
|
|
57
|
+
props?: ValueSource;
|
|
58
|
+
/** Steps that set up state once, at mount. Each writes its field only while it is still undefined. */
|
|
59
|
+
seed?: ValueSourceAssignment[];
|
|
60
|
+
/** An expression; truthy hides the element. Reactive. */
|
|
61
|
+
hidden?: Expression;
|
|
62
|
+
/** An expression; truthy renders the element as busy. Reactive, like `hidden`. */
|
|
63
|
+
loading?: Expression;
|
|
64
|
+
/** Steps per event, e.g. `{ onClick: [{ set: "scopes.root.open", expr: "!currentValue" }] }`. */
|
|
65
|
+
callbacks?: Record<string, CallbackValueSourceAssignment[]>;
|
|
66
|
+
/** Child keys, in render order. */
|
|
67
|
+
children?: string[];
|
|
68
|
+
/** An expression returning the array to repeat over; it makes the entry a list. */
|
|
69
|
+
each?: Expression;
|
|
70
|
+
/** Names each item's two scopes: `scopes.<as>` is the item, `scopes.$<as>` its row. */
|
|
71
|
+
as?: string;
|
|
72
|
+
/** The item field that gives the row its `id`. Absent, or missing on an item: the index. */
|
|
73
|
+
keyBy?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* An entry that repeats once per item of `each`.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const rows: ComponentListEntry = { key: "task-row", component: "TableRow", each: "scopes.root.tasks", as: "task" };
|
|
80
|
+
*/
|
|
81
|
+
export interface ComponentListEntry extends ComponentEntry {
|
|
82
|
+
/** An expression returning the array to repeat over. */
|
|
83
|
+
each: Expression;
|
|
84
|
+
/** Names each item's two scopes: `scopes.<as>` is the item, `scopes.$<as>` its row. */
|
|
85
|
+
as: string;
|
|
86
|
+
}
|
|
87
|
+
export declare function isComponentListEntry(entry: ComponentEntry): entry is ComponentListEntry;
|
|
88
|
+
/**
|
|
89
|
+
* Whether parsed JSON is an entry: string `key` and `component`, and `children`, if present, an array. The renderer
|
|
90
|
+
* checks the other fields at mount.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* const entries = parsedLines.filter(isComponentEntry);
|
|
94
|
+
*/
|
|
95
|
+
export declare function isComponentEntry(value: unknown): value is ComponentEntry;
|
|
96
|
+
/**
|
|
97
|
+
* A schema that validates at run time (Standard Schema) and converts to JSON Schema for the prompt (Standard JSON
|
|
98
|
+
* Schema). Zod 4.2+, Valibot and ArkType schemas fit.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const props: CombinedSpec = z.strictObject({ text: z.string() });
|
|
102
|
+
*/
|
|
103
|
+
export type CombinedSpec<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output> & StandardJSONSchemaV1<Input, Output>;
|
|
104
|
+
/**
|
|
105
|
+
* What the model reads about a component: its name, description, props and events. Make one with
|
|
106
|
+
* `createComponentDefinition`.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const definitions: ComponentDefinition[] = [...defs, BadgeDef];
|
|
110
|
+
* getComponentsPartialPrompt({ definitions });
|
|
111
|
+
*/
|
|
112
|
+
export type ComponentDefinition<TProps extends CombinedSpec = CombinedSpec, TCallbacks extends Record<string, CombinedSpec> = Record<string, CombinedSpec>> = {
|
|
113
|
+
/** What an entry's `component` names, e.g. `"Badge"`. Must be unique: the prompt builder and `<RendererProvider>` throw on a repeat. */
|
|
114
|
+
name: string;
|
|
115
|
+
/** When to use the component, printed word for word in the prompt. */
|
|
116
|
+
description: string;
|
|
117
|
+
/** The props schema: the prompt prints it, and the renderer parses props through it before `render`. */
|
|
118
|
+
props: TProps;
|
|
119
|
+
/** Event name → payload schema, e.g. `{ onChange: z.object({ value: z.string() }), onClear: z.null() }`. */
|
|
120
|
+
callbacks?: TCallbacks;
|
|
121
|
+
/** Kept out of the prompt; an entry that names it still renders. */
|
|
122
|
+
hidden?: boolean;
|
|
123
|
+
};
|
|
124
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function isComponentListEntry(entry) {
|
|
2
|
+
return entry.each !== undefined;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Whether parsed JSON is an entry: string `key` and `component`, and `children`, if present, an array. The renderer
|
|
6
|
+
* checks the other fields at mount.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const entries = parsedLines.filter(isComponentEntry);
|
|
10
|
+
*/
|
|
11
|
+
export function isComponentEntry(value) {
|
|
12
|
+
if (!value || typeof value !== "object")
|
|
13
|
+
return false;
|
|
14
|
+
const entry = value;
|
|
15
|
+
return (typeof entry.key === "string" &&
|
|
16
|
+
typeof entry.component === "string" &&
|
|
17
|
+
// A string `children` (a plausible LLM slip) would be iterated char by char downstream.
|
|
18
|
+
(entry.children === undefined || Array.isArray(entry.children)));
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type JSONSchema } from "./json-schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Which URLs a prop declared as a URL (`z.url()`) may hold. An object changes the default rules field by field; a
|
|
4
|
+
* predicate replaces them.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const urlPolicy: UrlPolicy = { hosts: ["cdn.example.com", "*.imgix.net"] };
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const urlPolicy: UrlPolicy = (url) => url.startsWith("https://cdn.example.com/");
|
|
11
|
+
*/
|
|
12
|
+
export type UrlPolicy = {
|
|
13
|
+
/** Relative URLs: `/a`, `a/b`, `?q=1`, `#x`. Default `true`. */
|
|
14
|
+
allowRelative?: boolean;
|
|
15
|
+
/** Absolute URLs on the page origin. Default `true`. */
|
|
16
|
+
allowSameOrigin?: boolean;
|
|
17
|
+
/** Extra http/https hosts: an exact match, or `*.example.com` for subdomains (not the apex, so list both). */
|
|
18
|
+
hosts?: readonly string[];
|
|
19
|
+
/** `data:` raster images. Default `true`. SVG is never allowed: it can carry script. */
|
|
20
|
+
allowDataImages?: boolean;
|
|
21
|
+
/** The page origin, for where there is no `location` (SSR); without either, only `hosts` matches an absolute URL. */
|
|
22
|
+
origin?: string;
|
|
23
|
+
} | ((url: string) => boolean);
|
|
24
|
+
type UrlCheck = {
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
export declare const resolveUrlPolicy: (policy?: Exclude<UrlPolicy, (url: string) => boolean>) => {
|
|
31
|
+
allowRelative: boolean;
|
|
32
|
+
allowSameOrigin: boolean;
|
|
33
|
+
hosts: readonly string[];
|
|
34
|
+
allowDataImages: boolean;
|
|
35
|
+
origin: string | undefined;
|
|
36
|
+
};
|
|
37
|
+
export declare const checkUrl: (value: string, policy?: UrlPolicy) => UrlCheck;
|
|
38
|
+
type UrlViolation = {
|
|
39
|
+
path: string;
|
|
40
|
+
url: string;
|
|
41
|
+
reason: string;
|
|
42
|
+
};
|
|
43
|
+
export declare const findUrlViolations: (schema: JSONSchema | undefined, value: unknown, policy?: UrlPolicy) => UrlViolation[];
|
|
44
|
+
export declare const schemaHasUrlFormat: (schema: JSONSchema | undefined) => boolean;
|
|
45
|
+
export {};
|