Code Export
Convert specs to copy-paste React/JSX code
The specToJSX() function converts any Compose spec into a self-contained React component file. This lets users go from AI-generated UI to real, editable code in one step.
Basic Usage
import { specToJSX } from "@prototyperco/compose";
const spec = {
root: "card",
elements: {
card: { type: "Card", props: {}, children: ["heading", "btn"] },
heading: { type: "Heading", props: { text: "Hello World", level: 2 } },
btn: { type: "Button", props: { label: "Click me" } },
},
};
const code = specToJSX(spec);Output:
"use client";
import { Card } from "@/registry/ui/card";
import { Button } from "@/registry/ui/button";
export function GeneratedUI() {
return (
<Card>
<h2>Hello World</h2>
<Button>Click me</Button>
</Card>
);
}Options
Pass a CodegenOptions object to customize the output:
const code = specToJSX(spec, {
indent: 4,
componentName: "LoginForm",
importPrefix: "@/components/ui",
});Prop
Type
How Components Map to Imports
Each component type maps to an import file. For example, Button imports from {prefix}/button, CardTitle imports from {prefix}/card, and SelectItem imports from {prefix}/select. Multiple components from the same file are grouped into a single import statement.
Two component types map to HTML elements instead of imports:
Headingrenders as<h1>through<h6>based on thelevelpropTextrenders as<p>
Dynamic Value Conversion
State Expressions
$state expressions convert to variable references using camelCase naming derived from the JSON Pointer path:
{ "text": { "$state": "/user/name" } }{
userName;
}Two-Way Bindings
$bindState expressions generate value + onChange pairs appropriate to the component type:
| Component | Generated Code |
|---|---|
| Input, Textarea | value={email} onChange={(e) => setEmail(e.target.value)} |
| Checkbox, Switch | checked={agreed} onCheckedChange={setAgreed} |
| Select, RadioGroup | value={role} onValueChange={setRole} |
| Slider | value={[volume]} onValueChange={([v]) => setVolume(v)} |
Template Expressions
$template strings convert to template literals:
{ "content": { "$template": "Hello ${/name}!" } }{
`Hello ${name}!`;
}Computed and Conditional Expressions
$computed and $cond expressions generate placeholder comments since their logic cannot be statically resolved:
{
/* computed: formatDate */
}
{
/* conditional value */
}Item and Index Expressions
Inside repeated elements, $item and $index expressions reference the .map() callback parameters:
{
item.title;
}
{
index;
}State Management
When the spec includes a state object, specToJSX generates useState hooks for each top-level state key:
{ "state": { "count": 0, "items": [], "name": "World" } }import { useState } from "react";
export function GeneratedUI() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
const [name, setName] = useState("World");
// ...
}Actions
Event bindings in the on field convert to inline handlers:
| Action | Generated Code |
|---|---|
setState | setCount(5) or setName(otherVar) |
toggleState | setActive((prev) => !prev) |
appendItem | setItems((prev) => [...prev, newItem]) |
removeItem | setItems((prev) => prev.filter((_, i) => i !== index)) |
| Custom actions | Comment with action name and params |
The press event maps to onClick in React.
Repeat (Lists)
Elements with a repeat binding generate .map() calls:
{
"type": "Card",
"repeat": { "source": "/todos", "itemKey": "id" },
"children": ["todoItem"]
}{
todos.map((item) => <Card key={item.id}>{/* children */}</Card>);
}Visibility (Conditional Rendering)
Elements with visible conditions generate conditional expressions:
{
"type": "Text",
"props": { "content": "Loading..." },
"visible": { "$state": "/isLoading" }
}{
isLoading && <p>Loading...</p>;
}Complex conditions use the appropriate operators:
{ "visible": { "$state": "/count", "gt": 0 } }{count > 0 && (
// ...
)}Limitations
The code export is designed to produce a working starting point, not a perfect final result. Some dynamic behavior cannot be statically converted:
$computedexpressions become comments since the function implementations live outside the spec$condexpressions become placeholder comments- Custom actions (not
setState/toggleState/appendItem/removeItem) generate comment stubs - Cross-field validation is not included in the exported code
- State is flat — nested state paths are converted to flat
useStatecalls at the top level
The intended workflow is: AI generates a spec, the user previews it live with the Renderer, and then exports to code for further customization.
Use Case
AI Model --> Streaming Spec --> Live Preview (Renderer)
|
[Export]
|
JSX Component --> Customize & ShipThe exported code uses your project's actual component imports, so it integrates directly into your codebase without any Compose runtime dependency.