github-actions[bot] c82eefd1c5 chore: version packages (#117) 4 kuukautta sitten
..
src 320c935bfb shadcn (#115) 4 kuukautta sitten
CHANGELOG.md c82eefd1c5 chore: version packages (#117) 4 kuukautta sitten
README.md ddae61805e inline mode (#107) 4 kuukautta sitten
package.json c82eefd1c5 chore: version packages (#117) 4 kuukautta sitten
tsconfig.json 7663fb22b0 init 5 kuukautta sitten
tsup.config.ts a6bf3ef3da format 5 kuukautta sitten

README.md

@json-render/core

Core library for json-render. Define schemas, create catalogs, generate AI prompts, and stream specs.

Installation

npm install @json-render/core zod

Key Concepts

  • Schema: Defines the structure of specs and catalogs
  • Catalog: Maps component/action names to their definitions with Zod props
  • Spec: JSON output from AI that conforms to the schema
  • SpecStream: JSONL streaming format for progressive spec building

Quick Start

Define a Schema

import { defineSchema } from "@json-render/core";

export const schema = defineSchema((s) => ({
  spec: s.object({
    root: s.object({
      type: s.ref("catalog.components"),
      props: s.propsOf("catalog.components"),
      children: s.array(s.string()), // Element keys (flat spec format)
    }),
  }),
  catalog: s.object({
    components: s.map({
      props: s.zod(),
      description: s.string(),
    }),
    actions: s.map({
      description: s.string(),
    }),
  }),
}), {
  promptTemplate: myPromptTemplate, // Optional custom AI prompt generator
});

Create a Catalog

import { defineCatalog } from "@json-render/core";
import { schema } from "./schema";
import { z } from "zod";

export const catalog = defineCatalog(schema, {
  components: {
    Card: {
      props: z.object({
        title: z.string(),
        subtitle: z.string().nullable(),
      }),
      description: "A card container with title",
    },
    Button: {
      props: z.object({
        label: z.string(),
        variant: z.enum(["primary", "secondary"]).nullable(),
      }),
      description: "A clickable button",
    },
  },
  actions: {
    submit: { description: "Submit the form" },
    cancel: { description: "Cancel and close" },
  },
});

Generate AI Prompts

// Generate system prompt for AI
const systemPrompt = catalog.prompt();

// With custom rules
const systemPrompt = catalog.prompt({
  system: "You are a dashboard builder.",
  customRules: [
    "Always include a header",
    "Use Card components for grouping",
  ],
});

Stream AI Responses (SpecStream)

The SpecStream format uses JSONL patches to progressively build specs:

import { createSpecStreamCompiler } from "@json-render/core";

// Create a compiler for your spec type
const compiler = createSpecStreamCompiler<MySpec>();

// Process streaming chunks from AI
while (streaming) {
  const chunk = await reader.read();
  const { result, newPatches } = compiler.push(chunk);
  
  if (newPatches.length > 0) {
    // Update UI with partial result
    setSpec(result);
  }
}

// Get final compiled result
const finalSpec = compiler.getResult();

SpecStream format uses RFC 6902 JSON Patch operations (each line is a patch):

{"op":"add","path":"/root","value":"card-1"}
{"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Hello"},"children":["btn-1"]}}
{"op":"add","path":"/elements/btn-1","value":{"type":"Button","props":{"label":"Click"},"children":[]}}

All six RFC 6902 operations are supported: add, remove, replace, move, copy, test.

Low-Level Utilities

import {
  parseSpecStreamLine,
  applySpecStreamPatch,
  compileSpecStream,
} from "@json-render/core";

// Parse a single line
const patch = parseSpecStreamLine('{"op":"add","path":"/root","value":{}}');
// { op: "add", path: "/root", value: {} }

// Apply a patch to an object
const obj = {};
applySpecStreamPatch(obj, patch);
// obj is now { root: {} }

// Compile entire JSONL string at once
const spec = compileSpecStream<MySpec>(jsonlString);

API Reference

Schema

Export Purpose
defineSchema(builder, options?) Create a schema with spec/catalog structure
SchemaBuilder Builder with s.object(), s.array(), s.map(), etc.

Catalog

Export Purpose
defineCatalog(schema, data) Create a type-safe catalog from schema
catalog.prompt(options?) Generate AI system prompt

SpecStream

Export Purpose
createSpecStreamCompiler<T>() Create streaming compiler
parseSpecStreamLine(line) Parse single JSONL line
applySpecStreamPatch(obj, patch) Apply patch to object
compileSpecStream<T>(jsonl) Compile entire JSONL string

Dynamic Props

Export Purpose
resolvePropValue(value, ctx) Resolve a single prop expression
resolveElementProps(props, ctx) Resolve all prop expressions in an element
PropExpression<T> Type for prop values that may contain expressions

User Prompt

Export Purpose
buildUserPrompt(options) Build a user prompt with optional spec refinement and state context
UserPromptOptions Options type for buildUserPrompt

Spec Validation

Export Purpose
validateSpec(spec, options?) Validate spec structure and return issues
autoFixSpec(spec) Auto-fix common spec issues (returns corrected copy)
formatSpecIssues(issues) Format validation issues as readable strings

Types

Export Purpose
Spec Base spec type
Catalog Catalog type
VisibilityCondition Visibility condition type (used by $cond)
VisibilityContext Context for evaluating visibility and prop expressions
SpecStreamLine Single patch operation
SpecStreamCompiler Streaming compiler interface

Dynamic Prop Expressions

Any prop value can be a dynamic expression that resolves based on data state at render time. Expressions are resolved by the renderer before props reach components.

Data Binding ($state)

Read a value directly from the state model:

{
  "color": { "$state": "/theme/primary" },
  "label": { "$state": "/user/name" }
}

Two-Way Binding ($bindState / $bindItem)

Use { "$bindState": "/path" } on the natural value prop for form components that need read/write access. The component reads from and writes to the state path:

{
  "type": "Input",
  "props": {
    "value": { "$bindState": "/form/email" },
    "placeholder": "Email"
  }
}

Inside a repeat scope, use { "$bindItem": "completed" } to bind to a field on the current item:

Conditional ($cond / $then / $else)

Evaluate a condition (same syntax as visibility conditions) and pick a value:

{
  "color": {
    "$cond": { "$state": "/activeTab", "eq": "home" },
    "$then": "#007AFF",
    "$else": "#8E8E93"
  },
  "name": {
    "$cond": { "$state": "/activeTab", "eq": "home" },
    "$then": "home",
    "$else": "home-outline"
  }
}

$then and $else can themselves be expressions (recursive):

{
  "label": {
    "$cond": { "$state": "/user/isAdmin" },
    "$then": { "$state": "/admin/greeting" },
    "$else": "Welcome"
  }
}

Repeat Item ($item)

Inside children of a repeated element, read a field from the current array item:

{ "$item": "title" }

Use "" to get the entire item object. $item takes a path string because items are typically objects with nested fields to navigate.

Repeat Index ($index)

Get the current array index inside a repeat:

{ "$index": true }

$index uses true as a sentinel flag because the index is a scalar value with no sub-path to navigate (unlike $item which needs a path).

API

import { resolvePropValue, resolveElementProps } from "@json-render/core";

// Resolve a single value
const color = resolvePropValue(
  { $cond: { $state: "/active", eq: "yes" }, $then: "blue", $else: "gray" },
  { stateModel: myState }
);

// Resolve all props on an element
const resolved = resolveElementProps(element.props, { stateModel: myState });

Visibility Conditions

Visibility conditions control when elements are shown. VisibilityContext is { stateModel: StateModel, repeatItem?: unknown, repeatIndex?: number }.

Syntax

{ "$state": "/path" }                          // truthiness
{ "$state": "/path", "not": true }             // falsy
{ "$state": "/path", "eq": value }             // equality
{ "$state": "/path", "neq": value }            // inequality
{ "$state": "/path", "gt": number }            // greater than
{ "$item": "field" }                          // repeat item field
{ "$index": true, "gt": 0 }                   // repeat index
[ condition, condition ]                       // implicit AND
{ "$and": [ condition, condition ] }           // explicit AND
{ "$or": [ condition, condition ] }            // OR
true / false                                   // always / never

TypeScript Helpers

import { visibility } from "@json-render/core";

visibility.always              // true
visibility.never               // false
visibility.when("/path")       // { $state: "/path" }
visibility.unless("/path")     // { $state: "/path", not: true }
visibility.eq("/path", val)    // { $state: "/path", eq: val }
visibility.neq("/path", val)   // { $state: "/path", neq: val }
visibility.gt("/path", n)      // { $state: "/path", gt: n }
visibility.gte("/path", n)     // { $state: "/path", gte: n }
visibility.lt("/path", n)      // { $state: "/path", lt: n }
visibility.lte("/path", n)     // { $state: "/path", lte: n }
visibility.and(cond1, cond2)   // { $and: [cond1, cond2] }
visibility.or(cond1, cond2)    // { $or: [cond1, cond2] }

User Prompt Builder

Build structured user prompts for AI generation, with support for refinement and state context:

import { buildUserPrompt } from "@json-render/core";

// Fresh generation
const prompt = buildUserPrompt({ prompt: "create a todo app" });

// Refinement with existing spec (triggers patch-only mode)
const refinementPrompt = buildUserPrompt({
  prompt: "add a dark mode toggle",
  currentSpec: existingSpec,
});

// With runtime state context
const contextPrompt = buildUserPrompt({
  prompt: "show my data",
  state: { todos: [{ text: "Buy milk" }] },
});

Spec Validation

Validate spec structure and auto-fix common issues:

import { validateSpec, autoFixSpec, formatSpecIssues } from "@json-render/core";

// Validate a spec
const { valid, issues } = validateSpec(spec);

// Format issues for display
console.log(formatSpecIssues(issues));

// Auto-fix common issues (returns a corrected copy)
const fixed = autoFixSpec(spec);

Custom Schemas

json-render supports completely different spec formats for different renderers:

// React: Flat element map
{ root: "card-1", elements: { "card-1": { type: "Card", props: {...}, children: [...] } } }

// Remotion: Timeline
{ composition: {...}, tracks: [...], clips: [...] }

// Your own: Whatever you need
{ pages: [...], navigation: {...}, theme: {...} }

Each renderer defines its own schema with defineSchema() and its own prompt template.