@thorprovider/create-storefront 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/bin/install.js +116 -0
- package/commands/sf-add-view.md +21 -0
- package/commands/sf-init.md +16 -0
- package/commands/sf-theme.md +15 -0
- package/commands/sf-view.md +20 -0
- package/package.json +40 -0
- package/recipes/archetype.schema.json +39 -0
- package/recipes/archetypes.json +148 -0
- package/recipes/recipe.schema.json +59 -0
- package/recipes/recipes.json +90 -0
- package/recipes/sections.json +46 -0
- package/recipes/validate.mjs +190 -0
- package/skills/building-storefronts/SKILL.md +178 -0
- package/skills/building-storefronts/references/frontend-integration.md +229 -0
- package/skills/json-render-core/SKILL.md +291 -0
- package/skills/json-render-next/SKILL.md +194 -0
- package/skills/json-render-react/SKILL.md +298 -0
- package/skills/json-render-remotion/SKILL.md +111 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/json-render-solid/SKILL.md +204 -0
- package/skills/nextjs-shadcn/SKILL.md +303 -0
- package/skills/nextjs-shadcn/references/architecture.md +499 -0
- package/skills/nextjs-shadcn/references/project-setup.md +127 -0
- package/skills/nextjs-shadcn/references/shadcn-platform.md +258 -0
- package/skills/nextjs-shadcn/references/sidebar.md +274 -0
- package/skills/nextjs-shadcn/references/styling.md +555 -0
- package/skills/sf-scaffold/SKILL.md +118 -0
- package/skills/sf-theme-gen/SKILL.md +44 -0
- package/skills/sf-view-gen/SKILL.md +94 -0
- package/skills/shadcn-component-discovery/SKILL.md +273 -0
- package/skills/shadcn-component-discovery/references/registries.md +226 -0
- package/skills/shadcn-theming/SKILL.md +104 -0
- package/skills/shadcn-theming/references/templates/theme-setup.md +109 -0
- package/skills/shadcn-theming/references/theming-guide.md +90 -0
- package/skills/storefront-best-practices/SKILL.md +421 -0
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +123 -0
- package/skills/storefront-best-practices/reference/components/cart-popup.md +189 -0
- package/skills/storefront-best-practices/reference/components/country-selector.md +298 -0
- package/skills/storefront-best-practices/reference/components/footer.md +112 -0
- package/skills/storefront-best-practices/reference/components/hero.md +241 -0
- package/skills/storefront-best-practices/reference/components/megamenu.md +239 -0
- package/skills/storefront-best-practices/reference/components/navbar.md +397 -0
- package/skills/storefront-best-practices/reference/components/popups.md +221 -0
- package/skills/storefront-best-practices/reference/components/product-card.md +125 -0
- package/skills/storefront-best-practices/reference/components/product-reviews.md +217 -0
- package/skills/storefront-best-practices/reference/components/product-slider.md +174 -0
- package/skills/storefront-best-practices/reference/components/search.md +101 -0
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +391 -0
- package/skills/storefront-best-practices/reference/design.md +388 -0
- package/skills/storefront-best-practices/reference/features/promotions.md +307 -0
- package/skills/storefront-best-practices/reference/features/wishlist.md +230 -0
- package/skills/storefront-best-practices/reference/layouts/account.md +380 -0
- package/skills/storefront-best-practices/reference/layouts/cart.md +316 -0
- package/skills/storefront-best-practices/reference/layouts/checkout.md +486 -0
- package/skills/storefront-best-practices/reference/layouts/home-page.md +264 -0
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +231 -0
- package/skills/storefront-best-practices/reference/layouts/product-details.md +527 -0
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +520 -0
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +356 -0
- package/skills/storefront-best-practices/reference/medusa.md +307 -0
- package/skills/storefront-best-practices/reference/mobile-responsiveness.md +183 -0
- package/skills/storefront-best-practices/reference/seo.md +195 -0
- package/templates/app/app/[[...slug]]/page.tsx +17 -0
- package/templates/app/app/[[...slug]]/renderer.tsx +10 -0
- package/templates/app/app/globals.css +101 -0
- package/templates/app/app/layout.tsx +35 -0
- package/templates/app/lib/__STOREFRONT__/catalog.ts +132 -0
- package/templates/app/lib/__STOREFRONT__/handlers.ts +33 -0
- package/templates/app/lib/__STOREFRONT__/registry.tsx +134 -0
- package/templates/app/lib/__STOREFRONT__/runtime.ts +25 -0
- package/templates/app/lib/__STOREFRONT__/spec/home.ts +62 -0
- package/templates/app/lib/__STOREFRONT__/spec/index.ts +59 -0
- package/templates/app/lib/__STOREFRONT__/spec/types.ts +14 -0
- package/templates/app/lib/__STOREFRONT__/state.ts +35 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Frontend SDK Integration
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
- [Frontend SDK Pattern](#frontend-sdk-pattern)
|
|
5
|
+
- [Locating the SDK](#locating-the-sdk)
|
|
6
|
+
- [Using sdk.client.fetch()](#using-sdkclientfetch)
|
|
7
|
+
- [React Query Pattern](#react-query-pattern)
|
|
8
|
+
- [Query Key Best Practices](#query-key-best-practices)
|
|
9
|
+
- [Error Handling](#error-handling)
|
|
10
|
+
- [Optimistic Updates](#optimistic-updates)
|
|
11
|
+
|
|
12
|
+
This guide covers how to integrate Medusa custom API routes with frontend applications using the Medusa SDK and React Query.
|
|
13
|
+
|
|
14
|
+
**Note:** API routes are also referred to as "endpoints" - these terms are interchangeable.
|
|
15
|
+
|
|
16
|
+
## Frontend SDK Pattern
|
|
17
|
+
|
|
18
|
+
### Locating the SDK
|
|
19
|
+
|
|
20
|
+
**IMPORTANT:** Never hardcode SDK import paths. Always locate where the SDK is instantiated in the project first.
|
|
21
|
+
|
|
22
|
+
Look for `@medusajs/js-sdk`
|
|
23
|
+
|
|
24
|
+
The SDK instance is typically exported as `sdk`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { sdk } from "[LOCATE IN PROJECT]"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Using sdk.client.fetch()
|
|
31
|
+
|
|
32
|
+
**⚠️ CRITICAL: ALWAYS use the Medusa JS SDK for ALL API requests - NEVER use regular fetch()**
|
|
33
|
+
|
|
34
|
+
**Why this is critical:**
|
|
35
|
+
- **Store API routes** require the publishable API key in headers
|
|
36
|
+
- **Admin API routes** require authentication headers
|
|
37
|
+
- **Regular fetch()** without these headers will cause errors
|
|
38
|
+
- The SDK automatically handles all required headers for you
|
|
39
|
+
|
|
40
|
+
**When to use what:**
|
|
41
|
+
- **Existing endpoints** (built-in Medusa routes): Use existing SDK methods like `sdk.store.product.list()`, `sdk.admin.order.retrieve()`
|
|
42
|
+
- **Custom endpoints** (your custom API routes): Use `sdk.client.fetch()` for custom routes
|
|
43
|
+
|
|
44
|
+
**⚠️ CRITICAL: The SDK handles JSON serialization automatically. NEVER use JSON.stringify() on the body.**
|
|
45
|
+
|
|
46
|
+
Call custom API routes using the SDK:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
|
|
50
|
+
|
|
51
|
+
// ✅ CORRECT - Pass object directly
|
|
52
|
+
const result = await sdk.client.fetch("/store/my-route", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: {
|
|
55
|
+
email: "user@example.com",
|
|
56
|
+
name: "John Doe",
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// ❌ WRONG - Don't use JSON.stringify
|
|
61
|
+
const result = await sdk.client.fetch("/store/my-route", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
body: JSON.stringify({ // ❌ DON'T DO THIS!
|
|
64
|
+
email: "user@example.com",
|
|
65
|
+
}),
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Key points:**
|
|
70
|
+
|
|
71
|
+
- **The SDK handles JSON serialization automatically** - just pass plain objects
|
|
72
|
+
- **NEVER use JSON.stringify()** - this will break the request
|
|
73
|
+
- No need to set Content-Type headers - SDK adds them
|
|
74
|
+
- Session/JWT authentication is handled automatically
|
|
75
|
+
- Publishable API key is automatically added
|
|
76
|
+
|
|
77
|
+
### Built-in Endpoints vs Custom Endpoints
|
|
78
|
+
|
|
79
|
+
**⚠️ CRITICAL: Use the appropriate SDK method based on endpoint type**
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
|
|
83
|
+
|
|
84
|
+
// ✅ CORRECT - Built-in endpoint: Use existing SDK method
|
|
85
|
+
const products = await sdk.store.product.list({
|
|
86
|
+
limit: 10,
|
|
87
|
+
offset: 0
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// ✅ CORRECT - Custom endpoint: Use sdk.client.fetch()
|
|
91
|
+
const reviews = await sdk.client.fetch("/store/products/prod_123/reviews")
|
|
92
|
+
|
|
93
|
+
// ❌ WRONG - Using regular fetch for ANY endpoint
|
|
94
|
+
const products = await fetch("http://localhost:9000/store/products")
|
|
95
|
+
// ❌ Error: Missing publishable API key header!
|
|
96
|
+
|
|
97
|
+
// ❌ WRONG - Using regular fetch for custom endpoint
|
|
98
|
+
const reviews = await fetch("http://localhost:9000/store/products/prod_123/reviews")
|
|
99
|
+
// ❌ Error: Missing publishable API key header!
|
|
100
|
+
|
|
101
|
+
// ❌ WRONG - Using sdk.client.fetch() for built-in endpoint when SDK method exists
|
|
102
|
+
const products = await sdk.client.fetch("/store/products")
|
|
103
|
+
// ❌ Less type-safe than using sdk.store.product.list()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Why this matters:**
|
|
107
|
+
- **Store routes** require `x-publishable-api-key` header - SDK adds it automatically
|
|
108
|
+
- **Admin routes** require `Authorization` and session cookie headers - SDK adds them automatically
|
|
109
|
+
- **Regular fetch()** doesn't include these headers → API returns authentication/authorization errors
|
|
110
|
+
- Using existing SDK methods provides **better type safety** and autocomplete
|
|
111
|
+
|
|
112
|
+
## React Query Pattern
|
|
113
|
+
|
|
114
|
+
Use `useQuery` for GET requests and `useMutation` for POST/DELETE:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
|
|
118
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|
119
|
+
|
|
120
|
+
function MyComponent({ userId }: { userId: string }) {
|
|
121
|
+
const queryClient = useQueryClient()
|
|
122
|
+
|
|
123
|
+
// GET request - fetching data
|
|
124
|
+
const { data, isLoading } = useQuery({
|
|
125
|
+
queryKey: ["my-data", userId],
|
|
126
|
+
queryFn: () => sdk.client.fetch(`/store/my-route?userId=${userId}`),
|
|
127
|
+
enabled: !!userId,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// POST request - mutation with cache invalidation
|
|
131
|
+
const mutation = useMutation({
|
|
132
|
+
mutationFn: (input: { email: string }) =>
|
|
133
|
+
sdk.client.fetch("/store/my-route", { method: "POST", body: input }),
|
|
134
|
+
onSuccess: () => {
|
|
135
|
+
// Invalidate and refetch related queries
|
|
136
|
+
queryClient.invalidateQueries({ queryKey: ["my-data"] })
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (isLoading) return <p>Loading...</p>
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div>
|
|
144
|
+
<p>{data?.title}</p>
|
|
145
|
+
<button
|
|
146
|
+
onClick={() => mutation.mutate({ email: "test@example.com" })}
|
|
147
|
+
disabled={mutation.isPending}
|
|
148
|
+
>
|
|
149
|
+
{mutation.isPending ? "Loading..." : "Submit"}
|
|
150
|
+
</button>
|
|
151
|
+
{mutation.isError && <p>Error occurred</p>}
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Key states:** `isLoading`, `isPending`, `isSuccess`, `isError`, `error`
|
|
158
|
+
|
|
159
|
+
## Query Key Best Practices
|
|
160
|
+
|
|
161
|
+
Structure query keys for effective cache management:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// Good: Hierarchical structure
|
|
165
|
+
queryKey: ["products", productId]
|
|
166
|
+
queryKey: ["products", "list", { page, filters }]
|
|
167
|
+
|
|
168
|
+
// Invalidate all product queries
|
|
169
|
+
queryClient.invalidateQueries({ queryKey: ["products"] })
|
|
170
|
+
|
|
171
|
+
// Invalidate specific product
|
|
172
|
+
queryClient.invalidateQueries({ queryKey: ["products", productId] })
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Error Handling
|
|
176
|
+
|
|
177
|
+
Handle API errors gracefully:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const mutation = useMutation({
|
|
181
|
+
mutationFn: (input) => sdk.client.fetch("/store/my-route", {
|
|
182
|
+
method: "POST",
|
|
183
|
+
body: input
|
|
184
|
+
}),
|
|
185
|
+
onError: (error) => {
|
|
186
|
+
console.error("Mutation failed:", error)
|
|
187
|
+
// Show error message to user
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// In component
|
|
192
|
+
{mutation.isError && (
|
|
193
|
+
<p className="error">
|
|
194
|
+
{mutation.error?.message || "An error occurred"}
|
|
195
|
+
</p>
|
|
196
|
+
)}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Optimistic Updates
|
|
200
|
+
|
|
201
|
+
Update UI immediately before server confirms:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const mutation = useMutation({
|
|
205
|
+
mutationFn: (newItem) =>
|
|
206
|
+
sdk.client.fetch("/store/items", { method: "POST", body: newItem }),
|
|
207
|
+
onMutate: async (newItem) => {
|
|
208
|
+
// Cancel outgoing refetches
|
|
209
|
+
await queryClient.cancelQueries({ queryKey: ["items"] })
|
|
210
|
+
|
|
211
|
+
// Snapshot previous value
|
|
212
|
+
const previousItems = queryClient.getQueryData(["items"])
|
|
213
|
+
|
|
214
|
+
// Optimistically update
|
|
215
|
+
queryClient.setQueryData(["items"], (old) => [...old, newItem])
|
|
216
|
+
|
|
217
|
+
// Return context with snapshot
|
|
218
|
+
return { previousItems }
|
|
219
|
+
},
|
|
220
|
+
onError: (err, newItem, context) => {
|
|
221
|
+
// Rollback on error
|
|
222
|
+
queryClient.setQueryData(["items"], context.previousItems)
|
|
223
|
+
},
|
|
224
|
+
onSettled: () => {
|
|
225
|
+
// Refetch after mutation
|
|
226
|
+
queryClient.invalidateQueries({ queryKey: ["items"] })
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
```
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: core
|
|
3
|
+
description: Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @json-render/core
|
|
7
|
+
|
|
8
|
+
Core package for schema definition, catalog creation, and spec streaming.
|
|
9
|
+
|
|
10
|
+
## Key Concepts
|
|
11
|
+
|
|
12
|
+
- **Schema**: Defines the structure of specs and catalogs (use `defineSchema`)
|
|
13
|
+
- **Catalog**: Maps component/action names to their definitions (use `defineCatalog`)
|
|
14
|
+
- **Spec**: JSON output from AI that conforms to the schema
|
|
15
|
+
- **SpecStream**: JSONL streaming format for progressive spec building
|
|
16
|
+
|
|
17
|
+
## Experimental Decision-Model Composition
|
|
18
|
+
|
|
19
|
+
For decision-model composition, import `experimental_composeSpec` and `experimental_createEvaluator` from `@json-render/core`. These APIs are unreleased; use a source build until published, then pin exact versions. Experimental exports and `Experimental_` types can change in any release.
|
|
20
|
+
|
|
21
|
+
- Run the Gateway evaluator server-side with `{ model: "typesafe-ai/jev", apiKey: process.env.AI_GATEWAY_API_KEY! }`. A plain model identifier is required; Jev is the current example; do not import a provider constructor.
|
|
22
|
+
- Call `experimental_composeSpec({ catalog, candidates, prompt, evaluate, initialState, signal })`. It is an async generator; stream `step.spec` snapshots to your existing renderer and inspect `complete.stopReason` (`finish`, `limit`, `unavailable`). Errors and cancellation throw; retain the last snapshot as partial UI.
|
|
23
|
+
- New trees default to `strategy: "batch"`: one evaluation selects root/membership, then a second arranges the selected elements when needed. The first snapshot contains selected content in catalog order under the root's default/first slot. Resource variants share one exclusive question; repeated counts include the root. Root selection takes precedence over conflicting speculative membership for that recipe/resource. Equal sibling positions retain catalog order. Combined layouts are validated before publication; cycles or excessive depth throw. `maxElements` caps batched creation (default 32). Limit-truncated selections or a missing required layout call return `limit`. Use `strategy: "sequential"` for legacy `next`/`parent` adapters or sequential creation. Edits stay sequential.
|
|
24
|
+
- Batched trace steps use `choice: "select" | "layout"` and an `answers` record. Count each trace as one evaluation, including its tokens and latency once. Custom evaluators must answer every offered question; names/choices are opaque and include `root`/`select_*`, then `parent_*`/`order_*` for batches.
|
|
25
|
+
- For follow-up edits, pass the selected version as `initialSpec`. It is cloned and validated; the evaluator may add, replace, remove non-root subtrees, or move/reorder them. Unchanged IDs, bindings, and state are preserved. Optional `elementDescriptions` shares identifying descriptions without exposing raw props/state. `initialState` overrides the seed state. Seeds must be valid trees within the catalog, expression subset, and depth limit. Matching recipes consume usage/resource limits; removals/replacements release them. Replacements/moves use two evaluations (select target, then recipe/destination), each counted against the budget. Treat operation and position keys as opaque.
|
|
26
|
+
- Supply atomic candidates with `{ id, description, element: { type, props, on?, visible? }, root?, maxUses?, resource? }`. Catalog alone is insufficient: the app must supply values and binding recipes. Jev chooses elements and parent slots, never free-form text or code. It never executes actions.
|
|
27
|
+
- Candidates are configured component instances, not page templates. Build them from current app records/operations or bind props to `initialState`; offer explicit alternatives for chart types, field configurations, and layout variants. The model chooses grouping and order within those options. Name required sections in prompts; structural validity does not imply semantic completeness.
|
|
28
|
+
- V1 supports flat Spec catalogs, named slots, literals, `$state`, `$bindState`, and state visibility. No prebuilt children, repeat/watch, computed/template/conditional props, or custom directives. Success/error callbacks must reference allowed actions. Events must be declared in the component catalog.
|
|
29
|
+
- Props and action params are validated against initial state without applying schema transforms/defaults. Supply valid initial values and validate/authorize action calls at runtime. Built-ins without parameter schemas get name validation only.
|
|
30
|
+
- `root` defaults true, `maxUses` defaults one, shared `resource` values make alternatives mutually exclusive. Defaults: 32 evaluations (terminal calls included; no extra finish call for batches), depth eight, 10-second Gateway timeout per call. Supply an overall abort signal.
|
|
31
|
+
- Candidate descriptions, prompt, instructions, topology, and explicit `context` are sent to the evaluator. Initial state and raw props/binding values are not sent automatically.
|
|
32
|
+
- For custom providers implement `Experimental_CompositionEvaluator`: accept `{ state, questions, signal }`, return `{ answers: { [question]: { choice, confidence? } }, usage?: { inputTokens? } }`. Only return offered criteria keys.
|
|
33
|
+
|
|
34
|
+
See `packages/core/README.md` and `/docs/jev` for app integration and source-build instructions. The web playground is an example consumer, not a dependency of the API.
|
|
35
|
+
|
|
36
|
+
## Defining a Schema
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { defineSchema } from "@json-render/core";
|
|
40
|
+
|
|
41
|
+
export const schema = defineSchema((s) => ({
|
|
42
|
+
spec: s.object({
|
|
43
|
+
// Define spec structure
|
|
44
|
+
}),
|
|
45
|
+
catalog: s.object({
|
|
46
|
+
components: s.map({
|
|
47
|
+
props: s.zod(),
|
|
48
|
+
description: s.string(),
|
|
49
|
+
}),
|
|
50
|
+
}),
|
|
51
|
+
}), {
|
|
52
|
+
promptTemplate: myPromptTemplate, // Optional custom AI prompt
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Creating a Catalog
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { defineCatalog } from "@json-render/core";
|
|
60
|
+
import { schema } from "./schema";
|
|
61
|
+
import { z } from "zod";
|
|
62
|
+
|
|
63
|
+
export const catalog = defineCatalog(schema, {
|
|
64
|
+
components: {
|
|
65
|
+
Button: {
|
|
66
|
+
props: z.object({
|
|
67
|
+
label: z.string(),
|
|
68
|
+
variant: z.enum(["primary", "secondary"]).nullable(),
|
|
69
|
+
}),
|
|
70
|
+
description: "Clickable button component",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Generating AI Prompts
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const systemPrompt = catalog.prompt(); // Uses schema's promptTemplate
|
|
80
|
+
const systemPrompt = catalog.prompt({ customRules: ["Rule 1", "Rule 2"] });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## SpecStream Utilities
|
|
84
|
+
|
|
85
|
+
For streaming AI responses (JSONL patches):
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { createSpecStreamCompiler } from "@json-render/core";
|
|
89
|
+
|
|
90
|
+
const compiler = createSpecStreamCompiler<MySpec>();
|
|
91
|
+
|
|
92
|
+
// Process streaming chunks
|
|
93
|
+
const { result, newPatches } = compiler.push(chunk);
|
|
94
|
+
|
|
95
|
+
// Get final result
|
|
96
|
+
const finalSpec = compiler.getResult();
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Dynamic Prop Expressions
|
|
100
|
+
|
|
101
|
+
Any prop value can be a dynamic expression resolved at render time:
|
|
102
|
+
|
|
103
|
+
- **`{ "$state": "/state/key" }`** - reads a value from the state model (one-way read)
|
|
104
|
+
- **`{ "$bindState": "/path" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.
|
|
105
|
+
- **`{ "$bindItem": "field" }`** - two-way binding to a repeat item field. Use inside repeat scopes.
|
|
106
|
+
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - evaluates a visibility condition and picks a branch
|
|
107
|
+
- **`{ "$template": "Hello, ${/user/name}!" }`** - interpolates `${/path}` references with state values
|
|
108
|
+
- **`{ "$computed": "fnName", "args": { "key": <expression> } }`** - calls a registered function with resolved args
|
|
109
|
+
|
|
110
|
+
`$cond` uses the same syntax as visibility conditions (`$state`, `eq`, `neq`, `not`, arrays for AND). `$then` and `$else` can themselves be expressions (recursive).
|
|
111
|
+
|
|
112
|
+
Components do not use a `statePath` prop for two-way binding. Instead, use `{ "$bindState": "/path" }` on the natural value prop (e.g. `value`, `checked`, `pressed`).
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"color": {
|
|
117
|
+
"$cond": { "$state": "/activeTab", "eq": "home" },
|
|
118
|
+
"$then": "#007AFF",
|
|
119
|
+
"$else": "#8E8E93"
|
|
120
|
+
},
|
|
121
|
+
"label": { "$template": "Welcome, ${/user/name}!" },
|
|
122
|
+
"fullName": {
|
|
123
|
+
"$computed": "fullName",
|
|
124
|
+
"args": {
|
|
125
|
+
"first": { "$state": "/form/firstName" },
|
|
126
|
+
"last": { "$state": "/form/lastName" }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { resolvePropValue, resolveElementProps } from "@json-render/core";
|
|
134
|
+
|
|
135
|
+
const resolved = resolveElementProps(element.props, { stateModel: myState });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## State Watchers
|
|
139
|
+
|
|
140
|
+
Elements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"type": "Select",
|
|
145
|
+
"props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada"] },
|
|
146
|
+
"watch": {
|
|
147
|
+
"/form/country": { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } }
|
|
148
|
+
},
|
|
149
|
+
"children": []
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Watchers only fire on value changes, not on initial render.
|
|
154
|
+
|
|
155
|
+
## Validation
|
|
156
|
+
|
|
157
|
+
Built-in validation functions: `required`, `email`, `url`, `numeric`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `matches`, `equalTo`, `lessThan`, `greaterThan`, `requiredIf`.
|
|
158
|
+
|
|
159
|
+
Cross-field validation uses `$state` expressions in args:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { check } from "@json-render/core";
|
|
163
|
+
|
|
164
|
+
check.required("Field is required");
|
|
165
|
+
check.matches("/form/password", "Passwords must match");
|
|
166
|
+
check.lessThan("/form/endDate", "Must be before end date");
|
|
167
|
+
check.greaterThan("/form/startDate", "Must be after start date");
|
|
168
|
+
check.requiredIf("/form/enableNotifications", "Required when enabled");
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## User Prompt Builder
|
|
172
|
+
|
|
173
|
+
Build structured user prompts with optional spec refinement and state context:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { buildUserPrompt } from "@json-render/core";
|
|
177
|
+
|
|
178
|
+
// Fresh generation
|
|
179
|
+
buildUserPrompt({ prompt: "create a todo app" });
|
|
180
|
+
|
|
181
|
+
// Refinement with edit modes (default: patch-only)
|
|
182
|
+
buildUserPrompt({ prompt: "add a toggle", currentSpec: spec, editModes: ["patch", "merge"] });
|
|
183
|
+
|
|
184
|
+
// With runtime state
|
|
185
|
+
buildUserPrompt({ prompt: "show data", state: { todos: [] } });
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Available edit modes: `"patch"` (RFC 6902 JSON Patch), `"merge"` (RFC 7396 Merge Patch), `"diff"` (unified diff).
|
|
189
|
+
|
|
190
|
+
## Spec Validation
|
|
191
|
+
|
|
192
|
+
Validate spec structure and auto-fix common issues:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { validateSpec, autoFixSpec } from "@json-render/core";
|
|
196
|
+
|
|
197
|
+
const { valid, issues } = validateSpec(spec);
|
|
198
|
+
// issues include: missing_child, invalid_visible (malformed conditions),
|
|
199
|
+
// repeat_without_children, repeat_item_outside_scope, repeat_state_mismatch
|
|
200
|
+
|
|
201
|
+
const { spec: fixed, fixDetails } = autoFixSpec(spec);
|
|
202
|
+
// fixDetails entries are { message, lossy }. Lossless fixes relocate
|
|
203
|
+
// misplaced fields; lossy fixes prune dangling children references.
|
|
204
|
+
// In a repair loop, withhold lossy fixes until retries are exhausted:
|
|
205
|
+
const attempt = autoFixSpec(spec, { lossy: retriesExhausted });
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Visibility Conditions
|
|
209
|
+
|
|
210
|
+
Control element visibility with state-based conditions. `VisibilityContext` is `{ stateModel: StateModel }`.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { visibility } from "@json-render/core";
|
|
214
|
+
|
|
215
|
+
// Syntax
|
|
216
|
+
{ "$state": "/path" } // truthiness
|
|
217
|
+
{ "$state": "/path", "not": true } // falsy
|
|
218
|
+
{ "$state": "/path", "eq": value } // equality
|
|
219
|
+
[ cond1, cond2 ] // implicit AND
|
|
220
|
+
|
|
221
|
+
// Helpers
|
|
222
|
+
visibility.when("/path") // { $state: "/path" }
|
|
223
|
+
visibility.unless("/path") // { $state: "/path", not: true }
|
|
224
|
+
visibility.eq("/path", val) // { $state: "/path", eq: val }
|
|
225
|
+
visibility.and(cond1, cond2) // { $and: [cond1, cond2] }
|
|
226
|
+
visibility.or(cond1, cond2) // { $or: [cond1, cond2] }
|
|
227
|
+
visibility.always // true
|
|
228
|
+
visibility.never // false
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Built-in Actions in Schema
|
|
232
|
+
|
|
233
|
+
Schemas can declare `builtInActions` -- actions that are always available at runtime and auto-injected into prompts:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const schema = defineSchema(builder, {
|
|
237
|
+
builtInActions: [
|
|
238
|
+
{ name: "setState", description: "Update a value in the state model" },
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
These appear in prompts as `[built-in]` and don't require handlers in `defineRegistry`.
|
|
244
|
+
|
|
245
|
+
## StateStore
|
|
246
|
+
|
|
247
|
+
The `StateStore` interface allows external state management libraries (Redux, Zustand, XState, etc.) to be plugged into json-render renderers. The `createStateStore` factory creates a simple in-memory implementation:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { createStateStore, type StateStore } from "@json-render/core";
|
|
251
|
+
|
|
252
|
+
const store = createStateStore({ count: 0 });
|
|
253
|
+
|
|
254
|
+
store.get("/count"); // 0
|
|
255
|
+
store.set("/count", 1); // updates and notifies subscribers
|
|
256
|
+
store.update({ "/a": 1, "/b": 2 }); // batch update
|
|
257
|
+
|
|
258
|
+
store.subscribe(() => {
|
|
259
|
+
console.log(store.getSnapshot()); // { count: 1 }
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The `StateStore` interface: `get(path)`, `set(path, value)`, `update(updates)`, `getSnapshot()`, `subscribe(listener)`.
|
|
264
|
+
|
|
265
|
+
## Key Exports
|
|
266
|
+
|
|
267
|
+
| Export | Purpose |
|
|
268
|
+
|--------|---------|
|
|
269
|
+
| `defineSchema` | Create a new schema |
|
|
270
|
+
| `defineCatalog` | Create a catalog from schema |
|
|
271
|
+
| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |
|
|
272
|
+
| `resolvePropValue` | Resolve a single prop expression against data |
|
|
273
|
+
| `resolveElementProps` | Resolve all prop expressions in an element |
|
|
274
|
+
| `buildUserPrompt` | Build user prompts with refinement and state context |
|
|
275
|
+
| `buildEditUserPrompt` | Build user prompt for editing existing specs |
|
|
276
|
+
| `buildEditInstructions` | Generate prompt section for available edit modes |
|
|
277
|
+
| `isNonEmptySpec` | Check if spec has root and at least one element |
|
|
278
|
+
| `deepMergeSpec` | RFC 7396 deep merge (null deletes, arrays replace, objects recurse) |
|
|
279
|
+
| `diffToPatches` | Generate RFC 6902 JSON Patch operations from object diff |
|
|
280
|
+
| `EditMode` | Type: `"patch" \| "merge" \| "diff"` |
|
|
281
|
+
| `validateSpec` | Validate spec structure |
|
|
282
|
+
| `autoFixSpec` | Auto-fix common spec issues; classifies fixes lossy/lossless, `{ lossy: false }` withholds pruning |
|
|
283
|
+
| `createSpecStreamCompiler` | Stream JSONL patches into spec |
|
|
284
|
+
| `createJsonRenderTransform` | TransformStream separating text from JSONL in mixed streams |
|
|
285
|
+
| `parseSpecStreamLine` | Parse single JSONL line |
|
|
286
|
+
| `applySpecStreamPatch` | Apply patch to object |
|
|
287
|
+
| `StateStore` | Interface for plugging in external state management |
|
|
288
|
+
| `ComputedFunction` | Function signature for `$computed` expressions |
|
|
289
|
+
| `check` | TypeScript helpers for creating validation checks |
|
|
290
|
+
| `BuiltInAction` | Type for built-in action definitions (`name` + `description`) |
|
|
291
|
+
| `ActionBinding` | Action binding type (includes `preventDefault` field) |
|