@json-render/core 0.4.3 → 0.5.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 +134 -6
- package/dist/index.d.mts +404 -178
- package/dist/index.d.ts +404 -178
- package/dist/index.js +611 -80
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +601 -80
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -19,26 +19,36 @@ var DynamicBooleanSchema = z.union([
|
|
|
19
19
|
z.boolean(),
|
|
20
20
|
z.object({ path: z.string() })
|
|
21
21
|
]);
|
|
22
|
-
function resolveDynamicValue(value,
|
|
22
|
+
function resolveDynamicValue(value, stateModel) {
|
|
23
23
|
if (value === null || value === void 0) {
|
|
24
24
|
return void 0;
|
|
25
25
|
}
|
|
26
26
|
if (typeof value === "object" && "path" in value) {
|
|
27
|
-
return getByPath(
|
|
27
|
+
return getByPath(stateModel, value.path);
|
|
28
28
|
}
|
|
29
29
|
return value;
|
|
30
30
|
}
|
|
31
|
+
function unescapeJsonPointer(token) {
|
|
32
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
33
|
+
}
|
|
34
|
+
function parseJsonPointer(path) {
|
|
35
|
+
const raw = path.startsWith("/") ? path.slice(1).split("/") : path.split("/");
|
|
36
|
+
return raw.map(unescapeJsonPointer);
|
|
37
|
+
}
|
|
31
38
|
function getByPath(obj, path) {
|
|
32
39
|
if (!path || path === "/") {
|
|
33
40
|
return obj;
|
|
34
41
|
}
|
|
35
|
-
const segments =
|
|
42
|
+
const segments = parseJsonPointer(path);
|
|
36
43
|
let current = obj;
|
|
37
44
|
for (const segment of segments) {
|
|
38
45
|
if (current === null || current === void 0) {
|
|
39
46
|
return void 0;
|
|
40
47
|
}
|
|
41
|
-
if (
|
|
48
|
+
if (Array.isArray(current)) {
|
|
49
|
+
const index = parseInt(segment, 10);
|
|
50
|
+
current = current[index];
|
|
51
|
+
} else if (typeof current === "object") {
|
|
42
52
|
current = current[segment];
|
|
43
53
|
} else {
|
|
44
54
|
return void 0;
|
|
@@ -50,13 +60,13 @@ function isNumericIndex(str) {
|
|
|
50
60
|
return /^\d+$/.test(str);
|
|
51
61
|
}
|
|
52
62
|
function setByPath(obj, path, value) {
|
|
53
|
-
const segments =
|
|
63
|
+
const segments = parseJsonPointer(path);
|
|
54
64
|
if (segments.length === 0) return;
|
|
55
65
|
let current = obj;
|
|
56
66
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
57
67
|
const segment = segments[i];
|
|
58
68
|
const nextSegment = segments[i + 1];
|
|
59
|
-
const nextIsNumeric = nextSegment !== void 0 && isNumericIndex(nextSegment);
|
|
69
|
+
const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-");
|
|
60
70
|
if (Array.isArray(current)) {
|
|
61
71
|
const index = parseInt(segment, 10);
|
|
62
72
|
if (current[index] === void 0 || typeof current[index] !== "object") {
|
|
@@ -72,12 +82,95 @@ function setByPath(obj, path, value) {
|
|
|
72
82
|
}
|
|
73
83
|
const lastSegment = segments[segments.length - 1];
|
|
74
84
|
if (Array.isArray(current)) {
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
if (lastSegment === "-") {
|
|
86
|
+
current.push(value);
|
|
87
|
+
} else {
|
|
88
|
+
const index = parseInt(lastSegment, 10);
|
|
89
|
+
current[index] = value;
|
|
90
|
+
}
|
|
77
91
|
} else {
|
|
78
92
|
current[lastSegment] = value;
|
|
79
93
|
}
|
|
80
94
|
}
|
|
95
|
+
function addByPath(obj, path, value) {
|
|
96
|
+
const segments = parseJsonPointer(path);
|
|
97
|
+
if (segments.length === 0) return;
|
|
98
|
+
let current = obj;
|
|
99
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
100
|
+
const segment = segments[i];
|
|
101
|
+
const nextSegment = segments[i + 1];
|
|
102
|
+
const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-");
|
|
103
|
+
if (Array.isArray(current)) {
|
|
104
|
+
const index = parseInt(segment, 10);
|
|
105
|
+
if (current[index] === void 0 || typeof current[index] !== "object") {
|
|
106
|
+
current[index] = nextIsNumeric ? [] : {};
|
|
107
|
+
}
|
|
108
|
+
current = current[index];
|
|
109
|
+
} else {
|
|
110
|
+
if (!(segment in current) || typeof current[segment] !== "object") {
|
|
111
|
+
current[segment] = nextIsNumeric ? [] : {};
|
|
112
|
+
}
|
|
113
|
+
current = current[segment];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const lastSegment = segments[segments.length - 1];
|
|
117
|
+
if (Array.isArray(current)) {
|
|
118
|
+
if (lastSegment === "-") {
|
|
119
|
+
current.push(value);
|
|
120
|
+
} else {
|
|
121
|
+
const index = parseInt(lastSegment, 10);
|
|
122
|
+
current.splice(index, 0, value);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
current[lastSegment] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function removeByPath(obj, path) {
|
|
129
|
+
const segments = parseJsonPointer(path);
|
|
130
|
+
if (segments.length === 0) return;
|
|
131
|
+
let current = obj;
|
|
132
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
133
|
+
const segment = segments[i];
|
|
134
|
+
if (Array.isArray(current)) {
|
|
135
|
+
const index = parseInt(segment, 10);
|
|
136
|
+
if (current[index] === void 0 || typeof current[index] !== "object") {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
current = current[index];
|
|
140
|
+
} else {
|
|
141
|
+
if (!(segment in current) || typeof current[segment] !== "object") {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
current = current[segment];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const lastSegment = segments[segments.length - 1];
|
|
148
|
+
if (Array.isArray(current)) {
|
|
149
|
+
const index = parseInt(lastSegment, 10);
|
|
150
|
+
if (index >= 0 && index < current.length) {
|
|
151
|
+
current.splice(index, 1);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
delete current[lastSegment];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function deepEqual(a, b) {
|
|
158
|
+
if (a === b) return true;
|
|
159
|
+
if (a === null || b === null) return false;
|
|
160
|
+
if (typeof a !== typeof b) return false;
|
|
161
|
+
if (typeof a !== "object") return false;
|
|
162
|
+
if (Array.isArray(a)) {
|
|
163
|
+
if (!Array.isArray(b)) return false;
|
|
164
|
+
if (a.length !== b.length) return false;
|
|
165
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
166
|
+
}
|
|
167
|
+
const aObj = a;
|
|
168
|
+
const bObj = b;
|
|
169
|
+
const aKeys = Object.keys(aObj);
|
|
170
|
+
const bKeys = Object.keys(bObj);
|
|
171
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
172
|
+
return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
|
|
173
|
+
}
|
|
81
174
|
function findFormValue(fieldName, params, data) {
|
|
82
175
|
if (params?.[fieldName] !== void 0) {
|
|
83
176
|
const val = params[fieldName];
|
|
@@ -126,10 +219,38 @@ function parseSpecStreamLine(line) {
|
|
|
126
219
|
}
|
|
127
220
|
}
|
|
128
221
|
function applySpecStreamPatch(obj, patch) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
222
|
+
switch (patch.op) {
|
|
223
|
+
case "add":
|
|
224
|
+
addByPath(obj, patch.path, patch.value);
|
|
225
|
+
break;
|
|
226
|
+
case "replace":
|
|
227
|
+
setByPath(obj, patch.path, patch.value);
|
|
228
|
+
break;
|
|
229
|
+
case "remove":
|
|
230
|
+
removeByPath(obj, patch.path);
|
|
231
|
+
break;
|
|
232
|
+
case "move": {
|
|
233
|
+
if (!patch.from) break;
|
|
234
|
+
const moveValue = getByPath(obj, patch.from);
|
|
235
|
+
removeByPath(obj, patch.from);
|
|
236
|
+
addByPath(obj, patch.path, moveValue);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "copy": {
|
|
240
|
+
if (!patch.from) break;
|
|
241
|
+
const copyValue = getByPath(obj, patch.from);
|
|
242
|
+
addByPath(obj, patch.path, copyValue);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "test": {
|
|
246
|
+
const actual = getByPath(obj, patch.path);
|
|
247
|
+
if (!deepEqual(actual, patch.value)) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Test operation failed: value at "${patch.path}" does not match`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
133
254
|
}
|
|
134
255
|
return obj;
|
|
135
256
|
}
|
|
@@ -231,7 +352,7 @@ var VisibilityConditionSchema = z2.union([
|
|
|
231
352
|
LogicExpressionSchema
|
|
232
353
|
]);
|
|
233
354
|
function evaluateLogicExpression(expr, ctx) {
|
|
234
|
-
const {
|
|
355
|
+
const { stateModel } = ctx;
|
|
235
356
|
if ("and" in expr) {
|
|
236
357
|
return expr.and.every((subExpr) => evaluateLogicExpression(subExpr, ctx));
|
|
237
358
|
}
|
|
@@ -242,30 +363,30 @@ function evaluateLogicExpression(expr, ctx) {
|
|
|
242
363
|
return !evaluateLogicExpression(expr.not, ctx);
|
|
243
364
|
}
|
|
244
365
|
if ("path" in expr) {
|
|
245
|
-
const value = resolveDynamicValue({ path: expr.path },
|
|
366
|
+
const value = resolveDynamicValue({ path: expr.path }, stateModel);
|
|
246
367
|
return Boolean(value);
|
|
247
368
|
}
|
|
248
369
|
if ("eq" in expr) {
|
|
249
370
|
const [left, right] = expr.eq;
|
|
250
|
-
const leftValue = resolveDynamicValue(left,
|
|
251
|
-
const rightValue = resolveDynamicValue(right,
|
|
371
|
+
const leftValue = resolveDynamicValue(left, stateModel);
|
|
372
|
+
const rightValue = resolveDynamicValue(right, stateModel);
|
|
252
373
|
return leftValue === rightValue;
|
|
253
374
|
}
|
|
254
375
|
if ("neq" in expr) {
|
|
255
376
|
const [left, right] = expr.neq;
|
|
256
|
-
const leftValue = resolveDynamicValue(left,
|
|
257
|
-
const rightValue = resolveDynamicValue(right,
|
|
377
|
+
const leftValue = resolveDynamicValue(left, stateModel);
|
|
378
|
+
const rightValue = resolveDynamicValue(right, stateModel);
|
|
258
379
|
return leftValue !== rightValue;
|
|
259
380
|
}
|
|
260
381
|
if ("gt" in expr) {
|
|
261
382
|
const [left, right] = expr.gt;
|
|
262
383
|
const leftValue = resolveDynamicValue(
|
|
263
384
|
left,
|
|
264
|
-
|
|
385
|
+
stateModel
|
|
265
386
|
);
|
|
266
387
|
const rightValue = resolveDynamicValue(
|
|
267
388
|
right,
|
|
268
|
-
|
|
389
|
+
stateModel
|
|
269
390
|
);
|
|
270
391
|
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
271
392
|
return leftValue > rightValue;
|
|
@@ -276,11 +397,11 @@ function evaluateLogicExpression(expr, ctx) {
|
|
|
276
397
|
const [left, right] = expr.gte;
|
|
277
398
|
const leftValue = resolveDynamicValue(
|
|
278
399
|
left,
|
|
279
|
-
|
|
400
|
+
stateModel
|
|
280
401
|
);
|
|
281
402
|
const rightValue = resolveDynamicValue(
|
|
282
403
|
right,
|
|
283
|
-
|
|
404
|
+
stateModel
|
|
284
405
|
);
|
|
285
406
|
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
286
407
|
return leftValue >= rightValue;
|
|
@@ -291,11 +412,11 @@ function evaluateLogicExpression(expr, ctx) {
|
|
|
291
412
|
const [left, right] = expr.lt;
|
|
292
413
|
const leftValue = resolveDynamicValue(
|
|
293
414
|
left,
|
|
294
|
-
|
|
415
|
+
stateModel
|
|
295
416
|
);
|
|
296
417
|
const rightValue = resolveDynamicValue(
|
|
297
418
|
right,
|
|
298
|
-
|
|
419
|
+
stateModel
|
|
299
420
|
);
|
|
300
421
|
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
301
422
|
return leftValue < rightValue;
|
|
@@ -306,11 +427,11 @@ function evaluateLogicExpression(expr, ctx) {
|
|
|
306
427
|
const [left, right] = expr.lte;
|
|
307
428
|
const leftValue = resolveDynamicValue(
|
|
308
429
|
left,
|
|
309
|
-
|
|
430
|
+
stateModel
|
|
310
431
|
);
|
|
311
432
|
const rightValue = resolveDynamicValue(
|
|
312
433
|
right,
|
|
313
|
-
|
|
434
|
+
stateModel
|
|
314
435
|
);
|
|
315
436
|
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
316
437
|
return leftValue <= rightValue;
|
|
@@ -327,7 +448,7 @@ function evaluateVisibility(condition, ctx) {
|
|
|
327
448
|
return condition;
|
|
328
449
|
}
|
|
329
450
|
if ("path" in condition && !("and" in condition) && !("or" in condition)) {
|
|
330
|
-
const value = resolveDynamicValue({ path: condition.path }, ctx.
|
|
451
|
+
const value = resolveDynamicValue({ path: condition.path }, ctx.stateModel);
|
|
331
452
|
return Boolean(value);
|
|
332
453
|
}
|
|
333
454
|
if ("auth" in condition) {
|
|
@@ -381,6 +502,44 @@ var visibility = {
|
|
|
381
502
|
lte: (left, right) => ({ lte: [left, right] })
|
|
382
503
|
};
|
|
383
504
|
|
|
505
|
+
// src/props.ts
|
|
506
|
+
function isPathExpression(value) {
|
|
507
|
+
return typeof value === "object" && value !== null && "$path" in value && typeof value.$path === "string";
|
|
508
|
+
}
|
|
509
|
+
function isCondExpression(value) {
|
|
510
|
+
return typeof value === "object" && value !== null && "$cond" in value && "$then" in value && "$else" in value;
|
|
511
|
+
}
|
|
512
|
+
function resolvePropValue(value, ctx) {
|
|
513
|
+
if (value === null || value === void 0) {
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
if (isPathExpression(value)) {
|
|
517
|
+
return getByPath(ctx.stateModel, value.$path);
|
|
518
|
+
}
|
|
519
|
+
if (isCondExpression(value)) {
|
|
520
|
+
const result = evaluateVisibility(value.$cond, ctx);
|
|
521
|
+
return resolvePropValue(result ? value.$then : value.$else, ctx);
|
|
522
|
+
}
|
|
523
|
+
if (Array.isArray(value)) {
|
|
524
|
+
return value.map((item) => resolvePropValue(item, ctx));
|
|
525
|
+
}
|
|
526
|
+
if (typeof value === "object") {
|
|
527
|
+
const resolved = {};
|
|
528
|
+
for (const [key, val] of Object.entries(value)) {
|
|
529
|
+
resolved[key] = resolvePropValue(val, ctx);
|
|
530
|
+
}
|
|
531
|
+
return resolved;
|
|
532
|
+
}
|
|
533
|
+
return value;
|
|
534
|
+
}
|
|
535
|
+
function resolveElementProps(props, ctx) {
|
|
536
|
+
const resolved = {};
|
|
537
|
+
for (const [key, value] of Object.entries(props)) {
|
|
538
|
+
resolved[key] = resolvePropValue(value, ctx);
|
|
539
|
+
}
|
|
540
|
+
return resolved;
|
|
541
|
+
}
|
|
542
|
+
|
|
384
543
|
// src/actions.ts
|
|
385
544
|
import { z as z3 } from "zod";
|
|
386
545
|
var ActionConfirmSchema = z3.object({
|
|
@@ -399,44 +558,45 @@ var ActionOnErrorSchema = z3.union([
|
|
|
399
558
|
z3.object({ set: z3.record(z3.string(), z3.unknown()) }),
|
|
400
559
|
z3.object({ action: z3.string() })
|
|
401
560
|
]);
|
|
402
|
-
var
|
|
403
|
-
|
|
561
|
+
var ActionBindingSchema = z3.object({
|
|
562
|
+
action: z3.string(),
|
|
404
563
|
params: z3.record(z3.string(), DynamicValueSchema).optional(),
|
|
405
564
|
confirm: ActionConfirmSchema.optional(),
|
|
406
565
|
onSuccess: ActionOnSuccessSchema.optional(),
|
|
407
566
|
onError: ActionOnErrorSchema.optional()
|
|
408
567
|
});
|
|
409
|
-
|
|
568
|
+
var ActionSchema = ActionBindingSchema;
|
|
569
|
+
function resolveAction(binding, stateModel) {
|
|
410
570
|
const resolvedParams = {};
|
|
411
|
-
if (
|
|
412
|
-
for (const [key, value] of Object.entries(
|
|
413
|
-
resolvedParams[key] = resolveDynamicValue(value,
|
|
571
|
+
if (binding.params) {
|
|
572
|
+
for (const [key, value] of Object.entries(binding.params)) {
|
|
573
|
+
resolvedParams[key] = resolveDynamicValue(value, stateModel);
|
|
414
574
|
}
|
|
415
575
|
}
|
|
416
|
-
let confirm =
|
|
576
|
+
let confirm = binding.confirm;
|
|
417
577
|
if (confirm) {
|
|
418
578
|
confirm = {
|
|
419
579
|
...confirm,
|
|
420
|
-
message: interpolateString(confirm.message,
|
|
421
|
-
title: interpolateString(confirm.title,
|
|
580
|
+
message: interpolateString(confirm.message, stateModel),
|
|
581
|
+
title: interpolateString(confirm.title, stateModel)
|
|
422
582
|
};
|
|
423
583
|
}
|
|
424
584
|
return {
|
|
425
|
-
|
|
585
|
+
action: binding.action,
|
|
426
586
|
params: resolvedParams,
|
|
427
587
|
confirm,
|
|
428
|
-
onSuccess:
|
|
429
|
-
onError:
|
|
588
|
+
onSuccess: binding.onSuccess,
|
|
589
|
+
onError: binding.onError
|
|
430
590
|
};
|
|
431
591
|
}
|
|
432
|
-
function interpolateString(template,
|
|
592
|
+
function interpolateString(template, stateModel) {
|
|
433
593
|
return template.replace(/\$\{([^}]+)\}/g, (_, path) => {
|
|
434
|
-
const value = resolveDynamicValue({ path },
|
|
594
|
+
const value = resolveDynamicValue({ path }, stateModel);
|
|
435
595
|
return String(value ?? "");
|
|
436
596
|
});
|
|
437
597
|
}
|
|
438
598
|
async function executeAction(ctx) {
|
|
439
|
-
const { action: action2, handler,
|
|
599
|
+
const { action: action2, handler, setState, navigate, executeAction: executeAction2 } = ctx;
|
|
440
600
|
try {
|
|
441
601
|
await handler(action2.params);
|
|
442
602
|
if (action2.onSuccess) {
|
|
@@ -444,7 +604,7 @@ async function executeAction(ctx) {
|
|
|
444
604
|
navigate(action2.onSuccess.navigate);
|
|
445
605
|
} else if ("set" in action2.onSuccess) {
|
|
446
606
|
for (const [path, value] of Object.entries(action2.onSuccess.set)) {
|
|
447
|
-
|
|
607
|
+
setState(path, value);
|
|
448
608
|
}
|
|
449
609
|
} else if ("action" in action2.onSuccess && executeAction2) {
|
|
450
610
|
await executeAction2(action2.onSuccess.action);
|
|
@@ -455,7 +615,7 @@ async function executeAction(ctx) {
|
|
|
455
615
|
if ("set" in action2.onError) {
|
|
456
616
|
for (const [path, value] of Object.entries(action2.onError.set)) {
|
|
457
617
|
const resolvedValue = typeof value === "string" && value === "$error.message" ? error.message : value;
|
|
458
|
-
|
|
618
|
+
setState(path, resolvedValue);
|
|
459
619
|
}
|
|
460
620
|
} else if ("action" in action2.onError && executeAction2) {
|
|
461
621
|
await executeAction2(action2.onError.action);
|
|
@@ -465,25 +625,26 @@ async function executeAction(ctx) {
|
|
|
465
625
|
}
|
|
466
626
|
}
|
|
467
627
|
}
|
|
468
|
-
var
|
|
469
|
-
/** Create a simple action */
|
|
470
|
-
simple: (
|
|
471
|
-
|
|
628
|
+
var actionBinding = {
|
|
629
|
+
/** Create a simple action binding */
|
|
630
|
+
simple: (actionName, params) => ({
|
|
631
|
+
action: actionName,
|
|
472
632
|
params
|
|
473
633
|
}),
|
|
474
|
-
/** Create an action with confirmation */
|
|
475
|
-
withConfirm: (
|
|
476
|
-
|
|
634
|
+
/** Create an action binding with confirmation */
|
|
635
|
+
withConfirm: (actionName, confirm, params) => ({
|
|
636
|
+
action: actionName,
|
|
477
637
|
params,
|
|
478
638
|
confirm
|
|
479
639
|
}),
|
|
480
|
-
/** Create an action with success handler */
|
|
481
|
-
withSuccess: (
|
|
482
|
-
|
|
640
|
+
/** Create an action binding with success handler */
|
|
641
|
+
withSuccess: (actionName, onSuccess, params) => ({
|
|
642
|
+
action: actionName,
|
|
483
643
|
params,
|
|
484
644
|
onSuccess
|
|
485
645
|
})
|
|
486
646
|
};
|
|
647
|
+
var action = actionBinding;
|
|
487
648
|
|
|
488
649
|
// src/validation.ts
|
|
489
650
|
import { z as z4 } from "zod";
|
|
@@ -592,11 +753,11 @@ var builtInValidationFunctions = {
|
|
|
592
753
|
}
|
|
593
754
|
};
|
|
594
755
|
function runValidationCheck(check2, ctx) {
|
|
595
|
-
const { value,
|
|
756
|
+
const { value, stateModel, customFunctions } = ctx;
|
|
596
757
|
const resolvedArgs = {};
|
|
597
758
|
if (check2.args) {
|
|
598
759
|
for (const [key, argValue] of Object.entries(check2.args)) {
|
|
599
|
-
resolvedArgs[key] = resolveDynamicValue(argValue,
|
|
760
|
+
resolvedArgs[key] = resolveDynamicValue(argValue, stateModel);
|
|
600
761
|
}
|
|
601
762
|
}
|
|
602
763
|
const fn = builtInValidationFunctions[check2.fn] ?? customFunctions?.[check2.fn];
|
|
@@ -621,7 +782,7 @@ function runValidation(config, ctx) {
|
|
|
621
782
|
const errors = [];
|
|
622
783
|
if (config.enabled) {
|
|
623
784
|
const enabled = evaluateLogicExpression(config.enabled, {
|
|
624
|
-
|
|
785
|
+
stateModel: ctx.stateModel,
|
|
625
786
|
authState: ctx.authState
|
|
626
787
|
});
|
|
627
788
|
if (!enabled) {
|
|
@@ -688,6 +849,155 @@ var check = {
|
|
|
688
849
|
})
|
|
689
850
|
};
|
|
690
851
|
|
|
852
|
+
// src/spec-validator.ts
|
|
853
|
+
function validateSpec(spec, options = {}) {
|
|
854
|
+
const { checkOrphans = false } = options;
|
|
855
|
+
const issues = [];
|
|
856
|
+
if (!spec.root) {
|
|
857
|
+
issues.push({
|
|
858
|
+
severity: "error",
|
|
859
|
+
message: "Spec has no root element defined.",
|
|
860
|
+
code: "missing_root"
|
|
861
|
+
});
|
|
862
|
+
return { valid: false, issues };
|
|
863
|
+
}
|
|
864
|
+
if (!spec.elements[spec.root]) {
|
|
865
|
+
issues.push({
|
|
866
|
+
severity: "error",
|
|
867
|
+
message: `Root element "${spec.root}" not found in elements map.`,
|
|
868
|
+
code: "root_not_found"
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
if (Object.keys(spec.elements).length === 0) {
|
|
872
|
+
issues.push({
|
|
873
|
+
severity: "error",
|
|
874
|
+
message: "Spec has no elements.",
|
|
875
|
+
code: "empty_spec"
|
|
876
|
+
});
|
|
877
|
+
return { valid: false, issues };
|
|
878
|
+
}
|
|
879
|
+
for (const [key, element] of Object.entries(spec.elements)) {
|
|
880
|
+
if (element.children) {
|
|
881
|
+
for (const childKey of element.children) {
|
|
882
|
+
if (!spec.elements[childKey]) {
|
|
883
|
+
issues.push({
|
|
884
|
+
severity: "error",
|
|
885
|
+
message: `Element "${key}" references child "${childKey}" which does not exist in the elements map.`,
|
|
886
|
+
elementKey: key,
|
|
887
|
+
code: "missing_child"
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const props = element.props;
|
|
893
|
+
if (props && "visible" in props && props.visible !== void 0) {
|
|
894
|
+
issues.push({
|
|
895
|
+
severity: "error",
|
|
896
|
+
message: `Element "${key}" has "visible" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
|
|
897
|
+
elementKey: key,
|
|
898
|
+
code: "visible_in_props"
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
if (props && "on" in props && props.on !== void 0) {
|
|
902
|
+
issues.push({
|
|
903
|
+
severity: "error",
|
|
904
|
+
message: `Element "${key}" has "on" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
|
|
905
|
+
elementKey: key,
|
|
906
|
+
code: "on_in_props"
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (props && "repeat" in props && props.repeat !== void 0) {
|
|
910
|
+
issues.push({
|
|
911
|
+
severity: "error",
|
|
912
|
+
message: `Element "${key}" has "repeat" inside "props". It should be a top-level field on the element (sibling of type/props/children).`,
|
|
913
|
+
elementKey: key,
|
|
914
|
+
code: "repeat_in_props"
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (checkOrphans) {
|
|
919
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
920
|
+
const walk = (key) => {
|
|
921
|
+
if (reachable.has(key)) return;
|
|
922
|
+
reachable.add(key);
|
|
923
|
+
const el = spec.elements[key];
|
|
924
|
+
if (el?.children) {
|
|
925
|
+
for (const childKey of el.children) {
|
|
926
|
+
if (spec.elements[childKey]) {
|
|
927
|
+
walk(childKey);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
if (spec.elements[spec.root]) {
|
|
933
|
+
walk(spec.root);
|
|
934
|
+
}
|
|
935
|
+
for (const key of Object.keys(spec.elements)) {
|
|
936
|
+
if (!reachable.has(key)) {
|
|
937
|
+
issues.push({
|
|
938
|
+
severity: "warning",
|
|
939
|
+
message: `Element "${key}" is not reachable from root "${spec.root}".`,
|
|
940
|
+
elementKey: key,
|
|
941
|
+
code: "orphaned_element"
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
947
|
+
return { valid: !hasErrors, issues };
|
|
948
|
+
}
|
|
949
|
+
function autoFixSpec(spec) {
|
|
950
|
+
const fixes = [];
|
|
951
|
+
const fixedElements = {};
|
|
952
|
+
for (const [key, element] of Object.entries(spec.elements)) {
|
|
953
|
+
const props = element.props;
|
|
954
|
+
let fixed = element;
|
|
955
|
+
if (props && "visible" in props && props.visible !== void 0) {
|
|
956
|
+
const { visible, ...restProps } = fixed.props;
|
|
957
|
+
fixed = {
|
|
958
|
+
...fixed,
|
|
959
|
+
props: restProps,
|
|
960
|
+
visible
|
|
961
|
+
};
|
|
962
|
+
fixes.push(`Moved "visible" from props to element level on "${key}".`);
|
|
963
|
+
}
|
|
964
|
+
let currentProps = fixed.props;
|
|
965
|
+
if (currentProps && "on" in currentProps && currentProps.on !== void 0) {
|
|
966
|
+
const { on, ...restProps } = currentProps;
|
|
967
|
+
fixed = {
|
|
968
|
+
...fixed,
|
|
969
|
+
props: restProps,
|
|
970
|
+
on
|
|
971
|
+
};
|
|
972
|
+
fixes.push(`Moved "on" from props to element level on "${key}".`);
|
|
973
|
+
}
|
|
974
|
+
currentProps = fixed.props;
|
|
975
|
+
if (currentProps && "repeat" in currentProps && currentProps.repeat !== void 0) {
|
|
976
|
+
const { repeat, ...restProps } = currentProps;
|
|
977
|
+
fixed = {
|
|
978
|
+
...fixed,
|
|
979
|
+
props: restProps,
|
|
980
|
+
repeat
|
|
981
|
+
};
|
|
982
|
+
fixes.push(`Moved "repeat" from props to element level on "${key}".`);
|
|
983
|
+
}
|
|
984
|
+
fixedElements[key] = fixed;
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
spec: { root: spec.root, elements: fixedElements },
|
|
988
|
+
fixes
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
function formatSpecIssues(issues) {
|
|
992
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
993
|
+
if (errors.length === 0) return "";
|
|
994
|
+
const lines = ["The generated UI spec has the following errors:"];
|
|
995
|
+
for (const issue of errors) {
|
|
996
|
+
lines.push(`- ${issue.message}`);
|
|
997
|
+
}
|
|
998
|
+
return lines.join("\n");
|
|
999
|
+
}
|
|
1000
|
+
|
|
691
1001
|
// src/schema.ts
|
|
692
1002
|
import { z as z5 } from "zod";
|
|
693
1003
|
function createBuilder() {
|
|
@@ -712,6 +1022,7 @@ function defineSchema(builder, options) {
|
|
|
712
1022
|
return {
|
|
713
1023
|
definition,
|
|
714
1024
|
promptTemplate: options?.promptTemplate,
|
|
1025
|
+
defaultRules: options?.defaultRules,
|
|
715
1026
|
createCatalog(catalog) {
|
|
716
1027
|
return createCatalogFromSchema(this, catalog);
|
|
717
1028
|
}
|
|
@@ -867,15 +1178,95 @@ function generatePrompt(catalog, options) {
|
|
|
867
1178
|
"Output JSONL (one JSON object per line) with patches to build a UI tree."
|
|
868
1179
|
);
|
|
869
1180
|
lines.push(
|
|
870
|
-
"Each line is a JSON patch operation. Start with
|
|
1181
|
+
"Each line is a JSON patch operation. Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams."
|
|
871
1182
|
);
|
|
872
1183
|
lines.push("");
|
|
873
1184
|
lines.push("Example output (each line is a separate JSON object):");
|
|
874
1185
|
lines.push("");
|
|
875
|
-
lines.push(`{"op":"
|
|
876
|
-
{"op":"
|
|
877
|
-
{"op":"
|
|
878
|
-
{"op":"
|
|
1186
|
+
lines.push(`{"op":"add","path":"/root","value":"blog"}
|
|
1187
|
+
{"op":"add","path":"/elements/blog","value":{"type":"Stack","props":{"direction":"vertical","gap":"md"},"children":["heading","posts-grid"]}}
|
|
1188
|
+
{"op":"add","path":"/elements/heading","value":{"type":"Heading","props":{"text":"Blog","level":"h1"},"children":[]}}
|
|
1189
|
+
{"op":"add","path":"/elements/posts-grid","value":{"type":"Grid","props":{"columns":2,"gap":"md"},"repeat":{"path":"/posts","key":"id"},"children":["post-card"]}}
|
|
1190
|
+
{"op":"add","path":"/elements/post-card","value":{"type":"Card","props":{"title":{"$path":"$item/title"}},"children":["post-meta"]}}
|
|
1191
|
+
{"op":"add","path":"/elements/post-meta","value":{"type":"Text","props":{"text":{"$path":"$item/author"},"variant":"muted"},"children":[]}}
|
|
1192
|
+
{"op":"add","path":"/state/posts","value":[]}
|
|
1193
|
+
{"op":"add","path":"/state/posts/0","value":{"id":"1","title":"Getting Started","author":"Jane","date":"Jan 15"}}
|
|
1194
|
+
{"op":"add","path":"/state/posts/1","value":{"id":"2","title":"Advanced Tips","author":"Bob","date":"Feb 3"}}
|
|
1195
|
+
|
|
1196
|
+
Note: state patches appear right after the elements that use them, so the UI fills in as it streams.`);
|
|
1197
|
+
lines.push("");
|
|
1198
|
+
lines.push("INITIAL STATE:");
|
|
1199
|
+
lines.push(
|
|
1200
|
+
"Specs include a /state field to seed the state model. Components with statePath read from and write to this state, and $path expressions read from it."
|
|
1201
|
+
);
|
|
1202
|
+
lines.push(
|
|
1203
|
+
"CRITICAL: You MUST include state patches whenever your UI displays data via $path expressions, uses repeat to iterate over arrays, or uses statePath bindings. Without state, $path references resolve to nothing and repeat lists render zero items."
|
|
1204
|
+
);
|
|
1205
|
+
lines.push(
|
|
1206
|
+
"Output state patches right after the elements that reference them, so the UI fills in progressively as it streams."
|
|
1207
|
+
);
|
|
1208
|
+
lines.push(
|
|
1209
|
+
"Stream state progressively - output one patch per array item instead of one giant blob:"
|
|
1210
|
+
);
|
|
1211
|
+
lines.push(
|
|
1212
|
+
' For arrays: {"op":"add","path":"/state/posts/0","value":{"id":"1","title":"First Post",...}} then /state/posts/1, /state/posts/2, etc.'
|
|
1213
|
+
);
|
|
1214
|
+
lines.push(
|
|
1215
|
+
' For scalars: {"op":"add","path":"/state/newTodoText","value":""}'
|
|
1216
|
+
);
|
|
1217
|
+
lines.push(
|
|
1218
|
+
' Initialize the array first if needed: {"op":"add","path":"/state/posts","value":[]}'
|
|
1219
|
+
);
|
|
1220
|
+
lines.push(
|
|
1221
|
+
'When content comes from the state model, use { "$path": "/some/path" } dynamic props to display it instead of hardcoding the same value in both state and props. The state model is the single source of truth.'
|
|
1222
|
+
);
|
|
1223
|
+
lines.push(
|
|
1224
|
+
"Include realistic sample data in state. For blogs: 3-4 posts with titles, excerpts, authors, dates. For product lists: 3-5 items with names, prices, descriptions. Never leave arrays empty."
|
|
1225
|
+
);
|
|
1226
|
+
lines.push("");
|
|
1227
|
+
lines.push("DYNAMIC LISTS (repeat field):");
|
|
1228
|
+
lines.push(
|
|
1229
|
+
'Any element can have a top-level "repeat" field to render its children once per item in a state array: { "repeat": { "path": "/arrayPath", "key": "id" } }.'
|
|
1230
|
+
);
|
|
1231
|
+
lines.push(
|
|
1232
|
+
'The element itself renders once (as the container), and its children are expanded once per array item. "path" is the state array path. "key" is an optional field name on each item for stable React keys.'
|
|
1233
|
+
);
|
|
1234
|
+
lines.push(
|
|
1235
|
+
'Example: { "type": "Column", "props": { "gap": 8 }, "repeat": { "path": "/todos", "key": "id" }, "children": ["todo-item"] }'
|
|
1236
|
+
);
|
|
1237
|
+
lines.push(
|
|
1238
|
+
'Inside children of a repeated element, use "$item/field" for per-item paths: statePath:"$item/completed", { "$path": "$item/title" }. Use "$index" for the current array index.'
|
|
1239
|
+
);
|
|
1240
|
+
lines.push(
|
|
1241
|
+
"ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item."
|
|
1242
|
+
);
|
|
1243
|
+
lines.push(
|
|
1244
|
+
'IMPORTANT: "repeat" is a top-level field on the element (sibling of type/props/children), NOT inside props.'
|
|
1245
|
+
);
|
|
1246
|
+
lines.push("");
|
|
1247
|
+
lines.push("ARRAY STATE ACTIONS:");
|
|
1248
|
+
lines.push(
|
|
1249
|
+
'Use action "pushState" to append items to arrays. Params: { path: "/arrayPath", value: { ...item }, clearPath: "/inputPath" }.'
|
|
1250
|
+
);
|
|
1251
|
+
lines.push(
|
|
1252
|
+
'Values inside pushState can contain { "path": "/statePath" } references to read current state (e.g. the text from an input field).'
|
|
1253
|
+
);
|
|
1254
|
+
lines.push(
|
|
1255
|
+
'Use "$id" inside a pushState value to auto-generate a unique ID.'
|
|
1256
|
+
);
|
|
1257
|
+
lines.push(
|
|
1258
|
+
'Example: on: { "press": { "action": "pushState", "params": { "path": "/todos", "value": { "id": "$id", "title": { "path": "/newTodoText" }, "completed": false }, "clearPath": "/newTodoText" } } }'
|
|
1259
|
+
);
|
|
1260
|
+
lines.push(
|
|
1261
|
+
`Use action "removeState" to remove items from arrays by index. Params: { path: "/arrayPath", index: N }. Inside a repeated element's children, use "$index" for the current item index.`
|
|
1262
|
+
);
|
|
1263
|
+
lines.push(
|
|
1264
|
+
"For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState."
|
|
1265
|
+
);
|
|
1266
|
+
lines.push("");
|
|
1267
|
+
lines.push(
|
|
1268
|
+
'IMPORTANT: State paths use RFC 6901 JSON Pointer syntax (e.g. "/todos/0/title"). Do NOT use JavaScript-style dot notation (e.g. "/todos.length" is WRONG). To generate unique IDs for new items, use "$id" instead of trying to read array length.'
|
|
1269
|
+
);
|
|
879
1270
|
lines.push("");
|
|
880
1271
|
const components = catalog.data.components;
|
|
881
1272
|
if (components) {
|
|
@@ -885,8 +1276,9 @@ function generatePrompt(catalog, options) {
|
|
|
885
1276
|
const propsStr = def.props ? formatZodType(def.props) : "{}";
|
|
886
1277
|
const hasChildren = def.slots && def.slots.length > 0;
|
|
887
1278
|
const childrenStr = hasChildren ? " [accepts children]" : "";
|
|
1279
|
+
const eventsStr = def.events && def.events.length > 0 ? ` [events: ${def.events.join(", ")}]` : "";
|
|
888
1280
|
const descStr = def.description ? ` - ${def.description}` : "";
|
|
889
|
-
lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}`);
|
|
1281
|
+
lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}${eventsStr}`);
|
|
890
1282
|
}
|
|
891
1283
|
lines.push("");
|
|
892
1284
|
}
|
|
@@ -899,17 +1291,90 @@ function generatePrompt(catalog, options) {
|
|
|
899
1291
|
}
|
|
900
1292
|
lines.push("");
|
|
901
1293
|
}
|
|
1294
|
+
lines.push("EVENTS (the `on` field):");
|
|
1295
|
+
lines.push(
|
|
1296
|
+
"Elements can have an optional `on` field to bind events to actions. The `on` field is a top-level field on the element (sibling of type/props/children), NOT inside props."
|
|
1297
|
+
);
|
|
1298
|
+
lines.push(
|
|
1299
|
+
'Each key in `on` is an event name (from the component\'s supported events), and the value is an action binding: `{ "action": "<actionName>", "params": { ... } }`.'
|
|
1300
|
+
);
|
|
1301
|
+
lines.push("");
|
|
1302
|
+
lines.push("Example:");
|
|
1303
|
+
lines.push(
|
|
1304
|
+
' {"type":"Button","props":{"label":"Save"},"on":{"press":{"action":"setState","params":{"path":"/saved","value":true}}},"children":[]}'
|
|
1305
|
+
);
|
|
1306
|
+
lines.push("");
|
|
1307
|
+
lines.push(
|
|
1308
|
+
'Action params can use dynamic path references to read from state: { "path": "/statePath" }.'
|
|
1309
|
+
);
|
|
1310
|
+
lines.push(
|
|
1311
|
+
"IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings."
|
|
1312
|
+
);
|
|
1313
|
+
lines.push("");
|
|
1314
|
+
lines.push("VISIBILITY CONDITIONS:");
|
|
1315
|
+
lines.push(
|
|
1316
|
+
"Elements can have an optional `visible` field to conditionally show/hide based on data state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props."
|
|
1317
|
+
);
|
|
1318
|
+
lines.push(
|
|
1319
|
+
'Correct: {"type":"Column","props":{"gap":8},"visible":{"eq":[{"path":"/tab"},"home"]},"children":[...]}'
|
|
1320
|
+
);
|
|
1321
|
+
lines.push(
|
|
1322
|
+
'- `{ "eq": [{ "path": "/statePath" }, "value"] }` - visible when state at path equals value'
|
|
1323
|
+
);
|
|
1324
|
+
lines.push(
|
|
1325
|
+
'- `{ "neq": [{ "path": "/statePath" }, "value"] }` - visible when state at path does not equal value'
|
|
1326
|
+
);
|
|
1327
|
+
lines.push('- `{ "path": "/statePath" }` - visible when path is truthy');
|
|
1328
|
+
lines.push(
|
|
1329
|
+
'- `{ "and": [...] }`, `{ "or": [...] }`, `{ "not": {...} }` - combine conditions'
|
|
1330
|
+
);
|
|
1331
|
+
lines.push("- `true` / `false` - always visible/hidden");
|
|
1332
|
+
lines.push("");
|
|
1333
|
+
lines.push(
|
|
1334
|
+
"Use the Pressable component with on.press bound to setState to update state and drive visibility."
|
|
1335
|
+
);
|
|
1336
|
+
lines.push(
|
|
1337
|
+
'Example: A Pressable with on: { "press": { "action": "setState", "params": { "path": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "eq": [{ "path": "/activeTab" }, "home"] } shows only when that tab is active.'
|
|
1338
|
+
);
|
|
1339
|
+
lines.push("");
|
|
1340
|
+
lines.push("DYNAMIC PROPS:");
|
|
1341
|
+
lines.push(
|
|
1342
|
+
"Any prop value can be a dynamic expression that resolves based on state. Two forms are supported:"
|
|
1343
|
+
);
|
|
1344
|
+
lines.push("");
|
|
1345
|
+
lines.push(
|
|
1346
|
+
'1. State binding: `{ "$path": "/statePath" }` - resolves to the value at that state path.'
|
|
1347
|
+
);
|
|
1348
|
+
lines.push(
|
|
1349
|
+
' Example: `"color": { "$path": "/theme/primary" }` reads the color from state.'
|
|
1350
|
+
);
|
|
1351
|
+
lines.push("");
|
|
1352
|
+
lines.push(
|
|
1353
|
+
'2. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.'
|
|
1354
|
+
);
|
|
1355
|
+
lines.push(
|
|
1356
|
+
' Example: `"color": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "#007AFF", "$else": "#8E8E93" }`'
|
|
1357
|
+
);
|
|
1358
|
+
lines.push(
|
|
1359
|
+
' Example: `"name": { "$cond": { "eq": [{ "path": "/activeTab" }, "home"] }, "$then": "home", "$else": "home-outline" }`'
|
|
1360
|
+
);
|
|
1361
|
+
lines.push("");
|
|
1362
|
+
lines.push(
|
|
1363
|
+
"Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ."
|
|
1364
|
+
);
|
|
1365
|
+
lines.push("");
|
|
902
1366
|
lines.push("RULES:");
|
|
903
1367
|
const baseRules = [
|
|
904
1368
|
"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
|
|
905
|
-
'First
|
|
906
|
-
'Then add each element: {"op":"
|
|
1369
|
+
'First set root: {"op":"add","path":"/root","value":"<root-key>"}',
|
|
1370
|
+
'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
1371
|
+
"Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $path, repeat, or statePath.",
|
|
907
1372
|
"ONLY use components listed above",
|
|
908
|
-
"Each element value needs:
|
|
909
|
-
"Use unique keys (e.g., 'header', 'metric-1', 'chart-revenue')"
|
|
910
|
-
"Root element's parentKey is empty string, children reference their parent's key"
|
|
1373
|
+
"Each element value needs: type, props, children (array of child keys)",
|
|
1374
|
+
"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')"
|
|
911
1375
|
];
|
|
912
|
-
const
|
|
1376
|
+
const schemaRules = catalog.schema.defaultRules ?? [];
|
|
1377
|
+
const allRules = [...baseRules, ...schemaRules, ...customRules];
|
|
913
1378
|
allRules.forEach((rule, i) => {
|
|
914
1379
|
lines.push(`${i + 1}. ${rule}`);
|
|
915
1380
|
});
|
|
@@ -1047,6 +1512,56 @@ function defineCatalog(schema, catalog) {
|
|
|
1047
1512
|
return schema.createCatalog(catalog);
|
|
1048
1513
|
}
|
|
1049
1514
|
|
|
1515
|
+
// src/prompt.ts
|
|
1516
|
+
function isNonEmptySpec(spec) {
|
|
1517
|
+
if (!spec || typeof spec !== "object") return false;
|
|
1518
|
+
const s = spec;
|
|
1519
|
+
return typeof s.root === "string" && typeof s.elements === "object" && s.elements !== null && Object.keys(s.elements).length > 0;
|
|
1520
|
+
}
|
|
1521
|
+
var PATCH_INSTRUCTIONS = `IMPORTANT: The current UI is already loaded. Output ONLY the patches needed to make the requested change:
|
|
1522
|
+
- To add a new element: {"op":"add","path":"/elements/new-key","value":{...}}
|
|
1523
|
+
- To modify an existing element: {"op":"replace","path":"/elements/existing-key","value":{...}}
|
|
1524
|
+
- To remove an element: {"op":"remove","path":"/elements/old-key"}
|
|
1525
|
+
- To update the root: {"op":"replace","path":"/root","value":"new-root-key"}
|
|
1526
|
+
- To add children: update the parent element with new children array
|
|
1527
|
+
|
|
1528
|
+
DO NOT output patches for elements that don't need to change. Only output what's necessary for the requested modification.`;
|
|
1529
|
+
function buildUserPrompt(options) {
|
|
1530
|
+
const { prompt, currentSpec, state, maxPromptLength } = options;
|
|
1531
|
+
let userText = String(prompt || "");
|
|
1532
|
+
if (maxPromptLength !== void 0 && maxPromptLength > 0) {
|
|
1533
|
+
userText = userText.slice(0, maxPromptLength);
|
|
1534
|
+
}
|
|
1535
|
+
if (isNonEmptySpec(currentSpec)) {
|
|
1536
|
+
const parts2 = [];
|
|
1537
|
+
parts2.push(
|
|
1538
|
+
`CURRENT UI STATE (already loaded, DO NOT recreate existing elements):`
|
|
1539
|
+
);
|
|
1540
|
+
parts2.push(JSON.stringify(currentSpec, null, 2));
|
|
1541
|
+
parts2.push("");
|
|
1542
|
+
parts2.push(`USER REQUEST: ${userText}`);
|
|
1543
|
+
if (state && Object.keys(state).length > 0) {
|
|
1544
|
+
parts2.push("");
|
|
1545
|
+
parts2.push(`AVAILABLE STATE:
|
|
1546
|
+
${JSON.stringify(state, null, 2)}`);
|
|
1547
|
+
}
|
|
1548
|
+
parts2.push("");
|
|
1549
|
+
parts2.push(PATCH_INSTRUCTIONS);
|
|
1550
|
+
return parts2.join("\n");
|
|
1551
|
+
}
|
|
1552
|
+
const parts = [userText];
|
|
1553
|
+
if (state && Object.keys(state).length > 0) {
|
|
1554
|
+
parts.push(`
|
|
1555
|
+
AVAILABLE STATE:
|
|
1556
|
+
${JSON.stringify(state, null, 2)}`);
|
|
1557
|
+
}
|
|
1558
|
+
parts.push(
|
|
1559
|
+
`
|
|
1560
|
+
Remember: Output /root first, then interleave /elements and /state patches so the UI fills in progressively as it streams. Output each state patch right after the elements that use it, one per array item.`
|
|
1561
|
+
);
|
|
1562
|
+
return parts.join("\n");
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1050
1565
|
// src/catalog.ts
|
|
1051
1566
|
import { z as z6 } from "zod";
|
|
1052
1567
|
function createCatalog(config) {
|
|
@@ -1063,22 +1578,18 @@ function createCatalog(config) {
|
|
|
1063
1578
|
const componentSchemas = componentNames.map((componentName) => {
|
|
1064
1579
|
const def = components[componentName];
|
|
1065
1580
|
return z6.object({
|
|
1066
|
-
key: z6.string(),
|
|
1067
1581
|
type: z6.literal(componentName),
|
|
1068
1582
|
props: def.props,
|
|
1069
1583
|
children: z6.array(z6.string()).optional(),
|
|
1070
|
-
parentKey: z6.string().nullable().optional(),
|
|
1071
1584
|
visible: VisibilityConditionSchema.optional()
|
|
1072
1585
|
});
|
|
1073
1586
|
});
|
|
1074
1587
|
let elementSchema;
|
|
1075
1588
|
if (componentSchemas.length === 0) {
|
|
1076
1589
|
elementSchema = z6.object({
|
|
1077
|
-
key: z6.string(),
|
|
1078
1590
|
type: z6.string(),
|
|
1079
1591
|
props: z6.record(z6.string(), z6.unknown()),
|
|
1080
1592
|
children: z6.array(z6.string()).optional(),
|
|
1081
|
-
parentKey: z6.string().nullable().optional(),
|
|
1082
1593
|
visible: VisibilityConditionSchema.optional()
|
|
1083
1594
|
});
|
|
1084
1595
|
} else if (componentSchemas.length === 1) {
|
|
@@ -1160,7 +1671,7 @@ function generateCatalogPrompt(catalog) {
|
|
|
1160
1671
|
lines.push("");
|
|
1161
1672
|
lines.push("Components can have a `visible` property:");
|
|
1162
1673
|
lines.push("- `true` / `false` - Always visible/hidden");
|
|
1163
|
-
lines.push('- `{ "path": "/
|
|
1674
|
+
lines.push('- `{ "path": "/state/path" }` - Visible when path is truthy');
|
|
1164
1675
|
lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
|
|
1165
1676
|
lines.push('- `{ "and": [...] }` - All conditions must be true');
|
|
1166
1677
|
lines.push('- `{ "or": [...] }` - Any condition must be true');
|
|
@@ -1299,21 +1810,21 @@ function generateSystemPrompt(catalog, options = {}) {
|
|
|
1299
1810
|
}
|
|
1300
1811
|
lines.push("");
|
|
1301
1812
|
}
|
|
1302
|
-
lines.push("OUTPUT FORMAT (JSONL):");
|
|
1303
|
-
lines.push('{"op":"
|
|
1813
|
+
lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):");
|
|
1814
|
+
lines.push('{"op":"add","path":"/root","value":"element-key"}');
|
|
1304
1815
|
lines.push(
|
|
1305
|
-
'{"op":"add","path":"/elements/key","value":{"
|
|
1816
|
+
'{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}'
|
|
1306
1817
|
);
|
|
1307
1818
|
lines.push('{"op":"remove","path":"/elements/key"}');
|
|
1308
1819
|
lines.push("");
|
|
1309
1820
|
lines.push("RULES:");
|
|
1310
1821
|
const baseRules = [
|
|
1311
|
-
|
|
1312
|
-
|
|
1822
|
+
'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}',
|
|
1823
|
+
'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}',
|
|
1313
1824
|
"Remove elements with op:remove - also update the parent's children array to exclude the removed key",
|
|
1314
1825
|
"Children array contains string keys, not objects",
|
|
1315
1826
|
"Parent first, then children",
|
|
1316
|
-
"Each element needs:
|
|
1827
|
+
"Each element needs: type, props",
|
|
1317
1828
|
"ONLY use props listed above - never invent new props"
|
|
1318
1829
|
];
|
|
1319
1830
|
const allRules = [...baseRules, ...customRules];
|
|
@@ -1330,6 +1841,7 @@ function generateSystemPrompt(catalog, options = {}) {
|
|
|
1330
1841
|
return lines.join("\n");
|
|
1331
1842
|
}
|
|
1332
1843
|
export {
|
|
1844
|
+
ActionBindingSchema,
|
|
1333
1845
|
ActionConfirmSchema,
|
|
1334
1846
|
ActionOnErrorSchema,
|
|
1335
1847
|
ActionOnSuccessSchema,
|
|
@@ -1343,7 +1855,11 @@ export {
|
|
|
1343
1855
|
ValidationConfigSchema,
|
|
1344
1856
|
VisibilityConditionSchema,
|
|
1345
1857
|
action,
|
|
1858
|
+
actionBinding,
|
|
1859
|
+
addByPath,
|
|
1346
1860
|
applySpecStreamPatch,
|
|
1861
|
+
autoFixSpec,
|
|
1862
|
+
buildUserPrompt,
|
|
1347
1863
|
builtInValidationFunctions,
|
|
1348
1864
|
check,
|
|
1349
1865
|
compileSpecStream,
|
|
@@ -1355,16 +1871,21 @@ export {
|
|
|
1355
1871
|
evaluateVisibility,
|
|
1356
1872
|
executeAction,
|
|
1357
1873
|
findFormValue,
|
|
1874
|
+
formatSpecIssues,
|
|
1358
1875
|
generateCatalogPrompt,
|
|
1359
1876
|
generateSystemPrompt,
|
|
1360
1877
|
getByPath,
|
|
1361
1878
|
interpolateString,
|
|
1362
1879
|
parseSpecStreamLine,
|
|
1880
|
+
removeByPath,
|
|
1363
1881
|
resolveAction,
|
|
1364
1882
|
resolveDynamicValue,
|
|
1883
|
+
resolveElementProps,
|
|
1884
|
+
resolvePropValue,
|
|
1365
1885
|
runValidation,
|
|
1366
1886
|
runValidationCheck,
|
|
1367
1887
|
setByPath,
|
|
1888
|
+
validateSpec,
|
|
1368
1889
|
visibility
|
|
1369
1890
|
};
|
|
1370
1891
|
//# sourceMappingURL=index.mjs.map
|