Code Export
The @nuforge/codegen package turns a program into a clean, standalone React component — with no nuforge at runtime. The result is plain .tsx you can drop into any React project.
import { toReact } from '@nuforge/codegen';
const tsx = toReact(nu.state.program); // a .tsx component string
What it maps
- A component becomes
function Name(props: Props) { ... }, plus aPropsinterface generated from its props. - A
valstate becomesconst [x, setX] = useState(init); an assignmentx = ...becomessetX(...). @if={c}becomes{c && (<jsx/>)}.@each={(item, i) in list}becomes{list.map((item, i) => <jsx key={i} />)}.classand@classListbecomeclassName={[...].filter(Boolean).join(' ')}.- A two-way binding becomes
value={x} onChange={(e) => setX(e.target.value)}. ==and!=become===and!==.- A
valbound to an arrow function (val inc = () => count = count + 1;) becomes a plain callback —const inc = () => setCount(count + 1);— notuseState, whether it's called inline ({f(3)}) or passed by reference (onClick={inc}). - A nested-state assignment (
obj.a = obj.a + 1) clones through every ancestor level and calls the setter —setObj({ ...obj, a: obj.a + 1 })— rather than mutating the state object in place. - The default
<slot/>becomes{children}, and the component'spropsalways destructureschildrenwhen its template contains one (see Slots below).
Example
Input DSL:
component App() {
val name = "nuforge";
} => (
<div>
<input value:={name} />
<strong>{name}</strong>
</div>
)
Generated output:
import { useState } from 'react';
export function App() {
const [name, setName] = useState("nuforge");
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<strong>{name}</strong>
</div>
);
}
This powers an "export to real code" feature: a no-code builder can hand the user a real, framework-native component instead of locking them into a proprietary runtime. It is a key differentiator — the output owes nothing to nuforge once exported.
Next.js: 'use client' only when needed
The output follows the App Router convention. toReact adds the leading 'use client' directive only when the module actually needs the client — a component with state, an event handler, or a two-way binding. A purely presentational page stays a React Server Component (no directive, and no useState import):
// static page → Server Component
export function Card(props: CardProps) {
const { title = 'Card' } = props;
return <h3 className="card">{title}</h3>;
}
// has state → client module
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Pass toReact(program, { useClient: true }) to force the directive, or imports: false to inline into an existing module.
External components & imports
If your page uses components from another package — e.g. an external <$Motion/>
backed by motion/react, or an icon component — map their names so the export
stays self-contained. Only names actually used (and not defined locally) are
imported, grouped per module:
const tsx = toReact(program, {
componentImports: {
Motion: { from: 'motion/react', named: 'motion' }, // import { motion as Motion } from 'motion/react'
Icon: 'lucide-react', // import { Icon } from 'lucide-react'
Chart: { from: './chart', default: true }, // import Chart from './chart'
},
});
'use client';
import { motion as Motion } from 'motion/react';
export function App() {
return (
<Motion animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4 }}>
Hello
</Motion>
);
}
This is what keeps an animated page (Motion / GSAP wrappers / design-system components) intact once exported.
Slots
A default (unnamed) <slot/> exports to React as {children} and to Vue as <slot /> — the component's generated props/interface always includes children, even when it declares no other props.
component Card() {} => (<div class={"card"}><slot /></div>)
interface CardProps {
children?: any;
}
export function Card(props: CardProps) {
const { children } = props;
return (
<div className="card">
{children}
</div>
);
}
Named slots (<slot @name="header" />) export to Vue as <slot name="header" /> — Vue has native named-slot support. React does not have an equivalent mechanism yet: toReact throws a clear error naming the slot rather than silently emitting the same {children} for every named slot (which would lose the distinction between them and produce broken output). This is a known, deliberate limitation — build named-slot content as separate reusable components instead until React codegen supports it.
Other targets: HTML and Vue
Export is not limited to React.
toHtml(frame.view) renders an evaluated View tree to a static HTML string — no JavaScript, no nuforge runtime. Good for static export, email templates, and previews. Event handlers and external (host React) components are omitted by design — static HTML has no JavaScript to attach a handler to, or a React tree to mount an external component into. This isn't a gap to fix; if you need working handlers or external components, export to React instead.
import { Nu } from '@nuforge/core';
import { toHtml } from '@nuforge/codegen';
const nu = Nu.fromSource(SOURCE);
const frame = nu.createFrame({ component: { name: 'App' } });
const html = toHtml(frame.view); // a static HTML string
toVue(program, opts?) emits a Vue 3 Single-File Component: val becomes ref, @if/@each become v-if/v-for, two-way bindings become v-model, and events become @event.
import { toVue } from '@nuforge/codegen';
const sfc = toVue(program, { component: 'App' });
<script setup lang="ts">
import { ref } from 'vue';
const name = ref("nuforge");
</script>
<template>
<input v-model="name" />
<strong>{{ name }}</strong>
</template>
Reactive-props destructure in the generated SFC requires Vue 3.5+.
Next steps
- React Rendering — render a Frame's reactive View tree live with React.
- Editor SDK — build the visual editing experience around a program.