@json-render/core
Advanced tools
| // src/types.ts | ||
| import { z } from "zod"; | ||
| var DynamicValueSchema = z.union([ | ||
| z.string(), | ||
| z.number(), | ||
| z.boolean(), | ||
| z.null(), | ||
| z.object({ $state: z.string() }) | ||
| ]); | ||
| var DynamicStringSchema = z.union([ | ||
| z.string(), | ||
| z.object({ $state: z.string() }) | ||
| ]); | ||
| var DynamicNumberSchema = z.union([ | ||
| z.number(), | ||
| z.object({ $state: z.string() }) | ||
| ]); | ||
| var DynamicBooleanSchema = z.union([ | ||
| z.boolean(), | ||
| z.object({ $state: z.string() }) | ||
| ]); | ||
| function resolveDynamicValue(value, stateModel) { | ||
| if (value === null || value === void 0) { | ||
| return void 0; | ||
| } | ||
| if (typeof value === "object" && "$state" in value) { | ||
| return getByPath(stateModel, value.$state); | ||
| } | ||
| return value; | ||
| } | ||
| function unescapeJsonPointer(token) { | ||
| return token.replace(/~1/g, "/").replace(/~0/g, "~"); | ||
| } | ||
| function parseJsonPointer(path) { | ||
| const raw = path.startsWith("/") ? path.slice(1).split("/") : path.split("/"); | ||
| return raw.map(unescapeJsonPointer); | ||
| } | ||
| function getByPath(obj, path) { | ||
| if (!path || path === "/") { | ||
| return obj; | ||
| } | ||
| const segments = parseJsonPointer(path); | ||
| let current = obj; | ||
| for (const segment of segments) { | ||
| if (current === null || current === void 0) { | ||
| return void 0; | ||
| } | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| current = current[index]; | ||
| } else if (typeof current === "object") { | ||
| current = current[segment]; | ||
| } else { | ||
| return void 0; | ||
| } | ||
| } | ||
| return current; | ||
| } | ||
| function resolveRepeatStatePath(statePath, repeatBasePath) { | ||
| if (typeof statePath === "string") { | ||
| return statePath; | ||
| } | ||
| if (repeatBasePath == null) { | ||
| return void 0; | ||
| } | ||
| if (statePath.$item === "" || statePath.$item === "/") { | ||
| return repeatBasePath; | ||
| } | ||
| return joinStatePath(repeatBasePath, statePath.$item); | ||
| } | ||
| function resolveRepeatItemStatePath(statePath, index) { | ||
| return joinStatePath(statePath, String(index)); | ||
| } | ||
| function joinStatePath(basePath, childPath) { | ||
| const child = childPath.startsWith("/") ? childPath.slice(1) : childPath; | ||
| if (basePath === "" || basePath === "/") { | ||
| return `/${child}`; | ||
| } | ||
| return `${basePath}/${child}`; | ||
| } | ||
| function isNumericIndex(str) { | ||
| return /^\d+$/.test(str); | ||
| } | ||
| function setByPath(obj, path, value) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| const nextSegment = segments[i + 1]; | ||
| const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-"); | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| current[index] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| current[segment] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| if (lastSegment === "-") { | ||
| current.push(value); | ||
| } else { | ||
| const index = parseInt(lastSegment, 10); | ||
| current[index] = value; | ||
| } | ||
| } else { | ||
| current[lastSegment] = value; | ||
| } | ||
| } | ||
| function addByPath(obj, path, value) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| const nextSegment = segments[i + 1]; | ||
| const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-"); | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| current[index] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| current[segment] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| if (lastSegment === "-") { | ||
| current.push(value); | ||
| } else { | ||
| const index = parseInt(lastSegment, 10); | ||
| current.splice(index, 0, value); | ||
| } | ||
| } else { | ||
| current[lastSegment] = value; | ||
| } | ||
| } | ||
| function removeByPath(obj, path) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| return; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| return; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(lastSegment, 10); | ||
| if (index >= 0 && index < current.length) { | ||
| current.splice(index, 1); | ||
| } | ||
| } else { | ||
| delete current[lastSegment]; | ||
| } | ||
| } | ||
| function deepEqual(a, b) { | ||
| if (a === b) return true; | ||
| if (a === null || b === null) return false; | ||
| if (typeof a !== typeof b) return false; | ||
| if (typeof a !== "object") return false; | ||
| if (Array.isArray(a)) { | ||
| if (!Array.isArray(b)) return false; | ||
| if (a.length !== b.length) return false; | ||
| return a.every((item, i) => deepEqual(item, b[i])); | ||
| } | ||
| const aObj = a; | ||
| const bObj = b; | ||
| const aKeys = Object.keys(aObj); | ||
| const bKeys = Object.keys(bObj); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| return aKeys.every((key) => deepEqual(aObj[key], bObj[key])); | ||
| } | ||
| function findFormValue(fieldName, params, state) { | ||
| if (params?.[fieldName] !== void 0) { | ||
| const val = params[fieldName]; | ||
| if (typeof val !== "string" || !val.includes(".")) { | ||
| return val; | ||
| } | ||
| } | ||
| if (params) { | ||
| for (const key of Object.keys(params)) { | ||
| if (key.endsWith(`.${fieldName}`)) { | ||
| const val = params[key]; | ||
| if (typeof val !== "string" || !val.includes(".")) { | ||
| return val; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (state) { | ||
| for (const key of Object.keys(state)) { | ||
| if (key === fieldName || key.endsWith(`.${fieldName}`)) { | ||
| return state[key]; | ||
| } | ||
| } | ||
| const val = getByPath(state, fieldName); | ||
| if (val !== void 0) { | ||
| return val; | ||
| } | ||
| } | ||
| return void 0; | ||
| } | ||
| function parseSpecStreamLine(line) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || !trimmed.startsWith("{")) return null; | ||
| try { | ||
| const patch = JSON.parse(trimmed); | ||
| if (patch.op && patch.path !== void 0) { | ||
| return patch; | ||
| } | ||
| return null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function applySpecStreamPatch(obj, patch) { | ||
| switch (patch.op) { | ||
| case "add": | ||
| addByPath(obj, patch.path, patch.value); | ||
| break; | ||
| case "replace": | ||
| setByPath(obj, patch.path, patch.value); | ||
| break; | ||
| case "remove": | ||
| removeByPath(obj, patch.path); | ||
| break; | ||
| case "move": { | ||
| if (!patch.from) break; | ||
| const moveValue = getByPath(obj, patch.from); | ||
| removeByPath(obj, patch.from); | ||
| addByPath(obj, patch.path, moveValue); | ||
| break; | ||
| } | ||
| case "copy": { | ||
| if (!patch.from) break; | ||
| const copyValue = getByPath(obj, patch.from); | ||
| addByPath(obj, patch.path, copyValue); | ||
| break; | ||
| } | ||
| case "test": { | ||
| const actual = getByPath(obj, patch.path); | ||
| if (!deepEqual(actual, patch.value)) { | ||
| throw new Error( | ||
| `Test operation failed: value at "${patch.path}" does not match` | ||
| ); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| return obj; | ||
| } | ||
| function applySpecPatch(spec, patch) { | ||
| applySpecStreamPatch(spec, patch); | ||
| return spec; | ||
| } | ||
| function nestedToFlat(nested) { | ||
| const elements = {}; | ||
| let counter = 0; | ||
| function walk(node) { | ||
| const key = `el-${counter++}`; | ||
| const { | ||
| type, | ||
| props, | ||
| children: rawChildren, | ||
| slots: rawSlots, | ||
| ...rest | ||
| } = node; | ||
| const childKeys = []; | ||
| if (Array.isArray(rawChildren)) { | ||
| for (const child of rawChildren) { | ||
| if (child && typeof child === "object" && "type" in child) { | ||
| childKeys.push(walk(child)); | ||
| } | ||
| } | ||
| } | ||
| const slots = {}; | ||
| if (rawSlots && typeof rawSlots === "object") { | ||
| for (const [slotName, slotChildren] of Object.entries(rawSlots)) { | ||
| if (!Array.isArray(slotChildren)) continue; | ||
| slots[slotName] = slotChildren.flatMap( | ||
| (child) => child && typeof child === "object" && "type" in child ? [walk(child)] : [] | ||
| ); | ||
| } | ||
| } | ||
| const element = { | ||
| type: type ?? "unknown", | ||
| props: props ?? {}, | ||
| children: childKeys, | ||
| ...Object.keys(slots).length > 0 ? { slots } : {} | ||
| }; | ||
| for (const [k, v] of Object.entries(rest)) { | ||
| if (k !== "state" && v !== void 0) { | ||
| element[k] = v; | ||
| } | ||
| } | ||
| elements[key] = element; | ||
| return key; | ||
| } | ||
| const root = walk(nested); | ||
| const spec = { root, elements }; | ||
| if (nested.state && typeof nested.state === "object" && !Array.isArray(nested.state)) { | ||
| spec.state = nested.state; | ||
| } | ||
| return spec; | ||
| } | ||
| function compileSpecStream(stream, initial = {}) { | ||
| const lines = stream.split("\n"); | ||
| const result = { ...initial }; | ||
| for (const line of lines) { | ||
| const patch = parseSpecStreamLine(line); | ||
| if (patch) { | ||
| applySpecStreamPatch(result, patch); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function createSpecStreamCompiler(initial = {}) { | ||
| let result = { ...initial }; | ||
| let buffer = ""; | ||
| const appliedPatches = []; | ||
| const processedLines = /* @__PURE__ */ new Set(); | ||
| return { | ||
| push(chunk) { | ||
| buffer += chunk; | ||
| const newPatches = []; | ||
| const lines = buffer.split("\n"); | ||
| buffer = lines.pop() || ""; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || processedLines.has(trimmed)) continue; | ||
| processedLines.add(trimmed); | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) { | ||
| applySpecStreamPatch(result, patch); | ||
| appliedPatches.push(patch); | ||
| newPatches.push(patch); | ||
| } | ||
| } | ||
| if (newPatches.length > 0) { | ||
| result = { ...result }; | ||
| } | ||
| return { result, newPatches }; | ||
| }, | ||
| getResult() { | ||
| if (buffer.trim()) { | ||
| const patch = parseSpecStreamLine(buffer); | ||
| if (patch && !processedLines.has(buffer.trim())) { | ||
| processedLines.add(buffer.trim()); | ||
| applySpecStreamPatch(result, patch); | ||
| appliedPatches.push(patch); | ||
| result = { ...result }; | ||
| } | ||
| buffer = ""; | ||
| } | ||
| return result; | ||
| }, | ||
| getPatches() { | ||
| return [...appliedPatches]; | ||
| }, | ||
| reset(newInitial = {}) { | ||
| result = { ...newInitial }; | ||
| buffer = ""; | ||
| appliedPatches.length = 0; | ||
| processedLines.clear(); | ||
| } | ||
| }; | ||
| } | ||
| function createMixedStreamParser(callbacks) { | ||
| let buffer = ""; | ||
| let inSpecFence = false; | ||
| function processLine(line) { | ||
| const trimmed = line.trim(); | ||
| if (!inSpecFence && trimmed.startsWith("```spec")) { | ||
| inSpecFence = true; | ||
| return; | ||
| } | ||
| if (inSpecFence && trimmed === "```") { | ||
| inSpecFence = false; | ||
| return; | ||
| } | ||
| if (!trimmed) return; | ||
| if (inSpecFence) { | ||
| const patch2 = parseSpecStreamLine(trimmed); | ||
| if (patch2) { | ||
| callbacks.onPatch(patch2); | ||
| } | ||
| return; | ||
| } | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) { | ||
| callbacks.onPatch(patch); | ||
| } else { | ||
| callbacks.onText(line); | ||
| } | ||
| } | ||
| return { | ||
| push(chunk) { | ||
| buffer += chunk; | ||
| const lines = buffer.split("\n"); | ||
| buffer = lines.pop() || ""; | ||
| for (const line of lines) { | ||
| processLine(line); | ||
| } | ||
| }, | ||
| flush() { | ||
| if (buffer.trim()) { | ||
| processLine(buffer); | ||
| } | ||
| buffer = ""; | ||
| } | ||
| }; | ||
| } | ||
| var SPEC_FENCE_OPEN = "```spec"; | ||
| var SPEC_FENCE_CLOSE = "```"; | ||
| function createJsonRenderTransform() { | ||
| let lineBuffer = ""; | ||
| let currentTextId = ""; | ||
| let buffering = false; | ||
| let inSpecFence = false; | ||
| let inTextBlock = false; | ||
| let textIdCounter = 0; | ||
| function closeTextBlock(controller) { | ||
| if (inTextBlock) { | ||
| controller.enqueue({ type: "text-end", id: currentTextId }); | ||
| inTextBlock = false; | ||
| } | ||
| } | ||
| function ensureTextBlock(controller) { | ||
| if (!inTextBlock) { | ||
| textIdCounter++; | ||
| currentTextId = String(textIdCounter); | ||
| controller.enqueue({ type: "text-start", id: currentTextId }); | ||
| inTextBlock = true; | ||
| } | ||
| } | ||
| function emitTextDelta(delta, controller) { | ||
| ensureTextBlock(controller); | ||
| controller.enqueue({ type: "text-delta", id: currentTextId, delta }); | ||
| } | ||
| function emitPatch(patch, controller) { | ||
| closeTextBlock(controller); | ||
| controller.enqueue({ | ||
| type: SPEC_DATA_PART_TYPE, | ||
| data: { type: "patch", patch } | ||
| }); | ||
| } | ||
| function flushBuffer(controller) { | ||
| if (!lineBuffer) return; | ||
| const trimmed = lineBuffer.trim(); | ||
| if (inSpecFence) { | ||
| if (trimmed) { | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) emitPatch(patch, controller); | ||
| } | ||
| lineBuffer = ""; | ||
| buffering = false; | ||
| return; | ||
| } | ||
| if (trimmed) { | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) { | ||
| emitPatch(patch, controller); | ||
| } else { | ||
| emitTextDelta(lineBuffer, controller); | ||
| } | ||
| } else { | ||
| emitTextDelta(lineBuffer, controller); | ||
| } | ||
| lineBuffer = ""; | ||
| buffering = false; | ||
| } | ||
| function processCompleteLine(line, controller) { | ||
| const trimmed = line.trim(); | ||
| if (!inSpecFence && trimmed.startsWith(SPEC_FENCE_OPEN)) { | ||
| inSpecFence = true; | ||
| return; | ||
| } | ||
| if (inSpecFence && trimmed === SPEC_FENCE_CLOSE) { | ||
| inSpecFence = false; | ||
| return; | ||
| } | ||
| if (inSpecFence) { | ||
| if (trimmed) { | ||
| const patch2 = parseSpecStreamLine(trimmed); | ||
| if (patch2) emitPatch(patch2, controller); | ||
| } | ||
| return; | ||
| } | ||
| if (!trimmed) { | ||
| emitTextDelta("\n", controller); | ||
| return; | ||
| } | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) { | ||
| emitPatch(patch, controller); | ||
| } else { | ||
| emitTextDelta(line + "\n", controller); | ||
| } | ||
| } | ||
| return new TransformStream({ | ||
| transform(chunk, controller) { | ||
| switch (chunk.type) { | ||
| case "text-start": { | ||
| const id = chunk.id; | ||
| const idNum = parseInt(id, 10); | ||
| if (!isNaN(idNum) && idNum >= textIdCounter) { | ||
| textIdCounter = idNum; | ||
| } | ||
| currentTextId = id; | ||
| inTextBlock = true; | ||
| controller.enqueue(chunk); | ||
| break; | ||
| } | ||
| case "text-delta": { | ||
| const delta = chunk; | ||
| const text = delta.delta; | ||
| for (let i = 0; i < text.length; i++) { | ||
| const ch = text.charAt(i); | ||
| if (ch === "\n") { | ||
| if (buffering) { | ||
| processCompleteLine(lineBuffer, controller); | ||
| lineBuffer = ""; | ||
| buffering = false; | ||
| } else { | ||
| if (!inSpecFence) { | ||
| emitTextDelta("\n", controller); | ||
| } | ||
| } | ||
| } else if (lineBuffer.length === 0 && !buffering) { | ||
| if (inSpecFence || ch === "{" || ch === "`") { | ||
| buffering = true; | ||
| lineBuffer += ch; | ||
| } else { | ||
| emitTextDelta(ch, controller); | ||
| } | ||
| } else if (buffering) { | ||
| lineBuffer += ch; | ||
| } else { | ||
| emitTextDelta(ch, controller); | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| case "text-end": { | ||
| flushBuffer(controller); | ||
| if (inTextBlock) { | ||
| controller.enqueue({ type: "text-end", id: currentTextId }); | ||
| inTextBlock = false; | ||
| } | ||
| break; | ||
| } | ||
| default: { | ||
| controller.enqueue(chunk); | ||
| break; | ||
| } | ||
| } | ||
| }, | ||
| flush(controller) { | ||
| flushBuffer(controller); | ||
| closeTextBlock(controller); | ||
| } | ||
| }); | ||
| } | ||
| var SPEC_DATA_PART = "spec"; | ||
| var SPEC_DATA_PART_TYPE = `data-${SPEC_DATA_PART}`; | ||
| function pipeJsonRender(stream) { | ||
| return stream.pipeThrough( | ||
| createJsonRenderTransform() | ||
| ); | ||
| } | ||
| // src/state-store.ts | ||
| function immutableSetByPath(root, path, value) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return root; | ||
| const result = { ...root }; | ||
| let current = result; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const seg = segments[i]; | ||
| const child = current[seg]; | ||
| if (Array.isArray(child)) { | ||
| current[seg] = [...child]; | ||
| } else if (child !== null && typeof child === "object") { | ||
| current[seg] = { ...child }; | ||
| } else { | ||
| const nextSeg = segments[i + 1]; | ||
| current[seg] = nextSeg !== void 0 && /^\d+$/.test(nextSeg) ? [] : {}; | ||
| } | ||
| current = current[seg]; | ||
| } | ||
| const lastSeg = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| if (lastSeg === "-") { | ||
| current.push(value); | ||
| } else { | ||
| current[parseInt(lastSeg, 10)] = value; | ||
| } | ||
| } else { | ||
| current[lastSeg] = value; | ||
| } | ||
| return result; | ||
| } | ||
| function createStateStore(initialState = {}) { | ||
| let state = { ...initialState }; | ||
| const listeners = /* @__PURE__ */ new Set(); | ||
| function notify() { | ||
| for (const listener of listeners) { | ||
| listener(); | ||
| } | ||
| } | ||
| return { | ||
| get(path) { | ||
| return getByPath(state, path); | ||
| }, | ||
| set(path, value) { | ||
| if (getByPath(state, path) === value) return; | ||
| state = immutableSetByPath(state, path, value); | ||
| notify(); | ||
| }, | ||
| update(updates) { | ||
| let changed = false; | ||
| let next = state; | ||
| for (const [path, value] of Object.entries(updates)) { | ||
| if (getByPath(next, path) !== value) { | ||
| next = immutableSetByPath(next, path, value); | ||
| changed = true; | ||
| } | ||
| } | ||
| if (!changed) return; | ||
| state = next; | ||
| notify(); | ||
| }, | ||
| getSnapshot() { | ||
| return state; | ||
| }, | ||
| getServerSnapshot() { | ||
| return state; | ||
| }, | ||
| subscribe(listener) { | ||
| listeners.add(listener); | ||
| return () => { | ||
| listeners.delete(listener); | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| function createStoreAdapter(config) { | ||
| return { | ||
| get(path) { | ||
| return getByPath(config.getSnapshot(), path); | ||
| }, | ||
| set(path, value) { | ||
| const current = config.getSnapshot(); | ||
| if (getByPath(current, path) === value) return; | ||
| config.setSnapshot(immutableSetByPath(current, path, value)); | ||
| }, | ||
| update(updates) { | ||
| let next = config.getSnapshot(); | ||
| let changed = false; | ||
| for (const [path, value] of Object.entries(updates)) { | ||
| if (getByPath(next, path) !== value) { | ||
| next = immutableSetByPath(next, path, value); | ||
| changed = true; | ||
| } | ||
| } | ||
| if (!changed) return; | ||
| config.setSnapshot(next); | ||
| }, | ||
| getSnapshot: config.getSnapshot, | ||
| getServerSnapshot: config.getSnapshot, | ||
| subscribe: config.subscribe | ||
| }; | ||
| } | ||
| var MAX_FLATTEN_DEPTH = 20; | ||
| function flattenToPointers(obj, prefix = "", _depth = 0, _seen, _warned) { | ||
| const seen = _seen ?? /* @__PURE__ */ new Set(); | ||
| const warned = _warned ?? { current: false }; | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| const pointer = `${prefix}/${key}`; | ||
| if (_depth < MAX_FLATTEN_DEPTH && value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype && !seen.has(value)) { | ||
| seen.add(value); | ||
| Object.assign( | ||
| result, | ||
| flattenToPointers( | ||
| value, | ||
| pointer, | ||
| _depth + 1, | ||
| seen, | ||
| warned | ||
| ) | ||
| ); | ||
| } else { | ||
| if (process.env.NODE_ENV !== "production" && !warned.current && _depth >= MAX_FLATTEN_DEPTH && value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype && !seen.has(value)) { | ||
| warned.current = true; | ||
| console.warn( | ||
| `flattenToPointers: depth limit (${MAX_FLATTEN_DEPTH}) reached. Nested state beyond this depth will be treated as a leaf value.` | ||
| ); | ||
| } | ||
| result[pointer] = value; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| export { | ||
| DynamicValueSchema, | ||
| DynamicStringSchema, | ||
| DynamicNumberSchema, | ||
| DynamicBooleanSchema, | ||
| resolveDynamicValue, | ||
| getByPath, | ||
| resolveRepeatStatePath, | ||
| resolveRepeatItemStatePath, | ||
| setByPath, | ||
| addByPath, | ||
| removeByPath, | ||
| findFormValue, | ||
| parseSpecStreamLine, | ||
| applySpecStreamPatch, | ||
| applySpecPatch, | ||
| nestedToFlat, | ||
| compileSpecStream, | ||
| createSpecStreamCompiler, | ||
| createMixedStreamParser, | ||
| createJsonRenderTransform, | ||
| SPEC_DATA_PART, | ||
| SPEC_DATA_PART_TYPE, | ||
| pipeJsonRender, | ||
| immutableSetByPath, | ||
| createStateStore, | ||
| createStoreAdapter, | ||
| flattenToPointers | ||
| }; | ||
| //# sourceMappingURL=chunk-7V7ZCHEJ.mjs.map |
Sorry, the diff of this file is too big to display
| import { z } from 'zod'; | ||
| /** | ||
| * Confirmation dialog configuration | ||
| */ | ||
| interface ActionConfirm { | ||
| title: string; | ||
| message: string; | ||
| confirmLabel?: string; | ||
| cancelLabel?: string; | ||
| variant?: "default" | "danger"; | ||
| } | ||
| /** | ||
| * Action success handler | ||
| */ | ||
| type ActionOnSuccess = { | ||
| navigate: string; | ||
| } | { | ||
| set: Record<string, unknown>; | ||
| } | { | ||
| action: string; | ||
| params?: Record<string, DynamicValue>; | ||
| }; | ||
| /** | ||
| * Action error handler | ||
| */ | ||
| type ActionOnError = { | ||
| set: Record<string, unknown>; | ||
| } | { | ||
| action: string; | ||
| params?: Record<string, DynamicValue>; | ||
| }; | ||
| /** | ||
| * Action binding — maps an event to an action invocation. | ||
| * | ||
| * Used inside the `on` field of a UIElement: | ||
| * ```json | ||
| * { "on": { "press": { "action": "setState", "params": { "statePath": "/x", "value": 1 } } } } | ||
| * ``` | ||
| */ | ||
| interface ActionBinding { | ||
| /** Action name (must be in catalog) */ | ||
| action: string; | ||
| /** Parameters to pass to the action handler */ | ||
| params?: Record<string, DynamicValue>; | ||
| /** Confirmation dialog before execution */ | ||
| confirm?: ActionConfirm; | ||
| /** Handler after successful execution */ | ||
| onSuccess?: ActionOnSuccess; | ||
| /** Handler after failed execution */ | ||
| onError?: ActionOnError; | ||
| /** Whether to prevent default browser behavior (e.g. navigation on links) */ | ||
| preventDefault?: boolean; | ||
| } | ||
| /** | ||
| * @deprecated Use ActionBinding instead | ||
| */ | ||
| type Action = ActionBinding; | ||
| /** | ||
| * Schema for action confirmation | ||
| */ | ||
| declare const ActionConfirmSchema: z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * Schema for success handlers | ||
| */ | ||
| declare const ActionOnSuccessSchema: z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Schema for error handlers | ||
| */ | ||
| declare const ActionOnErrorSchema: z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Full action binding schema | ||
| */ | ||
| declare const ActionBindingSchema: z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| confirm: z.ZodOptional<z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>>; | ||
| onSuccess: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>>; | ||
| onError: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>>; | ||
| preventDefault: z.ZodOptional<z.ZodBoolean>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * @deprecated Use ActionBindingSchema instead | ||
| */ | ||
| declare const ActionSchema: z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| confirm: z.ZodOptional<z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>>; | ||
| onSuccess: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>>; | ||
| onError: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>>; | ||
| preventDefault: z.ZodOptional<z.ZodBoolean>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * Action handler function signature | ||
| */ | ||
| type ActionHandler<TParams = Record<string, unknown>, TResult = unknown> = (params: TParams) => Promise<TResult> | TResult; | ||
| /** | ||
| * Action definition in catalog | ||
| */ | ||
| interface ActionDefinition<TParams = Record<string, unknown>> { | ||
| /** Zod schema for params validation */ | ||
| params?: z.ZodType<TParams>; | ||
| /** Description for AI */ | ||
| description?: string; | ||
| } | ||
| /** | ||
| * Resolved action with all dynamic values resolved | ||
| */ | ||
| interface ResolvedAction { | ||
| action: string; | ||
| params: Record<string, unknown>; | ||
| confirm?: ActionConfirm; | ||
| onSuccess?: ActionOnSuccess; | ||
| onError?: ActionOnError; | ||
| } | ||
| /** | ||
| * Resolve all dynamic values in an action binding | ||
| */ | ||
| declare function resolveAction(binding: ActionBinding, stateModel: StateModel): ResolvedAction; | ||
| /** | ||
| * Interpolate ${path} expressions in a string | ||
| */ | ||
| declare function interpolateString(template: string, stateModel: StateModel): string; | ||
| /** | ||
| * Context for action execution | ||
| */ | ||
| interface ActionExecutionContext { | ||
| /** The resolved action */ | ||
| action: ResolvedAction; | ||
| /** The action handler from the host */ | ||
| handler: ActionHandler; | ||
| /** Function to update state model */ | ||
| setState: (path: string, value: unknown) => void; | ||
| /** Function to navigate */ | ||
| navigate?: (path: string) => void; | ||
| /** Function to execute another action */ | ||
| executeAction?: (binding: ActionBinding) => Promise<void>; | ||
| } | ||
| /** | ||
| * Execute an action with all callbacks | ||
| */ | ||
| declare function executeAction(ctx: ActionExecutionContext): Promise<void>; | ||
| /** | ||
| * Helper to create action bindings | ||
| */ | ||
| declare const actionBinding: { | ||
| /** Create a simple action binding */ | ||
| simple: (actionName: string, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with confirmation */ | ||
| withConfirm: (actionName: string, confirm: ActionConfirm, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with success handler */ | ||
| withSuccess: (actionName: string, onSuccess: ActionOnSuccess, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| }; | ||
| /** | ||
| * @deprecated Use actionBinding instead | ||
| */ | ||
| declare const action: { | ||
| /** Create a simple action binding */ | ||
| simple: (actionName: string, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with confirmation */ | ||
| withConfirm: (actionName: string, confirm: ActionConfirm, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with success handler */ | ||
| withSuccess: (actionName: string, onSuccess: ActionOnSuccess, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| }; | ||
| /** | ||
| * Dynamic value - can be a literal or a `{ $state }` reference to the state model. | ||
| * | ||
| * Used in action params and validation args where values can either be | ||
| * hardcoded or resolved from state at runtime. | ||
| */ | ||
| type DynamicValue<T = unknown> = T | { | ||
| $state: string; | ||
| }; | ||
| /** | ||
| * Dynamic string value | ||
| */ | ||
| type DynamicString = DynamicValue<string>; | ||
| /** | ||
| * Dynamic number value | ||
| */ | ||
| type DynamicNumber = DynamicValue<number>; | ||
| /** | ||
| * Dynamic boolean value | ||
| */ | ||
| type DynamicBoolean = DynamicValue<boolean>; | ||
| /** | ||
| * Zod schema for dynamic values | ||
| */ | ||
| declare const DynamicValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicStringSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicNumberSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicBooleanSchema: z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| type RepeatStatePath = string | { | ||
| $item: string; | ||
| }; | ||
| /** | ||
| * Base UI element structure for v2 | ||
| */ | ||
| interface UIElement<T extends string = string, P = Record<string, unknown>> { | ||
| /** Component type from the catalog */ | ||
| type: T; | ||
| /** Component props */ | ||
| props: P; | ||
| /** Child element keys (flat structure) */ | ||
| children?: string[]; | ||
| slots?: Record<string, string[]>; | ||
| /** Visibility condition */ | ||
| visible?: VisibilityCondition; | ||
| /** Event bindings — maps event names to action bindings */ | ||
| on?: Record<string, ActionBinding | ActionBinding[]>; | ||
| /** Repeat children once per item in a state array */ | ||
| repeat?: { | ||
| statePath: RepeatStatePath; | ||
| key?: string; | ||
| }; | ||
| /** | ||
| * State watchers — maps JSON Pointer state paths to action bindings. | ||
| * When the value at a watched path changes, the bound actions fire. | ||
| * Useful for cascading dependencies (e.g. country → city option loading). | ||
| */ | ||
| watch?: Record<string, ActionBinding | ActionBinding[]>; | ||
| } | ||
| /** | ||
| * Element with key and parentKey for use with flatToTree. | ||
| * When elements are in an array (not a keyed map), key and parentKey | ||
| * are needed to establish identity and parent-child relationships. | ||
| */ | ||
| interface FlatElement<T extends string = string, P = Record<string, unknown>> extends UIElement<T, P> { | ||
| /** Unique key identifying this element */ | ||
| key: string; | ||
| /** Parent element key (null for root) */ | ||
| parentKey?: string | null; | ||
| } | ||
| /** | ||
| * Shared comparison operators for visibility conditions. | ||
| * | ||
| * Use at most ONE comparison operator per condition. If multiple are | ||
| * provided, only the first matching one is evaluated (precedence: | ||
| * eq > neq > gt > gte > lt > lte). With no operator, truthiness is checked. | ||
| * | ||
| * `not` inverts the final result of whichever operator (or truthiness | ||
| * check) is used. | ||
| */ | ||
| type ComparisonOperators = { | ||
| eq?: unknown; | ||
| neq?: unknown; | ||
| gt?: number | { | ||
| $state: string; | ||
| }; | ||
| gte?: number | { | ||
| $state: string; | ||
| }; | ||
| lt?: number | { | ||
| $state: string; | ||
| }; | ||
| lte?: number | { | ||
| $state: string; | ||
| }; | ||
| not?: true; | ||
| }; | ||
| /** | ||
| * A single state-based condition. | ||
| * Resolves `$state` to a value from the state model, then applies the operator. | ||
| * Without an operator, checks truthiness. | ||
| * | ||
| * When `not` is `true`, the result of the entire condition is inverted. | ||
| * For example `{ $state: "/count", gt: 5, not: true }` means "NOT greater than 5". | ||
| */ | ||
| type StateCondition = { | ||
| $state: string; | ||
| } & ComparisonOperators; | ||
| /** | ||
| * A condition that resolves `$item` to a field on the current repeat item. | ||
| * Only meaningful inside a `repeat` scope. | ||
| * | ||
| * Use `""` to reference the whole item, or `"field"` for a specific field. | ||
| */ | ||
| type ItemCondition = { | ||
| $item: string; | ||
| } & ComparisonOperators; | ||
| /** | ||
| * A condition that resolves `$index` to the current repeat array index. | ||
| * Only meaningful inside a `repeat` scope. | ||
| */ | ||
| type IndexCondition = { | ||
| $index: true; | ||
| } & ComparisonOperators; | ||
| /** A single visibility condition (state, item, or index). */ | ||
| type SingleCondition = StateCondition | ItemCondition | IndexCondition; | ||
| /** | ||
| * AND wrapper — all child conditions must be true. | ||
| * This is the explicit form of the implicit array AND (`SingleCondition[]`). | ||
| * Unlike the implicit form, `$and` supports nested `$or` and `$and` conditions. | ||
| */ | ||
| type AndCondition = { | ||
| $and: VisibilityCondition[]; | ||
| }; | ||
| /** | ||
| * OR wrapper — at least one child condition must be true. | ||
| */ | ||
| type OrCondition = { | ||
| $or: VisibilityCondition[]; | ||
| }; | ||
| /** | ||
| * Visibility condition types. | ||
| * - `boolean` — always/never | ||
| * - `SingleCondition` — single condition (`$state`, `$item`, or `$index`) | ||
| * - `SingleCondition[]` — implicit AND (all must be true) | ||
| * - `AndCondition` — `{ $and: [...] }`, explicit AND (all must be true) | ||
| * - `OrCondition` — `{ $or: [...] }`, at least one must be true | ||
| */ | ||
| type VisibilityCondition = boolean | SingleCondition | SingleCondition[] | AndCondition | OrCondition; | ||
| /** | ||
| * Flat UI tree structure (optimized for LLM generation) | ||
| */ | ||
| interface Spec { | ||
| /** Root element key */ | ||
| root: string; | ||
| /** Flat map of elements by key */ | ||
| elements: Record<string, UIElement>; | ||
| /** Optional initial state to seed the state model. | ||
| * Components using statePath will read from / write to this state. */ | ||
| state?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * State model type | ||
| */ | ||
| type StateModel = Record<string, unknown>; | ||
| /** | ||
| * An abstract store that owns state and notifies subscribers on change. | ||
| * | ||
| * Consumers can supply their own implementation (backed by Redux, Zustand, | ||
| * XState, etc.) or use the built-in {@link createStateStore} for a simple | ||
| * in-memory store. | ||
| */ | ||
| interface StateStore { | ||
| /** Read a value by JSON Pointer path. */ | ||
| get: (path: string) => unknown; | ||
| /** | ||
| * Write a value by JSON Pointer path and notify subscribers. | ||
| * Equality is checked by reference (`===`), not deep comparison. | ||
| * Callers must pass a new object/array reference for changes to be detected. | ||
| */ | ||
| set: (path: string, value: unknown) => void; | ||
| /** | ||
| * Write multiple values at once and notify subscribers (single notification). | ||
| * Each value is compared by reference (`===`); only paths whose value | ||
| * actually changed are applied. | ||
| */ | ||
| update: (updates: Record<string, unknown>) => void; | ||
| /** Return the full state object (used by `useSyncExternalStore`). */ | ||
| getSnapshot: () => StateModel; | ||
| /** Optional server snapshot for SSR (passed to `useSyncExternalStore`). Falls back to `getSnapshot` when omitted. */ | ||
| getServerSnapshot?: () => StateModel; | ||
| /** Register a listener that is called on every state change. Returns an unsubscribe function. */ | ||
| subscribe: (listener: () => void) => () => void; | ||
| } | ||
| /** | ||
| * Component schema definition using Zod | ||
| */ | ||
| type ComponentSchema = z.ZodType<Record<string, unknown>>; | ||
| /** | ||
| * Validation mode for catalog validation | ||
| */ | ||
| type ValidationMode = "strict" | "warn" | "ignore"; | ||
| /** | ||
| * JSON patch operation types (RFC 6902) | ||
| */ | ||
| type PatchOp = "add" | "remove" | "replace" | "move" | "copy" | "test"; | ||
| /** | ||
| * JSON patch operation (RFC 6902) | ||
| */ | ||
| interface JsonPatch { | ||
| op: PatchOp; | ||
| path: string; | ||
| /** Required for add, replace, test */ | ||
| value?: unknown; | ||
| /** Required for move, copy (source location) */ | ||
| from?: string; | ||
| } | ||
| /** | ||
| * Resolve a dynamic value against a state model | ||
| */ | ||
| declare function resolveDynamicValue<T>(value: DynamicValue<T>, stateModel: StateModel): T | undefined; | ||
| /** | ||
| * Get a value from an object by JSON Pointer path (RFC 6901) | ||
| */ | ||
| declare function getByPath(obj: unknown, path: string): unknown; | ||
| declare function resolveRepeatStatePath(statePath: RepeatStatePath, repeatBasePath?: string | null): string | undefined; | ||
| declare function resolveRepeatItemStatePath(statePath: string, index: number): string; | ||
| /** | ||
| * Set a value in an object by JSON Pointer path (RFC 6901). | ||
| * Automatically creates arrays when the path segment is a numeric index. | ||
| */ | ||
| declare function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Add a value per RFC 6902 "add" semantics. | ||
| * For objects: create-or-replace the member. | ||
| * For arrays: insert before the given index, or append if "-". | ||
| */ | ||
| declare function addByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Remove a value per RFC 6902 "remove" semantics. | ||
| * For objects: delete the property. | ||
| * For arrays: splice out the element at the given index. | ||
| */ | ||
| declare function removeByPath(obj: Record<string, unknown>, path: string): void; | ||
| /** | ||
| * Find a form value from params and/or state. | ||
| * Useful in action handlers to locate form input values regardless of path format. | ||
| * | ||
| * Checks in order: | ||
| * 1. Direct param key (if not a path reference) | ||
| * 2. Param keys ending with the field name | ||
| * 3. State keys ending with the field name (dot notation) | ||
| * 4. State path using getByPath (slash notation) | ||
| * | ||
| * @example | ||
| * // Find "name" from params or state | ||
| * const name = findFormValue("name", params, state); | ||
| * | ||
| * // Will find from: params.name, params["form.name"], state["form.name"], or getByPath(state, "name") | ||
| */ | ||
| declare function findFormValue(fieldName: string, params?: Record<string, unknown>, state?: Record<string, unknown>): unknown; | ||
| /** | ||
| * A SpecStream line - a single patch operation in the stream. | ||
| */ | ||
| type SpecStreamLine = JsonPatch; | ||
| /** | ||
| * Parse a single SpecStream line into a patch operation. | ||
| * Returns null if the line is invalid or empty. | ||
| * | ||
| * SpecStream is json-render's streaming format where each line is a JSON patch | ||
| * operation that progressively builds up the final spec. | ||
| */ | ||
| declare function parseSpecStreamLine(line: string): SpecStreamLine | null; | ||
| /** | ||
| * Apply a single RFC 6902 JSON Patch operation to an object. | ||
| * Mutates the object in place. | ||
| * | ||
| * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test. | ||
| * | ||
| * @throws {Error} If a "test" operation fails (value mismatch). | ||
| */ | ||
| declare function applySpecStreamPatch<T extends Record<string, unknown>>(obj: T, patch: SpecStreamLine): T; | ||
| /** | ||
| * Apply a single RFC 6902 JSON Patch operation to a Spec. | ||
| * Mutates the spec in place and returns it. | ||
| * | ||
| * This is a typed convenience wrapper around `applySpecStreamPatch` that | ||
| * accepts a `Spec` directly without requiring a cast to `Record<string, unknown>`. | ||
| * | ||
| * Note: This mutates the spec. For React state updates, spread the result | ||
| * to create a new reference: `setSpec({ ...applySpecPatch(spec, patch) })`. | ||
| * | ||
| * @example | ||
| * let spec: Spec = { root: "", elements: {} }; | ||
| * applySpecPatch(spec, { op: "add", path: "/root", value: "main" }); | ||
| */ | ||
| declare function applySpecPatch(spec: Spec, patch: SpecStreamLine): Spec; | ||
| /** | ||
| * Convert a nested (tree-structured) spec into the flat `Spec` format used | ||
| * by json-render renderers. | ||
| * | ||
| * In the nested format each node has inline `children` as an array of child | ||
| * objects. This function walks the tree, assigns auto-generated keys | ||
| * (`el-0`, `el-1`, ...), and produces a flat `{ root, elements, state }` spec. | ||
| * | ||
| * The top-level `state` field (if present on the root node) is hoisted to | ||
| * `spec.state`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const nested = { | ||
| * type: "Card", | ||
| * props: { title: "Hello" }, | ||
| * children: [ | ||
| * { type: "Text", props: { content: "World" } }, | ||
| * ], | ||
| * state: { count: 0 }, | ||
| * }; | ||
| * const spec = nestedToFlat(nested); | ||
| * // { | ||
| * // root: "el-0", | ||
| * // elements: { | ||
| * // "el-0": { type: "Card", props: { title: "Hello" }, children: ["el-1"] }, | ||
| * // "el-1": { type: "Text", props: { content: "World" }, children: [] }, | ||
| * // }, | ||
| * // state: { count: 0 }, | ||
| * // } | ||
| * ``` | ||
| */ | ||
| declare function nestedToFlat(nested: Record<string, unknown>): Spec; | ||
| /** | ||
| * Compile a SpecStream string into a JSON object. | ||
| * Each line should be a patch operation. | ||
| * | ||
| * @example | ||
| * const stream = `{"op":"add","path":"/name","value":"Alice"} | ||
| * {"op":"add","path":"/age","value":30}`; | ||
| * const result = compileSpecStream(stream); | ||
| * // { name: "Alice", age: 30 } | ||
| */ | ||
| declare function compileSpecStream<T extends Record<string, unknown> = Record<string, unknown>>(stream: string, initial?: T): T; | ||
| /** | ||
| * Streaming SpecStream compiler. | ||
| * Useful for processing SpecStream data as it streams in from AI. | ||
| * | ||
| * @example | ||
| * const compiler = createSpecStreamCompiler<MySpec>(); | ||
| * | ||
| * // As chunks arrive: | ||
| * const { result, newPatches } = compiler.push(chunk); | ||
| * if (newPatches.length > 0) { | ||
| * updateUI(result); | ||
| * } | ||
| * | ||
| * // When done: | ||
| * const finalResult = compiler.getResult(); | ||
| */ | ||
| interface SpecStreamCompiler<T> { | ||
| /** Push a chunk of text. Returns the current result and any new patches applied. */ | ||
| push(chunk: string): { | ||
| result: T; | ||
| newPatches: SpecStreamLine[]; | ||
| }; | ||
| /** Get the current compiled result */ | ||
| getResult(): T; | ||
| /** Get all patches that have been applied */ | ||
| getPatches(): SpecStreamLine[]; | ||
| /** Reset the compiler to initial state */ | ||
| reset(initial?: Partial<T>): void; | ||
| } | ||
| /** | ||
| * Create a streaming SpecStream compiler. | ||
| * | ||
| * SpecStream is json-render's streaming format. AI outputs patch operations | ||
| * line by line, and this compiler progressively builds the final spec. | ||
| * | ||
| * @example | ||
| * const compiler = createSpecStreamCompiler<TimelineSpec>(); | ||
| * | ||
| * // Process streaming response | ||
| * const reader = response.body.getReader(); | ||
| * while (true) { | ||
| * const { done, value } = await reader.read(); | ||
| * if (done) break; | ||
| * | ||
| * const { result, newPatches } = compiler.push(decoder.decode(value)); | ||
| * if (newPatches.length > 0) { | ||
| * setSpec(result); // Update UI with partial result | ||
| * } | ||
| * } | ||
| */ | ||
| declare function createSpecStreamCompiler<T = Record<string, unknown>>(initial?: Partial<T>): SpecStreamCompiler<T>; | ||
| /** | ||
| * Callbacks for the mixed stream parser. | ||
| */ | ||
| interface MixedStreamCallbacks { | ||
| /** Called when a JSONL patch line is parsed */ | ||
| onPatch: (patch: SpecStreamLine) => void; | ||
| /** Called when a text (non-JSONL) line is received */ | ||
| onText: (text: string) => void; | ||
| } | ||
| /** | ||
| * A stateful parser for mixed streams that contain both text and JSONL patches. | ||
| * Used in chat + GenUI scenarios where an LLM responds with conversational text | ||
| * interleaved with json-render JSONL patch operations. | ||
| */ | ||
| interface MixedStreamParser { | ||
| /** Push a chunk of streamed data. Calls onPatch/onText for each complete line. */ | ||
| push(chunk: string): void; | ||
| /** Flush any remaining buffered content. Call when the stream ends. */ | ||
| flush(): void; | ||
| } | ||
| /** | ||
| * Create a parser for mixed text + JSONL streams. | ||
| * | ||
| * In chat + GenUI scenarios, an LLM streams a response that contains both | ||
| * conversational text and json-render JSONL patch lines. This parser buffers | ||
| * incoming chunks, splits them into lines, and classifies each line as either | ||
| * a JSONL patch (via `parseSpecStreamLine`) or plain text. | ||
| * | ||
| * @example | ||
| * const parser = createMixedStreamParser({ | ||
| * onText: (text) => appendToMessage(text), | ||
| * onPatch: (patch) => applySpecPatch(spec, patch), | ||
| * }); | ||
| * | ||
| * // As chunks arrive from the stream: | ||
| * for await (const chunk of stream) { | ||
| * parser.push(chunk); | ||
| * } | ||
| * parser.flush(); | ||
| */ | ||
| declare function createMixedStreamParser(callbacks: MixedStreamCallbacks): MixedStreamParser; | ||
| /** | ||
| * Minimal chunk shape compatible with the AI SDK's `UIMessageChunk`. | ||
| * | ||
| * Defined here so that `@json-render/core` has no dependency on the `ai` | ||
| * package. The discriminated union covers the three text-related chunk types | ||
| * the transform inspects; all other chunk types pass through via the fallback. | ||
| */ | ||
| type StreamChunk = { | ||
| type: "text-start"; | ||
| id: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: "text-delta"; | ||
| id: string; | ||
| delta: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: "text-end"; | ||
| id: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: string; | ||
| [k: string]: unknown; | ||
| }; | ||
| /** | ||
| * Creates a `TransformStream` that intercepts AI SDK UI message stream chunks | ||
| * and classifies text content as either prose or json-render JSONL patches. | ||
| * | ||
| * Two classification modes: | ||
| * | ||
| * 1. **Fence mode** (preferred): Lines between ` ```spec ` and ` ``` ` are | ||
| * parsed as JSONL patches. Fence delimiters are swallowed (not emitted). | ||
| * 2. **Heuristic mode** (backward compat): Outside of fences, lines starting | ||
| * with `{` are buffered and tested with `parseSpecStreamLine`. Valid patches | ||
| * are emitted as {@link SPEC_DATA_PART_TYPE} parts; everything else is | ||
| * flushed as text. | ||
| * | ||
| * Non-text chunks (tool events, step markers, etc.) are passed through unchanged. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { createJsonRenderTransform } from "@json-render/core"; | ||
| * import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; | ||
| * | ||
| * const stream = createUIMessageStream({ | ||
| * execute: async ({ writer }) => { | ||
| * writer.merge( | ||
| * result.toUIMessageStream().pipeThrough(createJsonRenderTransform()), | ||
| * ); | ||
| * }, | ||
| * }); | ||
| * return createUIMessageStreamResponse({ stream }); | ||
| * ``` | ||
| */ | ||
| declare function createJsonRenderTransform(): TransformStream<StreamChunk, StreamChunk>; | ||
| /** | ||
| * The key registered in `AppDataParts` for json-render specs. | ||
| * The AI SDK automatically prefixes this with `"data-"` on the wire, | ||
| * so the actual stream chunk type is `"data-spec"` (see {@link SPEC_DATA_PART_TYPE}). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { SPEC_DATA_PART, type SpecDataPart } from "@json-render/core"; | ||
| * type AppDataParts = { [SPEC_DATA_PART]: SpecDataPart }; | ||
| * ``` | ||
| */ | ||
| declare const SPEC_DATA_PART: "spec"; | ||
| /** | ||
| * The wire-format type string as it appears in stream chunks and message parts. | ||
| * This is `"data-"` + {@link SPEC_DATA_PART} — i.e. `"data-spec"`. | ||
| * | ||
| * Use this constant when filtering message parts or enqueuing stream chunks. | ||
| */ | ||
| declare const SPEC_DATA_PART_TYPE: "data-spec"; | ||
| /** | ||
| * Discriminated union for the payload of a {@link SPEC_DATA_PART_TYPE} SSE part. | ||
| * | ||
| * - `"patch"`: A single RFC 6902 JSON Patch operation (streaming, progressive UI). | ||
| * - `"flat"`: A complete flat spec with `root`, `elements`, and optional `state`. | ||
| * - `"nested"`: A complete nested spec (tree structure — schema depends on catalog). | ||
| */ | ||
| type SpecDataPart = { | ||
| type: "patch"; | ||
| patch: JsonPatch; | ||
| } | { | ||
| type: "flat"; | ||
| spec: Spec; | ||
| } | { | ||
| type: "nested"; | ||
| spec: Record<string, unknown>; | ||
| }; | ||
| /** | ||
| * Convenience wrapper that pipes an AI SDK UI message stream through the | ||
| * json-render transform, classifying text as prose or JSONL patches. | ||
| * | ||
| * Eliminates the need for manual `pipeThrough(createJsonRenderTransform())` | ||
| * and the associated type cast. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { pipeJsonRender } from "@json-render/core"; | ||
| * | ||
| * const stream = createUIMessageStream({ | ||
| * execute: async ({ writer }) => { | ||
| * writer.merge(pipeJsonRender(result.toUIMessageStream())); | ||
| * }, | ||
| * }); | ||
| * return createUIMessageStreamResponse({ stream }); | ||
| * ``` | ||
| */ | ||
| declare function pipeJsonRender<T = StreamChunk>(stream: ReadableStream<T>): ReadableStream<T>; | ||
| /** | ||
| * Immutably set a value at a JSON Pointer path using structural sharing. | ||
| * Only objects along the path are shallow-cloned; untouched branches keep | ||
| * their original references. | ||
| */ | ||
| declare function immutableSetByPath(root: StateModel, path: string, value: unknown): StateModel; | ||
| /** | ||
| * Create a simple in-memory {@link StateStore}. | ||
| * | ||
| * This is the default store used by `StateProvider` when no external store is | ||
| * provided. It mirrors the previous `useState`-based behaviour but is | ||
| * framework-agnostic so it can also be used in tests or non-React contexts. | ||
| */ | ||
| declare function createStateStore(initialState?: StateModel): StateStore; | ||
| /** | ||
| * Configuration for {@link createStoreAdapter}. Adapter authors supply these | ||
| * three callbacks; everything else (get, set, update, no-op detection, | ||
| * getServerSnapshot) is handled by the returned {@link StateStore}. | ||
| */ | ||
| interface StoreAdapterConfig { | ||
| /** Return the current state snapshot from the underlying store. */ | ||
| getSnapshot: () => StateModel; | ||
| /** Write a new state snapshot to the underlying store. */ | ||
| setSnapshot: (next: StateModel) => void; | ||
| /** Subscribe to changes in the underlying store. Return an unsubscribe fn. */ | ||
| subscribe: (listener: () => void) => () => void; | ||
| } | ||
| /** | ||
| * Build a full {@link StateStore} from a minimal adapter config. | ||
| * | ||
| * Handles `get`, `set` (with no-op detection), `update` (batched, with no-op | ||
| * detection), `getSnapshot`, `getServerSnapshot`, and `subscribe` -- so each | ||
| * adapter only needs to wire its snapshot source, write API, and subscribe | ||
| * mechanism. | ||
| */ | ||
| declare function createStoreAdapter(config: StoreAdapterConfig): StateStore; | ||
| /** | ||
| * Recursively flatten a plain object into a `Record<string, unknown>` keyed by | ||
| * JSON Pointer paths. Only leaf values (non-plain-object) appear in the output. | ||
| * | ||
| * Includes circular reference protection and a depth cap to prevent stack | ||
| * overflow on pathological inputs. | ||
| * | ||
| * ```ts | ||
| * flattenToPointers({ user: { name: "Alice" }, count: 1 }) | ||
| * // => { "/user/name": "Alice", "/count": 1 } | ||
| * ``` | ||
| */ | ||
| declare function flattenToPointers(obj: Record<string, unknown>, prefix?: string, _depth?: number, _seen?: Set<object>, _warned?: { | ||
| current: boolean; | ||
| }): Record<string, unknown>; | ||
| export { type Action as $, type AndCondition as A, parseSpecStreamLine as B, type ComponentSchema as C, type DynamicValue as D, applySpecStreamPatch as E, type FlatElement as F, applySpecPatch as G, nestedToFlat as H, type ItemCondition as I, type JsonPatch as J, compileSpecStream as K, createSpecStreamCompiler as L, type MixedStreamCallbacks as M, createMixedStreamParser as N, type OrCondition as O, type PatchOp as P, createJsonRenderTransform as Q, type RepeatStatePath as R, type StateModel as S, pipeJsonRender as T, type UIElement as U, type VisibilityCondition as V, SPEC_DATA_PART as W, SPEC_DATA_PART_TYPE as X, type StoreAdapterConfig as Y, createStateStore as Z, type ActionBinding as _, type StateCondition as a, type ActionConfirm as a0, type ActionOnSuccess as a1, type ActionOnError as a2, type ActionHandler as a3, type ActionDefinition as a4, type ResolvedAction as a5, type ActionExecutionContext as a6, ActionBindingSchema as a7, ActionSchema as a8, ActionConfirmSchema as a9, ActionOnSuccessSchema as aa, ActionOnErrorSchema as ab, resolveAction as ac, executeAction as ad, interpolateString as ae, actionBinding as af, action as ag, immutableSetByPath as ah, flattenToPointers as ai, createStoreAdapter as aj, type Spec as b, type DynamicString as c, type DynamicNumber as d, type DynamicBoolean as e, type IndexCondition as f, type SingleCondition as g, type StateStore as h, type ValidationMode as i, type SpecStreamLine as j, type SpecStreamCompiler as k, type MixedStreamParser as l, type StreamChunk as m, type SpecDataPart as n, DynamicValueSchema as o, DynamicStringSchema as p, DynamicNumberSchema as q, DynamicBooleanSchema as r, resolveDynamicValue as s, getByPath as t, resolveRepeatStatePath as u, resolveRepeatItemStatePath as v, setByPath as w, addByPath as x, removeByPath as y, findFormValue as z }; |
| import { z } from 'zod'; | ||
| /** | ||
| * Confirmation dialog configuration | ||
| */ | ||
| interface ActionConfirm { | ||
| title: string; | ||
| message: string; | ||
| confirmLabel?: string; | ||
| cancelLabel?: string; | ||
| variant?: "default" | "danger"; | ||
| } | ||
| /** | ||
| * Action success handler | ||
| */ | ||
| type ActionOnSuccess = { | ||
| navigate: string; | ||
| } | { | ||
| set: Record<string, unknown>; | ||
| } | { | ||
| action: string; | ||
| params?: Record<string, DynamicValue>; | ||
| }; | ||
| /** | ||
| * Action error handler | ||
| */ | ||
| type ActionOnError = { | ||
| set: Record<string, unknown>; | ||
| } | { | ||
| action: string; | ||
| params?: Record<string, DynamicValue>; | ||
| }; | ||
| /** | ||
| * Action binding — maps an event to an action invocation. | ||
| * | ||
| * Used inside the `on` field of a UIElement: | ||
| * ```json | ||
| * { "on": { "press": { "action": "setState", "params": { "statePath": "/x", "value": 1 } } } } | ||
| * ``` | ||
| */ | ||
| interface ActionBinding { | ||
| /** Action name (must be in catalog) */ | ||
| action: string; | ||
| /** Parameters to pass to the action handler */ | ||
| params?: Record<string, DynamicValue>; | ||
| /** Confirmation dialog before execution */ | ||
| confirm?: ActionConfirm; | ||
| /** Handler after successful execution */ | ||
| onSuccess?: ActionOnSuccess; | ||
| /** Handler after failed execution */ | ||
| onError?: ActionOnError; | ||
| /** Whether to prevent default browser behavior (e.g. navigation on links) */ | ||
| preventDefault?: boolean; | ||
| } | ||
| /** | ||
| * @deprecated Use ActionBinding instead | ||
| */ | ||
| type Action = ActionBinding; | ||
| /** | ||
| * Schema for action confirmation | ||
| */ | ||
| declare const ActionConfirmSchema: z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * Schema for success handlers | ||
| */ | ||
| declare const ActionOnSuccessSchema: z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Schema for error handlers | ||
| */ | ||
| declare const ActionOnErrorSchema: z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Full action binding schema | ||
| */ | ||
| declare const ActionBindingSchema: z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| confirm: z.ZodOptional<z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>>; | ||
| onSuccess: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>>; | ||
| onError: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>>; | ||
| preventDefault: z.ZodOptional<z.ZodBoolean>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * @deprecated Use ActionBindingSchema instead | ||
| */ | ||
| declare const ActionSchema: z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| confirm: z.ZodOptional<z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>>; | ||
| onSuccess: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>>; | ||
| onError: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| }, z.core.$strip>]>>; | ||
| preventDefault: z.ZodOptional<z.ZodBoolean>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * Action handler function signature | ||
| */ | ||
| type ActionHandler<TParams = Record<string, unknown>, TResult = unknown> = (params: TParams) => Promise<TResult> | TResult; | ||
| /** | ||
| * Action definition in catalog | ||
| */ | ||
| interface ActionDefinition<TParams = Record<string, unknown>> { | ||
| /** Zod schema for params validation */ | ||
| params?: z.ZodType<TParams>; | ||
| /** Description for AI */ | ||
| description?: string; | ||
| } | ||
| /** | ||
| * Resolved action with all dynamic values resolved | ||
| */ | ||
| interface ResolvedAction { | ||
| action: string; | ||
| params: Record<string, unknown>; | ||
| confirm?: ActionConfirm; | ||
| onSuccess?: ActionOnSuccess; | ||
| onError?: ActionOnError; | ||
| } | ||
| /** | ||
| * Resolve all dynamic values in an action binding | ||
| */ | ||
| declare function resolveAction(binding: ActionBinding, stateModel: StateModel): ResolvedAction; | ||
| /** | ||
| * Interpolate ${path} expressions in a string | ||
| */ | ||
| declare function interpolateString(template: string, stateModel: StateModel): string; | ||
| /** | ||
| * Context for action execution | ||
| */ | ||
| interface ActionExecutionContext { | ||
| /** The resolved action */ | ||
| action: ResolvedAction; | ||
| /** The action handler from the host */ | ||
| handler: ActionHandler; | ||
| /** Function to update state model */ | ||
| setState: (path: string, value: unknown) => void; | ||
| /** Function to navigate */ | ||
| navigate?: (path: string) => void; | ||
| /** Function to execute another action */ | ||
| executeAction?: (binding: ActionBinding) => Promise<void>; | ||
| } | ||
| /** | ||
| * Execute an action with all callbacks | ||
| */ | ||
| declare function executeAction(ctx: ActionExecutionContext): Promise<void>; | ||
| /** | ||
| * Helper to create action bindings | ||
| */ | ||
| declare const actionBinding: { | ||
| /** Create a simple action binding */ | ||
| simple: (actionName: string, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with confirmation */ | ||
| withConfirm: (actionName: string, confirm: ActionConfirm, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with success handler */ | ||
| withSuccess: (actionName: string, onSuccess: ActionOnSuccess, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| }; | ||
| /** | ||
| * @deprecated Use actionBinding instead | ||
| */ | ||
| declare const action: { | ||
| /** Create a simple action binding */ | ||
| simple: (actionName: string, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with confirmation */ | ||
| withConfirm: (actionName: string, confirm: ActionConfirm, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with success handler */ | ||
| withSuccess: (actionName: string, onSuccess: ActionOnSuccess, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| }; | ||
| /** | ||
| * Dynamic value - can be a literal or a `{ $state }` reference to the state model. | ||
| * | ||
| * Used in action params and validation args where values can either be | ||
| * hardcoded or resolved from state at runtime. | ||
| */ | ||
| type DynamicValue<T = unknown> = T | { | ||
| $state: string; | ||
| }; | ||
| /** | ||
| * Dynamic string value | ||
| */ | ||
| type DynamicString = DynamicValue<string>; | ||
| /** | ||
| * Dynamic number value | ||
| */ | ||
| type DynamicNumber = DynamicValue<number>; | ||
| /** | ||
| * Dynamic boolean value | ||
| */ | ||
| type DynamicBoolean = DynamicValue<boolean>; | ||
| /** | ||
| * Zod schema for dynamic values | ||
| */ | ||
| declare const DynamicValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicStringSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicNumberSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicBooleanSchema: z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| type RepeatStatePath = string | { | ||
| $item: string; | ||
| }; | ||
| /** | ||
| * Base UI element structure for v2 | ||
| */ | ||
| interface UIElement<T extends string = string, P = Record<string, unknown>> { | ||
| /** Component type from the catalog */ | ||
| type: T; | ||
| /** Component props */ | ||
| props: P; | ||
| /** Child element keys (flat structure) */ | ||
| children?: string[]; | ||
| slots?: Record<string, string[]>; | ||
| /** Visibility condition */ | ||
| visible?: VisibilityCondition; | ||
| /** Event bindings — maps event names to action bindings */ | ||
| on?: Record<string, ActionBinding | ActionBinding[]>; | ||
| /** Repeat children once per item in a state array */ | ||
| repeat?: { | ||
| statePath: RepeatStatePath; | ||
| key?: string; | ||
| }; | ||
| /** | ||
| * State watchers — maps JSON Pointer state paths to action bindings. | ||
| * When the value at a watched path changes, the bound actions fire. | ||
| * Useful for cascading dependencies (e.g. country → city option loading). | ||
| */ | ||
| watch?: Record<string, ActionBinding | ActionBinding[]>; | ||
| } | ||
| /** | ||
| * Element with key and parentKey for use with flatToTree. | ||
| * When elements are in an array (not a keyed map), key and parentKey | ||
| * are needed to establish identity and parent-child relationships. | ||
| */ | ||
| interface FlatElement<T extends string = string, P = Record<string, unknown>> extends UIElement<T, P> { | ||
| /** Unique key identifying this element */ | ||
| key: string; | ||
| /** Parent element key (null for root) */ | ||
| parentKey?: string | null; | ||
| } | ||
| /** | ||
| * Shared comparison operators for visibility conditions. | ||
| * | ||
| * Use at most ONE comparison operator per condition. If multiple are | ||
| * provided, only the first matching one is evaluated (precedence: | ||
| * eq > neq > gt > gte > lt > lte). With no operator, truthiness is checked. | ||
| * | ||
| * `not` inverts the final result of whichever operator (or truthiness | ||
| * check) is used. | ||
| */ | ||
| type ComparisonOperators = { | ||
| eq?: unknown; | ||
| neq?: unknown; | ||
| gt?: number | { | ||
| $state: string; | ||
| }; | ||
| gte?: number | { | ||
| $state: string; | ||
| }; | ||
| lt?: number | { | ||
| $state: string; | ||
| }; | ||
| lte?: number | { | ||
| $state: string; | ||
| }; | ||
| not?: true; | ||
| }; | ||
| /** | ||
| * A single state-based condition. | ||
| * Resolves `$state` to a value from the state model, then applies the operator. | ||
| * Without an operator, checks truthiness. | ||
| * | ||
| * When `not` is `true`, the result of the entire condition is inverted. | ||
| * For example `{ $state: "/count", gt: 5, not: true }` means "NOT greater than 5". | ||
| */ | ||
| type StateCondition = { | ||
| $state: string; | ||
| } & ComparisonOperators; | ||
| /** | ||
| * A condition that resolves `$item` to a field on the current repeat item. | ||
| * Only meaningful inside a `repeat` scope. | ||
| * | ||
| * Use `""` to reference the whole item, or `"field"` for a specific field. | ||
| */ | ||
| type ItemCondition = { | ||
| $item: string; | ||
| } & ComparisonOperators; | ||
| /** | ||
| * A condition that resolves `$index` to the current repeat array index. | ||
| * Only meaningful inside a `repeat` scope. | ||
| */ | ||
| type IndexCondition = { | ||
| $index: true; | ||
| } & ComparisonOperators; | ||
| /** A single visibility condition (state, item, or index). */ | ||
| type SingleCondition = StateCondition | ItemCondition | IndexCondition; | ||
| /** | ||
| * AND wrapper — all child conditions must be true. | ||
| * This is the explicit form of the implicit array AND (`SingleCondition[]`). | ||
| * Unlike the implicit form, `$and` supports nested `$or` and `$and` conditions. | ||
| */ | ||
| type AndCondition = { | ||
| $and: VisibilityCondition[]; | ||
| }; | ||
| /** | ||
| * OR wrapper — at least one child condition must be true. | ||
| */ | ||
| type OrCondition = { | ||
| $or: VisibilityCondition[]; | ||
| }; | ||
| /** | ||
| * Visibility condition types. | ||
| * - `boolean` — always/never | ||
| * - `SingleCondition` — single condition (`$state`, `$item`, or `$index`) | ||
| * - `SingleCondition[]` — implicit AND (all must be true) | ||
| * - `AndCondition` — `{ $and: [...] }`, explicit AND (all must be true) | ||
| * - `OrCondition` — `{ $or: [...] }`, at least one must be true | ||
| */ | ||
| type VisibilityCondition = boolean | SingleCondition | SingleCondition[] | AndCondition | OrCondition; | ||
| /** | ||
| * Flat UI tree structure (optimized for LLM generation) | ||
| */ | ||
| interface Spec { | ||
| /** Root element key */ | ||
| root: string; | ||
| /** Flat map of elements by key */ | ||
| elements: Record<string, UIElement>; | ||
| /** Optional initial state to seed the state model. | ||
| * Components using statePath will read from / write to this state. */ | ||
| state?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * State model type | ||
| */ | ||
| type StateModel = Record<string, unknown>; | ||
| /** | ||
| * An abstract store that owns state and notifies subscribers on change. | ||
| * | ||
| * Consumers can supply their own implementation (backed by Redux, Zustand, | ||
| * XState, etc.) or use the built-in {@link createStateStore} for a simple | ||
| * in-memory store. | ||
| */ | ||
| interface StateStore { | ||
| /** Read a value by JSON Pointer path. */ | ||
| get: (path: string) => unknown; | ||
| /** | ||
| * Write a value by JSON Pointer path and notify subscribers. | ||
| * Equality is checked by reference (`===`), not deep comparison. | ||
| * Callers must pass a new object/array reference for changes to be detected. | ||
| */ | ||
| set: (path: string, value: unknown) => void; | ||
| /** | ||
| * Write multiple values at once and notify subscribers (single notification). | ||
| * Each value is compared by reference (`===`); only paths whose value | ||
| * actually changed are applied. | ||
| */ | ||
| update: (updates: Record<string, unknown>) => void; | ||
| /** Return the full state object (used by `useSyncExternalStore`). */ | ||
| getSnapshot: () => StateModel; | ||
| /** Optional server snapshot for SSR (passed to `useSyncExternalStore`). Falls back to `getSnapshot` when omitted. */ | ||
| getServerSnapshot?: () => StateModel; | ||
| /** Register a listener that is called on every state change. Returns an unsubscribe function. */ | ||
| subscribe: (listener: () => void) => () => void; | ||
| } | ||
| /** | ||
| * Component schema definition using Zod | ||
| */ | ||
| type ComponentSchema = z.ZodType<Record<string, unknown>>; | ||
| /** | ||
| * Validation mode for catalog validation | ||
| */ | ||
| type ValidationMode = "strict" | "warn" | "ignore"; | ||
| /** | ||
| * JSON patch operation types (RFC 6902) | ||
| */ | ||
| type PatchOp = "add" | "remove" | "replace" | "move" | "copy" | "test"; | ||
| /** | ||
| * JSON patch operation (RFC 6902) | ||
| */ | ||
| interface JsonPatch { | ||
| op: PatchOp; | ||
| path: string; | ||
| /** Required for add, replace, test */ | ||
| value?: unknown; | ||
| /** Required for move, copy (source location) */ | ||
| from?: string; | ||
| } | ||
| /** | ||
| * Resolve a dynamic value against a state model | ||
| */ | ||
| declare function resolveDynamicValue<T>(value: DynamicValue<T>, stateModel: StateModel): T | undefined; | ||
| /** | ||
| * Get a value from an object by JSON Pointer path (RFC 6901) | ||
| */ | ||
| declare function getByPath(obj: unknown, path: string): unknown; | ||
| declare function resolveRepeatStatePath(statePath: RepeatStatePath, repeatBasePath?: string | null): string | undefined; | ||
| declare function resolveRepeatItemStatePath(statePath: string, index: number): string; | ||
| /** | ||
| * Set a value in an object by JSON Pointer path (RFC 6901). | ||
| * Automatically creates arrays when the path segment is a numeric index. | ||
| */ | ||
| declare function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Add a value per RFC 6902 "add" semantics. | ||
| * For objects: create-or-replace the member. | ||
| * For arrays: insert before the given index, or append if "-". | ||
| */ | ||
| declare function addByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Remove a value per RFC 6902 "remove" semantics. | ||
| * For objects: delete the property. | ||
| * For arrays: splice out the element at the given index. | ||
| */ | ||
| declare function removeByPath(obj: Record<string, unknown>, path: string): void; | ||
| /** | ||
| * Find a form value from params and/or state. | ||
| * Useful in action handlers to locate form input values regardless of path format. | ||
| * | ||
| * Checks in order: | ||
| * 1. Direct param key (if not a path reference) | ||
| * 2. Param keys ending with the field name | ||
| * 3. State keys ending with the field name (dot notation) | ||
| * 4. State path using getByPath (slash notation) | ||
| * | ||
| * @example | ||
| * // Find "name" from params or state | ||
| * const name = findFormValue("name", params, state); | ||
| * | ||
| * // Will find from: params.name, params["form.name"], state["form.name"], or getByPath(state, "name") | ||
| */ | ||
| declare function findFormValue(fieldName: string, params?: Record<string, unknown>, state?: Record<string, unknown>): unknown; | ||
| /** | ||
| * A SpecStream line - a single patch operation in the stream. | ||
| */ | ||
| type SpecStreamLine = JsonPatch; | ||
| /** | ||
| * Parse a single SpecStream line into a patch operation. | ||
| * Returns null if the line is invalid or empty. | ||
| * | ||
| * SpecStream is json-render's streaming format where each line is a JSON patch | ||
| * operation that progressively builds up the final spec. | ||
| */ | ||
| declare function parseSpecStreamLine(line: string): SpecStreamLine | null; | ||
| /** | ||
| * Apply a single RFC 6902 JSON Patch operation to an object. | ||
| * Mutates the object in place. | ||
| * | ||
| * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test. | ||
| * | ||
| * @throws {Error} If a "test" operation fails (value mismatch). | ||
| */ | ||
| declare function applySpecStreamPatch<T extends Record<string, unknown>>(obj: T, patch: SpecStreamLine): T; | ||
| /** | ||
| * Apply a single RFC 6902 JSON Patch operation to a Spec. | ||
| * Mutates the spec in place and returns it. | ||
| * | ||
| * This is a typed convenience wrapper around `applySpecStreamPatch` that | ||
| * accepts a `Spec` directly without requiring a cast to `Record<string, unknown>`. | ||
| * | ||
| * Note: This mutates the spec. For React state updates, spread the result | ||
| * to create a new reference: `setSpec({ ...applySpecPatch(spec, patch) })`. | ||
| * | ||
| * @example | ||
| * let spec: Spec = { root: "", elements: {} }; | ||
| * applySpecPatch(spec, { op: "add", path: "/root", value: "main" }); | ||
| */ | ||
| declare function applySpecPatch(spec: Spec, patch: SpecStreamLine): Spec; | ||
| /** | ||
| * Convert a nested (tree-structured) spec into the flat `Spec` format used | ||
| * by json-render renderers. | ||
| * | ||
| * In the nested format each node has inline `children` as an array of child | ||
| * objects. This function walks the tree, assigns auto-generated keys | ||
| * (`el-0`, `el-1`, ...), and produces a flat `{ root, elements, state }` spec. | ||
| * | ||
| * The top-level `state` field (if present on the root node) is hoisted to | ||
| * `spec.state`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const nested = { | ||
| * type: "Card", | ||
| * props: { title: "Hello" }, | ||
| * children: [ | ||
| * { type: "Text", props: { content: "World" } }, | ||
| * ], | ||
| * state: { count: 0 }, | ||
| * }; | ||
| * const spec = nestedToFlat(nested); | ||
| * // { | ||
| * // root: "el-0", | ||
| * // elements: { | ||
| * // "el-0": { type: "Card", props: { title: "Hello" }, children: ["el-1"] }, | ||
| * // "el-1": { type: "Text", props: { content: "World" }, children: [] }, | ||
| * // }, | ||
| * // state: { count: 0 }, | ||
| * // } | ||
| * ``` | ||
| */ | ||
| declare function nestedToFlat(nested: Record<string, unknown>): Spec; | ||
| /** | ||
| * Compile a SpecStream string into a JSON object. | ||
| * Each line should be a patch operation. | ||
| * | ||
| * @example | ||
| * const stream = `{"op":"add","path":"/name","value":"Alice"} | ||
| * {"op":"add","path":"/age","value":30}`; | ||
| * const result = compileSpecStream(stream); | ||
| * // { name: "Alice", age: 30 } | ||
| */ | ||
| declare function compileSpecStream<T extends Record<string, unknown> = Record<string, unknown>>(stream: string, initial?: T): T; | ||
| /** | ||
| * Streaming SpecStream compiler. | ||
| * Useful for processing SpecStream data as it streams in from AI. | ||
| * | ||
| * @example | ||
| * const compiler = createSpecStreamCompiler<MySpec>(); | ||
| * | ||
| * // As chunks arrive: | ||
| * const { result, newPatches } = compiler.push(chunk); | ||
| * if (newPatches.length > 0) { | ||
| * updateUI(result); | ||
| * } | ||
| * | ||
| * // When done: | ||
| * const finalResult = compiler.getResult(); | ||
| */ | ||
| interface SpecStreamCompiler<T> { | ||
| /** Push a chunk of text. Returns the current result and any new patches applied. */ | ||
| push(chunk: string): { | ||
| result: T; | ||
| newPatches: SpecStreamLine[]; | ||
| }; | ||
| /** Get the current compiled result */ | ||
| getResult(): T; | ||
| /** Get all patches that have been applied */ | ||
| getPatches(): SpecStreamLine[]; | ||
| /** Reset the compiler to initial state */ | ||
| reset(initial?: Partial<T>): void; | ||
| } | ||
| /** | ||
| * Create a streaming SpecStream compiler. | ||
| * | ||
| * SpecStream is json-render's streaming format. AI outputs patch operations | ||
| * line by line, and this compiler progressively builds the final spec. | ||
| * | ||
| * @example | ||
| * const compiler = createSpecStreamCompiler<TimelineSpec>(); | ||
| * | ||
| * // Process streaming response | ||
| * const reader = response.body.getReader(); | ||
| * while (true) { | ||
| * const { done, value } = await reader.read(); | ||
| * if (done) break; | ||
| * | ||
| * const { result, newPatches } = compiler.push(decoder.decode(value)); | ||
| * if (newPatches.length > 0) { | ||
| * setSpec(result); // Update UI with partial result | ||
| * } | ||
| * } | ||
| */ | ||
| declare function createSpecStreamCompiler<T = Record<string, unknown>>(initial?: Partial<T>): SpecStreamCompiler<T>; | ||
| /** | ||
| * Callbacks for the mixed stream parser. | ||
| */ | ||
| interface MixedStreamCallbacks { | ||
| /** Called when a JSONL patch line is parsed */ | ||
| onPatch: (patch: SpecStreamLine) => void; | ||
| /** Called when a text (non-JSONL) line is received */ | ||
| onText: (text: string) => void; | ||
| } | ||
| /** | ||
| * A stateful parser for mixed streams that contain both text and JSONL patches. | ||
| * Used in chat + GenUI scenarios where an LLM responds with conversational text | ||
| * interleaved with json-render JSONL patch operations. | ||
| */ | ||
| interface MixedStreamParser { | ||
| /** Push a chunk of streamed data. Calls onPatch/onText for each complete line. */ | ||
| push(chunk: string): void; | ||
| /** Flush any remaining buffered content. Call when the stream ends. */ | ||
| flush(): void; | ||
| } | ||
| /** | ||
| * Create a parser for mixed text + JSONL streams. | ||
| * | ||
| * In chat + GenUI scenarios, an LLM streams a response that contains both | ||
| * conversational text and json-render JSONL patch lines. This parser buffers | ||
| * incoming chunks, splits them into lines, and classifies each line as either | ||
| * a JSONL patch (via `parseSpecStreamLine`) or plain text. | ||
| * | ||
| * @example | ||
| * const parser = createMixedStreamParser({ | ||
| * onText: (text) => appendToMessage(text), | ||
| * onPatch: (patch) => applySpecPatch(spec, patch), | ||
| * }); | ||
| * | ||
| * // As chunks arrive from the stream: | ||
| * for await (const chunk of stream) { | ||
| * parser.push(chunk); | ||
| * } | ||
| * parser.flush(); | ||
| */ | ||
| declare function createMixedStreamParser(callbacks: MixedStreamCallbacks): MixedStreamParser; | ||
| /** | ||
| * Minimal chunk shape compatible with the AI SDK's `UIMessageChunk`. | ||
| * | ||
| * Defined here so that `@json-render/core` has no dependency on the `ai` | ||
| * package. The discriminated union covers the three text-related chunk types | ||
| * the transform inspects; all other chunk types pass through via the fallback. | ||
| */ | ||
| type StreamChunk = { | ||
| type: "text-start"; | ||
| id: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: "text-delta"; | ||
| id: string; | ||
| delta: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: "text-end"; | ||
| id: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: string; | ||
| [k: string]: unknown; | ||
| }; | ||
| /** | ||
| * Creates a `TransformStream` that intercepts AI SDK UI message stream chunks | ||
| * and classifies text content as either prose or json-render JSONL patches. | ||
| * | ||
| * Two classification modes: | ||
| * | ||
| * 1. **Fence mode** (preferred): Lines between ` ```spec ` and ` ``` ` are | ||
| * parsed as JSONL patches. Fence delimiters are swallowed (not emitted). | ||
| * 2. **Heuristic mode** (backward compat): Outside of fences, lines starting | ||
| * with `{` are buffered and tested with `parseSpecStreamLine`. Valid patches | ||
| * are emitted as {@link SPEC_DATA_PART_TYPE} parts; everything else is | ||
| * flushed as text. | ||
| * | ||
| * Non-text chunks (tool events, step markers, etc.) are passed through unchanged. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { createJsonRenderTransform } from "@json-render/core"; | ||
| * import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; | ||
| * | ||
| * const stream = createUIMessageStream({ | ||
| * execute: async ({ writer }) => { | ||
| * writer.merge( | ||
| * result.toUIMessageStream().pipeThrough(createJsonRenderTransform()), | ||
| * ); | ||
| * }, | ||
| * }); | ||
| * return createUIMessageStreamResponse({ stream }); | ||
| * ``` | ||
| */ | ||
| declare function createJsonRenderTransform(): TransformStream<StreamChunk, StreamChunk>; | ||
| /** | ||
| * The key registered in `AppDataParts` for json-render specs. | ||
| * The AI SDK automatically prefixes this with `"data-"` on the wire, | ||
| * so the actual stream chunk type is `"data-spec"` (see {@link SPEC_DATA_PART_TYPE}). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { SPEC_DATA_PART, type SpecDataPart } from "@json-render/core"; | ||
| * type AppDataParts = { [SPEC_DATA_PART]: SpecDataPart }; | ||
| * ``` | ||
| */ | ||
| declare const SPEC_DATA_PART: "spec"; | ||
| /** | ||
| * The wire-format type string as it appears in stream chunks and message parts. | ||
| * This is `"data-"` + {@link SPEC_DATA_PART} — i.e. `"data-spec"`. | ||
| * | ||
| * Use this constant when filtering message parts or enqueuing stream chunks. | ||
| */ | ||
| declare const SPEC_DATA_PART_TYPE: "data-spec"; | ||
| /** | ||
| * Discriminated union for the payload of a {@link SPEC_DATA_PART_TYPE} SSE part. | ||
| * | ||
| * - `"patch"`: A single RFC 6902 JSON Patch operation (streaming, progressive UI). | ||
| * - `"flat"`: A complete flat spec with `root`, `elements`, and optional `state`. | ||
| * - `"nested"`: A complete nested spec (tree structure — schema depends on catalog). | ||
| */ | ||
| type SpecDataPart = { | ||
| type: "patch"; | ||
| patch: JsonPatch; | ||
| } | { | ||
| type: "flat"; | ||
| spec: Spec; | ||
| } | { | ||
| type: "nested"; | ||
| spec: Record<string, unknown>; | ||
| }; | ||
| /** | ||
| * Convenience wrapper that pipes an AI SDK UI message stream through the | ||
| * json-render transform, classifying text as prose or JSONL patches. | ||
| * | ||
| * Eliminates the need for manual `pipeThrough(createJsonRenderTransform())` | ||
| * and the associated type cast. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { pipeJsonRender } from "@json-render/core"; | ||
| * | ||
| * const stream = createUIMessageStream({ | ||
| * execute: async ({ writer }) => { | ||
| * writer.merge(pipeJsonRender(result.toUIMessageStream())); | ||
| * }, | ||
| * }); | ||
| * return createUIMessageStreamResponse({ stream }); | ||
| * ``` | ||
| */ | ||
| declare function pipeJsonRender<T = StreamChunk>(stream: ReadableStream<T>): ReadableStream<T>; | ||
| /** | ||
| * Immutably set a value at a JSON Pointer path using structural sharing. | ||
| * Only objects along the path are shallow-cloned; untouched branches keep | ||
| * their original references. | ||
| */ | ||
| declare function immutableSetByPath(root: StateModel, path: string, value: unknown): StateModel; | ||
| /** | ||
| * Create a simple in-memory {@link StateStore}. | ||
| * | ||
| * This is the default store used by `StateProvider` when no external store is | ||
| * provided. It mirrors the previous `useState`-based behaviour but is | ||
| * framework-agnostic so it can also be used in tests or non-React contexts. | ||
| */ | ||
| declare function createStateStore(initialState?: StateModel): StateStore; | ||
| /** | ||
| * Configuration for {@link createStoreAdapter}. Adapter authors supply these | ||
| * three callbacks; everything else (get, set, update, no-op detection, | ||
| * getServerSnapshot) is handled by the returned {@link StateStore}. | ||
| */ | ||
| interface StoreAdapterConfig { | ||
| /** Return the current state snapshot from the underlying store. */ | ||
| getSnapshot: () => StateModel; | ||
| /** Write a new state snapshot to the underlying store. */ | ||
| setSnapshot: (next: StateModel) => void; | ||
| /** Subscribe to changes in the underlying store. Return an unsubscribe fn. */ | ||
| subscribe: (listener: () => void) => () => void; | ||
| } | ||
| /** | ||
| * Build a full {@link StateStore} from a minimal adapter config. | ||
| * | ||
| * Handles `get`, `set` (with no-op detection), `update` (batched, with no-op | ||
| * detection), `getSnapshot`, `getServerSnapshot`, and `subscribe` -- so each | ||
| * adapter only needs to wire its snapshot source, write API, and subscribe | ||
| * mechanism. | ||
| */ | ||
| declare function createStoreAdapter(config: StoreAdapterConfig): StateStore; | ||
| /** | ||
| * Recursively flatten a plain object into a `Record<string, unknown>` keyed by | ||
| * JSON Pointer paths. Only leaf values (non-plain-object) appear in the output. | ||
| * | ||
| * Includes circular reference protection and a depth cap to prevent stack | ||
| * overflow on pathological inputs. | ||
| * | ||
| * ```ts | ||
| * flattenToPointers({ user: { name: "Alice" }, count: 1 }) | ||
| * // => { "/user/name": "Alice", "/count": 1 } | ||
| * ``` | ||
| */ | ||
| declare function flattenToPointers(obj: Record<string, unknown>, prefix?: string, _depth?: number, _seen?: Set<object>, _warned?: { | ||
| current: boolean; | ||
| }): Record<string, unknown>; | ||
| export { type Action as $, type AndCondition as A, parseSpecStreamLine as B, type ComponentSchema as C, type DynamicValue as D, applySpecStreamPatch as E, type FlatElement as F, applySpecPatch as G, nestedToFlat as H, type ItemCondition as I, type JsonPatch as J, compileSpecStream as K, createSpecStreamCompiler as L, type MixedStreamCallbacks as M, createMixedStreamParser as N, type OrCondition as O, type PatchOp as P, createJsonRenderTransform as Q, type RepeatStatePath as R, type StateModel as S, pipeJsonRender as T, type UIElement as U, type VisibilityCondition as V, SPEC_DATA_PART as W, SPEC_DATA_PART_TYPE as X, type StoreAdapterConfig as Y, createStateStore as Z, type ActionBinding as _, type StateCondition as a, type ActionConfirm as a0, type ActionOnSuccess as a1, type ActionOnError as a2, type ActionHandler as a3, type ActionDefinition as a4, type ResolvedAction as a5, type ActionExecutionContext as a6, ActionBindingSchema as a7, ActionSchema as a8, ActionConfirmSchema as a9, ActionOnSuccessSchema as aa, ActionOnErrorSchema as ab, resolveAction as ac, executeAction as ad, interpolateString as ae, actionBinding as af, action as ag, immutableSetByPath as ah, flattenToPointers as ai, createStoreAdapter as aj, type Spec as b, type DynamicString as c, type DynamicNumber as d, type DynamicBoolean as e, type IndexCondition as f, type SingleCondition as g, type StateStore as h, type ValidationMode as i, type SpecStreamLine as j, type SpecStreamCompiler as k, type MixedStreamParser as l, type StreamChunk as m, type SpecDataPart as n, DynamicValueSchema as o, DynamicStringSchema as p, DynamicNumberSchema as q, DynamicBooleanSchema as r, resolveDynamicValue as s, getByPath as t, resolveRepeatStatePath as u, resolveRepeatItemStatePath as v, setByPath as w, addByPath as x, removeByPath as y, findFormValue as z }; |
+57
-6
@@ -1,3 +0,3 @@ | ||
| import { S as StateModel, V as VisibilityCondition, a as StateCondition, A as AndCondition, O as OrCondition, D as DynamicValue, b as Spec, J as JsonPatch } from './store-utils-D98Czbil.mjs'; | ||
| export { Y as Action, X as ActionBinding, a4 as ActionBindingSchema, Z as ActionConfirm, a6 as ActionConfirmSchema, a1 as ActionDefinition, a3 as ActionExecutionContext, a0 as ActionHandler, $ as ActionOnError, a8 as ActionOnErrorSchema, _ as ActionOnSuccess, a7 as ActionOnSuccessSchema, a5 as ActionSchema, C as ComponentSchema, e as DynamicBoolean, r as DynamicBooleanSchema, d as DynamicNumber, q as DynamicNumberSchema, c as DynamicString, p as DynamicStringSchema, o as DynamicValueSchema, F as FlatElement, f as IndexCondition, I as ItemCondition, M as MixedStreamCallbacks, l as MixedStreamParser, P as PatchOp, a2 as ResolvedAction, Q as SPEC_DATA_PART, R as SPEC_DATA_PART_TYPE, g as SingleCondition, n as SpecDataPart, k as SpecStreamCompiler, j as SpecStreamLine, h as StateStore, T as StoreAdapterConfig, m as StreamChunk, U as UIElement, i as ValidationMode, ad as action, ac as actionBinding, v as addByPath, B as applySpecPatch, z as applySpecStreamPatch, G as compileSpecStream, L as createJsonRenderTransform, K as createMixedStreamParser, H as createSpecStreamCompiler, W as createStateStore, aa as executeAction, x as findFormValue, t as getByPath, ab as interpolateString, E as nestedToFlat, y as parseSpecStreamLine, N as pipeJsonRender, w as removeByPath, a9 as resolveAction, s as resolveDynamicValue, u as setByPath } from './store-utils-D98Czbil.mjs'; | ||
| import { S as StateModel, V as VisibilityCondition, a as StateCondition, A as AndCondition, O as OrCondition, D as DynamicValue, b as Spec, J as JsonPatch } from './store-utils-CGwRAVOR.mjs'; | ||
| export { $ as Action, _ as ActionBinding, a7 as ActionBindingSchema, a0 as ActionConfirm, a9 as ActionConfirmSchema, a4 as ActionDefinition, a6 as ActionExecutionContext, a3 as ActionHandler, a2 as ActionOnError, ab as ActionOnErrorSchema, a1 as ActionOnSuccess, aa as ActionOnSuccessSchema, a8 as ActionSchema, C as ComponentSchema, e as DynamicBoolean, r as DynamicBooleanSchema, d as DynamicNumber, q as DynamicNumberSchema, c as DynamicString, p as DynamicStringSchema, o as DynamicValueSchema, F as FlatElement, f as IndexCondition, I as ItemCondition, M as MixedStreamCallbacks, l as MixedStreamParser, P as PatchOp, R as RepeatStatePath, a5 as ResolvedAction, W as SPEC_DATA_PART, X as SPEC_DATA_PART_TYPE, g as SingleCondition, n as SpecDataPart, k as SpecStreamCompiler, j as SpecStreamLine, h as StateStore, Y as StoreAdapterConfig, m as StreamChunk, U as UIElement, i as ValidationMode, ag as action, af as actionBinding, x as addByPath, G as applySpecPatch, E as applySpecStreamPatch, K as compileSpecStream, Q as createJsonRenderTransform, N as createMixedStreamParser, L as createSpecStreamCompiler, Z as createStateStore, ad as executeAction, z as findFormValue, t as getByPath, ae as interpolateString, H as nestedToFlat, B as parseSpecStreamLine, T as pipeJsonRender, y as removeByPath, ac as resolveAction, s as resolveDynamicValue, v as resolveRepeatItemStatePath, u as resolveRepeatStatePath, w as setByPath } from './store-utils-CGwRAVOR.mjs'; | ||
| import { z } from 'zod'; | ||
@@ -12,2 +12,27 @@ | ||
| /** | ||
| * Strict variant for spec validation: rejects unknown keys, so malformed | ||
| * conditions (e.g. mixing $state and $item in one object) are caught at | ||
| * validation time instead of silently evaluating to hidden at runtime. | ||
| */ | ||
| /** | ||
| * True when a condition references the repeat-item scope ($item or $index) | ||
| * anywhere in its tree. Renderers use this to apply a repeat container's own | ||
| * visible condition as a per-item filter instead of evaluating it (and | ||
| * failing) outside the repeat scope. | ||
| */ | ||
| declare function conditionUsesItemScope(condition: VisibilityCondition | undefined): boolean; | ||
| /** | ||
| * Splits a repeat container's visible condition into a container-level gate | ||
| * and a per-item filter. Top-level AND structures (arrays, $and) partition | ||
| * cleanly: conjuncts that reference $item/$index filter items, the rest gate | ||
| * the container. An $or that mixes scopes cannot be partitioned soundly and | ||
| * is applied entirely per item (state parts still evaluate correctly there; | ||
| * the container shell just cannot be hidden by it). | ||
| */ | ||
| declare function splitRepeatVisibility(condition: VisibilityCondition | undefined): { | ||
| container: VisibilityCondition | undefined; | ||
| itemFilter: VisibilityCondition | undefined; | ||
| }; | ||
| declare const VisibilityConditionStrictSchema: z.ZodType<VisibilityCondition>; | ||
| /** | ||
| * Context for evaluating visibility conditions. | ||
@@ -473,3 +498,3 @@ * | ||
| /** Machine-readable issue code for programmatic handling */ | ||
| code: "missing_root" | "root_not_found" | "missing_child" | "visible_in_props" | "orphaned_element" | "empty_spec" | "on_in_props" | "repeat_in_props" | "watch_in_props"; | ||
| code: "missing_root" | "root_not_found" | "missing_child" | "invalid_visible" | "repeat_without_children" | "repeat_item_outside_scope" | "repeat_state_mismatch" | "visible_in_props" | "orphaned_element" | "empty_spec" | "on_in_props" | "repeat_in_props" | "watch_in_props"; | ||
| } | ||
@@ -524,5 +549,25 @@ /** | ||
| */ | ||
| declare function autoFixSpec(spec: Spec): { | ||
| interface SpecFix { | ||
| message: string; | ||
| /** | ||
| * Lossy fixes change what renders (e.g. pruning a dangling child | ||
| * reference); lossless fixes only relocate misplaced fields. Callers with a | ||
| * repair loop should prefer re-prompting over accepting lossy fixes, and | ||
| * use the lossy-fixed spec as a last resort. | ||
| */ | ||
| lossy: boolean; | ||
| } | ||
| interface AutoFixOptions { | ||
| /** | ||
| * Apply lossy fixes (content pruning). Default true. Callers with a repair | ||
| * loop should pass false while retries remain so the model regenerates the | ||
| * missing content, then true as a last resort. | ||
| */ | ||
| lossy?: boolean; | ||
| } | ||
| declare function autoFixSpec(spec: Spec, options?: AutoFixOptions): { | ||
| spec: Spec; | ||
| fixes: string[]; | ||
| /** Structured fix records; fixes is the plain-message projection. */ | ||
| fixDetails: SpecFix[]; | ||
| }; | ||
@@ -804,3 +849,9 @@ /** | ||
| type InferSpecObject<Shape, TCatalog> = { | ||
| [K in keyof Shape]: InferSpecField<Shape[K], TCatalog>; | ||
| [K in keyof Shape as Shape[K] extends { | ||
| optional: true; | ||
| } ? never : K]: InferSpecField<Shape[K], TCatalog>; | ||
| } & { | ||
| [K in keyof Shape as Shape[K] extends { | ||
| optional: true; | ||
| } ? K : never]?: InferSpecField<Shape[K], TCatalog>; | ||
| }; | ||
@@ -892,2 +943,2 @@ type InferSpecField<T, TCatalog> = T extends SchemaType<"string"> ? string : T extends SchemaType<"number"> ? number : T extends SchemaType<"boolean"> ? boolean : T extends SchemaType<"array", infer Item> ? InferSpecField<Item, TCatalog>[] : T extends SchemaType<"object", infer Shape> ? InferSpecObject<Shape, TCatalog> : T extends SchemaType<"record", infer Value> ? Record<string, InferSpecField<Value, TCatalog>> : T extends SchemaType<"ref", infer Path> ? InferRefType<Path, TCatalog> : T extends SchemaType<"propsOf", infer Path> ? InferPropsOfType<Path, TCatalog> : T extends SchemaType<"any"> ? unknown : unknown; | ||
| export { type ActionDispatchInfo, type ActionObserver, type ActionSettleInfo, AndCondition, type BuildEditUserPromptOptions, type BuiltInAction, type Catalog, type ComputedFunction, type DirectiveDefinition, type DirectiveRegistry, DynamicValue, type EditConfig, type EditMode, type InferActionParams, type InferCatalogActions, type InferCatalogComponents, type InferCatalogInput, type InferComponentProps, type InferSpec, JsonPatch, type JsonSchemaOptions, OrCondition, type PromptContext, type PromptOptions, type PromptTemplate, type PropExpression, type PropResolutionContext, type Schema, type SchemaBuilder, type SchemaDefinition, type SchemaOptions, type SchemaType, Spec, type SpecIssue, type SpecIssueSeverity, type SpecValidationIssues, type SpecValidationResult, StateCondition, StateModel, type UserPromptOptions, type ValidateSpecOptions, type ValidationCheck, type ValidationCheckResult, ValidationCheckSchema, type ValidationConfig, ValidationConfigSchema, type ValidationContext, type ValidationFunction, type ValidationFunctionDefinition, type ValidationResult, VisibilityCondition, VisibilityConditionSchema, type VisibilityContext, autoFixSpec, buildEditInstructions, buildEditUserPrompt, buildUserPrompt, builtInValidationFunctions, check, createDirectiveRegistry, deepMergeSpec, defineCatalog, defineDirective, defineSchema, diffToPatches, evaluateVisibility, findDirective, formatSpecIssues, isDevtoolsActive, isNonEmptySpec, markDevtoolsActive, nextActionDispatchId, notifyActionDispatch, notifyActionSettle, registerActionObserver, resolveActionParam, resolveBindings, resolveElementProps, resolvePropValue, runValidation, runValidationCheck, subscribeDevtoolsActive, validateSpec, visibility }; | ||
| export { type ActionDispatchInfo, type ActionObserver, type ActionSettleInfo, AndCondition, type BuildEditUserPromptOptions, type BuiltInAction, type Catalog, type ComputedFunction, type DirectiveDefinition, type DirectiveRegistry, DynamicValue, type EditConfig, type EditMode, type InferActionParams, type InferCatalogActions, type InferCatalogComponents, type InferCatalogInput, type InferComponentProps, type InferSpec, JsonPatch, type JsonSchemaOptions, OrCondition, type PromptContext, type PromptOptions, type PromptTemplate, type PropExpression, type PropResolutionContext, type Schema, type SchemaBuilder, type SchemaDefinition, type SchemaOptions, type SchemaType, Spec, type SpecIssue, type SpecIssueSeverity, type SpecValidationIssues, type SpecValidationResult, StateCondition, StateModel, type UserPromptOptions, type ValidateSpecOptions, type ValidationCheck, type ValidationCheckResult, ValidationCheckSchema, type ValidationConfig, ValidationConfigSchema, type ValidationContext, type ValidationFunction, type ValidationFunctionDefinition, type ValidationResult, VisibilityCondition, VisibilityConditionSchema, VisibilityConditionStrictSchema, type VisibilityContext, autoFixSpec, buildEditInstructions, buildEditUserPrompt, buildUserPrompt, builtInValidationFunctions, check, conditionUsesItemScope, createDirectiveRegistry, deepMergeSpec, defineCatalog, defineDirective, defineSchema, diffToPatches, evaluateVisibility, findDirective, formatSpecIssues, isDevtoolsActive, isNonEmptySpec, markDevtoolsActive, nextActionDispatchId, notifyActionDispatch, notifyActionSettle, registerActionObserver, resolveActionParam, resolveBindings, resolveElementProps, resolvePropValue, runValidation, runValidationCheck, splitRepeatVisibility, subscribeDevtoolsActive, validateSpec, visibility }; |
+57
-6
@@ -1,3 +0,3 @@ | ||
| import { S as StateModel, V as VisibilityCondition, a as StateCondition, A as AndCondition, O as OrCondition, D as DynamicValue, b as Spec, J as JsonPatch } from './store-utils-D98Czbil.js'; | ||
| export { Y as Action, X as ActionBinding, a4 as ActionBindingSchema, Z as ActionConfirm, a6 as ActionConfirmSchema, a1 as ActionDefinition, a3 as ActionExecutionContext, a0 as ActionHandler, $ as ActionOnError, a8 as ActionOnErrorSchema, _ as ActionOnSuccess, a7 as ActionOnSuccessSchema, a5 as ActionSchema, C as ComponentSchema, e as DynamicBoolean, r as DynamicBooleanSchema, d as DynamicNumber, q as DynamicNumberSchema, c as DynamicString, p as DynamicStringSchema, o as DynamicValueSchema, F as FlatElement, f as IndexCondition, I as ItemCondition, M as MixedStreamCallbacks, l as MixedStreamParser, P as PatchOp, a2 as ResolvedAction, Q as SPEC_DATA_PART, R as SPEC_DATA_PART_TYPE, g as SingleCondition, n as SpecDataPart, k as SpecStreamCompiler, j as SpecStreamLine, h as StateStore, T as StoreAdapterConfig, m as StreamChunk, U as UIElement, i as ValidationMode, ad as action, ac as actionBinding, v as addByPath, B as applySpecPatch, z as applySpecStreamPatch, G as compileSpecStream, L as createJsonRenderTransform, K as createMixedStreamParser, H as createSpecStreamCompiler, W as createStateStore, aa as executeAction, x as findFormValue, t as getByPath, ab as interpolateString, E as nestedToFlat, y as parseSpecStreamLine, N as pipeJsonRender, w as removeByPath, a9 as resolveAction, s as resolveDynamicValue, u as setByPath } from './store-utils-D98Czbil.js'; | ||
| import { S as StateModel, V as VisibilityCondition, a as StateCondition, A as AndCondition, O as OrCondition, D as DynamicValue, b as Spec, J as JsonPatch } from './store-utils-CGwRAVOR.js'; | ||
| export { $ as Action, _ as ActionBinding, a7 as ActionBindingSchema, a0 as ActionConfirm, a9 as ActionConfirmSchema, a4 as ActionDefinition, a6 as ActionExecutionContext, a3 as ActionHandler, a2 as ActionOnError, ab as ActionOnErrorSchema, a1 as ActionOnSuccess, aa as ActionOnSuccessSchema, a8 as ActionSchema, C as ComponentSchema, e as DynamicBoolean, r as DynamicBooleanSchema, d as DynamicNumber, q as DynamicNumberSchema, c as DynamicString, p as DynamicStringSchema, o as DynamicValueSchema, F as FlatElement, f as IndexCondition, I as ItemCondition, M as MixedStreamCallbacks, l as MixedStreamParser, P as PatchOp, R as RepeatStatePath, a5 as ResolvedAction, W as SPEC_DATA_PART, X as SPEC_DATA_PART_TYPE, g as SingleCondition, n as SpecDataPart, k as SpecStreamCompiler, j as SpecStreamLine, h as StateStore, Y as StoreAdapterConfig, m as StreamChunk, U as UIElement, i as ValidationMode, ag as action, af as actionBinding, x as addByPath, G as applySpecPatch, E as applySpecStreamPatch, K as compileSpecStream, Q as createJsonRenderTransform, N as createMixedStreamParser, L as createSpecStreamCompiler, Z as createStateStore, ad as executeAction, z as findFormValue, t as getByPath, ae as interpolateString, H as nestedToFlat, B as parseSpecStreamLine, T as pipeJsonRender, y as removeByPath, ac as resolveAction, s as resolveDynamicValue, v as resolveRepeatItemStatePath, u as resolveRepeatStatePath, w as setByPath } from './store-utils-CGwRAVOR.js'; | ||
| import { z } from 'zod'; | ||
@@ -12,2 +12,27 @@ | ||
| /** | ||
| * Strict variant for spec validation: rejects unknown keys, so malformed | ||
| * conditions (e.g. mixing $state and $item in one object) are caught at | ||
| * validation time instead of silently evaluating to hidden at runtime. | ||
| */ | ||
| /** | ||
| * True when a condition references the repeat-item scope ($item or $index) | ||
| * anywhere in its tree. Renderers use this to apply a repeat container's own | ||
| * visible condition as a per-item filter instead of evaluating it (and | ||
| * failing) outside the repeat scope. | ||
| */ | ||
| declare function conditionUsesItemScope(condition: VisibilityCondition | undefined): boolean; | ||
| /** | ||
| * Splits a repeat container's visible condition into a container-level gate | ||
| * and a per-item filter. Top-level AND structures (arrays, $and) partition | ||
| * cleanly: conjuncts that reference $item/$index filter items, the rest gate | ||
| * the container. An $or that mixes scopes cannot be partitioned soundly and | ||
| * is applied entirely per item (state parts still evaluate correctly there; | ||
| * the container shell just cannot be hidden by it). | ||
| */ | ||
| declare function splitRepeatVisibility(condition: VisibilityCondition | undefined): { | ||
| container: VisibilityCondition | undefined; | ||
| itemFilter: VisibilityCondition | undefined; | ||
| }; | ||
| declare const VisibilityConditionStrictSchema: z.ZodType<VisibilityCondition>; | ||
| /** | ||
| * Context for evaluating visibility conditions. | ||
@@ -473,3 +498,3 @@ * | ||
| /** Machine-readable issue code for programmatic handling */ | ||
| code: "missing_root" | "root_not_found" | "missing_child" | "visible_in_props" | "orphaned_element" | "empty_spec" | "on_in_props" | "repeat_in_props" | "watch_in_props"; | ||
| code: "missing_root" | "root_not_found" | "missing_child" | "invalid_visible" | "repeat_without_children" | "repeat_item_outside_scope" | "repeat_state_mismatch" | "visible_in_props" | "orphaned_element" | "empty_spec" | "on_in_props" | "repeat_in_props" | "watch_in_props"; | ||
| } | ||
@@ -524,5 +549,25 @@ /** | ||
| */ | ||
| declare function autoFixSpec(spec: Spec): { | ||
| interface SpecFix { | ||
| message: string; | ||
| /** | ||
| * Lossy fixes change what renders (e.g. pruning a dangling child | ||
| * reference); lossless fixes only relocate misplaced fields. Callers with a | ||
| * repair loop should prefer re-prompting over accepting lossy fixes, and | ||
| * use the lossy-fixed spec as a last resort. | ||
| */ | ||
| lossy: boolean; | ||
| } | ||
| interface AutoFixOptions { | ||
| /** | ||
| * Apply lossy fixes (content pruning). Default true. Callers with a repair | ||
| * loop should pass false while retries remain so the model regenerates the | ||
| * missing content, then true as a last resort. | ||
| */ | ||
| lossy?: boolean; | ||
| } | ||
| declare function autoFixSpec(spec: Spec, options?: AutoFixOptions): { | ||
| spec: Spec; | ||
| fixes: string[]; | ||
| /** Structured fix records; fixes is the plain-message projection. */ | ||
| fixDetails: SpecFix[]; | ||
| }; | ||
@@ -804,3 +849,9 @@ /** | ||
| type InferSpecObject<Shape, TCatalog> = { | ||
| [K in keyof Shape]: InferSpecField<Shape[K], TCatalog>; | ||
| [K in keyof Shape as Shape[K] extends { | ||
| optional: true; | ||
| } ? never : K]: InferSpecField<Shape[K], TCatalog>; | ||
| } & { | ||
| [K in keyof Shape as Shape[K] extends { | ||
| optional: true; | ||
| } ? K : never]?: InferSpecField<Shape[K], TCatalog>; | ||
| }; | ||
@@ -892,2 +943,2 @@ type InferSpecField<T, TCatalog> = T extends SchemaType<"string"> ? string : T extends SchemaType<"number"> ? number : T extends SchemaType<"boolean"> ? boolean : T extends SchemaType<"array", infer Item> ? InferSpecField<Item, TCatalog>[] : T extends SchemaType<"object", infer Shape> ? InferSpecObject<Shape, TCatalog> : T extends SchemaType<"record", infer Value> ? Record<string, InferSpecField<Value, TCatalog>> : T extends SchemaType<"ref", infer Path> ? InferRefType<Path, TCatalog> : T extends SchemaType<"propsOf", infer Path> ? InferPropsOfType<Path, TCatalog> : T extends SchemaType<"any"> ? unknown : unknown; | ||
| export { type ActionDispatchInfo, type ActionObserver, type ActionSettleInfo, AndCondition, type BuildEditUserPromptOptions, type BuiltInAction, type Catalog, type ComputedFunction, type DirectiveDefinition, type DirectiveRegistry, DynamicValue, type EditConfig, type EditMode, type InferActionParams, type InferCatalogActions, type InferCatalogComponents, type InferCatalogInput, type InferComponentProps, type InferSpec, JsonPatch, type JsonSchemaOptions, OrCondition, type PromptContext, type PromptOptions, type PromptTemplate, type PropExpression, type PropResolutionContext, type Schema, type SchemaBuilder, type SchemaDefinition, type SchemaOptions, type SchemaType, Spec, type SpecIssue, type SpecIssueSeverity, type SpecValidationIssues, type SpecValidationResult, StateCondition, StateModel, type UserPromptOptions, type ValidateSpecOptions, type ValidationCheck, type ValidationCheckResult, ValidationCheckSchema, type ValidationConfig, ValidationConfigSchema, type ValidationContext, type ValidationFunction, type ValidationFunctionDefinition, type ValidationResult, VisibilityCondition, VisibilityConditionSchema, type VisibilityContext, autoFixSpec, buildEditInstructions, buildEditUserPrompt, buildUserPrompt, builtInValidationFunctions, check, createDirectiveRegistry, deepMergeSpec, defineCatalog, defineDirective, defineSchema, diffToPatches, evaluateVisibility, findDirective, formatSpecIssues, isDevtoolsActive, isNonEmptySpec, markDevtoolsActive, nextActionDispatchId, notifyActionDispatch, notifyActionSettle, registerActionObserver, resolveActionParam, resolveBindings, resolveElementProps, resolvePropValue, runValidation, runValidationCheck, subscribeDevtoolsActive, validateSpec, visibility }; | ||
| export { type ActionDispatchInfo, type ActionObserver, type ActionSettleInfo, AndCondition, type BuildEditUserPromptOptions, type BuiltInAction, type Catalog, type ComputedFunction, type DirectiveDefinition, type DirectiveRegistry, DynamicValue, type EditConfig, type EditMode, type InferActionParams, type InferCatalogActions, type InferCatalogComponents, type InferCatalogInput, type InferComponentProps, type InferSpec, JsonPatch, type JsonSchemaOptions, OrCondition, type PromptContext, type PromptOptions, type PromptTemplate, type PropExpression, type PropResolutionContext, type Schema, type SchemaBuilder, type SchemaDefinition, type SchemaOptions, type SchemaType, Spec, type SpecIssue, type SpecIssueSeverity, type SpecValidationIssues, type SpecValidationResult, StateCondition, StateModel, type UserPromptOptions, type ValidateSpecOptions, type ValidationCheck, type ValidationCheckResult, ValidationCheckSchema, type ValidationConfig, ValidationConfigSchema, type ValidationContext, type ValidationFunction, type ValidationFunctionDefinition, type ValidationResult, VisibilityCondition, VisibilityConditionSchema, VisibilityConditionStrictSchema, type VisibilityContext, autoFixSpec, buildEditInstructions, buildEditUserPrompt, buildUserPrompt, builtInValidationFunctions, check, conditionUsesItemScope, createDirectiveRegistry, deepMergeSpec, defineCatalog, defineDirective, defineSchema, diffToPatches, evaluateVisibility, findDirective, formatSpecIssues, isDevtoolsActive, isNonEmptySpec, markDevtoolsActive, nextActionDispatchId, notifyActionDispatch, notifyActionSettle, registerActionObserver, resolveActionParam, resolveBindings, resolveElementProps, resolvePropValue, runValidation, runValidationCheck, splitRepeatVisibility, subscribeDevtoolsActive, validateSpec, visibility }; |
@@ -1,2 +0,2 @@ | ||
| export { T as StoreAdapterConfig, ag as createStoreAdapter, af as flattenToPointers, ae as immutableSetByPath } from './store-utils-D98Czbil.mjs'; | ||
| export { Y as StoreAdapterConfig, aj as createStoreAdapter, ai as flattenToPointers, ah as immutableSetByPath } from './store-utils-CGwRAVOR.mjs'; | ||
| import 'zod'; |
@@ -1,2 +0,2 @@ | ||
| export { T as StoreAdapterConfig, ag as createStoreAdapter, af as flattenToPointers, ae as immutableSetByPath } from './store-utils-D98Czbil.js'; | ||
| export { Y as StoreAdapterConfig, aj as createStoreAdapter, ai as flattenToPointers, ah as immutableSetByPath } from './store-utils-CGwRAVOR.js'; | ||
| import 'zod'; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../src/store-utils.ts","../src/types.ts","../src/state-store.ts"],"sourcesContent":["export {\n immutableSetByPath,\n flattenToPointers,\n createStoreAdapter,\n} from \"./state-store\";\nexport type { StoreAdapterConfig } from \"./state-store\";\n","import { z } from \"zod\";\nimport type { ActionBinding } from \"./actions\";\n\n/**\n * Dynamic value - can be a literal or a `{ $state }` reference to the state model.\n *\n * Used in action params and validation args where values can either be\n * hardcoded or resolved from state at runtime.\n */\nexport type DynamicValue<T = unknown> = T | { $state: string };\n\n/**\n * Dynamic string value\n */\nexport type DynamicString = DynamicValue<string>;\n\n/**\n * Dynamic number value\n */\nexport type DynamicNumber = DynamicValue<number>;\n\n/**\n * Dynamic boolean value\n */\nexport type DynamicBoolean = DynamicValue<boolean>;\n\n/**\n * Zod schema for dynamic values\n */\nexport const DynamicValueSchema = z.union([\n z.string(),\n z.number(),\n z.boolean(),\n z.null(),\n z.object({ $state: z.string() }),\n]);\n\nexport const DynamicStringSchema = z.union([\n z.string(),\n z.object({ $state: z.string() }),\n]);\n\nexport const DynamicNumberSchema = z.union([\n z.number(),\n z.object({ $state: z.string() }),\n]);\n\nexport const DynamicBooleanSchema = z.union([\n z.boolean(),\n z.object({ $state: z.string() }),\n]);\n\n/**\n * Base UI element structure for v2\n */\nexport interface UIElement<\n T extends string = string,\n P = Record<string, unknown>,\n> {\n /** Component type from the catalog */\n type: T;\n /** Component props */\n props: P;\n /** Child element keys (flat structure) */\n children?: string[];\n /** Visibility condition */\n visible?: VisibilityCondition;\n /** Event bindings — maps event names to action bindings */\n on?: Record<string, ActionBinding | ActionBinding[]>;\n /** Repeat children once per item in a state array */\n repeat?: { statePath: string; key?: string };\n /**\n * State watchers — maps JSON Pointer state paths to action bindings.\n * When the value at a watched path changes, the bound actions fire.\n * Useful for cascading dependencies (e.g. country → city option loading).\n */\n watch?: Record<string, ActionBinding | ActionBinding[]>;\n}\n\n/**\n * Element with key and parentKey for use with flatToTree.\n * When elements are in an array (not a keyed map), key and parentKey\n * are needed to establish identity and parent-child relationships.\n */\nexport interface FlatElement<\n T extends string = string,\n P = Record<string, unknown>,\n> extends UIElement<T, P> {\n /** Unique key identifying this element */\n key: string;\n /** Parent element key (null for root) */\n parentKey?: string | null;\n}\n\n/**\n * Shared comparison operators for visibility conditions.\n *\n * Use at most ONE comparison operator per condition. If multiple are\n * provided, only the first matching one is evaluated (precedence:\n * eq > neq > gt > gte > lt > lte). With no operator, truthiness is checked.\n *\n * `not` inverts the final result of whichever operator (or truthiness\n * check) is used.\n */\ntype ComparisonOperators = {\n eq?: unknown;\n neq?: unknown;\n gt?: number | { $state: string };\n gte?: number | { $state: string };\n lt?: number | { $state: string };\n lte?: number | { $state: string };\n not?: true;\n};\n\n/**\n * A single state-based condition.\n * Resolves `$state` to a value from the state model, then applies the operator.\n * Without an operator, checks truthiness.\n *\n * When `not` is `true`, the result of the entire condition is inverted.\n * For example `{ $state: \"/count\", gt: 5, not: true }` means \"NOT greater than 5\".\n */\nexport type StateCondition = { $state: string } & ComparisonOperators;\n\n/**\n * A condition that resolves `$item` to a field on the current repeat item.\n * Only meaningful inside a `repeat` scope.\n *\n * Use `\"\"` to reference the whole item, or `\"field\"` for a specific field.\n */\nexport type ItemCondition = { $item: string } & ComparisonOperators;\n\n/**\n * A condition that resolves `$index` to the current repeat array index.\n * Only meaningful inside a `repeat` scope.\n */\nexport type IndexCondition = { $index: true } & ComparisonOperators;\n\n/** A single visibility condition (state, item, or index). */\nexport type SingleCondition = StateCondition | ItemCondition | IndexCondition;\n\n/**\n * AND wrapper — all child conditions must be true.\n * This is the explicit form of the implicit array AND (`SingleCondition[]`).\n * Unlike the implicit form, `$and` supports nested `$or` and `$and` conditions.\n */\nexport type AndCondition = { $and: VisibilityCondition[] };\n\n/**\n * OR wrapper — at least one child condition must be true.\n */\nexport type OrCondition = { $or: VisibilityCondition[] };\n\n/**\n * Visibility condition types.\n * - `boolean` — always/never\n * - `SingleCondition` — single condition (`$state`, `$item`, or `$index`)\n * - `SingleCondition[]` — implicit AND (all must be true)\n * - `AndCondition` — `{ $and: [...] }`, explicit AND (all must be true)\n * - `OrCondition` — `{ $or: [...] }`, at least one must be true\n */\nexport type VisibilityCondition =\n | boolean\n | SingleCondition\n | SingleCondition[]\n | AndCondition\n | OrCondition;\n\n/**\n * Flat UI tree structure (optimized for LLM generation)\n */\nexport interface Spec {\n /** Root element key */\n root: string;\n /** Flat map of elements by key */\n elements: Record<string, UIElement>;\n /** Optional initial state to seed the state model.\n * Components using statePath will read from / write to this state. */\n state?: Record<string, unknown>;\n}\n\n/**\n * State model type\n */\nexport type StateModel = Record<string, unknown>;\n\n/**\n * An abstract store that owns state and notifies subscribers on change.\n *\n * Consumers can supply their own implementation (backed by Redux, Zustand,\n * XState, etc.) or use the built-in {@link createStateStore} for a simple\n * in-memory store.\n */\nexport interface StateStore {\n /** Read a value by JSON Pointer path. */\n get: (path: string) => unknown;\n /**\n * Write a value by JSON Pointer path and notify subscribers.\n * Equality is checked by reference (`===`), not deep comparison.\n * Callers must pass a new object/array reference for changes to be detected.\n */\n set: (path: string, value: unknown) => void;\n /**\n * Write multiple values at once and notify subscribers (single notification).\n * Each value is compared by reference (`===`); only paths whose value\n * actually changed are applied.\n */\n update: (updates: Record<string, unknown>) => void;\n /** Return the full state object (used by `useSyncExternalStore`). */\n getSnapshot: () => StateModel;\n /** Optional server snapshot for SSR (passed to `useSyncExternalStore`). Falls back to `getSnapshot` when omitted. */\n getServerSnapshot?: () => StateModel;\n /** Register a listener that is called on every state change. Returns an unsubscribe function. */\n subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * Component schema definition using Zod\n */\nexport type ComponentSchema = z.ZodType<Record<string, unknown>>;\n\n/**\n * Validation mode for catalog validation\n */\nexport type ValidationMode = \"strict\" | \"warn\" | \"ignore\";\n\n/**\n * JSON patch operation types (RFC 6902)\n */\nexport type PatchOp = \"add\" | \"remove\" | \"replace\" | \"move\" | \"copy\" | \"test\";\n\n/**\n * JSON patch operation (RFC 6902)\n */\nexport interface JsonPatch {\n op: PatchOp;\n path: string;\n /** Required for add, replace, test */\n value?: unknown;\n /** Required for move, copy (source location) */\n from?: string;\n}\n\n/**\n * Resolve a dynamic value against a state model\n */\nexport function resolveDynamicValue<T>(\n value: DynamicValue<T>,\n stateModel: StateModel,\n): T | undefined {\n if (value === null || value === undefined) {\n return undefined;\n }\n\n if (typeof value === \"object\" && \"$state\" in value) {\n return getByPath(stateModel, (value as { $state: string }).$state) as\n | T\n | undefined;\n }\n\n return value as T;\n}\n\n/**\n * Unescape a JSON Pointer token per RFC 6901 Section 4.\n * ~1 is decoded to / and ~0 is decoded to ~ (order matters).\n */\nfunction unescapeJsonPointer(token: string): string {\n return token.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n}\n\n/**\n * Parse a JSON Pointer path into unescaped segments.\n */\nexport function parseJsonPointer(path: string): string[] {\n const raw = path.startsWith(\"/\") ? path.slice(1).split(\"/\") : path.split(\"/\");\n return raw.map(unescapeJsonPointer);\n}\n\n/**\n * Get a value from an object by JSON Pointer path (RFC 6901)\n */\nexport function getByPath(obj: unknown, path: string): unknown {\n if (!path || path === \"/\") {\n return obj;\n }\n\n const segments = parseJsonPointer(path);\n\n let current: unknown = obj;\n\n for (const segment of segments) {\n if (current === null || current === undefined) {\n return undefined;\n }\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n current = current[index];\n } else if (typeof current === \"object\") {\n current = (current as Record<string, unknown>)[segment];\n } else {\n return undefined;\n }\n }\n\n return current;\n}\n\n/**\n * Check if a string is a numeric index\n */\nfunction isNumericIndex(str: string): boolean {\n return /^\\d+$/.test(str);\n}\n\n/**\n * Set a value in an object by JSON Pointer path (RFC 6901).\n * Automatically creates arrays when the path segment is a numeric index.\n */\nexport function setByPath(\n obj: Record<string, unknown>,\n path: string,\n value: unknown,\n): void {\n const segments = parseJsonPointer(path);\n\n if (segments.length === 0) return;\n\n let current: Record<string, unknown> | unknown[] = obj;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!;\n const nextSegment = segments[i + 1];\n const nextIsNumeric =\n nextSegment !== undefined &&\n (isNumericIndex(nextSegment) || nextSegment === \"-\");\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n if (current[index] === undefined || typeof current[index] !== \"object\") {\n current[index] = nextIsNumeric ? [] : {};\n }\n current = current[index] as Record<string, unknown> | unknown[];\n } else {\n if (!(segment in current) || typeof current[segment] !== \"object\") {\n current[segment] = nextIsNumeric ? [] : {};\n }\n current = current[segment] as Record<string, unknown> | unknown[];\n }\n }\n\n const lastSegment = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n if (lastSegment === \"-\") {\n current.push(value);\n } else {\n const index = parseInt(lastSegment, 10);\n current[index] = value;\n }\n } else {\n current[lastSegment] = value;\n }\n}\n\n/**\n * Add a value per RFC 6902 \"add\" semantics.\n * For objects: create-or-replace the member.\n * For arrays: insert before the given index, or append if \"-\".\n */\nexport function addByPath(\n obj: Record<string, unknown>,\n path: string,\n value: unknown,\n): void {\n const segments = parseJsonPointer(path);\n\n if (segments.length === 0) return;\n\n let current: Record<string, unknown> | unknown[] = obj;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!;\n const nextSegment = segments[i + 1];\n const nextIsNumeric =\n nextSegment !== undefined &&\n (isNumericIndex(nextSegment) || nextSegment === \"-\");\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n if (current[index] === undefined || typeof current[index] !== \"object\") {\n current[index] = nextIsNumeric ? [] : {};\n }\n current = current[index] as Record<string, unknown> | unknown[];\n } else {\n if (!(segment in current) || typeof current[segment] !== \"object\") {\n current[segment] = nextIsNumeric ? [] : {};\n }\n current = current[segment] as Record<string, unknown> | unknown[];\n }\n }\n\n const lastSegment = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n if (lastSegment === \"-\") {\n current.push(value);\n } else {\n const index = parseInt(lastSegment, 10);\n current.splice(index, 0, value);\n }\n } else {\n current[lastSegment] = value;\n }\n}\n\n/**\n * Remove a value per RFC 6902 \"remove\" semantics.\n * For objects: delete the property.\n * For arrays: splice out the element at the given index.\n */\nexport function removeByPath(obj: Record<string, unknown>, path: string): void {\n const segments = parseJsonPointer(path);\n\n if (segments.length === 0) return;\n\n let current: Record<string, unknown> | unknown[] = obj;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!;\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n if (current[index] === undefined || typeof current[index] !== \"object\") {\n return; // path does not exist\n }\n current = current[index] as Record<string, unknown> | unknown[];\n } else {\n if (!(segment in current) || typeof current[segment] !== \"object\") {\n return; // path does not exist\n }\n current = current[segment] as Record<string, unknown> | unknown[];\n }\n }\n\n const lastSegment = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n const index = parseInt(lastSegment, 10);\n if (index >= 0 && index < current.length) {\n current.splice(index, 1);\n }\n } else {\n delete current[lastSegment];\n }\n}\n\n/**\n * Deep equality check for RFC 6902 \"test\" operation.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n if (typeof a !== \"object\") return false;\n\n if (Array.isArray(a)) {\n if (!Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n return a.every((item, i) => deepEqual(item, b[i]));\n }\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));\n}\n\n/**\n * Find a form value from params and/or state.\n * Useful in action handlers to locate form input values regardless of path format.\n *\n * Checks in order:\n * 1. Direct param key (if not a path reference)\n * 2. Param keys ending with the field name\n * 3. State keys ending with the field name (dot notation)\n * 4. State path using getByPath (slash notation)\n *\n * @example\n * // Find \"name\" from params or state\n * const name = findFormValue(\"name\", params, state);\n *\n * // Will find from: params.name, params[\"form.name\"], state[\"form.name\"], or getByPath(state, \"name\")\n */\nexport function findFormValue(\n fieldName: string,\n params?: Record<string, unknown>,\n state?: Record<string, unknown>,\n): unknown {\n // Check params first (but not if it looks like a state path reference)\n if (params?.[fieldName] !== undefined) {\n const val = params[fieldName];\n // If the value looks like a path reference (contains dots), skip it\n if (typeof val !== \"string\" || !val.includes(\".\")) {\n return val;\n }\n }\n\n // Check param keys that end with the field name\n if (params) {\n for (const key of Object.keys(params)) {\n if (key.endsWith(`.${fieldName}`)) {\n const val = params[key];\n if (typeof val !== \"string\" || !val.includes(\".\")) {\n return val;\n }\n }\n }\n }\n\n // Check state keys that end with the field name (handles any form naming)\n if (state) {\n for (const key of Object.keys(state)) {\n if (key === fieldName || key.endsWith(`.${fieldName}`)) {\n return state[key];\n }\n }\n\n // Try getByPath with the raw field name\n const val = getByPath(state, fieldName);\n if (val !== undefined) {\n return val;\n }\n }\n\n return undefined;\n}\n\n// =============================================================================\n// SpecStream - Streaming format for progressively building specs\n// =============================================================================\n\n/**\n * A SpecStream line - a single patch operation in the stream.\n */\nexport type SpecStreamLine = JsonPatch;\n\n/**\n * Parse a single SpecStream line into a patch operation.\n * Returns null if the line is invalid or empty.\n *\n * SpecStream is json-render's streaming format where each line is a JSON patch\n * operation that progressively builds up the final spec.\n */\nexport function parseSpecStreamLine(line: string): SpecStreamLine | null {\n const trimmed = line.trim();\n if (!trimmed || !trimmed.startsWith(\"{\")) return null;\n\n try {\n const patch = JSON.parse(trimmed) as SpecStreamLine;\n if (patch.op && patch.path !== undefined) {\n return patch;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Apply a single RFC 6902 JSON Patch operation to an object.\n * Mutates the object in place.\n *\n * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test.\n *\n * @throws {Error} If a \"test\" operation fails (value mismatch).\n */\nexport function applySpecStreamPatch<T extends Record<string, unknown>>(\n obj: T,\n patch: SpecStreamLine,\n): T {\n switch (patch.op) {\n case \"add\":\n addByPath(obj, patch.path, patch.value);\n break;\n case \"replace\":\n // RFC 6902: target must exist. For streaming tolerance we set regardless.\n setByPath(obj, patch.path, patch.value);\n break;\n case \"remove\":\n removeByPath(obj, patch.path);\n break;\n case \"move\": {\n if (!patch.from) break;\n const moveValue = getByPath(obj, patch.from);\n removeByPath(obj, patch.from);\n addByPath(obj, patch.path, moveValue);\n break;\n }\n case \"copy\": {\n if (!patch.from) break;\n const copyValue = getByPath(obj, patch.from);\n addByPath(obj, patch.path, copyValue);\n break;\n }\n case \"test\": {\n const actual = getByPath(obj, patch.path);\n if (!deepEqual(actual, patch.value)) {\n throw new Error(\n `Test operation failed: value at \"${patch.path}\" does not match`,\n );\n }\n break;\n }\n }\n return obj;\n}\n\n/**\n * Apply a single RFC 6902 JSON Patch operation to a Spec.\n * Mutates the spec in place and returns it.\n *\n * This is a typed convenience wrapper around `applySpecStreamPatch` that\n * accepts a `Spec` directly without requiring a cast to `Record<string, unknown>`.\n *\n * Note: This mutates the spec. For React state updates, spread the result\n * to create a new reference: `setSpec({ ...applySpecPatch(spec, patch) })`.\n *\n * @example\n * let spec: Spec = { root: \"\", elements: {} };\n * applySpecPatch(spec, { op: \"add\", path: \"/root\", value: \"main\" });\n */\nexport function applySpecPatch(spec: Spec, patch: SpecStreamLine): Spec {\n applySpecStreamPatch(spec as unknown as Record<string, unknown>, patch);\n return spec;\n}\n\n// =============================================================================\n// Nested-to-Flat Conversion\n// =============================================================================\n\n/**\n * A nested spec node. This is the tree format that humans naturally write —\n * each node has inline `children` as an array of child node objects rather\n * than string keys.\n */\ninterface NestedNode {\n type: string;\n props: Record<string, unknown>;\n children?: NestedNode[];\n /** Any other top-level fields (visible, on, repeat, etc.) */\n [key: string]: unknown;\n}\n\n/**\n * Convert a nested (tree-structured) spec into the flat `Spec` format used\n * by json-render renderers.\n *\n * In the nested format each node has inline `children` as an array of child\n * objects. This function walks the tree, assigns auto-generated keys\n * (`el-0`, `el-1`, ...), and produces a flat `{ root, elements, state }` spec.\n *\n * The top-level `state` field (if present on the root node) is hoisted to\n * `spec.state`.\n *\n * @example\n * ```ts\n * const nested = {\n * type: \"Card\",\n * props: { title: \"Hello\" },\n * children: [\n * { type: \"Text\", props: { content: \"World\" } },\n * ],\n * state: { count: 0 },\n * };\n * const spec = nestedToFlat(nested);\n * // {\n * // root: \"el-0\",\n * // elements: {\n * // \"el-0\": { type: \"Card\", props: { title: \"Hello\" }, children: [\"el-1\"] },\n * // \"el-1\": { type: \"Text\", props: { content: \"World\" }, children: [] },\n * // },\n * // state: { count: 0 },\n * // }\n * ```\n */\nexport function nestedToFlat(nested: Record<string, unknown>): Spec {\n const elements: Record<string, UIElement> = {};\n let counter = 0;\n\n function walk(node: Record<string, unknown>): string {\n const key = `el-${counter++}`;\n const { type, props, children: rawChildren, ...rest } = node as NestedNode;\n\n // Recursively flatten children\n const childKeys: string[] = [];\n if (Array.isArray(rawChildren)) {\n for (const child of rawChildren) {\n if (child && typeof child === \"object\" && \"type\" in child) {\n childKeys.push(walk(child as Record<string, unknown>));\n }\n }\n }\n\n // Build the flat element, preserving extra fields (visible, on, repeat, etc.)\n // but excluding `state` which is hoisted to spec-level.\n const element: UIElement = {\n type: type ?? \"unknown\",\n props: (props as Record<string, unknown>) ?? {},\n children: childKeys,\n };\n\n // Copy extra fields (visible, on, repeat) but not state\n for (const [k, v] of Object.entries(rest)) {\n if (k !== \"state\" && v !== undefined) {\n (element as unknown as Record<string, unknown>)[k] = v;\n }\n }\n\n elements[key] = element;\n return key;\n }\n\n const root = walk(nested);\n\n const spec: Spec = { root, elements };\n\n // Hoist state from root node if present\n if (\n nested.state &&\n typeof nested.state === \"object\" &&\n !Array.isArray(nested.state)\n ) {\n spec.state = nested.state as Record<string, unknown>;\n }\n\n return spec;\n}\n\n/**\n * Compile a SpecStream string into a JSON object.\n * Each line should be a patch operation.\n *\n * @example\n * const stream = `{\"op\":\"add\",\"path\":\"/name\",\"value\":\"Alice\"}\n * {\"op\":\"add\",\"path\":\"/age\",\"value\":30}`;\n * const result = compileSpecStream(stream);\n * // { name: \"Alice\", age: 30 }\n */\nexport function compileSpecStream<\n T extends Record<string, unknown> = Record<string, unknown>,\n>(stream: string, initial: T = {} as T): T {\n const lines = stream.split(\"\\n\");\n const result = { ...initial };\n\n for (const line of lines) {\n const patch = parseSpecStreamLine(line);\n if (patch) {\n applySpecStreamPatch(result, patch);\n }\n }\n\n return result as T;\n}\n\n/**\n * Streaming SpecStream compiler.\n * Useful for processing SpecStream data as it streams in from AI.\n *\n * @example\n * const compiler = createSpecStreamCompiler<MySpec>();\n *\n * // As chunks arrive:\n * const { result, newPatches } = compiler.push(chunk);\n * if (newPatches.length > 0) {\n * updateUI(result);\n * }\n *\n * // When done:\n * const finalResult = compiler.getResult();\n */\nexport interface SpecStreamCompiler<T> {\n /** Push a chunk of text. Returns the current result and any new patches applied. */\n push(chunk: string): { result: T; newPatches: SpecStreamLine[] };\n /** Get the current compiled result */\n getResult(): T;\n /** Get all patches that have been applied */\n getPatches(): SpecStreamLine[];\n /** Reset the compiler to initial state */\n reset(initial?: Partial<T>): void;\n}\n\n/**\n * Create a streaming SpecStream compiler.\n *\n * SpecStream is json-render's streaming format. AI outputs patch operations\n * line by line, and this compiler progressively builds the final spec.\n *\n * @example\n * const compiler = createSpecStreamCompiler<TimelineSpec>();\n *\n * // Process streaming response\n * const reader = response.body.getReader();\n * while (true) {\n * const { done, value } = await reader.read();\n * if (done) break;\n *\n * const { result, newPatches } = compiler.push(decoder.decode(value));\n * if (newPatches.length > 0) {\n * setSpec(result); // Update UI with partial result\n * }\n * }\n */\nexport function createSpecStreamCompiler<T = Record<string, unknown>>(\n initial: Partial<T> = {},\n): SpecStreamCompiler<T> {\n let result = { ...initial } as T;\n let buffer = \"\";\n const appliedPatches: SpecStreamLine[] = [];\n const processedLines = new Set<string>();\n\n return {\n push(chunk: string): { result: T; newPatches: SpecStreamLine[] } {\n buffer += chunk;\n const newPatches: SpecStreamLine[] = [];\n\n // Process complete lines\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || processedLines.has(trimmed)) continue;\n processedLines.add(trimmed);\n\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n applySpecStreamPatch(result as Record<string, unknown>, patch);\n appliedPatches.push(patch);\n newPatches.push(patch);\n }\n }\n\n // Return a shallow copy to trigger re-renders\n if (newPatches.length > 0) {\n result = { ...result };\n }\n\n return { result, newPatches };\n },\n\n getResult(): T {\n // Process any remaining buffer\n if (buffer.trim()) {\n const patch = parseSpecStreamLine(buffer);\n if (patch && !processedLines.has(buffer.trim())) {\n processedLines.add(buffer.trim());\n applySpecStreamPatch(result as Record<string, unknown>, patch);\n appliedPatches.push(patch);\n result = { ...result };\n }\n buffer = \"\";\n }\n return result;\n },\n\n getPatches(): SpecStreamLine[] {\n return [...appliedPatches];\n },\n\n reset(newInitial: Partial<T> = {}): void {\n result = { ...newInitial } as T;\n buffer = \"\";\n appliedPatches.length = 0;\n processedLines.clear();\n },\n };\n}\n\n// =============================================================================\n// Mixed Stream Parser — for chat + GenUI (text interleaved with JSONL patches)\n// =============================================================================\n\n/**\n * Callbacks for the mixed stream parser.\n */\nexport interface MixedStreamCallbacks {\n /** Called when a JSONL patch line is parsed */\n onPatch: (patch: SpecStreamLine) => void;\n /** Called when a text (non-JSONL) line is received */\n onText: (text: string) => void;\n}\n\n/**\n * A stateful parser for mixed streams that contain both text and JSONL patches.\n * Used in chat + GenUI scenarios where an LLM responds with conversational text\n * interleaved with json-render JSONL patch operations.\n */\nexport interface MixedStreamParser {\n /** Push a chunk of streamed data. Calls onPatch/onText for each complete line. */\n push(chunk: string): void;\n /** Flush any remaining buffered content. Call when the stream ends. */\n flush(): void;\n}\n\n/**\n * Create a parser for mixed text + JSONL streams.\n *\n * In chat + GenUI scenarios, an LLM streams a response that contains both\n * conversational text and json-render JSONL patch lines. This parser buffers\n * incoming chunks, splits them into lines, and classifies each line as either\n * a JSONL patch (via `parseSpecStreamLine`) or plain text.\n *\n * @example\n * const parser = createMixedStreamParser({\n * onText: (text) => appendToMessage(text),\n * onPatch: (patch) => applySpecPatch(spec, patch),\n * });\n *\n * // As chunks arrive from the stream:\n * for await (const chunk of stream) {\n * parser.push(chunk);\n * }\n * parser.flush();\n */\nexport function createMixedStreamParser(\n callbacks: MixedStreamCallbacks,\n): MixedStreamParser {\n let buffer = \"\";\n let inSpecFence = false;\n\n function processLine(line: string): void {\n const trimmed = line.trim();\n\n // Fence detection\n if (!inSpecFence && trimmed.startsWith(\"```spec\")) {\n inSpecFence = true;\n return;\n }\n if (inSpecFence && trimmed === \"```\") {\n inSpecFence = false;\n return;\n }\n\n if (!trimmed) return;\n\n if (inSpecFence) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n callbacks.onPatch(patch);\n }\n return;\n }\n\n // Outside fence: heuristic mode\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n callbacks.onPatch(patch);\n } else {\n callbacks.onText(line);\n }\n }\n\n return {\n push(chunk: string): void {\n buffer += chunk;\n\n // Process complete lines\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n for (const line of lines) {\n processLine(line);\n }\n },\n\n flush(): void {\n if (buffer.trim()) {\n processLine(buffer);\n }\n buffer = \"\";\n },\n };\n}\n\n// =============================================================================\n// AI SDK Stream Transform\n// =============================================================================\n\n/**\n * Minimal chunk shape compatible with the AI SDK's `UIMessageChunk`.\n *\n * Defined here so that `@json-render/core` has no dependency on the `ai`\n * package. The discriminated union covers the three text-related chunk types\n * the transform inspects; all other chunk types pass through via the fallback.\n */\nexport type StreamChunk =\n | { type: \"text-start\"; id: string; [k: string]: unknown }\n | { type: \"text-delta\"; id: string; delta: string; [k: string]: unknown }\n | { type: \"text-end\"; id: string; [k: string]: unknown }\n | { type: string; [k: string]: unknown };\n\n/** The opening fence for a spec block (e.g. ` ```spec `). */\nconst SPEC_FENCE_OPEN = \"```spec\";\n/** The closing fence for a spec block. */\nconst SPEC_FENCE_CLOSE = \"```\";\n\n/**\n * Creates a `TransformStream` that intercepts AI SDK UI message stream chunks\n * and classifies text content as either prose or json-render JSONL patches.\n *\n * Two classification modes:\n *\n * 1. **Fence mode** (preferred): Lines between ` ```spec ` and ` ``` ` are\n * parsed as JSONL patches. Fence delimiters are swallowed (not emitted).\n * 2. **Heuristic mode** (backward compat): Outside of fences, lines starting\n * with `{` are buffered and tested with `parseSpecStreamLine`. Valid patches\n * are emitted as {@link SPEC_DATA_PART_TYPE} parts; everything else is\n * flushed as text.\n *\n * Non-text chunks (tool events, step markers, etc.) are passed through unchanged.\n *\n * @example\n * ```ts\n * import { createJsonRenderTransform } from \"@json-render/core\";\n * import { createUIMessageStream, createUIMessageStreamResponse } from \"ai\";\n *\n * const stream = createUIMessageStream({\n * execute: async ({ writer }) => {\n * writer.merge(\n * result.toUIMessageStream().pipeThrough(createJsonRenderTransform()),\n * );\n * },\n * });\n * return createUIMessageStreamResponse({ stream });\n * ```\n */\nexport function createJsonRenderTransform(): TransformStream<\n StreamChunk,\n StreamChunk\n> {\n let lineBuffer = \"\";\n let currentTextId = \"\";\n // Whether the current incomplete line might be JSONL (starts with '{')\n let buffering = false;\n // Whether we are inside a ```spec fence\n let inSpecFence = false;\n // Whether we are currently inside a text block (between text-start/text-end).\n // Used to split text blocks around spec data so the AI SDK creates separate\n // text parts, preserving interleaving of prose and UI in message.parts.\n let inTextBlock = false;\n let textIdCounter = 0;\n\n /** Close the current text block if one is open. */\n function closeTextBlock(\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n if (inTextBlock) {\n controller.enqueue({ type: \"text-end\", id: currentTextId });\n inTextBlock = false;\n }\n }\n\n /** Ensure a text block is open, starting a new one if needed. */\n function ensureTextBlock(\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n if (!inTextBlock) {\n textIdCounter++;\n currentTextId = String(textIdCounter);\n controller.enqueue({ type: \"text-start\", id: currentTextId });\n inTextBlock = true;\n }\n }\n\n /** Emit a text-delta, opening a text block first if necessary. */\n function emitTextDelta(\n delta: string,\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n ensureTextBlock(controller);\n controller.enqueue({ type: \"text-delta\", id: currentTextId, delta });\n }\n\n function emitPatch(\n patch: SpecStreamLine,\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n closeTextBlock(controller);\n controller.enqueue({\n type: SPEC_DATA_PART_TYPE,\n data: { type: \"patch\", patch },\n });\n }\n\n function flushBuffer(\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n if (!lineBuffer) return;\n\n const trimmed = lineBuffer.trim();\n\n // Inside a fence, everything is spec data\n if (inSpecFence) {\n if (trimmed) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) emitPatch(patch, controller);\n // Non-patch lines inside the fence are silently dropped\n }\n lineBuffer = \"\";\n buffering = false;\n return;\n }\n\n if (trimmed) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n emitPatch(patch, controller);\n } else {\n // Was buffered but isn't JSONL — flush as text\n emitTextDelta(lineBuffer, controller);\n }\n } else {\n // Whitespace-only buffer — forward as-is (preserves blank lines)\n emitTextDelta(lineBuffer, controller);\n }\n lineBuffer = \"\";\n buffering = false;\n }\n\n function processCompleteLine(\n line: string,\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n const trimmed = line.trim();\n\n // --- Fence detection ---\n if (!inSpecFence && trimmed.startsWith(SPEC_FENCE_OPEN)) {\n inSpecFence = true;\n return; // Swallow the opening fence\n }\n if (inSpecFence && trimmed === SPEC_FENCE_CLOSE) {\n inSpecFence = false;\n return; // Swallow the closing fence\n }\n\n // Inside a fence: parse as spec data\n if (inSpecFence) {\n if (trimmed) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) emitPatch(patch, controller);\n }\n return;\n }\n\n // --- Outside fence: heuristic mode ---\n if (!trimmed) {\n // Empty line — forward for markdown paragraph breaks\n emitTextDelta(\"\\n\", controller);\n return;\n }\n\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n emitPatch(patch, controller);\n } else {\n emitTextDelta(line + \"\\n\", controller);\n }\n }\n\n return new TransformStream<StreamChunk, StreamChunk>({\n transform(chunk, controller) {\n switch (chunk.type) {\n case \"text-start\": {\n const id = (chunk as { id: string }).id;\n const idNum = parseInt(id, 10);\n if (!isNaN(idNum) && idNum >= textIdCounter) {\n textIdCounter = idNum;\n }\n currentTextId = id;\n inTextBlock = true;\n controller.enqueue(chunk);\n break;\n }\n\n case \"text-delta\": {\n const delta = chunk as { id: string; delta: string };\n const text = delta.delta;\n\n for (let i = 0; i < text.length; i++) {\n const ch = text.charAt(i);\n\n if (ch === \"\\n\") {\n // Line complete — classify and emit\n if (buffering) {\n processCompleteLine(lineBuffer, controller);\n lineBuffer = \"\";\n buffering = false;\n } else {\n // Outside fence, emit newline; inside fence, swallow it\n if (!inSpecFence) {\n emitTextDelta(\"\\n\", controller);\n }\n }\n } else if (lineBuffer.length === 0 && !buffering) {\n // Start of a new line — decide whether to buffer or stream\n if (inSpecFence || ch === \"{\" || ch === \"`\") {\n // Buffer: inside fence (everything), or heuristic mode ({), or potential fence (`)\n buffering = true;\n lineBuffer += ch;\n } else {\n emitTextDelta(ch, controller);\n }\n } else if (buffering) {\n lineBuffer += ch;\n } else {\n emitTextDelta(ch, controller);\n }\n }\n break;\n }\n\n case \"text-end\": {\n flushBuffer(controller);\n if (inTextBlock) {\n controller.enqueue({ type: \"text-end\", id: currentTextId });\n inTextBlock = false;\n }\n break;\n }\n\n default: {\n controller.enqueue(chunk);\n break;\n }\n }\n },\n\n flush(controller) {\n flushBuffer(controller);\n closeTextBlock(controller);\n },\n });\n}\n\n/**\n * The key registered in `AppDataParts` for json-render specs.\n * The AI SDK automatically prefixes this with `\"data-\"` on the wire,\n * so the actual stream chunk type is `\"data-spec\"` (see {@link SPEC_DATA_PART_TYPE}).\n *\n * @example\n * ```ts\n * import { SPEC_DATA_PART, type SpecDataPart } from \"@json-render/core\";\n * type AppDataParts = { [SPEC_DATA_PART]: SpecDataPart };\n * ```\n */\nexport const SPEC_DATA_PART = \"spec\" as const;\n\n/**\n * The wire-format type string as it appears in stream chunks and message parts.\n * This is `\"data-\"` + {@link SPEC_DATA_PART} — i.e. `\"data-spec\"`.\n *\n * Use this constant when filtering message parts or enqueuing stream chunks.\n */\nexport const SPEC_DATA_PART_TYPE = `data-${SPEC_DATA_PART}` as const;\n\n/**\n * Discriminated union for the payload of a {@link SPEC_DATA_PART_TYPE} SSE part.\n *\n * - `\"patch\"`: A single RFC 6902 JSON Patch operation (streaming, progressive UI).\n * - `\"flat\"`: A complete flat spec with `root`, `elements`, and optional `state`.\n * - `\"nested\"`: A complete nested spec (tree structure — schema depends on catalog).\n */\nexport type SpecDataPart =\n | { type: \"patch\"; patch: JsonPatch }\n | { type: \"flat\"; spec: Spec }\n | { type: \"nested\"; spec: Record<string, unknown> };\n\n/**\n * Convenience wrapper that pipes an AI SDK UI message stream through the\n * json-render transform, classifying text as prose or JSONL patches.\n *\n * Eliminates the need for manual `pipeThrough(createJsonRenderTransform())`\n * and the associated type cast.\n *\n * @example\n * ```ts\n * import { pipeJsonRender } from \"@json-render/core\";\n *\n * const stream = createUIMessageStream({\n * execute: async ({ writer }) => {\n * writer.merge(pipeJsonRender(result.toUIMessageStream()));\n * },\n * });\n * return createUIMessageStreamResponse({ stream });\n * ```\n */\nexport function pipeJsonRender<T = StreamChunk>(\n stream: ReadableStream<T>,\n): ReadableStream<T> {\n return stream.pipeThrough(\n createJsonRenderTransform() as unknown as TransformStream<T, T>,\n );\n}\n","import {\n getByPath,\n parseJsonPointer,\n type StateModel,\n type StateStore,\n} from \"./types\";\n\n/**\n * Immutably set a value at a JSON Pointer path using structural sharing.\n * Only objects along the path are shallow-cloned; untouched branches keep\n * their original references.\n */\nexport function immutableSetByPath(\n root: StateModel,\n path: string,\n value: unknown,\n): StateModel {\n const segments = parseJsonPointer(path);\n if (segments.length === 0) return root;\n\n const result = { ...root };\n let current: Record<string, unknown> = result;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!;\n const child = current[seg];\n if (Array.isArray(child)) {\n current[seg] = [...child];\n } else if (child !== null && typeof child === \"object\") {\n current[seg] = { ...(child as Record<string, unknown>) };\n } else {\n const nextSeg = segments[i + 1];\n current[seg] = nextSeg !== undefined && /^\\d+$/.test(nextSeg) ? [] : {};\n }\n current = current[seg] as Record<string, unknown>;\n }\n\n const lastSeg = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n if (lastSeg === \"-\") {\n (current as unknown[]).push(value);\n } else {\n (current as unknown[])[parseInt(lastSeg, 10)] = value;\n }\n } else {\n current[lastSeg] = value;\n }\n\n return result;\n}\n\n/**\n * Create a simple in-memory {@link StateStore}.\n *\n * This is the default store used by `StateProvider` when no external store is\n * provided. It mirrors the previous `useState`-based behaviour but is\n * framework-agnostic so it can also be used in tests or non-React contexts.\n */\nexport function createStateStore(initialState: StateModel = {}): StateStore {\n let state: StateModel = { ...initialState };\n const listeners = new Set<() => void>();\n\n function notify() {\n for (const listener of listeners) {\n listener();\n }\n }\n\n return {\n get(path: string): unknown {\n return getByPath(state, path);\n },\n\n set(path: string, value: unknown): void {\n if (getByPath(state, path) === value) return;\n state = immutableSetByPath(state, path, value);\n notify();\n },\n\n update(updates: Record<string, unknown>): void {\n let changed = false;\n let next = state;\n for (const [path, value] of Object.entries(updates)) {\n if (getByPath(next, path) !== value) {\n next = immutableSetByPath(next, path, value);\n changed = true;\n }\n }\n if (!changed) return;\n state = next;\n notify();\n },\n\n getSnapshot(): StateModel {\n return state;\n },\n\n getServerSnapshot(): StateModel {\n return state;\n },\n\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/**\n * Configuration for {@link createStoreAdapter}. Adapter authors supply these\n * three callbacks; everything else (get, set, update, no-op detection,\n * getServerSnapshot) is handled by the returned {@link StateStore}.\n */\nexport interface StoreAdapterConfig {\n /** Return the current state snapshot from the underlying store. */\n getSnapshot: () => StateModel;\n /** Write a new state snapshot to the underlying store. */\n setSnapshot: (next: StateModel) => void;\n /** Subscribe to changes in the underlying store. Return an unsubscribe fn. */\n subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * Build a full {@link StateStore} from a minimal adapter config.\n *\n * Handles `get`, `set` (with no-op detection), `update` (batched, with no-op\n * detection), `getSnapshot`, `getServerSnapshot`, and `subscribe` -- so each\n * adapter only needs to wire its snapshot source, write API, and subscribe\n * mechanism.\n */\nexport function createStoreAdapter(config: StoreAdapterConfig): StateStore {\n return {\n get(path: string): unknown {\n return getByPath(config.getSnapshot(), path);\n },\n\n set(path: string, value: unknown): void {\n const current = config.getSnapshot();\n if (getByPath(current, path) === value) return;\n config.setSnapshot(immutableSetByPath(current, path, value));\n },\n\n update(updates: Record<string, unknown>): void {\n let next = config.getSnapshot();\n let changed = false;\n for (const [path, value] of Object.entries(updates)) {\n if (getByPath(next, path) !== value) {\n next = immutableSetByPath(next, path, value);\n changed = true;\n }\n }\n if (!changed) return;\n config.setSnapshot(next);\n },\n\n getSnapshot: config.getSnapshot,\n\n getServerSnapshot: config.getSnapshot,\n\n subscribe: config.subscribe,\n };\n}\n\nconst MAX_FLATTEN_DEPTH = 20;\n\n/**\n * Recursively flatten a plain object into a `Record<string, unknown>` keyed by\n * JSON Pointer paths. Only leaf values (non-plain-object) appear in the output.\n *\n * Includes circular reference protection and a depth cap to prevent stack\n * overflow on pathological inputs.\n *\n * ```ts\n * flattenToPointers({ user: { name: \"Alice\" }, count: 1 })\n * // => { \"/user/name\": \"Alice\", \"/count\": 1 }\n * ```\n */\nexport function flattenToPointers(\n obj: Record<string, unknown>,\n prefix = \"\",\n _depth = 0,\n _seen?: Set<object>,\n _warned?: { current: boolean },\n): Record<string, unknown> {\n const seen = _seen ?? new Set<object>();\n const warned = _warned ?? { current: false };\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const pointer = `${prefix}/${key}`;\n if (\n _depth < MAX_FLATTEN_DEPTH &&\n value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) === Object.prototype &&\n !seen.has(value)\n ) {\n seen.add(value);\n Object.assign(\n result,\n flattenToPointers(\n value as Record<string, unknown>,\n pointer,\n _depth + 1,\n seen,\n warned,\n ),\n );\n } else {\n if (\n process.env.NODE_ENV !== \"production\" &&\n !warned.current &&\n _depth >= MAX_FLATTEN_DEPTH &&\n value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) === Object.prototype &&\n !seen.has(value as object)\n ) {\n warned.current = true;\n console.warn(\n `flattenToPointers: depth limit (${MAX_FLATTEN_DEPTH}) reached. Nested state beyond this depth will be treated as a leaf value.`,\n );\n }\n result[pointer] = value;\n }\n }\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,iBAAkB;AA6BX,IAAM,qBAAqB,aAAE,MAAM;AAAA,EACxC,aAAE,OAAO;AAAA,EACT,aAAE,OAAO;AAAA,EACT,aAAE,QAAQ;AAAA,EACV,aAAE,KAAK;AAAA,EACP,aAAE,OAAO,EAAE,QAAQ,aAAE,OAAO,EAAE,CAAC;AACjC,CAAC;AAEM,IAAM,sBAAsB,aAAE,MAAM;AAAA,EACzC,aAAE,OAAO;AAAA,EACT,aAAE,OAAO,EAAE,QAAQ,aAAE,OAAO,EAAE,CAAC;AACjC,CAAC;AAEM,IAAM,sBAAsB,aAAE,MAAM;AAAA,EACzC,aAAE,OAAO;AAAA,EACT,aAAE,OAAO,EAAE,QAAQ,aAAE,OAAO,EAAE,CAAC;AACjC,CAAC;AAEM,IAAM,uBAAuB,aAAE,MAAM;AAAA,EAC1C,aAAE,QAAQ;AAAA,EACV,aAAE,OAAO,EAAE,QAAQ,aAAE,OAAO,EAAE,CAAC;AACjC,CAAC;AAyND,SAAS,oBAAoB,OAAuB;AAClD,SAAO,MAAM,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG;AACrD;AAKO,SAAS,iBAAiB,MAAwB;AACvD,QAAM,MAAM,KAAK,WAAW,GAAG,IAAI,KAAK,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,GAAG;AAC5E,SAAO,IAAI,IAAI,mBAAmB;AACpC;AAKO,SAAS,UAAU,KAAc,MAAuB;AAC7D,MAAI,CAAC,QAAQ,SAAS,KAAK;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,iBAAiB,IAAI;AAEtC,MAAI,UAAmB;AAEvB,aAAW,WAAW,UAAU;AAC9B,QAAI,YAAY,QAAQ,YAAY,QAAW;AAC7C,aAAO;AAAA,IACT;AAEA,QAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,YAAM,QAAQ,SAAS,SAAS,EAAE;AAClC,gBAAU,QAAQ,KAAK;AAAA,IACzB,WAAW,OAAO,YAAY,UAAU;AACtC,gBAAW,QAAoC,OAAO;AAAA,IACxD,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;AAs7BO,IAAM,iBAAiB;AAQvB,IAAM,sBAAsB,QAAQ,cAAc;;;ACruClD,SAAS,mBACd,MACA,MACA,OACY;AACZ,QAAM,WAAW,iBAAiB,IAAI;AACtC,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,QAAM,SAAS,EAAE,GAAG,KAAK;AACzB,MAAI,UAAmC;AAEvC,WAAS,IAAI,GAAG,IAAI,SAAS,SAAS,GAAG,KAAK;AAC5C,UAAM,MAAM,SAAS,CAAC;AACtB,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,cAAQ,GAAG,IAAI,CAAC,GAAG,KAAK;AAAA,IAC1B,WAAW,UAAU,QAAQ,OAAO,UAAU,UAAU;AACtD,cAAQ,GAAG,IAAI,EAAE,GAAI,MAAkC;AAAA,IACzD,OAAO;AACL,YAAM,UAAU,SAAS,IAAI,CAAC;AAC9B,cAAQ,GAAG,IAAI,YAAY,UAAa,QAAQ,KAAK,OAAO,IAAI,CAAC,IAAI,CAAC;AAAA,IACxE;AACA,cAAU,QAAQ,GAAG;AAAA,EACvB;AAEA,QAAM,UAAU,SAAS,SAAS,SAAS,CAAC;AAC5C,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,QAAI,YAAY,KAAK;AACnB,MAAC,QAAsB,KAAK,KAAK;AAAA,IACnC,OAAO;AACL,MAAC,QAAsB,SAAS,SAAS,EAAE,CAAC,IAAI;AAAA,IAClD;AAAA,EACF,OAAO;AACL,YAAQ,OAAO,IAAI;AAAA,EACrB;AAEA,SAAO;AACT;AAmFO,SAAS,mBAAmB,QAAwC;AACzE,SAAO;AAAA,IACL,IAAI,MAAuB;AACzB,aAAO,UAAU,OAAO,YAAY,GAAG,IAAI;AAAA,IAC7C;AAAA,IAEA,IAAI,MAAc,OAAsB;AACtC,YAAM,UAAU,OAAO,YAAY;AACnC,UAAI,UAAU,SAAS,IAAI,MAAM,MAAO;AACxC,aAAO,YAAY,mBAAmB,SAAS,MAAM,KAAK,CAAC;AAAA,IAC7D;AAAA,IAEA,OAAO,SAAwC;AAC7C,UAAI,OAAO,OAAO,YAAY;AAC9B,UAAI,UAAU;AACd,iBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,YAAI,UAAU,MAAM,IAAI,MAAM,OAAO;AACnC,iBAAO,mBAAmB,MAAM,MAAM,KAAK;AAC3C,oBAAU;AAAA,QACZ;AAAA,MACF;AACA,UAAI,CAAC,QAAS;AACd,aAAO,YAAY,IAAI;AAAA,IACzB;AAAA,IAEA,aAAa,OAAO;AAAA,IAEpB,mBAAmB,OAAO;AAAA,IAE1B,WAAW,OAAO;AAAA,EACpB;AACF;AAEA,IAAM,oBAAoB;AAcnB,SAAS,kBACd,KACA,SAAS,IACT,SAAS,GACT,OACA,SACyB;AACzB,QAAM,OAAO,SAAS,oBAAI,IAAY;AACtC,QAAM,SAAS,WAAW,EAAE,SAAS,MAAM;AAC3C,QAAM,SAAkC,CAAC;AACzC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,UAAM,UAAU,GAAG,MAAM,IAAI,GAAG;AAChC,QACE,SAAS,qBACT,UAAU,QACV,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,eAAe,KAAK,MAAM,OAAO,aACxC,CAAC,KAAK,IAAI,KAAK,GACf;AACA,WAAK,IAAI,KAAK;AACd,aAAO;AAAA,QACL;AAAA,QACA;AAAA,UACE;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF,OAAO;AACL,UACE,QAAQ,IAAI,aAAa,gBACzB,CAAC,OAAO,WACR,UAAU,qBACV,UAAU,QACV,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,eAAe,KAAK,MAAM,OAAO,aACxC,CAAC,KAAK,IAAI,KAAe,GACzB;AACA,eAAO,UAAU;AACjB,gBAAQ;AAAA,UACN,mCAAmC,iBAAiB;AAAA,QACtD;AAAA,MACF;AACA,aAAO,OAAO,IAAI;AAAA,IACpB;AAAA,EACF;AACA,SAAO;AACT;","names":[]} | ||
| {"version":3,"sources":["../src/store-utils.ts","../src/types.ts","../src/state-store.ts"],"sourcesContent":["export {\n immutableSetByPath,\n flattenToPointers,\n createStoreAdapter,\n} from \"./state-store\";\nexport type { StoreAdapterConfig } from \"./state-store\";\n","import { z } from \"zod\";\nimport type { ActionBinding } from \"./actions\";\n\n/**\n * Dynamic value - can be a literal or a `{ $state }` reference to the state model.\n *\n * Used in action params and validation args where values can either be\n * hardcoded or resolved from state at runtime.\n */\nexport type DynamicValue<T = unknown> = T | { $state: string };\n\n/**\n * Dynamic string value\n */\nexport type DynamicString = DynamicValue<string>;\n\n/**\n * Dynamic number value\n */\nexport type DynamicNumber = DynamicValue<number>;\n\n/**\n * Dynamic boolean value\n */\nexport type DynamicBoolean = DynamicValue<boolean>;\n\n/**\n * Zod schema for dynamic values\n */\nexport const DynamicValueSchema = z.union([\n z.string(),\n z.number(),\n z.boolean(),\n z.null(),\n z.object({ $state: z.string() }),\n]);\n\nexport const DynamicStringSchema = z.union([\n z.string(),\n z.object({ $state: z.string() }),\n]);\n\nexport const DynamicNumberSchema = z.union([\n z.number(),\n z.object({ $state: z.string() }),\n]);\n\nexport const DynamicBooleanSchema = z.union([\n z.boolean(),\n z.object({ $state: z.string() }),\n]);\n\nexport type RepeatStatePath = string | { $item: string };\n\n/**\n * Base UI element structure for v2\n */\nexport interface UIElement<\n T extends string = string,\n P = Record<string, unknown>,\n> {\n /** Component type from the catalog */\n type: T;\n /** Component props */\n props: P;\n /** Child element keys (flat structure) */\n children?: string[];\n slots?: Record<string, string[]>;\n /** Visibility condition */\n visible?: VisibilityCondition;\n /** Event bindings — maps event names to action bindings */\n on?: Record<string, ActionBinding | ActionBinding[]>;\n /** Repeat children once per item in a state array */\n repeat?: { statePath: RepeatStatePath; key?: string };\n /**\n * State watchers — maps JSON Pointer state paths to action bindings.\n * When the value at a watched path changes, the bound actions fire.\n * Useful for cascading dependencies (e.g. country → city option loading).\n */\n watch?: Record<string, ActionBinding | ActionBinding[]>;\n}\n\n/**\n * Element with key and parentKey for use with flatToTree.\n * When elements are in an array (not a keyed map), key and parentKey\n * are needed to establish identity and parent-child relationships.\n */\nexport interface FlatElement<\n T extends string = string,\n P = Record<string, unknown>,\n> extends UIElement<T, P> {\n /** Unique key identifying this element */\n key: string;\n /** Parent element key (null for root) */\n parentKey?: string | null;\n}\n\n/**\n * Shared comparison operators for visibility conditions.\n *\n * Use at most ONE comparison operator per condition. If multiple are\n * provided, only the first matching one is evaluated (precedence:\n * eq > neq > gt > gte > lt > lte). With no operator, truthiness is checked.\n *\n * `not` inverts the final result of whichever operator (or truthiness\n * check) is used.\n */\ntype ComparisonOperators = {\n eq?: unknown;\n neq?: unknown;\n gt?: number | { $state: string };\n gte?: number | { $state: string };\n lt?: number | { $state: string };\n lte?: number | { $state: string };\n not?: true;\n};\n\n/**\n * A single state-based condition.\n * Resolves `$state` to a value from the state model, then applies the operator.\n * Without an operator, checks truthiness.\n *\n * When `not` is `true`, the result of the entire condition is inverted.\n * For example `{ $state: \"/count\", gt: 5, not: true }` means \"NOT greater than 5\".\n */\nexport type StateCondition = { $state: string } & ComparisonOperators;\n\n/**\n * A condition that resolves `$item` to a field on the current repeat item.\n * Only meaningful inside a `repeat` scope.\n *\n * Use `\"\"` to reference the whole item, or `\"field\"` for a specific field.\n */\nexport type ItemCondition = { $item: string } & ComparisonOperators;\n\n/**\n * A condition that resolves `$index` to the current repeat array index.\n * Only meaningful inside a `repeat` scope.\n */\nexport type IndexCondition = { $index: true } & ComparisonOperators;\n\n/** A single visibility condition (state, item, or index). */\nexport type SingleCondition = StateCondition | ItemCondition | IndexCondition;\n\n/**\n * AND wrapper — all child conditions must be true.\n * This is the explicit form of the implicit array AND (`SingleCondition[]`).\n * Unlike the implicit form, `$and` supports nested `$or` and `$and` conditions.\n */\nexport type AndCondition = { $and: VisibilityCondition[] };\n\n/**\n * OR wrapper — at least one child condition must be true.\n */\nexport type OrCondition = { $or: VisibilityCondition[] };\n\n/**\n * Visibility condition types.\n * - `boolean` — always/never\n * - `SingleCondition` — single condition (`$state`, `$item`, or `$index`)\n * - `SingleCondition[]` — implicit AND (all must be true)\n * - `AndCondition` — `{ $and: [...] }`, explicit AND (all must be true)\n * - `OrCondition` — `{ $or: [...] }`, at least one must be true\n */\nexport type VisibilityCondition =\n | boolean\n | SingleCondition\n | SingleCondition[]\n | AndCondition\n | OrCondition;\n\n/**\n * Flat UI tree structure (optimized for LLM generation)\n */\nexport interface Spec {\n /** Root element key */\n root: string;\n /** Flat map of elements by key */\n elements: Record<string, UIElement>;\n /** Optional initial state to seed the state model.\n * Components using statePath will read from / write to this state. */\n state?: Record<string, unknown>;\n}\n\n/**\n * State model type\n */\nexport type StateModel = Record<string, unknown>;\n\n/**\n * An abstract store that owns state and notifies subscribers on change.\n *\n * Consumers can supply their own implementation (backed by Redux, Zustand,\n * XState, etc.) or use the built-in {@link createStateStore} for a simple\n * in-memory store.\n */\nexport interface StateStore {\n /** Read a value by JSON Pointer path. */\n get: (path: string) => unknown;\n /**\n * Write a value by JSON Pointer path and notify subscribers.\n * Equality is checked by reference (`===`), not deep comparison.\n * Callers must pass a new object/array reference for changes to be detected.\n */\n set: (path: string, value: unknown) => void;\n /**\n * Write multiple values at once and notify subscribers (single notification).\n * Each value is compared by reference (`===`); only paths whose value\n * actually changed are applied.\n */\n update: (updates: Record<string, unknown>) => void;\n /** Return the full state object (used by `useSyncExternalStore`). */\n getSnapshot: () => StateModel;\n /** Optional server snapshot for SSR (passed to `useSyncExternalStore`). Falls back to `getSnapshot` when omitted. */\n getServerSnapshot?: () => StateModel;\n /** Register a listener that is called on every state change. Returns an unsubscribe function. */\n subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * Component schema definition using Zod\n */\nexport type ComponentSchema = z.ZodType<Record<string, unknown>>;\n\n/**\n * Validation mode for catalog validation\n */\nexport type ValidationMode = \"strict\" | \"warn\" | \"ignore\";\n\n/**\n * JSON patch operation types (RFC 6902)\n */\nexport type PatchOp = \"add\" | \"remove\" | \"replace\" | \"move\" | \"copy\" | \"test\";\n\n/**\n * JSON patch operation (RFC 6902)\n */\nexport interface JsonPatch {\n op: PatchOp;\n path: string;\n /** Required for add, replace, test */\n value?: unknown;\n /** Required for move, copy (source location) */\n from?: string;\n}\n\n/**\n * Resolve a dynamic value against a state model\n */\nexport function resolveDynamicValue<T>(\n value: DynamicValue<T>,\n stateModel: StateModel,\n): T | undefined {\n if (value === null || value === undefined) {\n return undefined;\n }\n\n if (typeof value === \"object\" && \"$state\" in value) {\n return getByPath(stateModel, (value as { $state: string }).$state) as\n | T\n | undefined;\n }\n\n return value as T;\n}\n\n/**\n * Unescape a JSON Pointer token per RFC 6901 Section 4.\n * ~1 is decoded to / and ~0 is decoded to ~ (order matters).\n */\nfunction unescapeJsonPointer(token: string): string {\n return token.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n}\n\n/**\n * Parse a JSON Pointer path into unescaped segments.\n */\nexport function parseJsonPointer(path: string): string[] {\n const raw = path.startsWith(\"/\") ? path.slice(1).split(\"/\") : path.split(\"/\");\n return raw.map(unescapeJsonPointer);\n}\n\n/**\n * Get a value from an object by JSON Pointer path (RFC 6901)\n */\nexport function getByPath(obj: unknown, path: string): unknown {\n if (!path || path === \"/\") {\n return obj;\n }\n\n const segments = parseJsonPointer(path);\n\n let current: unknown = obj;\n\n for (const segment of segments) {\n if (current === null || current === undefined) {\n return undefined;\n }\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n current = current[index];\n } else if (typeof current === \"object\") {\n current = (current as Record<string, unknown>)[segment];\n } else {\n return undefined;\n }\n }\n\n return current;\n}\n\nexport function resolveRepeatStatePath(\n statePath: RepeatStatePath,\n repeatBasePath?: string | null,\n): string | undefined {\n if (typeof statePath === \"string\") {\n return statePath;\n }\n\n if (repeatBasePath == null) {\n return undefined;\n }\n\n if (statePath.$item === \"\" || statePath.$item === \"/\") {\n return repeatBasePath;\n }\n\n return joinStatePath(repeatBasePath, statePath.$item);\n}\n\nexport function resolveRepeatItemStatePath(\n statePath: string,\n index: number,\n): string {\n return joinStatePath(statePath, String(index));\n}\n\nfunction joinStatePath(basePath: string, childPath: string): string {\n const child = childPath.startsWith(\"/\") ? childPath.slice(1) : childPath;\n if (basePath === \"\" || basePath === \"/\") {\n return `/${child}`;\n }\n return `${basePath}/${child}`;\n}\n\n/**\n * Check if a string is a numeric index\n */\nfunction isNumericIndex(str: string): boolean {\n return /^\\d+$/.test(str);\n}\n\n/**\n * Set a value in an object by JSON Pointer path (RFC 6901).\n * Automatically creates arrays when the path segment is a numeric index.\n */\nexport function setByPath(\n obj: Record<string, unknown>,\n path: string,\n value: unknown,\n): void {\n const segments = parseJsonPointer(path);\n\n if (segments.length === 0) return;\n\n let current: Record<string, unknown> | unknown[] = obj;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!;\n const nextSegment = segments[i + 1];\n const nextIsNumeric =\n nextSegment !== undefined &&\n (isNumericIndex(nextSegment) || nextSegment === \"-\");\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n if (current[index] === undefined || typeof current[index] !== \"object\") {\n current[index] = nextIsNumeric ? [] : {};\n }\n current = current[index] as Record<string, unknown> | unknown[];\n } else {\n if (!(segment in current) || typeof current[segment] !== \"object\") {\n current[segment] = nextIsNumeric ? [] : {};\n }\n current = current[segment] as Record<string, unknown> | unknown[];\n }\n }\n\n const lastSegment = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n if (lastSegment === \"-\") {\n current.push(value);\n } else {\n const index = parseInt(lastSegment, 10);\n current[index] = value;\n }\n } else {\n current[lastSegment] = value;\n }\n}\n\n/**\n * Add a value per RFC 6902 \"add\" semantics.\n * For objects: create-or-replace the member.\n * For arrays: insert before the given index, or append if \"-\".\n */\nexport function addByPath(\n obj: Record<string, unknown>,\n path: string,\n value: unknown,\n): void {\n const segments = parseJsonPointer(path);\n\n if (segments.length === 0) return;\n\n let current: Record<string, unknown> | unknown[] = obj;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!;\n const nextSegment = segments[i + 1];\n const nextIsNumeric =\n nextSegment !== undefined &&\n (isNumericIndex(nextSegment) || nextSegment === \"-\");\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n if (current[index] === undefined || typeof current[index] !== \"object\") {\n current[index] = nextIsNumeric ? [] : {};\n }\n current = current[index] as Record<string, unknown> | unknown[];\n } else {\n if (!(segment in current) || typeof current[segment] !== \"object\") {\n current[segment] = nextIsNumeric ? [] : {};\n }\n current = current[segment] as Record<string, unknown> | unknown[];\n }\n }\n\n const lastSegment = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n if (lastSegment === \"-\") {\n current.push(value);\n } else {\n const index = parseInt(lastSegment, 10);\n current.splice(index, 0, value);\n }\n } else {\n current[lastSegment] = value;\n }\n}\n\n/**\n * Remove a value per RFC 6902 \"remove\" semantics.\n * For objects: delete the property.\n * For arrays: splice out the element at the given index.\n */\nexport function removeByPath(obj: Record<string, unknown>, path: string): void {\n const segments = parseJsonPointer(path);\n\n if (segments.length === 0) return;\n\n let current: Record<string, unknown> | unknown[] = obj;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!;\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n if (current[index] === undefined || typeof current[index] !== \"object\") {\n return; // path does not exist\n }\n current = current[index] as Record<string, unknown> | unknown[];\n } else {\n if (!(segment in current) || typeof current[segment] !== \"object\") {\n return; // path does not exist\n }\n current = current[segment] as Record<string, unknown> | unknown[];\n }\n }\n\n const lastSegment = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n const index = parseInt(lastSegment, 10);\n if (index >= 0 && index < current.length) {\n current.splice(index, 1);\n }\n } else {\n delete current[lastSegment];\n }\n}\n\n/**\n * Deep equality check for RFC 6902 \"test\" operation.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n if (typeof a !== \"object\") return false;\n\n if (Array.isArray(a)) {\n if (!Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n return a.every((item, i) => deepEqual(item, b[i]));\n }\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));\n}\n\n/**\n * Find a form value from params and/or state.\n * Useful in action handlers to locate form input values regardless of path format.\n *\n * Checks in order:\n * 1. Direct param key (if not a path reference)\n * 2. Param keys ending with the field name\n * 3. State keys ending with the field name (dot notation)\n * 4. State path using getByPath (slash notation)\n *\n * @example\n * // Find \"name\" from params or state\n * const name = findFormValue(\"name\", params, state);\n *\n * // Will find from: params.name, params[\"form.name\"], state[\"form.name\"], or getByPath(state, \"name\")\n */\nexport function findFormValue(\n fieldName: string,\n params?: Record<string, unknown>,\n state?: Record<string, unknown>,\n): unknown {\n // Check params first (but not if it looks like a state path reference)\n if (params?.[fieldName] !== undefined) {\n const val = params[fieldName];\n // If the value looks like a path reference (contains dots), skip it\n if (typeof val !== \"string\" || !val.includes(\".\")) {\n return val;\n }\n }\n\n // Check param keys that end with the field name\n if (params) {\n for (const key of Object.keys(params)) {\n if (key.endsWith(`.${fieldName}`)) {\n const val = params[key];\n if (typeof val !== \"string\" || !val.includes(\".\")) {\n return val;\n }\n }\n }\n }\n\n // Check state keys that end with the field name (handles any form naming)\n if (state) {\n for (const key of Object.keys(state)) {\n if (key === fieldName || key.endsWith(`.${fieldName}`)) {\n return state[key];\n }\n }\n\n // Try getByPath with the raw field name\n const val = getByPath(state, fieldName);\n if (val !== undefined) {\n return val;\n }\n }\n\n return undefined;\n}\n\n// =============================================================================\n// SpecStream - Streaming format for progressively building specs\n// =============================================================================\n\n/**\n * A SpecStream line - a single patch operation in the stream.\n */\nexport type SpecStreamLine = JsonPatch;\n\n/**\n * Parse a single SpecStream line into a patch operation.\n * Returns null if the line is invalid or empty.\n *\n * SpecStream is json-render's streaming format where each line is a JSON patch\n * operation that progressively builds up the final spec.\n */\nexport function parseSpecStreamLine(line: string): SpecStreamLine | null {\n const trimmed = line.trim();\n if (!trimmed || !trimmed.startsWith(\"{\")) return null;\n\n try {\n const patch = JSON.parse(trimmed) as SpecStreamLine;\n if (patch.op && patch.path !== undefined) {\n return patch;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Apply a single RFC 6902 JSON Patch operation to an object.\n * Mutates the object in place.\n *\n * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test.\n *\n * @throws {Error} If a \"test\" operation fails (value mismatch).\n */\nexport function applySpecStreamPatch<T extends Record<string, unknown>>(\n obj: T,\n patch: SpecStreamLine,\n): T {\n switch (patch.op) {\n case \"add\":\n addByPath(obj, patch.path, patch.value);\n break;\n case \"replace\":\n // RFC 6902: target must exist. For streaming tolerance we set regardless.\n setByPath(obj, patch.path, patch.value);\n break;\n case \"remove\":\n removeByPath(obj, patch.path);\n break;\n case \"move\": {\n if (!patch.from) break;\n const moveValue = getByPath(obj, patch.from);\n removeByPath(obj, patch.from);\n addByPath(obj, patch.path, moveValue);\n break;\n }\n case \"copy\": {\n if (!patch.from) break;\n const copyValue = getByPath(obj, patch.from);\n addByPath(obj, patch.path, copyValue);\n break;\n }\n case \"test\": {\n const actual = getByPath(obj, patch.path);\n if (!deepEqual(actual, patch.value)) {\n throw new Error(\n `Test operation failed: value at \"${patch.path}\" does not match`,\n );\n }\n break;\n }\n }\n return obj;\n}\n\n/**\n * Apply a single RFC 6902 JSON Patch operation to a Spec.\n * Mutates the spec in place and returns it.\n *\n * This is a typed convenience wrapper around `applySpecStreamPatch` that\n * accepts a `Spec` directly without requiring a cast to `Record<string, unknown>`.\n *\n * Note: This mutates the spec. For React state updates, spread the result\n * to create a new reference: `setSpec({ ...applySpecPatch(spec, patch) })`.\n *\n * @example\n * let spec: Spec = { root: \"\", elements: {} };\n * applySpecPatch(spec, { op: \"add\", path: \"/root\", value: \"main\" });\n */\nexport function applySpecPatch(spec: Spec, patch: SpecStreamLine): Spec {\n applySpecStreamPatch(spec as unknown as Record<string, unknown>, patch);\n return spec;\n}\n\n// =============================================================================\n// Nested-to-Flat Conversion\n// =============================================================================\n\n/**\n * A nested spec node. This is the tree format that humans naturally write —\n * each node has inline `children` as an array of child node objects rather\n * than string keys.\n */\ninterface NestedNode {\n type: string;\n props: Record<string, unknown>;\n children?: NestedNode[];\n slots?: Record<string, NestedNode[]>;\n /** Any other top-level fields (visible, on, repeat, etc.) */\n [key: string]: unknown;\n}\n\n/**\n * Convert a nested (tree-structured) spec into the flat `Spec` format used\n * by json-render renderers.\n *\n * In the nested format each node has inline `children` as an array of child\n * objects. This function walks the tree, assigns auto-generated keys\n * (`el-0`, `el-1`, ...), and produces a flat `{ root, elements, state }` spec.\n *\n * The top-level `state` field (if present on the root node) is hoisted to\n * `spec.state`.\n *\n * @example\n * ```ts\n * const nested = {\n * type: \"Card\",\n * props: { title: \"Hello\" },\n * children: [\n * { type: \"Text\", props: { content: \"World\" } },\n * ],\n * state: { count: 0 },\n * };\n * const spec = nestedToFlat(nested);\n * // {\n * // root: \"el-0\",\n * // elements: {\n * // \"el-0\": { type: \"Card\", props: { title: \"Hello\" }, children: [\"el-1\"] },\n * // \"el-1\": { type: \"Text\", props: { content: \"World\" }, children: [] },\n * // },\n * // state: { count: 0 },\n * // }\n * ```\n */\nexport function nestedToFlat(nested: Record<string, unknown>): Spec {\n const elements: Record<string, UIElement> = {};\n let counter = 0;\n\n function walk(node: Record<string, unknown>): string {\n const key = `el-${counter++}`;\n const {\n type,\n props,\n children: rawChildren,\n slots: rawSlots,\n ...rest\n } = node as NestedNode;\n\n // Recursively flatten children\n const childKeys: string[] = [];\n if (Array.isArray(rawChildren)) {\n for (const child of rawChildren) {\n if (child && typeof child === \"object\" && \"type\" in child) {\n childKeys.push(walk(child as Record<string, unknown>));\n }\n }\n }\n\n const slots: Record<string, string[]> = {};\n if (rawSlots && typeof rawSlots === \"object\") {\n for (const [slotName, slotChildren] of Object.entries(rawSlots)) {\n if (!Array.isArray(slotChildren)) continue;\n slots[slotName] = slotChildren.flatMap((child) =>\n child && typeof child === \"object\" && \"type\" in child\n ? [walk(child as Record<string, unknown>)]\n : [],\n );\n }\n }\n\n // Build the flat element, preserving extra fields (visible, on, repeat, etc.)\n // but excluding `state` which is hoisted to spec-level.\n const element: UIElement = {\n type: type ?? \"unknown\",\n props: (props as Record<string, unknown>) ?? {},\n children: childKeys,\n ...(Object.keys(slots).length > 0 ? { slots } : {}),\n };\n\n // Copy extra fields (visible, on, repeat) but not state\n for (const [k, v] of Object.entries(rest)) {\n if (k !== \"state\" && v !== undefined) {\n (element as unknown as Record<string, unknown>)[k] = v;\n }\n }\n\n elements[key] = element;\n return key;\n }\n\n const root = walk(nested);\n\n const spec: Spec = { root, elements };\n\n // Hoist state from root node if present\n if (\n nested.state &&\n typeof nested.state === \"object\" &&\n !Array.isArray(nested.state)\n ) {\n spec.state = nested.state as Record<string, unknown>;\n }\n\n return spec;\n}\n\n/**\n * Compile a SpecStream string into a JSON object.\n * Each line should be a patch operation.\n *\n * @example\n * const stream = `{\"op\":\"add\",\"path\":\"/name\",\"value\":\"Alice\"}\n * {\"op\":\"add\",\"path\":\"/age\",\"value\":30}`;\n * const result = compileSpecStream(stream);\n * // { name: \"Alice\", age: 30 }\n */\nexport function compileSpecStream<\n T extends Record<string, unknown> = Record<string, unknown>,\n>(stream: string, initial: T = {} as T): T {\n const lines = stream.split(\"\\n\");\n const result = { ...initial };\n\n for (const line of lines) {\n const patch = parseSpecStreamLine(line);\n if (patch) {\n applySpecStreamPatch(result, patch);\n }\n }\n\n return result as T;\n}\n\n/**\n * Streaming SpecStream compiler.\n * Useful for processing SpecStream data as it streams in from AI.\n *\n * @example\n * const compiler = createSpecStreamCompiler<MySpec>();\n *\n * // As chunks arrive:\n * const { result, newPatches } = compiler.push(chunk);\n * if (newPatches.length > 0) {\n * updateUI(result);\n * }\n *\n * // When done:\n * const finalResult = compiler.getResult();\n */\nexport interface SpecStreamCompiler<T> {\n /** Push a chunk of text. Returns the current result and any new patches applied. */\n push(chunk: string): { result: T; newPatches: SpecStreamLine[] };\n /** Get the current compiled result */\n getResult(): T;\n /** Get all patches that have been applied */\n getPatches(): SpecStreamLine[];\n /** Reset the compiler to initial state */\n reset(initial?: Partial<T>): void;\n}\n\n/**\n * Create a streaming SpecStream compiler.\n *\n * SpecStream is json-render's streaming format. AI outputs patch operations\n * line by line, and this compiler progressively builds the final spec.\n *\n * @example\n * const compiler = createSpecStreamCompiler<TimelineSpec>();\n *\n * // Process streaming response\n * const reader = response.body.getReader();\n * while (true) {\n * const { done, value } = await reader.read();\n * if (done) break;\n *\n * const { result, newPatches } = compiler.push(decoder.decode(value));\n * if (newPatches.length > 0) {\n * setSpec(result); // Update UI with partial result\n * }\n * }\n */\nexport function createSpecStreamCompiler<T = Record<string, unknown>>(\n initial: Partial<T> = {},\n): SpecStreamCompiler<T> {\n let result = { ...initial } as T;\n let buffer = \"\";\n const appliedPatches: SpecStreamLine[] = [];\n const processedLines = new Set<string>();\n\n return {\n push(chunk: string): { result: T; newPatches: SpecStreamLine[] } {\n buffer += chunk;\n const newPatches: SpecStreamLine[] = [];\n\n // Process complete lines\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || processedLines.has(trimmed)) continue;\n processedLines.add(trimmed);\n\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n applySpecStreamPatch(result as Record<string, unknown>, patch);\n appliedPatches.push(patch);\n newPatches.push(patch);\n }\n }\n\n // Return a shallow copy to trigger re-renders\n if (newPatches.length > 0) {\n result = { ...result };\n }\n\n return { result, newPatches };\n },\n\n getResult(): T {\n // Process any remaining buffer\n if (buffer.trim()) {\n const patch = parseSpecStreamLine(buffer);\n if (patch && !processedLines.has(buffer.trim())) {\n processedLines.add(buffer.trim());\n applySpecStreamPatch(result as Record<string, unknown>, patch);\n appliedPatches.push(patch);\n result = { ...result };\n }\n buffer = \"\";\n }\n return result;\n },\n\n getPatches(): SpecStreamLine[] {\n return [...appliedPatches];\n },\n\n reset(newInitial: Partial<T> = {}): void {\n result = { ...newInitial } as T;\n buffer = \"\";\n appliedPatches.length = 0;\n processedLines.clear();\n },\n };\n}\n\n// =============================================================================\n// Mixed Stream Parser — for chat + GenUI (text interleaved with JSONL patches)\n// =============================================================================\n\n/**\n * Callbacks for the mixed stream parser.\n */\nexport interface MixedStreamCallbacks {\n /** Called when a JSONL patch line is parsed */\n onPatch: (patch: SpecStreamLine) => void;\n /** Called when a text (non-JSONL) line is received */\n onText: (text: string) => void;\n}\n\n/**\n * A stateful parser for mixed streams that contain both text and JSONL patches.\n * Used in chat + GenUI scenarios where an LLM responds with conversational text\n * interleaved with json-render JSONL patch operations.\n */\nexport interface MixedStreamParser {\n /** Push a chunk of streamed data. Calls onPatch/onText for each complete line. */\n push(chunk: string): void;\n /** Flush any remaining buffered content. Call when the stream ends. */\n flush(): void;\n}\n\n/**\n * Create a parser for mixed text + JSONL streams.\n *\n * In chat + GenUI scenarios, an LLM streams a response that contains both\n * conversational text and json-render JSONL patch lines. This parser buffers\n * incoming chunks, splits them into lines, and classifies each line as either\n * a JSONL patch (via `parseSpecStreamLine`) or plain text.\n *\n * @example\n * const parser = createMixedStreamParser({\n * onText: (text) => appendToMessage(text),\n * onPatch: (patch) => applySpecPatch(spec, patch),\n * });\n *\n * // As chunks arrive from the stream:\n * for await (const chunk of stream) {\n * parser.push(chunk);\n * }\n * parser.flush();\n */\nexport function createMixedStreamParser(\n callbacks: MixedStreamCallbacks,\n): MixedStreamParser {\n let buffer = \"\";\n let inSpecFence = false;\n\n function processLine(line: string): void {\n const trimmed = line.trim();\n\n // Fence detection\n if (!inSpecFence && trimmed.startsWith(\"```spec\")) {\n inSpecFence = true;\n return;\n }\n if (inSpecFence && trimmed === \"```\") {\n inSpecFence = false;\n return;\n }\n\n if (!trimmed) return;\n\n if (inSpecFence) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n callbacks.onPatch(patch);\n }\n return;\n }\n\n // Outside fence: heuristic mode\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n callbacks.onPatch(patch);\n } else {\n callbacks.onText(line);\n }\n }\n\n return {\n push(chunk: string): void {\n buffer += chunk;\n\n // Process complete lines\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n for (const line of lines) {\n processLine(line);\n }\n },\n\n flush(): void {\n if (buffer.trim()) {\n processLine(buffer);\n }\n buffer = \"\";\n },\n };\n}\n\n// =============================================================================\n// AI SDK Stream Transform\n// =============================================================================\n\n/**\n * Minimal chunk shape compatible with the AI SDK's `UIMessageChunk`.\n *\n * Defined here so that `@json-render/core` has no dependency on the `ai`\n * package. The discriminated union covers the three text-related chunk types\n * the transform inspects; all other chunk types pass through via the fallback.\n */\nexport type StreamChunk =\n | { type: \"text-start\"; id: string; [k: string]: unknown }\n | { type: \"text-delta\"; id: string; delta: string; [k: string]: unknown }\n | { type: \"text-end\"; id: string; [k: string]: unknown }\n | { type: string; [k: string]: unknown };\n\n/** The opening fence for a spec block (e.g. ` ```spec `). */\nconst SPEC_FENCE_OPEN = \"```spec\";\n/** The closing fence for a spec block. */\nconst SPEC_FENCE_CLOSE = \"```\";\n\n/**\n * Creates a `TransformStream` that intercepts AI SDK UI message stream chunks\n * and classifies text content as either prose or json-render JSONL patches.\n *\n * Two classification modes:\n *\n * 1. **Fence mode** (preferred): Lines between ` ```spec ` and ` ``` ` are\n * parsed as JSONL patches. Fence delimiters are swallowed (not emitted).\n * 2. **Heuristic mode** (backward compat): Outside of fences, lines starting\n * with `{` are buffered and tested with `parseSpecStreamLine`. Valid patches\n * are emitted as {@link SPEC_DATA_PART_TYPE} parts; everything else is\n * flushed as text.\n *\n * Non-text chunks (tool events, step markers, etc.) are passed through unchanged.\n *\n * @example\n * ```ts\n * import { createJsonRenderTransform } from \"@json-render/core\";\n * import { createUIMessageStream, createUIMessageStreamResponse } from \"ai\";\n *\n * const stream = createUIMessageStream({\n * execute: async ({ writer }) => {\n * writer.merge(\n * result.toUIMessageStream().pipeThrough(createJsonRenderTransform()),\n * );\n * },\n * });\n * return createUIMessageStreamResponse({ stream });\n * ```\n */\nexport function createJsonRenderTransform(): TransformStream<\n StreamChunk,\n StreamChunk\n> {\n let lineBuffer = \"\";\n let currentTextId = \"\";\n // Whether the current incomplete line might be JSONL (starts with '{')\n let buffering = false;\n // Whether we are inside a ```spec fence\n let inSpecFence = false;\n // Whether we are currently inside a text block (between text-start/text-end).\n // Used to split text blocks around spec data so the AI SDK creates separate\n // text parts, preserving interleaving of prose and UI in message.parts.\n let inTextBlock = false;\n let textIdCounter = 0;\n\n /** Close the current text block if one is open. */\n function closeTextBlock(\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n if (inTextBlock) {\n controller.enqueue({ type: \"text-end\", id: currentTextId });\n inTextBlock = false;\n }\n }\n\n /** Ensure a text block is open, starting a new one if needed. */\n function ensureTextBlock(\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n if (!inTextBlock) {\n textIdCounter++;\n currentTextId = String(textIdCounter);\n controller.enqueue({ type: \"text-start\", id: currentTextId });\n inTextBlock = true;\n }\n }\n\n /** Emit a text-delta, opening a text block first if necessary. */\n function emitTextDelta(\n delta: string,\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n ensureTextBlock(controller);\n controller.enqueue({ type: \"text-delta\", id: currentTextId, delta });\n }\n\n function emitPatch(\n patch: SpecStreamLine,\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n closeTextBlock(controller);\n controller.enqueue({\n type: SPEC_DATA_PART_TYPE,\n data: { type: \"patch\", patch },\n });\n }\n\n function flushBuffer(\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n if (!lineBuffer) return;\n\n const trimmed = lineBuffer.trim();\n\n // Inside a fence, everything is spec data\n if (inSpecFence) {\n if (trimmed) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) emitPatch(patch, controller);\n // Non-patch lines inside the fence are silently dropped\n }\n lineBuffer = \"\";\n buffering = false;\n return;\n }\n\n if (trimmed) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n emitPatch(patch, controller);\n } else {\n // Was buffered but isn't JSONL — flush as text\n emitTextDelta(lineBuffer, controller);\n }\n } else {\n // Whitespace-only buffer — forward as-is (preserves blank lines)\n emitTextDelta(lineBuffer, controller);\n }\n lineBuffer = \"\";\n buffering = false;\n }\n\n function processCompleteLine(\n line: string,\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n const trimmed = line.trim();\n\n // --- Fence detection ---\n if (!inSpecFence && trimmed.startsWith(SPEC_FENCE_OPEN)) {\n inSpecFence = true;\n return; // Swallow the opening fence\n }\n if (inSpecFence && trimmed === SPEC_FENCE_CLOSE) {\n inSpecFence = false;\n return; // Swallow the closing fence\n }\n\n // Inside a fence: parse as spec data\n if (inSpecFence) {\n if (trimmed) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) emitPatch(patch, controller);\n }\n return;\n }\n\n // --- Outside fence: heuristic mode ---\n if (!trimmed) {\n // Empty line — forward for markdown paragraph breaks\n emitTextDelta(\"\\n\", controller);\n return;\n }\n\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n emitPatch(patch, controller);\n } else {\n emitTextDelta(line + \"\\n\", controller);\n }\n }\n\n return new TransformStream<StreamChunk, StreamChunk>({\n transform(chunk, controller) {\n switch (chunk.type) {\n case \"text-start\": {\n const id = (chunk as { id: string }).id;\n const idNum = parseInt(id, 10);\n if (!isNaN(idNum) && idNum >= textIdCounter) {\n textIdCounter = idNum;\n }\n currentTextId = id;\n inTextBlock = true;\n controller.enqueue(chunk);\n break;\n }\n\n case \"text-delta\": {\n const delta = chunk as { id: string; delta: string };\n const text = delta.delta;\n\n for (let i = 0; i < text.length; i++) {\n const ch = text.charAt(i);\n\n if (ch === \"\\n\") {\n // Line complete — classify and emit\n if (buffering) {\n processCompleteLine(lineBuffer, controller);\n lineBuffer = \"\";\n buffering = false;\n } else {\n // Outside fence, emit newline; inside fence, swallow it\n if (!inSpecFence) {\n emitTextDelta(\"\\n\", controller);\n }\n }\n } else if (lineBuffer.length === 0 && !buffering) {\n // Start of a new line — decide whether to buffer or stream\n if (inSpecFence || ch === \"{\" || ch === \"`\") {\n // Buffer: inside fence (everything), or heuristic mode ({), or potential fence (`)\n buffering = true;\n lineBuffer += ch;\n } else {\n emitTextDelta(ch, controller);\n }\n } else if (buffering) {\n lineBuffer += ch;\n } else {\n emitTextDelta(ch, controller);\n }\n }\n break;\n }\n\n case \"text-end\": {\n flushBuffer(controller);\n if (inTextBlock) {\n controller.enqueue({ type: \"text-end\", id: currentTextId });\n inTextBlock = false;\n }\n break;\n }\n\n default: {\n controller.enqueue(chunk);\n break;\n }\n }\n },\n\n flush(controller) {\n flushBuffer(controller);\n closeTextBlock(controller);\n },\n });\n}\n\n/**\n * The key registered in `AppDataParts` for json-render specs.\n * The AI SDK automatically prefixes this with `\"data-\"` on the wire,\n * so the actual stream chunk type is `\"data-spec\"` (see {@link SPEC_DATA_PART_TYPE}).\n *\n * @example\n * ```ts\n * import { SPEC_DATA_PART, type SpecDataPart } from \"@json-render/core\";\n * type AppDataParts = { [SPEC_DATA_PART]: SpecDataPart };\n * ```\n */\nexport const SPEC_DATA_PART = \"spec\" as const;\n\n/**\n * The wire-format type string as it appears in stream chunks and message parts.\n * This is `\"data-\"` + {@link SPEC_DATA_PART} — i.e. `\"data-spec\"`.\n *\n * Use this constant when filtering message parts or enqueuing stream chunks.\n */\nexport const SPEC_DATA_PART_TYPE = `data-${SPEC_DATA_PART}` as const;\n\n/**\n * Discriminated union for the payload of a {@link SPEC_DATA_PART_TYPE} SSE part.\n *\n * - `\"patch\"`: A single RFC 6902 JSON Patch operation (streaming, progressive UI).\n * - `\"flat\"`: A complete flat spec with `root`, `elements`, and optional `state`.\n * - `\"nested\"`: A complete nested spec (tree structure — schema depends on catalog).\n */\nexport type SpecDataPart =\n | { type: \"patch\"; patch: JsonPatch }\n | { type: \"flat\"; spec: Spec }\n | { type: \"nested\"; spec: Record<string, unknown> };\n\n/**\n * Convenience wrapper that pipes an AI SDK UI message stream through the\n * json-render transform, classifying text as prose or JSONL patches.\n *\n * Eliminates the need for manual `pipeThrough(createJsonRenderTransform())`\n * and the associated type cast.\n *\n * @example\n * ```ts\n * import { pipeJsonRender } from \"@json-render/core\";\n *\n * const stream = createUIMessageStream({\n * execute: async ({ writer }) => {\n * writer.merge(pipeJsonRender(result.toUIMessageStream()));\n * },\n * });\n * return createUIMessageStreamResponse({ stream });\n * ```\n */\nexport function pipeJsonRender<T = StreamChunk>(\n stream: ReadableStream<T>,\n): ReadableStream<T> {\n return stream.pipeThrough(\n createJsonRenderTransform() as unknown as TransformStream<T, T>,\n );\n}\n","import {\n getByPath,\n parseJsonPointer,\n type StateModel,\n type StateStore,\n} from \"./types\";\n\n/**\n * Immutably set a value at a JSON Pointer path using structural sharing.\n * Only objects along the path are shallow-cloned; untouched branches keep\n * their original references.\n */\nexport function immutableSetByPath(\n root: StateModel,\n path: string,\n value: unknown,\n): StateModel {\n const segments = parseJsonPointer(path);\n if (segments.length === 0) return root;\n\n const result = { ...root };\n let current: Record<string, unknown> = result;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!;\n const child = current[seg];\n if (Array.isArray(child)) {\n current[seg] = [...child];\n } else if (child !== null && typeof child === \"object\") {\n current[seg] = { ...(child as Record<string, unknown>) };\n } else {\n const nextSeg = segments[i + 1];\n current[seg] = nextSeg !== undefined && /^\\d+$/.test(nextSeg) ? [] : {};\n }\n current = current[seg] as Record<string, unknown>;\n }\n\n const lastSeg = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n if (lastSeg === \"-\") {\n (current as unknown[]).push(value);\n } else {\n (current as unknown[])[parseInt(lastSeg, 10)] = value;\n }\n } else {\n current[lastSeg] = value;\n }\n\n return result;\n}\n\n/**\n * Create a simple in-memory {@link StateStore}.\n *\n * This is the default store used by `StateProvider` when no external store is\n * provided. It mirrors the previous `useState`-based behaviour but is\n * framework-agnostic so it can also be used in tests or non-React contexts.\n */\nexport function createStateStore(initialState: StateModel = {}): StateStore {\n let state: StateModel = { ...initialState };\n const listeners = new Set<() => void>();\n\n function notify() {\n for (const listener of listeners) {\n listener();\n }\n }\n\n return {\n get(path: string): unknown {\n return getByPath(state, path);\n },\n\n set(path: string, value: unknown): void {\n if (getByPath(state, path) === value) return;\n state = immutableSetByPath(state, path, value);\n notify();\n },\n\n update(updates: Record<string, unknown>): void {\n let changed = false;\n let next = state;\n for (const [path, value] of Object.entries(updates)) {\n if (getByPath(next, path) !== value) {\n next = immutableSetByPath(next, path, value);\n changed = true;\n }\n }\n if (!changed) return;\n state = next;\n notify();\n },\n\n getSnapshot(): StateModel {\n return state;\n },\n\n getServerSnapshot(): StateModel {\n return state;\n },\n\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/**\n * Configuration for {@link createStoreAdapter}. Adapter authors supply these\n * three callbacks; everything else (get, set, update, no-op detection,\n * getServerSnapshot) is handled by the returned {@link StateStore}.\n */\nexport interface StoreAdapterConfig {\n /** Return the current state snapshot from the underlying store. */\n getSnapshot: () => StateModel;\n /** Write a new state snapshot to the underlying store. */\n setSnapshot: (next: StateModel) => void;\n /** Subscribe to changes in the underlying store. Return an unsubscribe fn. */\n subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * Build a full {@link StateStore} from a minimal adapter config.\n *\n * Handles `get`, `set` (with no-op detection), `update` (batched, with no-op\n * detection), `getSnapshot`, `getServerSnapshot`, and `subscribe` -- so each\n * adapter only needs to wire its snapshot source, write API, and subscribe\n * mechanism.\n */\nexport function createStoreAdapter(config: StoreAdapterConfig): StateStore {\n return {\n get(path: string): unknown {\n return getByPath(config.getSnapshot(), path);\n },\n\n set(path: string, value: unknown): void {\n const current = config.getSnapshot();\n if (getByPath(current, path) === value) return;\n config.setSnapshot(immutableSetByPath(current, path, value));\n },\n\n update(updates: Record<string, unknown>): void {\n let next = config.getSnapshot();\n let changed = false;\n for (const [path, value] of Object.entries(updates)) {\n if (getByPath(next, path) !== value) {\n next = immutableSetByPath(next, path, value);\n changed = true;\n }\n }\n if (!changed) return;\n config.setSnapshot(next);\n },\n\n getSnapshot: config.getSnapshot,\n\n getServerSnapshot: config.getSnapshot,\n\n subscribe: config.subscribe,\n };\n}\n\nconst MAX_FLATTEN_DEPTH = 20;\n\n/**\n * Recursively flatten a plain object into a `Record<string, unknown>` keyed by\n * JSON Pointer paths. Only leaf values (non-plain-object) appear in the output.\n *\n * Includes circular reference protection and a depth cap to prevent stack\n * overflow on pathological inputs.\n *\n * ```ts\n * flattenToPointers({ user: { name: \"Alice\" }, count: 1 })\n * // => { \"/user/name\": \"Alice\", \"/count\": 1 }\n * ```\n */\nexport function flattenToPointers(\n obj: Record<string, unknown>,\n prefix = \"\",\n _depth = 0,\n _seen?: Set<object>,\n _warned?: { current: boolean },\n): Record<string, unknown> {\n const seen = _seen ?? new Set<object>();\n const warned = _warned ?? { current: false };\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const pointer = `${prefix}/${key}`;\n if (\n _depth < MAX_FLATTEN_DEPTH &&\n value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) === Object.prototype &&\n !seen.has(value)\n ) {\n seen.add(value);\n Object.assign(\n result,\n flattenToPointers(\n value as Record<string, unknown>,\n pointer,\n _depth + 1,\n seen,\n warned,\n ),\n );\n } else {\n if (\n process.env.NODE_ENV !== \"production\" &&\n !warned.current &&\n _depth >= MAX_FLATTEN_DEPTH &&\n value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) === Object.prototype &&\n !seen.has(value as object)\n ) {\n warned.current = true;\n console.warn(\n `flattenToPointers: depth limit (${MAX_FLATTEN_DEPTH}) reached. Nested state beyond this depth will be treated as a leaf value.`,\n );\n }\n result[pointer] = value;\n }\n }\n return result;\n}\n"],"mappings":"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","names":[]} |
@@ -5,3 +5,3 @@ import { | ||
| immutableSetByPath | ||
| } from "./chunk-AFLK3Q4T.mjs"; | ||
| } from "./chunk-7V7ZCHEJ.mjs"; | ||
| export { | ||
@@ -8,0 +8,0 @@ createStoreAdapter, |
+1
-1
| { | ||
| "name": "@json-render/core", | ||
| "version": "0.19.0", | ||
| "version": "0.20.0", | ||
| "license": "Apache-2.0", | ||
@@ -5,0 +5,0 @@ "description": "JSON becomes real things. Define your catalog, register your components, let AI generate.", |
+11
-2
@@ -233,3 +233,3 @@ # @json-render/core | ||
| | `validateSpec(spec, options?)` | Validate spec structure and return issues | | ||
| | `autoFixSpec(spec)` | Auto-fix common spec issues (returns corrected copy) | | ||
| | `autoFixSpec(spec, options?)` | Auto-fix common spec issues; `fixDetails` classifies each fix as lossy or lossless, `{ lossy: false }` withholds pruning | | ||
| | `formatSpecIssues(issues)` | Format validation issues as readable strings | | ||
@@ -557,5 +557,14 @@ | ||
| // Auto-fix common issues (returns a corrected copy) | ||
| const fixed = autoFixSpec(spec); | ||
| const { spec: fixed, fixes, fixDetails } = autoFixSpec(spec); | ||
| ``` | ||
| `validateSpec` checks structure beyond the catalog schema: missing or dangling `children` and named `slots` references, malformed `visible` conditions (anything outside the documented forms evaluates to hidden at runtime, so it is rejected with code `invalid_visible`), `repeat` containers with no children (`repeat_without_children`), relative repeat paths outside an enclosing repeat (`repeat_item_outside_scope`), and `repeat.statePath` values that do not reference an array in the spec's own `state` (`repeat_state_mismatch`). | ||
| `autoFixSpec` distinguishes lossless fixes (relocating `visible`/`on`/`repeat`/`watch` out of `props`) from lossy ones (pruning `children` or named `slots` references to elements that were never defined). Each entry in `fixDetails` carries `{ message, lossy }`. Callers with a repair loop should apply lossless fixes immediately and prefer re-prompting over lossy fixes, passing `{ lossy: false }` to withhold pruning until retries are exhausted: | ||
| ```typescript | ||
| const lastAttempt = retriesUsed >= maxRetries; | ||
| const { spec: fixed, fixDetails } = autoFixSpec(spec, { lossy: lastAttempt }); | ||
| ``` | ||
| ## State Watchers | ||
@@ -562,0 +571,0 @@ |
| // src/types.ts | ||
| import { z } from "zod"; | ||
| var DynamicValueSchema = z.union([ | ||
| z.string(), | ||
| z.number(), | ||
| z.boolean(), | ||
| z.null(), | ||
| z.object({ $state: z.string() }) | ||
| ]); | ||
| var DynamicStringSchema = z.union([ | ||
| z.string(), | ||
| z.object({ $state: z.string() }) | ||
| ]); | ||
| var DynamicNumberSchema = z.union([ | ||
| z.number(), | ||
| z.object({ $state: z.string() }) | ||
| ]); | ||
| var DynamicBooleanSchema = z.union([ | ||
| z.boolean(), | ||
| z.object({ $state: z.string() }) | ||
| ]); | ||
| function resolveDynamicValue(value, stateModel) { | ||
| if (value === null || value === void 0) { | ||
| return void 0; | ||
| } | ||
| if (typeof value === "object" && "$state" in value) { | ||
| return getByPath(stateModel, value.$state); | ||
| } | ||
| return value; | ||
| } | ||
| function unescapeJsonPointer(token) { | ||
| return token.replace(/~1/g, "/").replace(/~0/g, "~"); | ||
| } | ||
| function parseJsonPointer(path) { | ||
| const raw = path.startsWith("/") ? path.slice(1).split("/") : path.split("/"); | ||
| return raw.map(unescapeJsonPointer); | ||
| } | ||
| function getByPath(obj, path) { | ||
| if (!path || path === "/") { | ||
| return obj; | ||
| } | ||
| const segments = parseJsonPointer(path); | ||
| let current = obj; | ||
| for (const segment of segments) { | ||
| if (current === null || current === void 0) { | ||
| return void 0; | ||
| } | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| current = current[index]; | ||
| } else if (typeof current === "object") { | ||
| current = current[segment]; | ||
| } else { | ||
| return void 0; | ||
| } | ||
| } | ||
| return current; | ||
| } | ||
| function isNumericIndex(str) { | ||
| return /^\d+$/.test(str); | ||
| } | ||
| function setByPath(obj, path, value) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| const nextSegment = segments[i + 1]; | ||
| const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-"); | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| current[index] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| current[segment] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| if (lastSegment === "-") { | ||
| current.push(value); | ||
| } else { | ||
| const index = parseInt(lastSegment, 10); | ||
| current[index] = value; | ||
| } | ||
| } else { | ||
| current[lastSegment] = value; | ||
| } | ||
| } | ||
| function addByPath(obj, path, value) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| const nextSegment = segments[i + 1]; | ||
| const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-"); | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| current[index] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| current[segment] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| if (lastSegment === "-") { | ||
| current.push(value); | ||
| } else { | ||
| const index = parseInt(lastSegment, 10); | ||
| current.splice(index, 0, value); | ||
| } | ||
| } else { | ||
| current[lastSegment] = value; | ||
| } | ||
| } | ||
| function removeByPath(obj, path) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| return; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| return; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(lastSegment, 10); | ||
| if (index >= 0 && index < current.length) { | ||
| current.splice(index, 1); | ||
| } | ||
| } else { | ||
| delete current[lastSegment]; | ||
| } | ||
| } | ||
| function deepEqual(a, b) { | ||
| if (a === b) return true; | ||
| if (a === null || b === null) return false; | ||
| if (typeof a !== typeof b) return false; | ||
| if (typeof a !== "object") return false; | ||
| if (Array.isArray(a)) { | ||
| if (!Array.isArray(b)) return false; | ||
| if (a.length !== b.length) return false; | ||
| return a.every((item, i) => deepEqual(item, b[i])); | ||
| } | ||
| const aObj = a; | ||
| const bObj = b; | ||
| const aKeys = Object.keys(aObj); | ||
| const bKeys = Object.keys(bObj); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| return aKeys.every((key) => deepEqual(aObj[key], bObj[key])); | ||
| } | ||
| function findFormValue(fieldName, params, state) { | ||
| if (params?.[fieldName] !== void 0) { | ||
| const val = params[fieldName]; | ||
| if (typeof val !== "string" || !val.includes(".")) { | ||
| return val; | ||
| } | ||
| } | ||
| if (params) { | ||
| for (const key of Object.keys(params)) { | ||
| if (key.endsWith(`.${fieldName}`)) { | ||
| const val = params[key]; | ||
| if (typeof val !== "string" || !val.includes(".")) { | ||
| return val; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (state) { | ||
| for (const key of Object.keys(state)) { | ||
| if (key === fieldName || key.endsWith(`.${fieldName}`)) { | ||
| return state[key]; | ||
| } | ||
| } | ||
| const val = getByPath(state, fieldName); | ||
| if (val !== void 0) { | ||
| return val; | ||
| } | ||
| } | ||
| return void 0; | ||
| } | ||
| function parseSpecStreamLine(line) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || !trimmed.startsWith("{")) return null; | ||
| try { | ||
| const patch = JSON.parse(trimmed); | ||
| if (patch.op && patch.path !== void 0) { | ||
| return patch; | ||
| } | ||
| return null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function applySpecStreamPatch(obj, patch) { | ||
| switch (patch.op) { | ||
| case "add": | ||
| addByPath(obj, patch.path, patch.value); | ||
| break; | ||
| case "replace": | ||
| setByPath(obj, patch.path, patch.value); | ||
| break; | ||
| case "remove": | ||
| removeByPath(obj, patch.path); | ||
| break; | ||
| case "move": { | ||
| if (!patch.from) break; | ||
| const moveValue = getByPath(obj, patch.from); | ||
| removeByPath(obj, patch.from); | ||
| addByPath(obj, patch.path, moveValue); | ||
| break; | ||
| } | ||
| case "copy": { | ||
| if (!patch.from) break; | ||
| const copyValue = getByPath(obj, patch.from); | ||
| addByPath(obj, patch.path, copyValue); | ||
| break; | ||
| } | ||
| case "test": { | ||
| const actual = getByPath(obj, patch.path); | ||
| if (!deepEqual(actual, patch.value)) { | ||
| throw new Error( | ||
| `Test operation failed: value at "${patch.path}" does not match` | ||
| ); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| return obj; | ||
| } | ||
| function applySpecPatch(spec, patch) { | ||
| applySpecStreamPatch(spec, patch); | ||
| return spec; | ||
| } | ||
| function nestedToFlat(nested) { | ||
| const elements = {}; | ||
| let counter = 0; | ||
| function walk(node) { | ||
| const key = `el-${counter++}`; | ||
| const { type, props, children: rawChildren, ...rest } = node; | ||
| const childKeys = []; | ||
| if (Array.isArray(rawChildren)) { | ||
| for (const child of rawChildren) { | ||
| if (child && typeof child === "object" && "type" in child) { | ||
| childKeys.push(walk(child)); | ||
| } | ||
| } | ||
| } | ||
| const element = { | ||
| type: type ?? "unknown", | ||
| props: props ?? {}, | ||
| children: childKeys | ||
| }; | ||
| for (const [k, v] of Object.entries(rest)) { | ||
| if (k !== "state" && v !== void 0) { | ||
| element[k] = v; | ||
| } | ||
| } | ||
| elements[key] = element; | ||
| return key; | ||
| } | ||
| const root = walk(nested); | ||
| const spec = { root, elements }; | ||
| if (nested.state && typeof nested.state === "object" && !Array.isArray(nested.state)) { | ||
| spec.state = nested.state; | ||
| } | ||
| return spec; | ||
| } | ||
| function compileSpecStream(stream, initial = {}) { | ||
| const lines = stream.split("\n"); | ||
| const result = { ...initial }; | ||
| for (const line of lines) { | ||
| const patch = parseSpecStreamLine(line); | ||
| if (patch) { | ||
| applySpecStreamPatch(result, patch); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function createSpecStreamCompiler(initial = {}) { | ||
| let result = { ...initial }; | ||
| let buffer = ""; | ||
| const appliedPatches = []; | ||
| const processedLines = /* @__PURE__ */ new Set(); | ||
| return { | ||
| push(chunk) { | ||
| buffer += chunk; | ||
| const newPatches = []; | ||
| const lines = buffer.split("\n"); | ||
| buffer = lines.pop() || ""; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || processedLines.has(trimmed)) continue; | ||
| processedLines.add(trimmed); | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) { | ||
| applySpecStreamPatch(result, patch); | ||
| appliedPatches.push(patch); | ||
| newPatches.push(patch); | ||
| } | ||
| } | ||
| if (newPatches.length > 0) { | ||
| result = { ...result }; | ||
| } | ||
| return { result, newPatches }; | ||
| }, | ||
| getResult() { | ||
| if (buffer.trim()) { | ||
| const patch = parseSpecStreamLine(buffer); | ||
| if (patch && !processedLines.has(buffer.trim())) { | ||
| processedLines.add(buffer.trim()); | ||
| applySpecStreamPatch(result, patch); | ||
| appliedPatches.push(patch); | ||
| result = { ...result }; | ||
| } | ||
| buffer = ""; | ||
| } | ||
| return result; | ||
| }, | ||
| getPatches() { | ||
| return [...appliedPatches]; | ||
| }, | ||
| reset(newInitial = {}) { | ||
| result = { ...newInitial }; | ||
| buffer = ""; | ||
| appliedPatches.length = 0; | ||
| processedLines.clear(); | ||
| } | ||
| }; | ||
| } | ||
| function createMixedStreamParser(callbacks) { | ||
| let buffer = ""; | ||
| let inSpecFence = false; | ||
| function processLine(line) { | ||
| const trimmed = line.trim(); | ||
| if (!inSpecFence && trimmed.startsWith("```spec")) { | ||
| inSpecFence = true; | ||
| return; | ||
| } | ||
| if (inSpecFence && trimmed === "```") { | ||
| inSpecFence = false; | ||
| return; | ||
| } | ||
| if (!trimmed) return; | ||
| if (inSpecFence) { | ||
| const patch2 = parseSpecStreamLine(trimmed); | ||
| if (patch2) { | ||
| callbacks.onPatch(patch2); | ||
| } | ||
| return; | ||
| } | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) { | ||
| callbacks.onPatch(patch); | ||
| } else { | ||
| callbacks.onText(line); | ||
| } | ||
| } | ||
| return { | ||
| push(chunk) { | ||
| buffer += chunk; | ||
| const lines = buffer.split("\n"); | ||
| buffer = lines.pop() || ""; | ||
| for (const line of lines) { | ||
| processLine(line); | ||
| } | ||
| }, | ||
| flush() { | ||
| if (buffer.trim()) { | ||
| processLine(buffer); | ||
| } | ||
| buffer = ""; | ||
| } | ||
| }; | ||
| } | ||
| var SPEC_FENCE_OPEN = "```spec"; | ||
| var SPEC_FENCE_CLOSE = "```"; | ||
| function createJsonRenderTransform() { | ||
| let lineBuffer = ""; | ||
| let currentTextId = ""; | ||
| let buffering = false; | ||
| let inSpecFence = false; | ||
| let inTextBlock = false; | ||
| let textIdCounter = 0; | ||
| function closeTextBlock(controller) { | ||
| if (inTextBlock) { | ||
| controller.enqueue({ type: "text-end", id: currentTextId }); | ||
| inTextBlock = false; | ||
| } | ||
| } | ||
| function ensureTextBlock(controller) { | ||
| if (!inTextBlock) { | ||
| textIdCounter++; | ||
| currentTextId = String(textIdCounter); | ||
| controller.enqueue({ type: "text-start", id: currentTextId }); | ||
| inTextBlock = true; | ||
| } | ||
| } | ||
| function emitTextDelta(delta, controller) { | ||
| ensureTextBlock(controller); | ||
| controller.enqueue({ type: "text-delta", id: currentTextId, delta }); | ||
| } | ||
| function emitPatch(patch, controller) { | ||
| closeTextBlock(controller); | ||
| controller.enqueue({ | ||
| type: SPEC_DATA_PART_TYPE, | ||
| data: { type: "patch", patch } | ||
| }); | ||
| } | ||
| function flushBuffer(controller) { | ||
| if (!lineBuffer) return; | ||
| const trimmed = lineBuffer.trim(); | ||
| if (inSpecFence) { | ||
| if (trimmed) { | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) emitPatch(patch, controller); | ||
| } | ||
| lineBuffer = ""; | ||
| buffering = false; | ||
| return; | ||
| } | ||
| if (trimmed) { | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) { | ||
| emitPatch(patch, controller); | ||
| } else { | ||
| emitTextDelta(lineBuffer, controller); | ||
| } | ||
| } else { | ||
| emitTextDelta(lineBuffer, controller); | ||
| } | ||
| lineBuffer = ""; | ||
| buffering = false; | ||
| } | ||
| function processCompleteLine(line, controller) { | ||
| const trimmed = line.trim(); | ||
| if (!inSpecFence && trimmed.startsWith(SPEC_FENCE_OPEN)) { | ||
| inSpecFence = true; | ||
| return; | ||
| } | ||
| if (inSpecFence && trimmed === SPEC_FENCE_CLOSE) { | ||
| inSpecFence = false; | ||
| return; | ||
| } | ||
| if (inSpecFence) { | ||
| if (trimmed) { | ||
| const patch2 = parseSpecStreamLine(trimmed); | ||
| if (patch2) emitPatch(patch2, controller); | ||
| } | ||
| return; | ||
| } | ||
| if (!trimmed) { | ||
| emitTextDelta("\n", controller); | ||
| return; | ||
| } | ||
| const patch = parseSpecStreamLine(trimmed); | ||
| if (patch) { | ||
| emitPatch(patch, controller); | ||
| } else { | ||
| emitTextDelta(line + "\n", controller); | ||
| } | ||
| } | ||
| return new TransformStream({ | ||
| transform(chunk, controller) { | ||
| switch (chunk.type) { | ||
| case "text-start": { | ||
| const id = chunk.id; | ||
| const idNum = parseInt(id, 10); | ||
| if (!isNaN(idNum) && idNum >= textIdCounter) { | ||
| textIdCounter = idNum; | ||
| } | ||
| currentTextId = id; | ||
| inTextBlock = true; | ||
| controller.enqueue(chunk); | ||
| break; | ||
| } | ||
| case "text-delta": { | ||
| const delta = chunk; | ||
| const text = delta.delta; | ||
| for (let i = 0; i < text.length; i++) { | ||
| const ch = text.charAt(i); | ||
| if (ch === "\n") { | ||
| if (buffering) { | ||
| processCompleteLine(lineBuffer, controller); | ||
| lineBuffer = ""; | ||
| buffering = false; | ||
| } else { | ||
| if (!inSpecFence) { | ||
| emitTextDelta("\n", controller); | ||
| } | ||
| } | ||
| } else if (lineBuffer.length === 0 && !buffering) { | ||
| if (inSpecFence || ch === "{" || ch === "`") { | ||
| buffering = true; | ||
| lineBuffer += ch; | ||
| } else { | ||
| emitTextDelta(ch, controller); | ||
| } | ||
| } else if (buffering) { | ||
| lineBuffer += ch; | ||
| } else { | ||
| emitTextDelta(ch, controller); | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| case "text-end": { | ||
| flushBuffer(controller); | ||
| if (inTextBlock) { | ||
| controller.enqueue({ type: "text-end", id: currentTextId }); | ||
| inTextBlock = false; | ||
| } | ||
| break; | ||
| } | ||
| default: { | ||
| controller.enqueue(chunk); | ||
| break; | ||
| } | ||
| } | ||
| }, | ||
| flush(controller) { | ||
| flushBuffer(controller); | ||
| closeTextBlock(controller); | ||
| } | ||
| }); | ||
| } | ||
| var SPEC_DATA_PART = "spec"; | ||
| var SPEC_DATA_PART_TYPE = `data-${SPEC_DATA_PART}`; | ||
| function pipeJsonRender(stream) { | ||
| return stream.pipeThrough( | ||
| createJsonRenderTransform() | ||
| ); | ||
| } | ||
| // src/state-store.ts | ||
| function immutableSetByPath(root, path, value) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return root; | ||
| const result = { ...root }; | ||
| let current = result; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const seg = segments[i]; | ||
| const child = current[seg]; | ||
| if (Array.isArray(child)) { | ||
| current[seg] = [...child]; | ||
| } else if (child !== null && typeof child === "object") { | ||
| current[seg] = { ...child }; | ||
| } else { | ||
| const nextSeg = segments[i + 1]; | ||
| current[seg] = nextSeg !== void 0 && /^\d+$/.test(nextSeg) ? [] : {}; | ||
| } | ||
| current = current[seg]; | ||
| } | ||
| const lastSeg = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| if (lastSeg === "-") { | ||
| current.push(value); | ||
| } else { | ||
| current[parseInt(lastSeg, 10)] = value; | ||
| } | ||
| } else { | ||
| current[lastSeg] = value; | ||
| } | ||
| return result; | ||
| } | ||
| function createStateStore(initialState = {}) { | ||
| let state = { ...initialState }; | ||
| const listeners = /* @__PURE__ */ new Set(); | ||
| function notify() { | ||
| for (const listener of listeners) { | ||
| listener(); | ||
| } | ||
| } | ||
| return { | ||
| get(path) { | ||
| return getByPath(state, path); | ||
| }, | ||
| set(path, value) { | ||
| if (getByPath(state, path) === value) return; | ||
| state = immutableSetByPath(state, path, value); | ||
| notify(); | ||
| }, | ||
| update(updates) { | ||
| let changed = false; | ||
| let next = state; | ||
| for (const [path, value] of Object.entries(updates)) { | ||
| if (getByPath(next, path) !== value) { | ||
| next = immutableSetByPath(next, path, value); | ||
| changed = true; | ||
| } | ||
| } | ||
| if (!changed) return; | ||
| state = next; | ||
| notify(); | ||
| }, | ||
| getSnapshot() { | ||
| return state; | ||
| }, | ||
| getServerSnapshot() { | ||
| return state; | ||
| }, | ||
| subscribe(listener) { | ||
| listeners.add(listener); | ||
| return () => { | ||
| listeners.delete(listener); | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| function createStoreAdapter(config) { | ||
| return { | ||
| get(path) { | ||
| return getByPath(config.getSnapshot(), path); | ||
| }, | ||
| set(path, value) { | ||
| const current = config.getSnapshot(); | ||
| if (getByPath(current, path) === value) return; | ||
| config.setSnapshot(immutableSetByPath(current, path, value)); | ||
| }, | ||
| update(updates) { | ||
| let next = config.getSnapshot(); | ||
| let changed = false; | ||
| for (const [path, value] of Object.entries(updates)) { | ||
| if (getByPath(next, path) !== value) { | ||
| next = immutableSetByPath(next, path, value); | ||
| changed = true; | ||
| } | ||
| } | ||
| if (!changed) return; | ||
| config.setSnapshot(next); | ||
| }, | ||
| getSnapshot: config.getSnapshot, | ||
| getServerSnapshot: config.getSnapshot, | ||
| subscribe: config.subscribe | ||
| }; | ||
| } | ||
| var MAX_FLATTEN_DEPTH = 20; | ||
| function flattenToPointers(obj, prefix = "", _depth = 0, _seen, _warned) { | ||
| const seen = _seen ?? /* @__PURE__ */ new Set(); | ||
| const warned = _warned ?? { current: false }; | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| const pointer = `${prefix}/${key}`; | ||
| if (_depth < MAX_FLATTEN_DEPTH && value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype && !seen.has(value)) { | ||
| seen.add(value); | ||
| Object.assign( | ||
| result, | ||
| flattenToPointers( | ||
| value, | ||
| pointer, | ||
| _depth + 1, | ||
| seen, | ||
| warned | ||
| ) | ||
| ); | ||
| } else { | ||
| if (process.env.NODE_ENV !== "production" && !warned.current && _depth >= MAX_FLATTEN_DEPTH && value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype && !seen.has(value)) { | ||
| warned.current = true; | ||
| console.warn( | ||
| `flattenToPointers: depth limit (${MAX_FLATTEN_DEPTH}) reached. Nested state beyond this depth will be treated as a leaf value.` | ||
| ); | ||
| } | ||
| result[pointer] = value; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| export { | ||
| DynamicValueSchema, | ||
| DynamicStringSchema, | ||
| DynamicNumberSchema, | ||
| DynamicBooleanSchema, | ||
| resolveDynamicValue, | ||
| getByPath, | ||
| setByPath, | ||
| addByPath, | ||
| removeByPath, | ||
| findFormValue, | ||
| parseSpecStreamLine, | ||
| applySpecStreamPatch, | ||
| applySpecPatch, | ||
| nestedToFlat, | ||
| compileSpecStream, | ||
| createSpecStreamCompiler, | ||
| createMixedStreamParser, | ||
| createJsonRenderTransform, | ||
| SPEC_DATA_PART, | ||
| SPEC_DATA_PART_TYPE, | ||
| pipeJsonRender, | ||
| immutableSetByPath, | ||
| createStateStore, | ||
| createStoreAdapter, | ||
| flattenToPointers | ||
| }; | ||
| //# sourceMappingURL=chunk-AFLK3Q4T.mjs.map |
| {"version":3,"sources":["../src/types.ts","../src/state-store.ts"],"sourcesContent":["import { z } from \"zod\";\nimport type { ActionBinding } from \"./actions\";\n\n/**\n * Dynamic value - can be a literal or a `{ $state }` reference to the state model.\n *\n * Used in action params and validation args where values can either be\n * hardcoded or resolved from state at runtime.\n */\nexport type DynamicValue<T = unknown> = T | { $state: string };\n\n/**\n * Dynamic string value\n */\nexport type DynamicString = DynamicValue<string>;\n\n/**\n * Dynamic number value\n */\nexport type DynamicNumber = DynamicValue<number>;\n\n/**\n * Dynamic boolean value\n */\nexport type DynamicBoolean = DynamicValue<boolean>;\n\n/**\n * Zod schema for dynamic values\n */\nexport const DynamicValueSchema = z.union([\n z.string(),\n z.number(),\n z.boolean(),\n z.null(),\n z.object({ $state: z.string() }),\n]);\n\nexport const DynamicStringSchema = z.union([\n z.string(),\n z.object({ $state: z.string() }),\n]);\n\nexport const DynamicNumberSchema = z.union([\n z.number(),\n z.object({ $state: z.string() }),\n]);\n\nexport const DynamicBooleanSchema = z.union([\n z.boolean(),\n z.object({ $state: z.string() }),\n]);\n\n/**\n * Base UI element structure for v2\n */\nexport interface UIElement<\n T extends string = string,\n P = Record<string, unknown>,\n> {\n /** Component type from the catalog */\n type: T;\n /** Component props */\n props: P;\n /** Child element keys (flat structure) */\n children?: string[];\n /** Visibility condition */\n visible?: VisibilityCondition;\n /** Event bindings — maps event names to action bindings */\n on?: Record<string, ActionBinding | ActionBinding[]>;\n /** Repeat children once per item in a state array */\n repeat?: { statePath: string; key?: string };\n /**\n * State watchers — maps JSON Pointer state paths to action bindings.\n * When the value at a watched path changes, the bound actions fire.\n * Useful for cascading dependencies (e.g. country → city option loading).\n */\n watch?: Record<string, ActionBinding | ActionBinding[]>;\n}\n\n/**\n * Element with key and parentKey for use with flatToTree.\n * When elements are in an array (not a keyed map), key and parentKey\n * are needed to establish identity and parent-child relationships.\n */\nexport interface FlatElement<\n T extends string = string,\n P = Record<string, unknown>,\n> extends UIElement<T, P> {\n /** Unique key identifying this element */\n key: string;\n /** Parent element key (null for root) */\n parentKey?: string | null;\n}\n\n/**\n * Shared comparison operators for visibility conditions.\n *\n * Use at most ONE comparison operator per condition. If multiple are\n * provided, only the first matching one is evaluated (precedence:\n * eq > neq > gt > gte > lt > lte). With no operator, truthiness is checked.\n *\n * `not` inverts the final result of whichever operator (or truthiness\n * check) is used.\n */\ntype ComparisonOperators = {\n eq?: unknown;\n neq?: unknown;\n gt?: number | { $state: string };\n gte?: number | { $state: string };\n lt?: number | { $state: string };\n lte?: number | { $state: string };\n not?: true;\n};\n\n/**\n * A single state-based condition.\n * Resolves `$state` to a value from the state model, then applies the operator.\n * Without an operator, checks truthiness.\n *\n * When `not` is `true`, the result of the entire condition is inverted.\n * For example `{ $state: \"/count\", gt: 5, not: true }` means \"NOT greater than 5\".\n */\nexport type StateCondition = { $state: string } & ComparisonOperators;\n\n/**\n * A condition that resolves `$item` to a field on the current repeat item.\n * Only meaningful inside a `repeat` scope.\n *\n * Use `\"\"` to reference the whole item, or `\"field\"` for a specific field.\n */\nexport type ItemCondition = { $item: string } & ComparisonOperators;\n\n/**\n * A condition that resolves `$index` to the current repeat array index.\n * Only meaningful inside a `repeat` scope.\n */\nexport type IndexCondition = { $index: true } & ComparisonOperators;\n\n/** A single visibility condition (state, item, or index). */\nexport type SingleCondition = StateCondition | ItemCondition | IndexCondition;\n\n/**\n * AND wrapper — all child conditions must be true.\n * This is the explicit form of the implicit array AND (`SingleCondition[]`).\n * Unlike the implicit form, `$and` supports nested `$or` and `$and` conditions.\n */\nexport type AndCondition = { $and: VisibilityCondition[] };\n\n/**\n * OR wrapper — at least one child condition must be true.\n */\nexport type OrCondition = { $or: VisibilityCondition[] };\n\n/**\n * Visibility condition types.\n * - `boolean` — always/never\n * - `SingleCondition` — single condition (`$state`, `$item`, or `$index`)\n * - `SingleCondition[]` — implicit AND (all must be true)\n * - `AndCondition` — `{ $and: [...] }`, explicit AND (all must be true)\n * - `OrCondition` — `{ $or: [...] }`, at least one must be true\n */\nexport type VisibilityCondition =\n | boolean\n | SingleCondition\n | SingleCondition[]\n | AndCondition\n | OrCondition;\n\n/**\n * Flat UI tree structure (optimized for LLM generation)\n */\nexport interface Spec {\n /** Root element key */\n root: string;\n /** Flat map of elements by key */\n elements: Record<string, UIElement>;\n /** Optional initial state to seed the state model.\n * Components using statePath will read from / write to this state. */\n state?: Record<string, unknown>;\n}\n\n/**\n * State model type\n */\nexport type StateModel = Record<string, unknown>;\n\n/**\n * An abstract store that owns state and notifies subscribers on change.\n *\n * Consumers can supply their own implementation (backed by Redux, Zustand,\n * XState, etc.) or use the built-in {@link createStateStore} for a simple\n * in-memory store.\n */\nexport interface StateStore {\n /** Read a value by JSON Pointer path. */\n get: (path: string) => unknown;\n /**\n * Write a value by JSON Pointer path and notify subscribers.\n * Equality is checked by reference (`===`), not deep comparison.\n * Callers must pass a new object/array reference for changes to be detected.\n */\n set: (path: string, value: unknown) => void;\n /**\n * Write multiple values at once and notify subscribers (single notification).\n * Each value is compared by reference (`===`); only paths whose value\n * actually changed are applied.\n */\n update: (updates: Record<string, unknown>) => void;\n /** Return the full state object (used by `useSyncExternalStore`). */\n getSnapshot: () => StateModel;\n /** Optional server snapshot for SSR (passed to `useSyncExternalStore`). Falls back to `getSnapshot` when omitted. */\n getServerSnapshot?: () => StateModel;\n /** Register a listener that is called on every state change. Returns an unsubscribe function. */\n subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * Component schema definition using Zod\n */\nexport type ComponentSchema = z.ZodType<Record<string, unknown>>;\n\n/**\n * Validation mode for catalog validation\n */\nexport type ValidationMode = \"strict\" | \"warn\" | \"ignore\";\n\n/**\n * JSON patch operation types (RFC 6902)\n */\nexport type PatchOp = \"add\" | \"remove\" | \"replace\" | \"move\" | \"copy\" | \"test\";\n\n/**\n * JSON patch operation (RFC 6902)\n */\nexport interface JsonPatch {\n op: PatchOp;\n path: string;\n /** Required for add, replace, test */\n value?: unknown;\n /** Required for move, copy (source location) */\n from?: string;\n}\n\n/**\n * Resolve a dynamic value against a state model\n */\nexport function resolveDynamicValue<T>(\n value: DynamicValue<T>,\n stateModel: StateModel,\n): T | undefined {\n if (value === null || value === undefined) {\n return undefined;\n }\n\n if (typeof value === \"object\" && \"$state\" in value) {\n return getByPath(stateModel, (value as { $state: string }).$state) as\n | T\n | undefined;\n }\n\n return value as T;\n}\n\n/**\n * Unescape a JSON Pointer token per RFC 6901 Section 4.\n * ~1 is decoded to / and ~0 is decoded to ~ (order matters).\n */\nfunction unescapeJsonPointer(token: string): string {\n return token.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n}\n\n/**\n * Parse a JSON Pointer path into unescaped segments.\n */\nexport function parseJsonPointer(path: string): string[] {\n const raw = path.startsWith(\"/\") ? path.slice(1).split(\"/\") : path.split(\"/\");\n return raw.map(unescapeJsonPointer);\n}\n\n/**\n * Get a value from an object by JSON Pointer path (RFC 6901)\n */\nexport function getByPath(obj: unknown, path: string): unknown {\n if (!path || path === \"/\") {\n return obj;\n }\n\n const segments = parseJsonPointer(path);\n\n let current: unknown = obj;\n\n for (const segment of segments) {\n if (current === null || current === undefined) {\n return undefined;\n }\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n current = current[index];\n } else if (typeof current === \"object\") {\n current = (current as Record<string, unknown>)[segment];\n } else {\n return undefined;\n }\n }\n\n return current;\n}\n\n/**\n * Check if a string is a numeric index\n */\nfunction isNumericIndex(str: string): boolean {\n return /^\\d+$/.test(str);\n}\n\n/**\n * Set a value in an object by JSON Pointer path (RFC 6901).\n * Automatically creates arrays when the path segment is a numeric index.\n */\nexport function setByPath(\n obj: Record<string, unknown>,\n path: string,\n value: unknown,\n): void {\n const segments = parseJsonPointer(path);\n\n if (segments.length === 0) return;\n\n let current: Record<string, unknown> | unknown[] = obj;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!;\n const nextSegment = segments[i + 1];\n const nextIsNumeric =\n nextSegment !== undefined &&\n (isNumericIndex(nextSegment) || nextSegment === \"-\");\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n if (current[index] === undefined || typeof current[index] !== \"object\") {\n current[index] = nextIsNumeric ? [] : {};\n }\n current = current[index] as Record<string, unknown> | unknown[];\n } else {\n if (!(segment in current) || typeof current[segment] !== \"object\") {\n current[segment] = nextIsNumeric ? [] : {};\n }\n current = current[segment] as Record<string, unknown> | unknown[];\n }\n }\n\n const lastSegment = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n if (lastSegment === \"-\") {\n current.push(value);\n } else {\n const index = parseInt(lastSegment, 10);\n current[index] = value;\n }\n } else {\n current[lastSegment] = value;\n }\n}\n\n/**\n * Add a value per RFC 6902 \"add\" semantics.\n * For objects: create-or-replace the member.\n * For arrays: insert before the given index, or append if \"-\".\n */\nexport function addByPath(\n obj: Record<string, unknown>,\n path: string,\n value: unknown,\n): void {\n const segments = parseJsonPointer(path);\n\n if (segments.length === 0) return;\n\n let current: Record<string, unknown> | unknown[] = obj;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!;\n const nextSegment = segments[i + 1];\n const nextIsNumeric =\n nextSegment !== undefined &&\n (isNumericIndex(nextSegment) || nextSegment === \"-\");\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n if (current[index] === undefined || typeof current[index] !== \"object\") {\n current[index] = nextIsNumeric ? [] : {};\n }\n current = current[index] as Record<string, unknown> | unknown[];\n } else {\n if (!(segment in current) || typeof current[segment] !== \"object\") {\n current[segment] = nextIsNumeric ? [] : {};\n }\n current = current[segment] as Record<string, unknown> | unknown[];\n }\n }\n\n const lastSegment = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n if (lastSegment === \"-\") {\n current.push(value);\n } else {\n const index = parseInt(lastSegment, 10);\n current.splice(index, 0, value);\n }\n } else {\n current[lastSegment] = value;\n }\n}\n\n/**\n * Remove a value per RFC 6902 \"remove\" semantics.\n * For objects: delete the property.\n * For arrays: splice out the element at the given index.\n */\nexport function removeByPath(obj: Record<string, unknown>, path: string): void {\n const segments = parseJsonPointer(path);\n\n if (segments.length === 0) return;\n\n let current: Record<string, unknown> | unknown[] = obj;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!;\n\n if (Array.isArray(current)) {\n const index = parseInt(segment, 10);\n if (current[index] === undefined || typeof current[index] !== \"object\") {\n return; // path does not exist\n }\n current = current[index] as Record<string, unknown> | unknown[];\n } else {\n if (!(segment in current) || typeof current[segment] !== \"object\") {\n return; // path does not exist\n }\n current = current[segment] as Record<string, unknown> | unknown[];\n }\n }\n\n const lastSegment = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n const index = parseInt(lastSegment, 10);\n if (index >= 0 && index < current.length) {\n current.splice(index, 1);\n }\n } else {\n delete current[lastSegment];\n }\n}\n\n/**\n * Deep equality check for RFC 6902 \"test\" operation.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n if (typeof a !== \"object\") return false;\n\n if (Array.isArray(a)) {\n if (!Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n return a.every((item, i) => deepEqual(item, b[i]));\n }\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));\n}\n\n/**\n * Find a form value from params and/or state.\n * Useful in action handlers to locate form input values regardless of path format.\n *\n * Checks in order:\n * 1. Direct param key (if not a path reference)\n * 2. Param keys ending with the field name\n * 3. State keys ending with the field name (dot notation)\n * 4. State path using getByPath (slash notation)\n *\n * @example\n * // Find \"name\" from params or state\n * const name = findFormValue(\"name\", params, state);\n *\n * // Will find from: params.name, params[\"form.name\"], state[\"form.name\"], or getByPath(state, \"name\")\n */\nexport function findFormValue(\n fieldName: string,\n params?: Record<string, unknown>,\n state?: Record<string, unknown>,\n): unknown {\n // Check params first (but not if it looks like a state path reference)\n if (params?.[fieldName] !== undefined) {\n const val = params[fieldName];\n // If the value looks like a path reference (contains dots), skip it\n if (typeof val !== \"string\" || !val.includes(\".\")) {\n return val;\n }\n }\n\n // Check param keys that end with the field name\n if (params) {\n for (const key of Object.keys(params)) {\n if (key.endsWith(`.${fieldName}`)) {\n const val = params[key];\n if (typeof val !== \"string\" || !val.includes(\".\")) {\n return val;\n }\n }\n }\n }\n\n // Check state keys that end with the field name (handles any form naming)\n if (state) {\n for (const key of Object.keys(state)) {\n if (key === fieldName || key.endsWith(`.${fieldName}`)) {\n return state[key];\n }\n }\n\n // Try getByPath with the raw field name\n const val = getByPath(state, fieldName);\n if (val !== undefined) {\n return val;\n }\n }\n\n return undefined;\n}\n\n// =============================================================================\n// SpecStream - Streaming format for progressively building specs\n// =============================================================================\n\n/**\n * A SpecStream line - a single patch operation in the stream.\n */\nexport type SpecStreamLine = JsonPatch;\n\n/**\n * Parse a single SpecStream line into a patch operation.\n * Returns null if the line is invalid or empty.\n *\n * SpecStream is json-render's streaming format where each line is a JSON patch\n * operation that progressively builds up the final spec.\n */\nexport function parseSpecStreamLine(line: string): SpecStreamLine | null {\n const trimmed = line.trim();\n if (!trimmed || !trimmed.startsWith(\"{\")) return null;\n\n try {\n const patch = JSON.parse(trimmed) as SpecStreamLine;\n if (patch.op && patch.path !== undefined) {\n return patch;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Apply a single RFC 6902 JSON Patch operation to an object.\n * Mutates the object in place.\n *\n * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test.\n *\n * @throws {Error} If a \"test\" operation fails (value mismatch).\n */\nexport function applySpecStreamPatch<T extends Record<string, unknown>>(\n obj: T,\n patch: SpecStreamLine,\n): T {\n switch (patch.op) {\n case \"add\":\n addByPath(obj, patch.path, patch.value);\n break;\n case \"replace\":\n // RFC 6902: target must exist. For streaming tolerance we set regardless.\n setByPath(obj, patch.path, patch.value);\n break;\n case \"remove\":\n removeByPath(obj, patch.path);\n break;\n case \"move\": {\n if (!patch.from) break;\n const moveValue = getByPath(obj, patch.from);\n removeByPath(obj, patch.from);\n addByPath(obj, patch.path, moveValue);\n break;\n }\n case \"copy\": {\n if (!patch.from) break;\n const copyValue = getByPath(obj, patch.from);\n addByPath(obj, patch.path, copyValue);\n break;\n }\n case \"test\": {\n const actual = getByPath(obj, patch.path);\n if (!deepEqual(actual, patch.value)) {\n throw new Error(\n `Test operation failed: value at \"${patch.path}\" does not match`,\n );\n }\n break;\n }\n }\n return obj;\n}\n\n/**\n * Apply a single RFC 6902 JSON Patch operation to a Spec.\n * Mutates the spec in place and returns it.\n *\n * This is a typed convenience wrapper around `applySpecStreamPatch` that\n * accepts a `Spec` directly without requiring a cast to `Record<string, unknown>`.\n *\n * Note: This mutates the spec. For React state updates, spread the result\n * to create a new reference: `setSpec({ ...applySpecPatch(spec, patch) })`.\n *\n * @example\n * let spec: Spec = { root: \"\", elements: {} };\n * applySpecPatch(spec, { op: \"add\", path: \"/root\", value: \"main\" });\n */\nexport function applySpecPatch(spec: Spec, patch: SpecStreamLine): Spec {\n applySpecStreamPatch(spec as unknown as Record<string, unknown>, patch);\n return spec;\n}\n\n// =============================================================================\n// Nested-to-Flat Conversion\n// =============================================================================\n\n/**\n * A nested spec node. This is the tree format that humans naturally write —\n * each node has inline `children` as an array of child node objects rather\n * than string keys.\n */\ninterface NestedNode {\n type: string;\n props: Record<string, unknown>;\n children?: NestedNode[];\n /** Any other top-level fields (visible, on, repeat, etc.) */\n [key: string]: unknown;\n}\n\n/**\n * Convert a nested (tree-structured) spec into the flat `Spec` format used\n * by json-render renderers.\n *\n * In the nested format each node has inline `children` as an array of child\n * objects. This function walks the tree, assigns auto-generated keys\n * (`el-0`, `el-1`, ...), and produces a flat `{ root, elements, state }` spec.\n *\n * The top-level `state` field (if present on the root node) is hoisted to\n * `spec.state`.\n *\n * @example\n * ```ts\n * const nested = {\n * type: \"Card\",\n * props: { title: \"Hello\" },\n * children: [\n * { type: \"Text\", props: { content: \"World\" } },\n * ],\n * state: { count: 0 },\n * };\n * const spec = nestedToFlat(nested);\n * // {\n * // root: \"el-0\",\n * // elements: {\n * // \"el-0\": { type: \"Card\", props: { title: \"Hello\" }, children: [\"el-1\"] },\n * // \"el-1\": { type: \"Text\", props: { content: \"World\" }, children: [] },\n * // },\n * // state: { count: 0 },\n * // }\n * ```\n */\nexport function nestedToFlat(nested: Record<string, unknown>): Spec {\n const elements: Record<string, UIElement> = {};\n let counter = 0;\n\n function walk(node: Record<string, unknown>): string {\n const key = `el-${counter++}`;\n const { type, props, children: rawChildren, ...rest } = node as NestedNode;\n\n // Recursively flatten children\n const childKeys: string[] = [];\n if (Array.isArray(rawChildren)) {\n for (const child of rawChildren) {\n if (child && typeof child === \"object\" && \"type\" in child) {\n childKeys.push(walk(child as Record<string, unknown>));\n }\n }\n }\n\n // Build the flat element, preserving extra fields (visible, on, repeat, etc.)\n // but excluding `state` which is hoisted to spec-level.\n const element: UIElement = {\n type: type ?? \"unknown\",\n props: (props as Record<string, unknown>) ?? {},\n children: childKeys,\n };\n\n // Copy extra fields (visible, on, repeat) but not state\n for (const [k, v] of Object.entries(rest)) {\n if (k !== \"state\" && v !== undefined) {\n (element as unknown as Record<string, unknown>)[k] = v;\n }\n }\n\n elements[key] = element;\n return key;\n }\n\n const root = walk(nested);\n\n const spec: Spec = { root, elements };\n\n // Hoist state from root node if present\n if (\n nested.state &&\n typeof nested.state === \"object\" &&\n !Array.isArray(nested.state)\n ) {\n spec.state = nested.state as Record<string, unknown>;\n }\n\n return spec;\n}\n\n/**\n * Compile a SpecStream string into a JSON object.\n * Each line should be a patch operation.\n *\n * @example\n * const stream = `{\"op\":\"add\",\"path\":\"/name\",\"value\":\"Alice\"}\n * {\"op\":\"add\",\"path\":\"/age\",\"value\":30}`;\n * const result = compileSpecStream(stream);\n * // { name: \"Alice\", age: 30 }\n */\nexport function compileSpecStream<\n T extends Record<string, unknown> = Record<string, unknown>,\n>(stream: string, initial: T = {} as T): T {\n const lines = stream.split(\"\\n\");\n const result = { ...initial };\n\n for (const line of lines) {\n const patch = parseSpecStreamLine(line);\n if (patch) {\n applySpecStreamPatch(result, patch);\n }\n }\n\n return result as T;\n}\n\n/**\n * Streaming SpecStream compiler.\n * Useful for processing SpecStream data as it streams in from AI.\n *\n * @example\n * const compiler = createSpecStreamCompiler<MySpec>();\n *\n * // As chunks arrive:\n * const { result, newPatches } = compiler.push(chunk);\n * if (newPatches.length > 0) {\n * updateUI(result);\n * }\n *\n * // When done:\n * const finalResult = compiler.getResult();\n */\nexport interface SpecStreamCompiler<T> {\n /** Push a chunk of text. Returns the current result and any new patches applied. */\n push(chunk: string): { result: T; newPatches: SpecStreamLine[] };\n /** Get the current compiled result */\n getResult(): T;\n /** Get all patches that have been applied */\n getPatches(): SpecStreamLine[];\n /** Reset the compiler to initial state */\n reset(initial?: Partial<T>): void;\n}\n\n/**\n * Create a streaming SpecStream compiler.\n *\n * SpecStream is json-render's streaming format. AI outputs patch operations\n * line by line, and this compiler progressively builds the final spec.\n *\n * @example\n * const compiler = createSpecStreamCompiler<TimelineSpec>();\n *\n * // Process streaming response\n * const reader = response.body.getReader();\n * while (true) {\n * const { done, value } = await reader.read();\n * if (done) break;\n *\n * const { result, newPatches } = compiler.push(decoder.decode(value));\n * if (newPatches.length > 0) {\n * setSpec(result); // Update UI with partial result\n * }\n * }\n */\nexport function createSpecStreamCompiler<T = Record<string, unknown>>(\n initial: Partial<T> = {},\n): SpecStreamCompiler<T> {\n let result = { ...initial } as T;\n let buffer = \"\";\n const appliedPatches: SpecStreamLine[] = [];\n const processedLines = new Set<string>();\n\n return {\n push(chunk: string): { result: T; newPatches: SpecStreamLine[] } {\n buffer += chunk;\n const newPatches: SpecStreamLine[] = [];\n\n // Process complete lines\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || processedLines.has(trimmed)) continue;\n processedLines.add(trimmed);\n\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n applySpecStreamPatch(result as Record<string, unknown>, patch);\n appliedPatches.push(patch);\n newPatches.push(patch);\n }\n }\n\n // Return a shallow copy to trigger re-renders\n if (newPatches.length > 0) {\n result = { ...result };\n }\n\n return { result, newPatches };\n },\n\n getResult(): T {\n // Process any remaining buffer\n if (buffer.trim()) {\n const patch = parseSpecStreamLine(buffer);\n if (patch && !processedLines.has(buffer.trim())) {\n processedLines.add(buffer.trim());\n applySpecStreamPatch(result as Record<string, unknown>, patch);\n appliedPatches.push(patch);\n result = { ...result };\n }\n buffer = \"\";\n }\n return result;\n },\n\n getPatches(): SpecStreamLine[] {\n return [...appliedPatches];\n },\n\n reset(newInitial: Partial<T> = {}): void {\n result = { ...newInitial } as T;\n buffer = \"\";\n appliedPatches.length = 0;\n processedLines.clear();\n },\n };\n}\n\n// =============================================================================\n// Mixed Stream Parser — for chat + GenUI (text interleaved with JSONL patches)\n// =============================================================================\n\n/**\n * Callbacks for the mixed stream parser.\n */\nexport interface MixedStreamCallbacks {\n /** Called when a JSONL patch line is parsed */\n onPatch: (patch: SpecStreamLine) => void;\n /** Called when a text (non-JSONL) line is received */\n onText: (text: string) => void;\n}\n\n/**\n * A stateful parser for mixed streams that contain both text and JSONL patches.\n * Used in chat + GenUI scenarios where an LLM responds with conversational text\n * interleaved with json-render JSONL patch operations.\n */\nexport interface MixedStreamParser {\n /** Push a chunk of streamed data. Calls onPatch/onText for each complete line. */\n push(chunk: string): void;\n /** Flush any remaining buffered content. Call when the stream ends. */\n flush(): void;\n}\n\n/**\n * Create a parser for mixed text + JSONL streams.\n *\n * In chat + GenUI scenarios, an LLM streams a response that contains both\n * conversational text and json-render JSONL patch lines. This parser buffers\n * incoming chunks, splits them into lines, and classifies each line as either\n * a JSONL patch (via `parseSpecStreamLine`) or plain text.\n *\n * @example\n * const parser = createMixedStreamParser({\n * onText: (text) => appendToMessage(text),\n * onPatch: (patch) => applySpecPatch(spec, patch),\n * });\n *\n * // As chunks arrive from the stream:\n * for await (const chunk of stream) {\n * parser.push(chunk);\n * }\n * parser.flush();\n */\nexport function createMixedStreamParser(\n callbacks: MixedStreamCallbacks,\n): MixedStreamParser {\n let buffer = \"\";\n let inSpecFence = false;\n\n function processLine(line: string): void {\n const trimmed = line.trim();\n\n // Fence detection\n if (!inSpecFence && trimmed.startsWith(\"```spec\")) {\n inSpecFence = true;\n return;\n }\n if (inSpecFence && trimmed === \"```\") {\n inSpecFence = false;\n return;\n }\n\n if (!trimmed) return;\n\n if (inSpecFence) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n callbacks.onPatch(patch);\n }\n return;\n }\n\n // Outside fence: heuristic mode\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n callbacks.onPatch(patch);\n } else {\n callbacks.onText(line);\n }\n }\n\n return {\n push(chunk: string): void {\n buffer += chunk;\n\n // Process complete lines\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n for (const line of lines) {\n processLine(line);\n }\n },\n\n flush(): void {\n if (buffer.trim()) {\n processLine(buffer);\n }\n buffer = \"\";\n },\n };\n}\n\n// =============================================================================\n// AI SDK Stream Transform\n// =============================================================================\n\n/**\n * Minimal chunk shape compatible with the AI SDK's `UIMessageChunk`.\n *\n * Defined here so that `@json-render/core` has no dependency on the `ai`\n * package. The discriminated union covers the three text-related chunk types\n * the transform inspects; all other chunk types pass through via the fallback.\n */\nexport type StreamChunk =\n | { type: \"text-start\"; id: string; [k: string]: unknown }\n | { type: \"text-delta\"; id: string; delta: string; [k: string]: unknown }\n | { type: \"text-end\"; id: string; [k: string]: unknown }\n | { type: string; [k: string]: unknown };\n\n/** The opening fence for a spec block (e.g. ` ```spec `). */\nconst SPEC_FENCE_OPEN = \"```spec\";\n/** The closing fence for a spec block. */\nconst SPEC_FENCE_CLOSE = \"```\";\n\n/**\n * Creates a `TransformStream` that intercepts AI SDK UI message stream chunks\n * and classifies text content as either prose or json-render JSONL patches.\n *\n * Two classification modes:\n *\n * 1. **Fence mode** (preferred): Lines between ` ```spec ` and ` ``` ` are\n * parsed as JSONL patches. Fence delimiters are swallowed (not emitted).\n * 2. **Heuristic mode** (backward compat): Outside of fences, lines starting\n * with `{` are buffered and tested with `parseSpecStreamLine`. Valid patches\n * are emitted as {@link SPEC_DATA_PART_TYPE} parts; everything else is\n * flushed as text.\n *\n * Non-text chunks (tool events, step markers, etc.) are passed through unchanged.\n *\n * @example\n * ```ts\n * import { createJsonRenderTransform } from \"@json-render/core\";\n * import { createUIMessageStream, createUIMessageStreamResponse } from \"ai\";\n *\n * const stream = createUIMessageStream({\n * execute: async ({ writer }) => {\n * writer.merge(\n * result.toUIMessageStream().pipeThrough(createJsonRenderTransform()),\n * );\n * },\n * });\n * return createUIMessageStreamResponse({ stream });\n * ```\n */\nexport function createJsonRenderTransform(): TransformStream<\n StreamChunk,\n StreamChunk\n> {\n let lineBuffer = \"\";\n let currentTextId = \"\";\n // Whether the current incomplete line might be JSONL (starts with '{')\n let buffering = false;\n // Whether we are inside a ```spec fence\n let inSpecFence = false;\n // Whether we are currently inside a text block (between text-start/text-end).\n // Used to split text blocks around spec data so the AI SDK creates separate\n // text parts, preserving interleaving of prose and UI in message.parts.\n let inTextBlock = false;\n let textIdCounter = 0;\n\n /** Close the current text block if one is open. */\n function closeTextBlock(\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n if (inTextBlock) {\n controller.enqueue({ type: \"text-end\", id: currentTextId });\n inTextBlock = false;\n }\n }\n\n /** Ensure a text block is open, starting a new one if needed. */\n function ensureTextBlock(\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n if (!inTextBlock) {\n textIdCounter++;\n currentTextId = String(textIdCounter);\n controller.enqueue({ type: \"text-start\", id: currentTextId });\n inTextBlock = true;\n }\n }\n\n /** Emit a text-delta, opening a text block first if necessary. */\n function emitTextDelta(\n delta: string,\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n ensureTextBlock(controller);\n controller.enqueue({ type: \"text-delta\", id: currentTextId, delta });\n }\n\n function emitPatch(\n patch: SpecStreamLine,\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n closeTextBlock(controller);\n controller.enqueue({\n type: SPEC_DATA_PART_TYPE,\n data: { type: \"patch\", patch },\n });\n }\n\n function flushBuffer(\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n if (!lineBuffer) return;\n\n const trimmed = lineBuffer.trim();\n\n // Inside a fence, everything is spec data\n if (inSpecFence) {\n if (trimmed) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) emitPatch(patch, controller);\n // Non-patch lines inside the fence are silently dropped\n }\n lineBuffer = \"\";\n buffering = false;\n return;\n }\n\n if (trimmed) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n emitPatch(patch, controller);\n } else {\n // Was buffered but isn't JSONL — flush as text\n emitTextDelta(lineBuffer, controller);\n }\n } else {\n // Whitespace-only buffer — forward as-is (preserves blank lines)\n emitTextDelta(lineBuffer, controller);\n }\n lineBuffer = \"\";\n buffering = false;\n }\n\n function processCompleteLine(\n line: string,\n controller: TransformStreamDefaultController<StreamChunk>,\n ) {\n const trimmed = line.trim();\n\n // --- Fence detection ---\n if (!inSpecFence && trimmed.startsWith(SPEC_FENCE_OPEN)) {\n inSpecFence = true;\n return; // Swallow the opening fence\n }\n if (inSpecFence && trimmed === SPEC_FENCE_CLOSE) {\n inSpecFence = false;\n return; // Swallow the closing fence\n }\n\n // Inside a fence: parse as spec data\n if (inSpecFence) {\n if (trimmed) {\n const patch = parseSpecStreamLine(trimmed);\n if (patch) emitPatch(patch, controller);\n }\n return;\n }\n\n // --- Outside fence: heuristic mode ---\n if (!trimmed) {\n // Empty line — forward for markdown paragraph breaks\n emitTextDelta(\"\\n\", controller);\n return;\n }\n\n const patch = parseSpecStreamLine(trimmed);\n if (patch) {\n emitPatch(patch, controller);\n } else {\n emitTextDelta(line + \"\\n\", controller);\n }\n }\n\n return new TransformStream<StreamChunk, StreamChunk>({\n transform(chunk, controller) {\n switch (chunk.type) {\n case \"text-start\": {\n const id = (chunk as { id: string }).id;\n const idNum = parseInt(id, 10);\n if (!isNaN(idNum) && idNum >= textIdCounter) {\n textIdCounter = idNum;\n }\n currentTextId = id;\n inTextBlock = true;\n controller.enqueue(chunk);\n break;\n }\n\n case \"text-delta\": {\n const delta = chunk as { id: string; delta: string };\n const text = delta.delta;\n\n for (let i = 0; i < text.length; i++) {\n const ch = text.charAt(i);\n\n if (ch === \"\\n\") {\n // Line complete — classify and emit\n if (buffering) {\n processCompleteLine(lineBuffer, controller);\n lineBuffer = \"\";\n buffering = false;\n } else {\n // Outside fence, emit newline; inside fence, swallow it\n if (!inSpecFence) {\n emitTextDelta(\"\\n\", controller);\n }\n }\n } else if (lineBuffer.length === 0 && !buffering) {\n // Start of a new line — decide whether to buffer or stream\n if (inSpecFence || ch === \"{\" || ch === \"`\") {\n // Buffer: inside fence (everything), or heuristic mode ({), or potential fence (`)\n buffering = true;\n lineBuffer += ch;\n } else {\n emitTextDelta(ch, controller);\n }\n } else if (buffering) {\n lineBuffer += ch;\n } else {\n emitTextDelta(ch, controller);\n }\n }\n break;\n }\n\n case \"text-end\": {\n flushBuffer(controller);\n if (inTextBlock) {\n controller.enqueue({ type: \"text-end\", id: currentTextId });\n inTextBlock = false;\n }\n break;\n }\n\n default: {\n controller.enqueue(chunk);\n break;\n }\n }\n },\n\n flush(controller) {\n flushBuffer(controller);\n closeTextBlock(controller);\n },\n });\n}\n\n/**\n * The key registered in `AppDataParts` for json-render specs.\n * The AI SDK automatically prefixes this with `\"data-\"` on the wire,\n * so the actual stream chunk type is `\"data-spec\"` (see {@link SPEC_DATA_PART_TYPE}).\n *\n * @example\n * ```ts\n * import { SPEC_DATA_PART, type SpecDataPart } from \"@json-render/core\";\n * type AppDataParts = { [SPEC_DATA_PART]: SpecDataPart };\n * ```\n */\nexport const SPEC_DATA_PART = \"spec\" as const;\n\n/**\n * The wire-format type string as it appears in stream chunks and message parts.\n * This is `\"data-\"` + {@link SPEC_DATA_PART} — i.e. `\"data-spec\"`.\n *\n * Use this constant when filtering message parts or enqueuing stream chunks.\n */\nexport const SPEC_DATA_PART_TYPE = `data-${SPEC_DATA_PART}` as const;\n\n/**\n * Discriminated union for the payload of a {@link SPEC_DATA_PART_TYPE} SSE part.\n *\n * - `\"patch\"`: A single RFC 6902 JSON Patch operation (streaming, progressive UI).\n * - `\"flat\"`: A complete flat spec with `root`, `elements`, and optional `state`.\n * - `\"nested\"`: A complete nested spec (tree structure — schema depends on catalog).\n */\nexport type SpecDataPart =\n | { type: \"patch\"; patch: JsonPatch }\n | { type: \"flat\"; spec: Spec }\n | { type: \"nested\"; spec: Record<string, unknown> };\n\n/**\n * Convenience wrapper that pipes an AI SDK UI message stream through the\n * json-render transform, classifying text as prose or JSONL patches.\n *\n * Eliminates the need for manual `pipeThrough(createJsonRenderTransform())`\n * and the associated type cast.\n *\n * @example\n * ```ts\n * import { pipeJsonRender } from \"@json-render/core\";\n *\n * const stream = createUIMessageStream({\n * execute: async ({ writer }) => {\n * writer.merge(pipeJsonRender(result.toUIMessageStream()));\n * },\n * });\n * return createUIMessageStreamResponse({ stream });\n * ```\n */\nexport function pipeJsonRender<T = StreamChunk>(\n stream: ReadableStream<T>,\n): ReadableStream<T> {\n return stream.pipeThrough(\n createJsonRenderTransform() as unknown as TransformStream<T, T>,\n );\n}\n","import {\n getByPath,\n parseJsonPointer,\n type StateModel,\n type StateStore,\n} from \"./types\";\n\n/**\n * Immutably set a value at a JSON Pointer path using structural sharing.\n * Only objects along the path are shallow-cloned; untouched branches keep\n * their original references.\n */\nexport function immutableSetByPath(\n root: StateModel,\n path: string,\n value: unknown,\n): StateModel {\n const segments = parseJsonPointer(path);\n if (segments.length === 0) return root;\n\n const result = { ...root };\n let current: Record<string, unknown> = result;\n\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!;\n const child = current[seg];\n if (Array.isArray(child)) {\n current[seg] = [...child];\n } else if (child !== null && typeof child === \"object\") {\n current[seg] = { ...(child as Record<string, unknown>) };\n } else {\n const nextSeg = segments[i + 1];\n current[seg] = nextSeg !== undefined && /^\\d+$/.test(nextSeg) ? [] : {};\n }\n current = current[seg] as Record<string, unknown>;\n }\n\n const lastSeg = segments[segments.length - 1]!;\n if (Array.isArray(current)) {\n if (lastSeg === \"-\") {\n (current as unknown[]).push(value);\n } else {\n (current as unknown[])[parseInt(lastSeg, 10)] = value;\n }\n } else {\n current[lastSeg] = value;\n }\n\n return result;\n}\n\n/**\n * Create a simple in-memory {@link StateStore}.\n *\n * This is the default store used by `StateProvider` when no external store is\n * provided. It mirrors the previous `useState`-based behaviour but is\n * framework-agnostic so it can also be used in tests or non-React contexts.\n */\nexport function createStateStore(initialState: StateModel = {}): StateStore {\n let state: StateModel = { ...initialState };\n const listeners = new Set<() => void>();\n\n function notify() {\n for (const listener of listeners) {\n listener();\n }\n }\n\n return {\n get(path: string): unknown {\n return getByPath(state, path);\n },\n\n set(path: string, value: unknown): void {\n if (getByPath(state, path) === value) return;\n state = immutableSetByPath(state, path, value);\n notify();\n },\n\n update(updates: Record<string, unknown>): void {\n let changed = false;\n let next = state;\n for (const [path, value] of Object.entries(updates)) {\n if (getByPath(next, path) !== value) {\n next = immutableSetByPath(next, path, value);\n changed = true;\n }\n }\n if (!changed) return;\n state = next;\n notify();\n },\n\n getSnapshot(): StateModel {\n return state;\n },\n\n getServerSnapshot(): StateModel {\n return state;\n },\n\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/**\n * Configuration for {@link createStoreAdapter}. Adapter authors supply these\n * three callbacks; everything else (get, set, update, no-op detection,\n * getServerSnapshot) is handled by the returned {@link StateStore}.\n */\nexport interface StoreAdapterConfig {\n /** Return the current state snapshot from the underlying store. */\n getSnapshot: () => StateModel;\n /** Write a new state snapshot to the underlying store. */\n setSnapshot: (next: StateModel) => void;\n /** Subscribe to changes in the underlying store. Return an unsubscribe fn. */\n subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * Build a full {@link StateStore} from a minimal adapter config.\n *\n * Handles `get`, `set` (with no-op detection), `update` (batched, with no-op\n * detection), `getSnapshot`, `getServerSnapshot`, and `subscribe` -- so each\n * adapter only needs to wire its snapshot source, write API, and subscribe\n * mechanism.\n */\nexport function createStoreAdapter(config: StoreAdapterConfig): StateStore {\n return {\n get(path: string): unknown {\n return getByPath(config.getSnapshot(), path);\n },\n\n set(path: string, value: unknown): void {\n const current = config.getSnapshot();\n if (getByPath(current, path) === value) return;\n config.setSnapshot(immutableSetByPath(current, path, value));\n },\n\n update(updates: Record<string, unknown>): void {\n let next = config.getSnapshot();\n let changed = false;\n for (const [path, value] of Object.entries(updates)) {\n if (getByPath(next, path) !== value) {\n next = immutableSetByPath(next, path, value);\n changed = true;\n }\n }\n if (!changed) return;\n config.setSnapshot(next);\n },\n\n getSnapshot: config.getSnapshot,\n\n getServerSnapshot: config.getSnapshot,\n\n subscribe: config.subscribe,\n };\n}\n\nconst MAX_FLATTEN_DEPTH = 20;\n\n/**\n * Recursively flatten a plain object into a `Record<string, unknown>` keyed by\n * JSON Pointer paths. Only leaf values (non-plain-object) appear in the output.\n *\n * Includes circular reference protection and a depth cap to prevent stack\n * overflow on pathological inputs.\n *\n * ```ts\n * flattenToPointers({ user: { name: \"Alice\" }, count: 1 })\n * // => { \"/user/name\": \"Alice\", \"/count\": 1 }\n * ```\n */\nexport function flattenToPointers(\n obj: Record<string, unknown>,\n prefix = \"\",\n _depth = 0,\n _seen?: Set<object>,\n _warned?: { current: boolean },\n): Record<string, unknown> {\n const seen = _seen ?? new Set<object>();\n const warned = _warned ?? { current: false };\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const pointer = `${prefix}/${key}`;\n if (\n _depth < MAX_FLATTEN_DEPTH &&\n value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) === Object.prototype &&\n !seen.has(value)\n ) {\n seen.add(value);\n Object.assign(\n result,\n flattenToPointers(\n value as Record<string, unknown>,\n pointer,\n _depth + 1,\n seen,\n warned,\n ),\n );\n } else {\n if (\n process.env.NODE_ENV !== \"production\" &&\n !warned.current &&\n _depth >= MAX_FLATTEN_DEPTH &&\n value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.getPrototypeOf(value) === Object.prototype &&\n !seen.has(value as object)\n ) {\n warned.current = true;\n console.warn(\n `flattenToPointers: depth limit (${MAX_FLATTEN_DEPTH}) reached. Nested state beyond this depth will be treated as a leaf value.`,\n );\n }\n result[pointer] = value;\n }\n }\n return result;\n}\n"],"mappings":"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","names":["patch"]} |
| import { z } from 'zod'; | ||
| /** | ||
| * Confirmation dialog configuration | ||
| */ | ||
| interface ActionConfirm { | ||
| title: string; | ||
| message: string; | ||
| confirmLabel?: string; | ||
| cancelLabel?: string; | ||
| variant?: "default" | "danger"; | ||
| } | ||
| /** | ||
| * Action success handler | ||
| */ | ||
| type ActionOnSuccess = { | ||
| navigate: string; | ||
| } | { | ||
| set: Record<string, unknown>; | ||
| } | { | ||
| action: string; | ||
| }; | ||
| /** | ||
| * Action error handler | ||
| */ | ||
| type ActionOnError = { | ||
| set: Record<string, unknown>; | ||
| } | { | ||
| action: string; | ||
| }; | ||
| /** | ||
| * Action binding — maps an event to an action invocation. | ||
| * | ||
| * Used inside the `on` field of a UIElement: | ||
| * ```json | ||
| * { "on": { "press": { "action": "setState", "params": { "statePath": "/x", "value": 1 } } } } | ||
| * ``` | ||
| */ | ||
| interface ActionBinding { | ||
| /** Action name (must be in catalog) */ | ||
| action: string; | ||
| /** Parameters to pass to the action handler */ | ||
| params?: Record<string, DynamicValue>; | ||
| /** Confirmation dialog before execution */ | ||
| confirm?: ActionConfirm; | ||
| /** Handler after successful execution */ | ||
| onSuccess?: ActionOnSuccess; | ||
| /** Handler after failed execution */ | ||
| onError?: ActionOnError; | ||
| /** Whether to prevent default browser behavior (e.g. navigation on links) */ | ||
| preventDefault?: boolean; | ||
| } | ||
| /** | ||
| * @deprecated Use ActionBinding instead | ||
| */ | ||
| type Action = ActionBinding; | ||
| /** | ||
| * Schema for action confirmation | ||
| */ | ||
| declare const ActionConfirmSchema: z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * Schema for success handlers | ||
| */ | ||
| declare const ActionOnSuccessSchema: z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Schema for error handlers | ||
| */ | ||
| declare const ActionOnErrorSchema: z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Full action binding schema | ||
| */ | ||
| declare const ActionBindingSchema: z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| confirm: z.ZodOptional<z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>>; | ||
| onSuccess: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>>; | ||
| onError: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>>; | ||
| preventDefault: z.ZodOptional<z.ZodBoolean>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * @deprecated Use ActionBindingSchema instead | ||
| */ | ||
| declare const ActionSchema: z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| confirm: z.ZodOptional<z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>>; | ||
| onSuccess: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>>; | ||
| onError: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>>; | ||
| preventDefault: z.ZodOptional<z.ZodBoolean>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * Action handler function signature | ||
| */ | ||
| type ActionHandler<TParams = Record<string, unknown>, TResult = unknown> = (params: TParams) => Promise<TResult> | TResult; | ||
| /** | ||
| * Action definition in catalog | ||
| */ | ||
| interface ActionDefinition<TParams = Record<string, unknown>> { | ||
| /** Zod schema for params validation */ | ||
| params?: z.ZodType<TParams>; | ||
| /** Description for AI */ | ||
| description?: string; | ||
| } | ||
| /** | ||
| * Resolved action with all dynamic values resolved | ||
| */ | ||
| interface ResolvedAction { | ||
| action: string; | ||
| params: Record<string, unknown>; | ||
| confirm?: ActionConfirm; | ||
| onSuccess?: ActionOnSuccess; | ||
| onError?: ActionOnError; | ||
| } | ||
| /** | ||
| * Resolve all dynamic values in an action binding | ||
| */ | ||
| declare function resolveAction(binding: ActionBinding, stateModel: StateModel): ResolvedAction; | ||
| /** | ||
| * Interpolate ${path} expressions in a string | ||
| */ | ||
| declare function interpolateString(template: string, stateModel: StateModel): string; | ||
| /** | ||
| * Context for action execution | ||
| */ | ||
| interface ActionExecutionContext { | ||
| /** The resolved action */ | ||
| action: ResolvedAction; | ||
| /** The action handler from the host */ | ||
| handler: ActionHandler; | ||
| /** Function to update state model */ | ||
| setState: (path: string, value: unknown) => void; | ||
| /** Function to navigate */ | ||
| navigate?: (path: string) => void; | ||
| /** Function to execute another action */ | ||
| executeAction?: (name: string) => Promise<void>; | ||
| } | ||
| /** | ||
| * Execute an action with all callbacks | ||
| */ | ||
| declare function executeAction(ctx: ActionExecutionContext): Promise<void>; | ||
| /** | ||
| * Helper to create action bindings | ||
| */ | ||
| declare const actionBinding: { | ||
| /** Create a simple action binding */ | ||
| simple: (actionName: string, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with confirmation */ | ||
| withConfirm: (actionName: string, confirm: ActionConfirm, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with success handler */ | ||
| withSuccess: (actionName: string, onSuccess: ActionOnSuccess, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| }; | ||
| /** | ||
| * @deprecated Use actionBinding instead | ||
| */ | ||
| declare const action: { | ||
| /** Create a simple action binding */ | ||
| simple: (actionName: string, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with confirmation */ | ||
| withConfirm: (actionName: string, confirm: ActionConfirm, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with success handler */ | ||
| withSuccess: (actionName: string, onSuccess: ActionOnSuccess, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| }; | ||
| /** | ||
| * Dynamic value - can be a literal or a `{ $state }` reference to the state model. | ||
| * | ||
| * Used in action params and validation args where values can either be | ||
| * hardcoded or resolved from state at runtime. | ||
| */ | ||
| type DynamicValue<T = unknown> = T | { | ||
| $state: string; | ||
| }; | ||
| /** | ||
| * Dynamic string value | ||
| */ | ||
| type DynamicString = DynamicValue<string>; | ||
| /** | ||
| * Dynamic number value | ||
| */ | ||
| type DynamicNumber = DynamicValue<number>; | ||
| /** | ||
| * Dynamic boolean value | ||
| */ | ||
| type DynamicBoolean = DynamicValue<boolean>; | ||
| /** | ||
| * Zod schema for dynamic values | ||
| */ | ||
| declare const DynamicValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicStringSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicNumberSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicBooleanSchema: z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Base UI element structure for v2 | ||
| */ | ||
| interface UIElement<T extends string = string, P = Record<string, unknown>> { | ||
| /** Component type from the catalog */ | ||
| type: T; | ||
| /** Component props */ | ||
| props: P; | ||
| /** Child element keys (flat structure) */ | ||
| children?: string[]; | ||
| /** Visibility condition */ | ||
| visible?: VisibilityCondition; | ||
| /** Event bindings — maps event names to action bindings */ | ||
| on?: Record<string, ActionBinding | ActionBinding[]>; | ||
| /** Repeat children once per item in a state array */ | ||
| repeat?: { | ||
| statePath: string; | ||
| key?: string; | ||
| }; | ||
| /** | ||
| * State watchers — maps JSON Pointer state paths to action bindings. | ||
| * When the value at a watched path changes, the bound actions fire. | ||
| * Useful for cascading dependencies (e.g. country → city option loading). | ||
| */ | ||
| watch?: Record<string, ActionBinding | ActionBinding[]>; | ||
| } | ||
| /** | ||
| * Element with key and parentKey for use with flatToTree. | ||
| * When elements are in an array (not a keyed map), key and parentKey | ||
| * are needed to establish identity and parent-child relationships. | ||
| */ | ||
| interface FlatElement<T extends string = string, P = Record<string, unknown>> extends UIElement<T, P> { | ||
| /** Unique key identifying this element */ | ||
| key: string; | ||
| /** Parent element key (null for root) */ | ||
| parentKey?: string | null; | ||
| } | ||
| /** | ||
| * Shared comparison operators for visibility conditions. | ||
| * | ||
| * Use at most ONE comparison operator per condition. If multiple are | ||
| * provided, only the first matching one is evaluated (precedence: | ||
| * eq > neq > gt > gte > lt > lte). With no operator, truthiness is checked. | ||
| * | ||
| * `not` inverts the final result of whichever operator (or truthiness | ||
| * check) is used. | ||
| */ | ||
| type ComparisonOperators = { | ||
| eq?: unknown; | ||
| neq?: unknown; | ||
| gt?: number | { | ||
| $state: string; | ||
| }; | ||
| gte?: number | { | ||
| $state: string; | ||
| }; | ||
| lt?: number | { | ||
| $state: string; | ||
| }; | ||
| lte?: number | { | ||
| $state: string; | ||
| }; | ||
| not?: true; | ||
| }; | ||
| /** | ||
| * A single state-based condition. | ||
| * Resolves `$state` to a value from the state model, then applies the operator. | ||
| * Without an operator, checks truthiness. | ||
| * | ||
| * When `not` is `true`, the result of the entire condition is inverted. | ||
| * For example `{ $state: "/count", gt: 5, not: true }` means "NOT greater than 5". | ||
| */ | ||
| type StateCondition = { | ||
| $state: string; | ||
| } & ComparisonOperators; | ||
| /** | ||
| * A condition that resolves `$item` to a field on the current repeat item. | ||
| * Only meaningful inside a `repeat` scope. | ||
| * | ||
| * Use `""` to reference the whole item, or `"field"` for a specific field. | ||
| */ | ||
| type ItemCondition = { | ||
| $item: string; | ||
| } & ComparisonOperators; | ||
| /** | ||
| * A condition that resolves `$index` to the current repeat array index. | ||
| * Only meaningful inside a `repeat` scope. | ||
| */ | ||
| type IndexCondition = { | ||
| $index: true; | ||
| } & ComparisonOperators; | ||
| /** A single visibility condition (state, item, or index). */ | ||
| type SingleCondition = StateCondition | ItemCondition | IndexCondition; | ||
| /** | ||
| * AND wrapper — all child conditions must be true. | ||
| * This is the explicit form of the implicit array AND (`SingleCondition[]`). | ||
| * Unlike the implicit form, `$and` supports nested `$or` and `$and` conditions. | ||
| */ | ||
| type AndCondition = { | ||
| $and: VisibilityCondition[]; | ||
| }; | ||
| /** | ||
| * OR wrapper — at least one child condition must be true. | ||
| */ | ||
| type OrCondition = { | ||
| $or: VisibilityCondition[]; | ||
| }; | ||
| /** | ||
| * Visibility condition types. | ||
| * - `boolean` — always/never | ||
| * - `SingleCondition` — single condition (`$state`, `$item`, or `$index`) | ||
| * - `SingleCondition[]` — implicit AND (all must be true) | ||
| * - `AndCondition` — `{ $and: [...] }`, explicit AND (all must be true) | ||
| * - `OrCondition` — `{ $or: [...] }`, at least one must be true | ||
| */ | ||
| type VisibilityCondition = boolean | SingleCondition | SingleCondition[] | AndCondition | OrCondition; | ||
| /** | ||
| * Flat UI tree structure (optimized for LLM generation) | ||
| */ | ||
| interface Spec { | ||
| /** Root element key */ | ||
| root: string; | ||
| /** Flat map of elements by key */ | ||
| elements: Record<string, UIElement>; | ||
| /** Optional initial state to seed the state model. | ||
| * Components using statePath will read from / write to this state. */ | ||
| state?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * State model type | ||
| */ | ||
| type StateModel = Record<string, unknown>; | ||
| /** | ||
| * An abstract store that owns state and notifies subscribers on change. | ||
| * | ||
| * Consumers can supply their own implementation (backed by Redux, Zustand, | ||
| * XState, etc.) or use the built-in {@link createStateStore} for a simple | ||
| * in-memory store. | ||
| */ | ||
| interface StateStore { | ||
| /** Read a value by JSON Pointer path. */ | ||
| get: (path: string) => unknown; | ||
| /** | ||
| * Write a value by JSON Pointer path and notify subscribers. | ||
| * Equality is checked by reference (`===`), not deep comparison. | ||
| * Callers must pass a new object/array reference for changes to be detected. | ||
| */ | ||
| set: (path: string, value: unknown) => void; | ||
| /** | ||
| * Write multiple values at once and notify subscribers (single notification). | ||
| * Each value is compared by reference (`===`); only paths whose value | ||
| * actually changed are applied. | ||
| */ | ||
| update: (updates: Record<string, unknown>) => void; | ||
| /** Return the full state object (used by `useSyncExternalStore`). */ | ||
| getSnapshot: () => StateModel; | ||
| /** Optional server snapshot for SSR (passed to `useSyncExternalStore`). Falls back to `getSnapshot` when omitted. */ | ||
| getServerSnapshot?: () => StateModel; | ||
| /** Register a listener that is called on every state change. Returns an unsubscribe function. */ | ||
| subscribe: (listener: () => void) => () => void; | ||
| } | ||
| /** | ||
| * Component schema definition using Zod | ||
| */ | ||
| type ComponentSchema = z.ZodType<Record<string, unknown>>; | ||
| /** | ||
| * Validation mode for catalog validation | ||
| */ | ||
| type ValidationMode = "strict" | "warn" | "ignore"; | ||
| /** | ||
| * JSON patch operation types (RFC 6902) | ||
| */ | ||
| type PatchOp = "add" | "remove" | "replace" | "move" | "copy" | "test"; | ||
| /** | ||
| * JSON patch operation (RFC 6902) | ||
| */ | ||
| interface JsonPatch { | ||
| op: PatchOp; | ||
| path: string; | ||
| /** Required for add, replace, test */ | ||
| value?: unknown; | ||
| /** Required for move, copy (source location) */ | ||
| from?: string; | ||
| } | ||
| /** | ||
| * Resolve a dynamic value against a state model | ||
| */ | ||
| declare function resolveDynamicValue<T>(value: DynamicValue<T>, stateModel: StateModel): T | undefined; | ||
| /** | ||
| * Get a value from an object by JSON Pointer path (RFC 6901) | ||
| */ | ||
| declare function getByPath(obj: unknown, path: string): unknown; | ||
| /** | ||
| * Set a value in an object by JSON Pointer path (RFC 6901). | ||
| * Automatically creates arrays when the path segment is a numeric index. | ||
| */ | ||
| declare function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Add a value per RFC 6902 "add" semantics. | ||
| * For objects: create-or-replace the member. | ||
| * For arrays: insert before the given index, or append if "-". | ||
| */ | ||
| declare function addByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Remove a value per RFC 6902 "remove" semantics. | ||
| * For objects: delete the property. | ||
| * For arrays: splice out the element at the given index. | ||
| */ | ||
| declare function removeByPath(obj: Record<string, unknown>, path: string): void; | ||
| /** | ||
| * Find a form value from params and/or state. | ||
| * Useful in action handlers to locate form input values regardless of path format. | ||
| * | ||
| * Checks in order: | ||
| * 1. Direct param key (if not a path reference) | ||
| * 2. Param keys ending with the field name | ||
| * 3. State keys ending with the field name (dot notation) | ||
| * 4. State path using getByPath (slash notation) | ||
| * | ||
| * @example | ||
| * // Find "name" from params or state | ||
| * const name = findFormValue("name", params, state); | ||
| * | ||
| * // Will find from: params.name, params["form.name"], state["form.name"], or getByPath(state, "name") | ||
| */ | ||
| declare function findFormValue(fieldName: string, params?: Record<string, unknown>, state?: Record<string, unknown>): unknown; | ||
| /** | ||
| * A SpecStream line - a single patch operation in the stream. | ||
| */ | ||
| type SpecStreamLine = JsonPatch; | ||
| /** | ||
| * Parse a single SpecStream line into a patch operation. | ||
| * Returns null if the line is invalid or empty. | ||
| * | ||
| * SpecStream is json-render's streaming format where each line is a JSON patch | ||
| * operation that progressively builds up the final spec. | ||
| */ | ||
| declare function parseSpecStreamLine(line: string): SpecStreamLine | null; | ||
| /** | ||
| * Apply a single RFC 6902 JSON Patch operation to an object. | ||
| * Mutates the object in place. | ||
| * | ||
| * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test. | ||
| * | ||
| * @throws {Error} If a "test" operation fails (value mismatch). | ||
| */ | ||
| declare function applySpecStreamPatch<T extends Record<string, unknown>>(obj: T, patch: SpecStreamLine): T; | ||
| /** | ||
| * Apply a single RFC 6902 JSON Patch operation to a Spec. | ||
| * Mutates the spec in place and returns it. | ||
| * | ||
| * This is a typed convenience wrapper around `applySpecStreamPatch` that | ||
| * accepts a `Spec` directly without requiring a cast to `Record<string, unknown>`. | ||
| * | ||
| * Note: This mutates the spec. For React state updates, spread the result | ||
| * to create a new reference: `setSpec({ ...applySpecPatch(spec, patch) })`. | ||
| * | ||
| * @example | ||
| * let spec: Spec = { root: "", elements: {} }; | ||
| * applySpecPatch(spec, { op: "add", path: "/root", value: "main" }); | ||
| */ | ||
| declare function applySpecPatch(spec: Spec, patch: SpecStreamLine): Spec; | ||
| /** | ||
| * Convert a nested (tree-structured) spec into the flat `Spec` format used | ||
| * by json-render renderers. | ||
| * | ||
| * In the nested format each node has inline `children` as an array of child | ||
| * objects. This function walks the tree, assigns auto-generated keys | ||
| * (`el-0`, `el-1`, ...), and produces a flat `{ root, elements, state }` spec. | ||
| * | ||
| * The top-level `state` field (if present on the root node) is hoisted to | ||
| * `spec.state`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const nested = { | ||
| * type: "Card", | ||
| * props: { title: "Hello" }, | ||
| * children: [ | ||
| * { type: "Text", props: { content: "World" } }, | ||
| * ], | ||
| * state: { count: 0 }, | ||
| * }; | ||
| * const spec = nestedToFlat(nested); | ||
| * // { | ||
| * // root: "el-0", | ||
| * // elements: { | ||
| * // "el-0": { type: "Card", props: { title: "Hello" }, children: ["el-1"] }, | ||
| * // "el-1": { type: "Text", props: { content: "World" }, children: [] }, | ||
| * // }, | ||
| * // state: { count: 0 }, | ||
| * // } | ||
| * ``` | ||
| */ | ||
| declare function nestedToFlat(nested: Record<string, unknown>): Spec; | ||
| /** | ||
| * Compile a SpecStream string into a JSON object. | ||
| * Each line should be a patch operation. | ||
| * | ||
| * @example | ||
| * const stream = `{"op":"add","path":"/name","value":"Alice"} | ||
| * {"op":"add","path":"/age","value":30}`; | ||
| * const result = compileSpecStream(stream); | ||
| * // { name: "Alice", age: 30 } | ||
| */ | ||
| declare function compileSpecStream<T extends Record<string, unknown> = Record<string, unknown>>(stream: string, initial?: T): T; | ||
| /** | ||
| * Streaming SpecStream compiler. | ||
| * Useful for processing SpecStream data as it streams in from AI. | ||
| * | ||
| * @example | ||
| * const compiler = createSpecStreamCompiler<MySpec>(); | ||
| * | ||
| * // As chunks arrive: | ||
| * const { result, newPatches } = compiler.push(chunk); | ||
| * if (newPatches.length > 0) { | ||
| * updateUI(result); | ||
| * } | ||
| * | ||
| * // When done: | ||
| * const finalResult = compiler.getResult(); | ||
| */ | ||
| interface SpecStreamCompiler<T> { | ||
| /** Push a chunk of text. Returns the current result and any new patches applied. */ | ||
| push(chunk: string): { | ||
| result: T; | ||
| newPatches: SpecStreamLine[]; | ||
| }; | ||
| /** Get the current compiled result */ | ||
| getResult(): T; | ||
| /** Get all patches that have been applied */ | ||
| getPatches(): SpecStreamLine[]; | ||
| /** Reset the compiler to initial state */ | ||
| reset(initial?: Partial<T>): void; | ||
| } | ||
| /** | ||
| * Create a streaming SpecStream compiler. | ||
| * | ||
| * SpecStream is json-render's streaming format. AI outputs patch operations | ||
| * line by line, and this compiler progressively builds the final spec. | ||
| * | ||
| * @example | ||
| * const compiler = createSpecStreamCompiler<TimelineSpec>(); | ||
| * | ||
| * // Process streaming response | ||
| * const reader = response.body.getReader(); | ||
| * while (true) { | ||
| * const { done, value } = await reader.read(); | ||
| * if (done) break; | ||
| * | ||
| * const { result, newPatches } = compiler.push(decoder.decode(value)); | ||
| * if (newPatches.length > 0) { | ||
| * setSpec(result); // Update UI with partial result | ||
| * } | ||
| * } | ||
| */ | ||
| declare function createSpecStreamCompiler<T = Record<string, unknown>>(initial?: Partial<T>): SpecStreamCompiler<T>; | ||
| /** | ||
| * Callbacks for the mixed stream parser. | ||
| */ | ||
| interface MixedStreamCallbacks { | ||
| /** Called when a JSONL patch line is parsed */ | ||
| onPatch: (patch: SpecStreamLine) => void; | ||
| /** Called when a text (non-JSONL) line is received */ | ||
| onText: (text: string) => void; | ||
| } | ||
| /** | ||
| * A stateful parser for mixed streams that contain both text and JSONL patches. | ||
| * Used in chat + GenUI scenarios where an LLM responds with conversational text | ||
| * interleaved with json-render JSONL patch operations. | ||
| */ | ||
| interface MixedStreamParser { | ||
| /** Push a chunk of streamed data. Calls onPatch/onText for each complete line. */ | ||
| push(chunk: string): void; | ||
| /** Flush any remaining buffered content. Call when the stream ends. */ | ||
| flush(): void; | ||
| } | ||
| /** | ||
| * Create a parser for mixed text + JSONL streams. | ||
| * | ||
| * In chat + GenUI scenarios, an LLM streams a response that contains both | ||
| * conversational text and json-render JSONL patch lines. This parser buffers | ||
| * incoming chunks, splits them into lines, and classifies each line as either | ||
| * a JSONL patch (via `parseSpecStreamLine`) or plain text. | ||
| * | ||
| * @example | ||
| * const parser = createMixedStreamParser({ | ||
| * onText: (text) => appendToMessage(text), | ||
| * onPatch: (patch) => applySpecPatch(spec, patch), | ||
| * }); | ||
| * | ||
| * // As chunks arrive from the stream: | ||
| * for await (const chunk of stream) { | ||
| * parser.push(chunk); | ||
| * } | ||
| * parser.flush(); | ||
| */ | ||
| declare function createMixedStreamParser(callbacks: MixedStreamCallbacks): MixedStreamParser; | ||
| /** | ||
| * Minimal chunk shape compatible with the AI SDK's `UIMessageChunk`. | ||
| * | ||
| * Defined here so that `@json-render/core` has no dependency on the `ai` | ||
| * package. The discriminated union covers the three text-related chunk types | ||
| * the transform inspects; all other chunk types pass through via the fallback. | ||
| */ | ||
| type StreamChunk = { | ||
| type: "text-start"; | ||
| id: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: "text-delta"; | ||
| id: string; | ||
| delta: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: "text-end"; | ||
| id: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: string; | ||
| [k: string]: unknown; | ||
| }; | ||
| /** | ||
| * Creates a `TransformStream` that intercepts AI SDK UI message stream chunks | ||
| * and classifies text content as either prose or json-render JSONL patches. | ||
| * | ||
| * Two classification modes: | ||
| * | ||
| * 1. **Fence mode** (preferred): Lines between ` ```spec ` and ` ``` ` are | ||
| * parsed as JSONL patches. Fence delimiters are swallowed (not emitted). | ||
| * 2. **Heuristic mode** (backward compat): Outside of fences, lines starting | ||
| * with `{` are buffered and tested with `parseSpecStreamLine`. Valid patches | ||
| * are emitted as {@link SPEC_DATA_PART_TYPE} parts; everything else is | ||
| * flushed as text. | ||
| * | ||
| * Non-text chunks (tool events, step markers, etc.) are passed through unchanged. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { createJsonRenderTransform } from "@json-render/core"; | ||
| * import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; | ||
| * | ||
| * const stream = createUIMessageStream({ | ||
| * execute: async ({ writer }) => { | ||
| * writer.merge( | ||
| * result.toUIMessageStream().pipeThrough(createJsonRenderTransform()), | ||
| * ); | ||
| * }, | ||
| * }); | ||
| * return createUIMessageStreamResponse({ stream }); | ||
| * ``` | ||
| */ | ||
| declare function createJsonRenderTransform(): TransformStream<StreamChunk, StreamChunk>; | ||
| /** | ||
| * The key registered in `AppDataParts` for json-render specs. | ||
| * The AI SDK automatically prefixes this with `"data-"` on the wire, | ||
| * so the actual stream chunk type is `"data-spec"` (see {@link SPEC_DATA_PART_TYPE}). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { SPEC_DATA_PART, type SpecDataPart } from "@json-render/core"; | ||
| * type AppDataParts = { [SPEC_DATA_PART]: SpecDataPart }; | ||
| * ``` | ||
| */ | ||
| declare const SPEC_DATA_PART: "spec"; | ||
| /** | ||
| * The wire-format type string as it appears in stream chunks and message parts. | ||
| * This is `"data-"` + {@link SPEC_DATA_PART} — i.e. `"data-spec"`. | ||
| * | ||
| * Use this constant when filtering message parts or enqueuing stream chunks. | ||
| */ | ||
| declare const SPEC_DATA_PART_TYPE: "data-spec"; | ||
| /** | ||
| * Discriminated union for the payload of a {@link SPEC_DATA_PART_TYPE} SSE part. | ||
| * | ||
| * - `"patch"`: A single RFC 6902 JSON Patch operation (streaming, progressive UI). | ||
| * - `"flat"`: A complete flat spec with `root`, `elements`, and optional `state`. | ||
| * - `"nested"`: A complete nested spec (tree structure — schema depends on catalog). | ||
| */ | ||
| type SpecDataPart = { | ||
| type: "patch"; | ||
| patch: JsonPatch; | ||
| } | { | ||
| type: "flat"; | ||
| spec: Spec; | ||
| } | { | ||
| type: "nested"; | ||
| spec: Record<string, unknown>; | ||
| }; | ||
| /** | ||
| * Convenience wrapper that pipes an AI SDK UI message stream through the | ||
| * json-render transform, classifying text as prose or JSONL patches. | ||
| * | ||
| * Eliminates the need for manual `pipeThrough(createJsonRenderTransform())` | ||
| * and the associated type cast. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { pipeJsonRender } from "@json-render/core"; | ||
| * | ||
| * const stream = createUIMessageStream({ | ||
| * execute: async ({ writer }) => { | ||
| * writer.merge(pipeJsonRender(result.toUIMessageStream())); | ||
| * }, | ||
| * }); | ||
| * return createUIMessageStreamResponse({ stream }); | ||
| * ``` | ||
| */ | ||
| declare function pipeJsonRender<T = StreamChunk>(stream: ReadableStream<T>): ReadableStream<T>; | ||
| /** | ||
| * Immutably set a value at a JSON Pointer path using structural sharing. | ||
| * Only objects along the path are shallow-cloned; untouched branches keep | ||
| * their original references. | ||
| */ | ||
| declare function immutableSetByPath(root: StateModel, path: string, value: unknown): StateModel; | ||
| /** | ||
| * Create a simple in-memory {@link StateStore}. | ||
| * | ||
| * This is the default store used by `StateProvider` when no external store is | ||
| * provided. It mirrors the previous `useState`-based behaviour but is | ||
| * framework-agnostic so it can also be used in tests or non-React contexts. | ||
| */ | ||
| declare function createStateStore(initialState?: StateModel): StateStore; | ||
| /** | ||
| * Configuration for {@link createStoreAdapter}. Adapter authors supply these | ||
| * three callbacks; everything else (get, set, update, no-op detection, | ||
| * getServerSnapshot) is handled by the returned {@link StateStore}. | ||
| */ | ||
| interface StoreAdapterConfig { | ||
| /** Return the current state snapshot from the underlying store. */ | ||
| getSnapshot: () => StateModel; | ||
| /** Write a new state snapshot to the underlying store. */ | ||
| setSnapshot: (next: StateModel) => void; | ||
| /** Subscribe to changes in the underlying store. Return an unsubscribe fn. */ | ||
| subscribe: (listener: () => void) => () => void; | ||
| } | ||
| /** | ||
| * Build a full {@link StateStore} from a minimal adapter config. | ||
| * | ||
| * Handles `get`, `set` (with no-op detection), `update` (batched, with no-op | ||
| * detection), `getSnapshot`, `getServerSnapshot`, and `subscribe` -- so each | ||
| * adapter only needs to wire its snapshot source, write API, and subscribe | ||
| * mechanism. | ||
| */ | ||
| declare function createStoreAdapter(config: StoreAdapterConfig): StateStore; | ||
| /** | ||
| * Recursively flatten a plain object into a `Record<string, unknown>` keyed by | ||
| * JSON Pointer paths. Only leaf values (non-plain-object) appear in the output. | ||
| * | ||
| * Includes circular reference protection and a depth cap to prevent stack | ||
| * overflow on pathological inputs. | ||
| * | ||
| * ```ts | ||
| * flattenToPointers({ user: { name: "Alice" }, count: 1 }) | ||
| * // => { "/user/name": "Alice", "/count": 1 } | ||
| * ``` | ||
| */ | ||
| declare function flattenToPointers(obj: Record<string, unknown>, prefix?: string, _depth?: number, _seen?: Set<object>, _warned?: { | ||
| current: boolean; | ||
| }): Record<string, unknown>; | ||
| export { type ActionOnError as $, type AndCondition as A, applySpecPatch as B, type ComponentSchema as C, type DynamicValue as D, nestedToFlat as E, type FlatElement as F, compileSpecStream as G, createSpecStreamCompiler as H, type ItemCondition as I, type JsonPatch as J, createMixedStreamParser as K, createJsonRenderTransform as L, type MixedStreamCallbacks as M, pipeJsonRender as N, type OrCondition as O, type PatchOp as P, SPEC_DATA_PART as Q, SPEC_DATA_PART_TYPE as R, type StateModel as S, type StoreAdapterConfig as T, type UIElement as U, type VisibilityCondition as V, createStateStore as W, type ActionBinding as X, type Action as Y, type ActionConfirm as Z, type ActionOnSuccess as _, type StateCondition as a, type ActionHandler as a0, type ActionDefinition as a1, type ResolvedAction as a2, type ActionExecutionContext as a3, ActionBindingSchema as a4, ActionSchema as a5, ActionConfirmSchema as a6, ActionOnSuccessSchema as a7, ActionOnErrorSchema as a8, resolveAction as a9, executeAction as aa, interpolateString as ab, actionBinding as ac, action as ad, immutableSetByPath as ae, flattenToPointers as af, createStoreAdapter as ag, type Spec as b, type DynamicString as c, type DynamicNumber as d, type DynamicBoolean as e, type IndexCondition as f, type SingleCondition as g, type StateStore as h, type ValidationMode as i, type SpecStreamLine as j, type SpecStreamCompiler as k, type MixedStreamParser as l, type StreamChunk as m, type SpecDataPart as n, DynamicValueSchema as o, DynamicStringSchema as p, DynamicNumberSchema as q, DynamicBooleanSchema as r, resolveDynamicValue as s, getByPath as t, setByPath as u, addByPath as v, removeByPath as w, findFormValue as x, parseSpecStreamLine as y, applySpecStreamPatch as z }; |
| import { z } from 'zod'; | ||
| /** | ||
| * Confirmation dialog configuration | ||
| */ | ||
| interface ActionConfirm { | ||
| title: string; | ||
| message: string; | ||
| confirmLabel?: string; | ||
| cancelLabel?: string; | ||
| variant?: "default" | "danger"; | ||
| } | ||
| /** | ||
| * Action success handler | ||
| */ | ||
| type ActionOnSuccess = { | ||
| navigate: string; | ||
| } | { | ||
| set: Record<string, unknown>; | ||
| } | { | ||
| action: string; | ||
| }; | ||
| /** | ||
| * Action error handler | ||
| */ | ||
| type ActionOnError = { | ||
| set: Record<string, unknown>; | ||
| } | { | ||
| action: string; | ||
| }; | ||
| /** | ||
| * Action binding — maps an event to an action invocation. | ||
| * | ||
| * Used inside the `on` field of a UIElement: | ||
| * ```json | ||
| * { "on": { "press": { "action": "setState", "params": { "statePath": "/x", "value": 1 } } } } | ||
| * ``` | ||
| */ | ||
| interface ActionBinding { | ||
| /** Action name (must be in catalog) */ | ||
| action: string; | ||
| /** Parameters to pass to the action handler */ | ||
| params?: Record<string, DynamicValue>; | ||
| /** Confirmation dialog before execution */ | ||
| confirm?: ActionConfirm; | ||
| /** Handler after successful execution */ | ||
| onSuccess?: ActionOnSuccess; | ||
| /** Handler after failed execution */ | ||
| onError?: ActionOnError; | ||
| /** Whether to prevent default browser behavior (e.g. navigation on links) */ | ||
| preventDefault?: boolean; | ||
| } | ||
| /** | ||
| * @deprecated Use ActionBinding instead | ||
| */ | ||
| type Action = ActionBinding; | ||
| /** | ||
| * Schema for action confirmation | ||
| */ | ||
| declare const ActionConfirmSchema: z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * Schema for success handlers | ||
| */ | ||
| declare const ActionOnSuccessSchema: z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Schema for error handlers | ||
| */ | ||
| declare const ActionOnErrorSchema: z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Full action binding schema | ||
| */ | ||
| declare const ActionBindingSchema: z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| confirm: z.ZodOptional<z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>>; | ||
| onSuccess: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>>; | ||
| onError: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>>; | ||
| preventDefault: z.ZodOptional<z.ZodBoolean>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * @deprecated Use ActionBindingSchema instead | ||
| */ | ||
| declare const ActionSchema: z.ZodObject<{ | ||
| action: z.ZodString; | ||
| params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>>>; | ||
| confirm: z.ZodOptional<z.ZodObject<{ | ||
| title: z.ZodString; | ||
| message: z.ZodString; | ||
| confirmLabel: z.ZodOptional<z.ZodString>; | ||
| cancelLabel: z.ZodOptional<z.ZodString>; | ||
| variant: z.ZodOptional<z.ZodEnum<{ | ||
| default: "default"; | ||
| danger: "danger"; | ||
| }>>; | ||
| }, z.core.$strip>>; | ||
| onSuccess: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| navigate: z.ZodString; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>>; | ||
| onError: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{ | ||
| set: z.ZodRecord<z.ZodString, z.ZodUnknown>; | ||
| }, z.core.$strip>, z.ZodObject<{ | ||
| action: z.ZodString; | ||
| }, z.core.$strip>]>>; | ||
| preventDefault: z.ZodOptional<z.ZodBoolean>; | ||
| }, z.core.$strip>; | ||
| /** | ||
| * Action handler function signature | ||
| */ | ||
| type ActionHandler<TParams = Record<string, unknown>, TResult = unknown> = (params: TParams) => Promise<TResult> | TResult; | ||
| /** | ||
| * Action definition in catalog | ||
| */ | ||
| interface ActionDefinition<TParams = Record<string, unknown>> { | ||
| /** Zod schema for params validation */ | ||
| params?: z.ZodType<TParams>; | ||
| /** Description for AI */ | ||
| description?: string; | ||
| } | ||
| /** | ||
| * Resolved action with all dynamic values resolved | ||
| */ | ||
| interface ResolvedAction { | ||
| action: string; | ||
| params: Record<string, unknown>; | ||
| confirm?: ActionConfirm; | ||
| onSuccess?: ActionOnSuccess; | ||
| onError?: ActionOnError; | ||
| } | ||
| /** | ||
| * Resolve all dynamic values in an action binding | ||
| */ | ||
| declare function resolveAction(binding: ActionBinding, stateModel: StateModel): ResolvedAction; | ||
| /** | ||
| * Interpolate ${path} expressions in a string | ||
| */ | ||
| declare function interpolateString(template: string, stateModel: StateModel): string; | ||
| /** | ||
| * Context for action execution | ||
| */ | ||
| interface ActionExecutionContext { | ||
| /** The resolved action */ | ||
| action: ResolvedAction; | ||
| /** The action handler from the host */ | ||
| handler: ActionHandler; | ||
| /** Function to update state model */ | ||
| setState: (path: string, value: unknown) => void; | ||
| /** Function to navigate */ | ||
| navigate?: (path: string) => void; | ||
| /** Function to execute another action */ | ||
| executeAction?: (name: string) => Promise<void>; | ||
| } | ||
| /** | ||
| * Execute an action with all callbacks | ||
| */ | ||
| declare function executeAction(ctx: ActionExecutionContext): Promise<void>; | ||
| /** | ||
| * Helper to create action bindings | ||
| */ | ||
| declare const actionBinding: { | ||
| /** Create a simple action binding */ | ||
| simple: (actionName: string, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with confirmation */ | ||
| withConfirm: (actionName: string, confirm: ActionConfirm, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with success handler */ | ||
| withSuccess: (actionName: string, onSuccess: ActionOnSuccess, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| }; | ||
| /** | ||
| * @deprecated Use actionBinding instead | ||
| */ | ||
| declare const action: { | ||
| /** Create a simple action binding */ | ||
| simple: (actionName: string, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with confirmation */ | ||
| withConfirm: (actionName: string, confirm: ActionConfirm, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| /** Create an action binding with success handler */ | ||
| withSuccess: (actionName: string, onSuccess: ActionOnSuccess, params?: Record<string, DynamicValue>) => ActionBinding; | ||
| }; | ||
| /** | ||
| * Dynamic value - can be a literal or a `{ $state }` reference to the state model. | ||
| * | ||
| * Used in action params and validation args where values can either be | ||
| * hardcoded or resolved from state at runtime. | ||
| */ | ||
| type DynamicValue<T = unknown> = T | { | ||
| $state: string; | ||
| }; | ||
| /** | ||
| * Dynamic string value | ||
| */ | ||
| type DynamicString = DynamicValue<string>; | ||
| /** | ||
| * Dynamic number value | ||
| */ | ||
| type DynamicNumber = DynamicValue<number>; | ||
| /** | ||
| * Dynamic boolean value | ||
| */ | ||
| type DynamicBoolean = DynamicValue<boolean>; | ||
| /** | ||
| * Zod schema for dynamic values | ||
| */ | ||
| declare const DynamicValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicStringSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicNumberSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| declare const DynamicBooleanSchema: z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{ | ||
| $state: z.ZodString; | ||
| }, z.core.$strip>]>; | ||
| /** | ||
| * Base UI element structure for v2 | ||
| */ | ||
| interface UIElement<T extends string = string, P = Record<string, unknown>> { | ||
| /** Component type from the catalog */ | ||
| type: T; | ||
| /** Component props */ | ||
| props: P; | ||
| /** Child element keys (flat structure) */ | ||
| children?: string[]; | ||
| /** Visibility condition */ | ||
| visible?: VisibilityCondition; | ||
| /** Event bindings — maps event names to action bindings */ | ||
| on?: Record<string, ActionBinding | ActionBinding[]>; | ||
| /** Repeat children once per item in a state array */ | ||
| repeat?: { | ||
| statePath: string; | ||
| key?: string; | ||
| }; | ||
| /** | ||
| * State watchers — maps JSON Pointer state paths to action bindings. | ||
| * When the value at a watched path changes, the bound actions fire. | ||
| * Useful for cascading dependencies (e.g. country → city option loading). | ||
| */ | ||
| watch?: Record<string, ActionBinding | ActionBinding[]>; | ||
| } | ||
| /** | ||
| * Element with key and parentKey for use with flatToTree. | ||
| * When elements are in an array (not a keyed map), key and parentKey | ||
| * are needed to establish identity and parent-child relationships. | ||
| */ | ||
| interface FlatElement<T extends string = string, P = Record<string, unknown>> extends UIElement<T, P> { | ||
| /** Unique key identifying this element */ | ||
| key: string; | ||
| /** Parent element key (null for root) */ | ||
| parentKey?: string | null; | ||
| } | ||
| /** | ||
| * Shared comparison operators for visibility conditions. | ||
| * | ||
| * Use at most ONE comparison operator per condition. If multiple are | ||
| * provided, only the first matching one is evaluated (precedence: | ||
| * eq > neq > gt > gte > lt > lte). With no operator, truthiness is checked. | ||
| * | ||
| * `not` inverts the final result of whichever operator (or truthiness | ||
| * check) is used. | ||
| */ | ||
| type ComparisonOperators = { | ||
| eq?: unknown; | ||
| neq?: unknown; | ||
| gt?: number | { | ||
| $state: string; | ||
| }; | ||
| gte?: number | { | ||
| $state: string; | ||
| }; | ||
| lt?: number | { | ||
| $state: string; | ||
| }; | ||
| lte?: number | { | ||
| $state: string; | ||
| }; | ||
| not?: true; | ||
| }; | ||
| /** | ||
| * A single state-based condition. | ||
| * Resolves `$state` to a value from the state model, then applies the operator. | ||
| * Without an operator, checks truthiness. | ||
| * | ||
| * When `not` is `true`, the result of the entire condition is inverted. | ||
| * For example `{ $state: "/count", gt: 5, not: true }` means "NOT greater than 5". | ||
| */ | ||
| type StateCondition = { | ||
| $state: string; | ||
| } & ComparisonOperators; | ||
| /** | ||
| * A condition that resolves `$item` to a field on the current repeat item. | ||
| * Only meaningful inside a `repeat` scope. | ||
| * | ||
| * Use `""` to reference the whole item, or `"field"` for a specific field. | ||
| */ | ||
| type ItemCondition = { | ||
| $item: string; | ||
| } & ComparisonOperators; | ||
| /** | ||
| * A condition that resolves `$index` to the current repeat array index. | ||
| * Only meaningful inside a `repeat` scope. | ||
| */ | ||
| type IndexCondition = { | ||
| $index: true; | ||
| } & ComparisonOperators; | ||
| /** A single visibility condition (state, item, or index). */ | ||
| type SingleCondition = StateCondition | ItemCondition | IndexCondition; | ||
| /** | ||
| * AND wrapper — all child conditions must be true. | ||
| * This is the explicit form of the implicit array AND (`SingleCondition[]`). | ||
| * Unlike the implicit form, `$and` supports nested `$or` and `$and` conditions. | ||
| */ | ||
| type AndCondition = { | ||
| $and: VisibilityCondition[]; | ||
| }; | ||
| /** | ||
| * OR wrapper — at least one child condition must be true. | ||
| */ | ||
| type OrCondition = { | ||
| $or: VisibilityCondition[]; | ||
| }; | ||
| /** | ||
| * Visibility condition types. | ||
| * - `boolean` — always/never | ||
| * - `SingleCondition` — single condition (`$state`, `$item`, or `$index`) | ||
| * - `SingleCondition[]` — implicit AND (all must be true) | ||
| * - `AndCondition` — `{ $and: [...] }`, explicit AND (all must be true) | ||
| * - `OrCondition` — `{ $or: [...] }`, at least one must be true | ||
| */ | ||
| type VisibilityCondition = boolean | SingleCondition | SingleCondition[] | AndCondition | OrCondition; | ||
| /** | ||
| * Flat UI tree structure (optimized for LLM generation) | ||
| */ | ||
| interface Spec { | ||
| /** Root element key */ | ||
| root: string; | ||
| /** Flat map of elements by key */ | ||
| elements: Record<string, UIElement>; | ||
| /** Optional initial state to seed the state model. | ||
| * Components using statePath will read from / write to this state. */ | ||
| state?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * State model type | ||
| */ | ||
| type StateModel = Record<string, unknown>; | ||
| /** | ||
| * An abstract store that owns state and notifies subscribers on change. | ||
| * | ||
| * Consumers can supply their own implementation (backed by Redux, Zustand, | ||
| * XState, etc.) or use the built-in {@link createStateStore} for a simple | ||
| * in-memory store. | ||
| */ | ||
| interface StateStore { | ||
| /** Read a value by JSON Pointer path. */ | ||
| get: (path: string) => unknown; | ||
| /** | ||
| * Write a value by JSON Pointer path and notify subscribers. | ||
| * Equality is checked by reference (`===`), not deep comparison. | ||
| * Callers must pass a new object/array reference for changes to be detected. | ||
| */ | ||
| set: (path: string, value: unknown) => void; | ||
| /** | ||
| * Write multiple values at once and notify subscribers (single notification). | ||
| * Each value is compared by reference (`===`); only paths whose value | ||
| * actually changed are applied. | ||
| */ | ||
| update: (updates: Record<string, unknown>) => void; | ||
| /** Return the full state object (used by `useSyncExternalStore`). */ | ||
| getSnapshot: () => StateModel; | ||
| /** Optional server snapshot for SSR (passed to `useSyncExternalStore`). Falls back to `getSnapshot` when omitted. */ | ||
| getServerSnapshot?: () => StateModel; | ||
| /** Register a listener that is called on every state change. Returns an unsubscribe function. */ | ||
| subscribe: (listener: () => void) => () => void; | ||
| } | ||
| /** | ||
| * Component schema definition using Zod | ||
| */ | ||
| type ComponentSchema = z.ZodType<Record<string, unknown>>; | ||
| /** | ||
| * Validation mode for catalog validation | ||
| */ | ||
| type ValidationMode = "strict" | "warn" | "ignore"; | ||
| /** | ||
| * JSON patch operation types (RFC 6902) | ||
| */ | ||
| type PatchOp = "add" | "remove" | "replace" | "move" | "copy" | "test"; | ||
| /** | ||
| * JSON patch operation (RFC 6902) | ||
| */ | ||
| interface JsonPatch { | ||
| op: PatchOp; | ||
| path: string; | ||
| /** Required for add, replace, test */ | ||
| value?: unknown; | ||
| /** Required for move, copy (source location) */ | ||
| from?: string; | ||
| } | ||
| /** | ||
| * Resolve a dynamic value against a state model | ||
| */ | ||
| declare function resolveDynamicValue<T>(value: DynamicValue<T>, stateModel: StateModel): T | undefined; | ||
| /** | ||
| * Get a value from an object by JSON Pointer path (RFC 6901) | ||
| */ | ||
| declare function getByPath(obj: unknown, path: string): unknown; | ||
| /** | ||
| * Set a value in an object by JSON Pointer path (RFC 6901). | ||
| * Automatically creates arrays when the path segment is a numeric index. | ||
| */ | ||
| declare function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Add a value per RFC 6902 "add" semantics. | ||
| * For objects: create-or-replace the member. | ||
| * For arrays: insert before the given index, or append if "-". | ||
| */ | ||
| declare function addByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Remove a value per RFC 6902 "remove" semantics. | ||
| * For objects: delete the property. | ||
| * For arrays: splice out the element at the given index. | ||
| */ | ||
| declare function removeByPath(obj: Record<string, unknown>, path: string): void; | ||
| /** | ||
| * Find a form value from params and/or state. | ||
| * Useful in action handlers to locate form input values regardless of path format. | ||
| * | ||
| * Checks in order: | ||
| * 1. Direct param key (if not a path reference) | ||
| * 2. Param keys ending with the field name | ||
| * 3. State keys ending with the field name (dot notation) | ||
| * 4. State path using getByPath (slash notation) | ||
| * | ||
| * @example | ||
| * // Find "name" from params or state | ||
| * const name = findFormValue("name", params, state); | ||
| * | ||
| * // Will find from: params.name, params["form.name"], state["form.name"], or getByPath(state, "name") | ||
| */ | ||
| declare function findFormValue(fieldName: string, params?: Record<string, unknown>, state?: Record<string, unknown>): unknown; | ||
| /** | ||
| * A SpecStream line - a single patch operation in the stream. | ||
| */ | ||
| type SpecStreamLine = JsonPatch; | ||
| /** | ||
| * Parse a single SpecStream line into a patch operation. | ||
| * Returns null if the line is invalid or empty. | ||
| * | ||
| * SpecStream is json-render's streaming format where each line is a JSON patch | ||
| * operation that progressively builds up the final spec. | ||
| */ | ||
| declare function parseSpecStreamLine(line: string): SpecStreamLine | null; | ||
| /** | ||
| * Apply a single RFC 6902 JSON Patch operation to an object. | ||
| * Mutates the object in place. | ||
| * | ||
| * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test. | ||
| * | ||
| * @throws {Error} If a "test" operation fails (value mismatch). | ||
| */ | ||
| declare function applySpecStreamPatch<T extends Record<string, unknown>>(obj: T, patch: SpecStreamLine): T; | ||
| /** | ||
| * Apply a single RFC 6902 JSON Patch operation to a Spec. | ||
| * Mutates the spec in place and returns it. | ||
| * | ||
| * This is a typed convenience wrapper around `applySpecStreamPatch` that | ||
| * accepts a `Spec` directly without requiring a cast to `Record<string, unknown>`. | ||
| * | ||
| * Note: This mutates the spec. For React state updates, spread the result | ||
| * to create a new reference: `setSpec({ ...applySpecPatch(spec, patch) })`. | ||
| * | ||
| * @example | ||
| * let spec: Spec = { root: "", elements: {} }; | ||
| * applySpecPatch(spec, { op: "add", path: "/root", value: "main" }); | ||
| */ | ||
| declare function applySpecPatch(spec: Spec, patch: SpecStreamLine): Spec; | ||
| /** | ||
| * Convert a nested (tree-structured) spec into the flat `Spec` format used | ||
| * by json-render renderers. | ||
| * | ||
| * In the nested format each node has inline `children` as an array of child | ||
| * objects. This function walks the tree, assigns auto-generated keys | ||
| * (`el-0`, `el-1`, ...), and produces a flat `{ root, elements, state }` spec. | ||
| * | ||
| * The top-level `state` field (if present on the root node) is hoisted to | ||
| * `spec.state`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const nested = { | ||
| * type: "Card", | ||
| * props: { title: "Hello" }, | ||
| * children: [ | ||
| * { type: "Text", props: { content: "World" } }, | ||
| * ], | ||
| * state: { count: 0 }, | ||
| * }; | ||
| * const spec = nestedToFlat(nested); | ||
| * // { | ||
| * // root: "el-0", | ||
| * // elements: { | ||
| * // "el-0": { type: "Card", props: { title: "Hello" }, children: ["el-1"] }, | ||
| * // "el-1": { type: "Text", props: { content: "World" }, children: [] }, | ||
| * // }, | ||
| * // state: { count: 0 }, | ||
| * // } | ||
| * ``` | ||
| */ | ||
| declare function nestedToFlat(nested: Record<string, unknown>): Spec; | ||
| /** | ||
| * Compile a SpecStream string into a JSON object. | ||
| * Each line should be a patch operation. | ||
| * | ||
| * @example | ||
| * const stream = `{"op":"add","path":"/name","value":"Alice"} | ||
| * {"op":"add","path":"/age","value":30}`; | ||
| * const result = compileSpecStream(stream); | ||
| * // { name: "Alice", age: 30 } | ||
| */ | ||
| declare function compileSpecStream<T extends Record<string, unknown> = Record<string, unknown>>(stream: string, initial?: T): T; | ||
| /** | ||
| * Streaming SpecStream compiler. | ||
| * Useful for processing SpecStream data as it streams in from AI. | ||
| * | ||
| * @example | ||
| * const compiler = createSpecStreamCompiler<MySpec>(); | ||
| * | ||
| * // As chunks arrive: | ||
| * const { result, newPatches } = compiler.push(chunk); | ||
| * if (newPatches.length > 0) { | ||
| * updateUI(result); | ||
| * } | ||
| * | ||
| * // When done: | ||
| * const finalResult = compiler.getResult(); | ||
| */ | ||
| interface SpecStreamCompiler<T> { | ||
| /** Push a chunk of text. Returns the current result and any new patches applied. */ | ||
| push(chunk: string): { | ||
| result: T; | ||
| newPatches: SpecStreamLine[]; | ||
| }; | ||
| /** Get the current compiled result */ | ||
| getResult(): T; | ||
| /** Get all patches that have been applied */ | ||
| getPatches(): SpecStreamLine[]; | ||
| /** Reset the compiler to initial state */ | ||
| reset(initial?: Partial<T>): void; | ||
| } | ||
| /** | ||
| * Create a streaming SpecStream compiler. | ||
| * | ||
| * SpecStream is json-render's streaming format. AI outputs patch operations | ||
| * line by line, and this compiler progressively builds the final spec. | ||
| * | ||
| * @example | ||
| * const compiler = createSpecStreamCompiler<TimelineSpec>(); | ||
| * | ||
| * // Process streaming response | ||
| * const reader = response.body.getReader(); | ||
| * while (true) { | ||
| * const { done, value } = await reader.read(); | ||
| * if (done) break; | ||
| * | ||
| * const { result, newPatches } = compiler.push(decoder.decode(value)); | ||
| * if (newPatches.length > 0) { | ||
| * setSpec(result); // Update UI with partial result | ||
| * } | ||
| * } | ||
| */ | ||
| declare function createSpecStreamCompiler<T = Record<string, unknown>>(initial?: Partial<T>): SpecStreamCompiler<T>; | ||
| /** | ||
| * Callbacks for the mixed stream parser. | ||
| */ | ||
| interface MixedStreamCallbacks { | ||
| /** Called when a JSONL patch line is parsed */ | ||
| onPatch: (patch: SpecStreamLine) => void; | ||
| /** Called when a text (non-JSONL) line is received */ | ||
| onText: (text: string) => void; | ||
| } | ||
| /** | ||
| * A stateful parser for mixed streams that contain both text and JSONL patches. | ||
| * Used in chat + GenUI scenarios where an LLM responds with conversational text | ||
| * interleaved with json-render JSONL patch operations. | ||
| */ | ||
| interface MixedStreamParser { | ||
| /** Push a chunk of streamed data. Calls onPatch/onText for each complete line. */ | ||
| push(chunk: string): void; | ||
| /** Flush any remaining buffered content. Call when the stream ends. */ | ||
| flush(): void; | ||
| } | ||
| /** | ||
| * Create a parser for mixed text + JSONL streams. | ||
| * | ||
| * In chat + GenUI scenarios, an LLM streams a response that contains both | ||
| * conversational text and json-render JSONL patch lines. This parser buffers | ||
| * incoming chunks, splits them into lines, and classifies each line as either | ||
| * a JSONL patch (via `parseSpecStreamLine`) or plain text. | ||
| * | ||
| * @example | ||
| * const parser = createMixedStreamParser({ | ||
| * onText: (text) => appendToMessage(text), | ||
| * onPatch: (patch) => applySpecPatch(spec, patch), | ||
| * }); | ||
| * | ||
| * // As chunks arrive from the stream: | ||
| * for await (const chunk of stream) { | ||
| * parser.push(chunk); | ||
| * } | ||
| * parser.flush(); | ||
| */ | ||
| declare function createMixedStreamParser(callbacks: MixedStreamCallbacks): MixedStreamParser; | ||
| /** | ||
| * Minimal chunk shape compatible with the AI SDK's `UIMessageChunk`. | ||
| * | ||
| * Defined here so that `@json-render/core` has no dependency on the `ai` | ||
| * package. The discriminated union covers the three text-related chunk types | ||
| * the transform inspects; all other chunk types pass through via the fallback. | ||
| */ | ||
| type StreamChunk = { | ||
| type: "text-start"; | ||
| id: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: "text-delta"; | ||
| id: string; | ||
| delta: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: "text-end"; | ||
| id: string; | ||
| [k: string]: unknown; | ||
| } | { | ||
| type: string; | ||
| [k: string]: unknown; | ||
| }; | ||
| /** | ||
| * Creates a `TransformStream` that intercepts AI SDK UI message stream chunks | ||
| * and classifies text content as either prose or json-render JSONL patches. | ||
| * | ||
| * Two classification modes: | ||
| * | ||
| * 1. **Fence mode** (preferred): Lines between ` ```spec ` and ` ``` ` are | ||
| * parsed as JSONL patches. Fence delimiters are swallowed (not emitted). | ||
| * 2. **Heuristic mode** (backward compat): Outside of fences, lines starting | ||
| * with `{` are buffered and tested with `parseSpecStreamLine`. Valid patches | ||
| * are emitted as {@link SPEC_DATA_PART_TYPE} parts; everything else is | ||
| * flushed as text. | ||
| * | ||
| * Non-text chunks (tool events, step markers, etc.) are passed through unchanged. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { createJsonRenderTransform } from "@json-render/core"; | ||
| * import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; | ||
| * | ||
| * const stream = createUIMessageStream({ | ||
| * execute: async ({ writer }) => { | ||
| * writer.merge( | ||
| * result.toUIMessageStream().pipeThrough(createJsonRenderTransform()), | ||
| * ); | ||
| * }, | ||
| * }); | ||
| * return createUIMessageStreamResponse({ stream }); | ||
| * ``` | ||
| */ | ||
| declare function createJsonRenderTransform(): TransformStream<StreamChunk, StreamChunk>; | ||
| /** | ||
| * The key registered in `AppDataParts` for json-render specs. | ||
| * The AI SDK automatically prefixes this with `"data-"` on the wire, | ||
| * so the actual stream chunk type is `"data-spec"` (see {@link SPEC_DATA_PART_TYPE}). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { SPEC_DATA_PART, type SpecDataPart } from "@json-render/core"; | ||
| * type AppDataParts = { [SPEC_DATA_PART]: SpecDataPart }; | ||
| * ``` | ||
| */ | ||
| declare const SPEC_DATA_PART: "spec"; | ||
| /** | ||
| * The wire-format type string as it appears in stream chunks and message parts. | ||
| * This is `"data-"` + {@link SPEC_DATA_PART} — i.e. `"data-spec"`. | ||
| * | ||
| * Use this constant when filtering message parts or enqueuing stream chunks. | ||
| */ | ||
| declare const SPEC_DATA_PART_TYPE: "data-spec"; | ||
| /** | ||
| * Discriminated union for the payload of a {@link SPEC_DATA_PART_TYPE} SSE part. | ||
| * | ||
| * - `"patch"`: A single RFC 6902 JSON Patch operation (streaming, progressive UI). | ||
| * - `"flat"`: A complete flat spec with `root`, `elements`, and optional `state`. | ||
| * - `"nested"`: A complete nested spec (tree structure — schema depends on catalog). | ||
| */ | ||
| type SpecDataPart = { | ||
| type: "patch"; | ||
| patch: JsonPatch; | ||
| } | { | ||
| type: "flat"; | ||
| spec: Spec; | ||
| } | { | ||
| type: "nested"; | ||
| spec: Record<string, unknown>; | ||
| }; | ||
| /** | ||
| * Convenience wrapper that pipes an AI SDK UI message stream through the | ||
| * json-render transform, classifying text as prose or JSONL patches. | ||
| * | ||
| * Eliminates the need for manual `pipeThrough(createJsonRenderTransform())` | ||
| * and the associated type cast. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { pipeJsonRender } from "@json-render/core"; | ||
| * | ||
| * const stream = createUIMessageStream({ | ||
| * execute: async ({ writer }) => { | ||
| * writer.merge(pipeJsonRender(result.toUIMessageStream())); | ||
| * }, | ||
| * }); | ||
| * return createUIMessageStreamResponse({ stream }); | ||
| * ``` | ||
| */ | ||
| declare function pipeJsonRender<T = StreamChunk>(stream: ReadableStream<T>): ReadableStream<T>; | ||
| /** | ||
| * Immutably set a value at a JSON Pointer path using structural sharing. | ||
| * Only objects along the path are shallow-cloned; untouched branches keep | ||
| * their original references. | ||
| */ | ||
| declare function immutableSetByPath(root: StateModel, path: string, value: unknown): StateModel; | ||
| /** | ||
| * Create a simple in-memory {@link StateStore}. | ||
| * | ||
| * This is the default store used by `StateProvider` when no external store is | ||
| * provided. It mirrors the previous `useState`-based behaviour but is | ||
| * framework-agnostic so it can also be used in tests or non-React contexts. | ||
| */ | ||
| declare function createStateStore(initialState?: StateModel): StateStore; | ||
| /** | ||
| * Configuration for {@link createStoreAdapter}. Adapter authors supply these | ||
| * three callbacks; everything else (get, set, update, no-op detection, | ||
| * getServerSnapshot) is handled by the returned {@link StateStore}. | ||
| */ | ||
| interface StoreAdapterConfig { | ||
| /** Return the current state snapshot from the underlying store. */ | ||
| getSnapshot: () => StateModel; | ||
| /** Write a new state snapshot to the underlying store. */ | ||
| setSnapshot: (next: StateModel) => void; | ||
| /** Subscribe to changes in the underlying store. Return an unsubscribe fn. */ | ||
| subscribe: (listener: () => void) => () => void; | ||
| } | ||
| /** | ||
| * Build a full {@link StateStore} from a minimal adapter config. | ||
| * | ||
| * Handles `get`, `set` (with no-op detection), `update` (batched, with no-op | ||
| * detection), `getSnapshot`, `getServerSnapshot`, and `subscribe` -- so each | ||
| * adapter only needs to wire its snapshot source, write API, and subscribe | ||
| * mechanism. | ||
| */ | ||
| declare function createStoreAdapter(config: StoreAdapterConfig): StateStore; | ||
| /** | ||
| * Recursively flatten a plain object into a `Record<string, unknown>` keyed by | ||
| * JSON Pointer paths. Only leaf values (non-plain-object) appear in the output. | ||
| * | ||
| * Includes circular reference protection and a depth cap to prevent stack | ||
| * overflow on pathological inputs. | ||
| * | ||
| * ```ts | ||
| * flattenToPointers({ user: { name: "Alice" }, count: 1 }) | ||
| * // => { "/user/name": "Alice", "/count": 1 } | ||
| * ``` | ||
| */ | ||
| declare function flattenToPointers(obj: Record<string, unknown>, prefix?: string, _depth?: number, _seen?: Set<object>, _warned?: { | ||
| current: boolean; | ||
| }): Record<string, unknown>; | ||
| export { type ActionOnError as $, type AndCondition as A, applySpecPatch as B, type ComponentSchema as C, type DynamicValue as D, nestedToFlat as E, type FlatElement as F, compileSpecStream as G, createSpecStreamCompiler as H, type ItemCondition as I, type JsonPatch as J, createMixedStreamParser as K, createJsonRenderTransform as L, type MixedStreamCallbacks as M, pipeJsonRender as N, type OrCondition as O, type PatchOp as P, SPEC_DATA_PART as Q, SPEC_DATA_PART_TYPE as R, type StateModel as S, type StoreAdapterConfig as T, type UIElement as U, type VisibilityCondition as V, createStateStore as W, type ActionBinding as X, type Action as Y, type ActionConfirm as Z, type ActionOnSuccess as _, type StateCondition as a, type ActionHandler as a0, type ActionDefinition as a1, type ResolvedAction as a2, type ActionExecutionContext as a3, ActionBindingSchema as a4, ActionSchema as a5, ActionConfirmSchema as a6, ActionOnSuccessSchema as a7, ActionOnErrorSchema as a8, resolveAction as a9, executeAction as aa, interpolateString as ab, actionBinding as ac, action as ad, immutableSetByPath as ae, flattenToPointers as af, createStoreAdapter as ag, type Spec as b, type DynamicString as c, type DynamicNumber as d, type DynamicBoolean as e, type IndexCondition as f, type SingleCondition as g, type StateStore as h, type ValidationMode as i, type SpecStreamLine as j, type SpecStreamCompiler as k, type MixedStreamParser as l, type StreamChunk as m, type SpecDataPart as n, DynamicValueSchema as o, DynamicStringSchema as p, DynamicNumberSchema as q, DynamicBooleanSchema as r, resolveDynamicValue as s, getByPath as t, setByPath as u, addByPath as v, removeByPath as w, findFormValue as x, parseSpecStreamLine as y, applySpecStreamPatch as z }; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
992900
8.54%8633
7.99%643
1.42%