Refactored dev-docs folder after multiple additions to organise correctly

This commit is contained in:
Richard Osborne
2026-01-07 20:28:40 +01:00
parent beff9f0886
commit 4a1080d547
125 changed files with 18456 additions and 957 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,750 @@
# CODE-002: Visual Node Generator
## Overview
The Visual Node Generator transforms Noodl's visual component tree (Groups, Text, Images, Buttons, etc.) into clean React components with proper styling. This is the most straightforward part of code export since visual nodes map directly to HTML/React elements.
**Estimated Effort:** 1-2 weeks
**Priority:** HIGH
**Dependencies:** CODE-001 (@nodegx/core)
**Blocks:** CODE-006 (Project Scaffolding)
---
## Node → Element Mapping
### Container Nodes
| Noodl Node | React Element | CSS Layout | Notes |
|------------|---------------|------------|-------|
| Group | `<div>` | Flexbox | Main container |
| Page | `<div>` + Route | Flexbox | Route wrapper |
| Columns | `<div>` | CSS Grid | Multi-column |
| Circle | `<div>` | border-radius: 50% | Shape |
| Rectangle | `<div>` | - | Shape |
### Content Nodes
| Noodl Node | React Element | Notes |
|------------|---------------|-------|
| Text | `<span>` / `<p>` | Based on multiline |
| Image | `<img>` | With loading states |
| Video | `<video>` | With controls |
| Icon | `<svg>` / Icon component | From icon library |
| Lottie | LottiePlayer component | Animation |
### Form Nodes
| Noodl Node | React Element | Notes |
|------------|---------------|-------|
| Button | `<button>` | With states |
| TextInput | `<input>` | Controlled |
| TextArea | `<textarea>` | Controlled |
| Checkbox | `<input type="checkbox">` | Controlled |
| Radio Button | `<input type="radio">` | With group |
| Dropdown | `<select>` | Controlled |
| Slider | `<input type="range">` | Controlled |
### Navigation Nodes
| Noodl Node | Generated Code | Notes |
|------------|----------------|-------|
| Page Router | `<Routes>` | React Router |
| Navigate | `useNavigate()` | Imperative |
| External Link | `<a href>` | With target |
---
## Style Mapping
### Layout Properties
```typescript
interface NoodlLayoutProps {
// Position
position: 'relative' | 'absolute' | 'fixed' | 'sticky';
positionX?: number | string;
positionY?: number | string;
// Size
width?: number | string | 'auto' | 'content';
height?: number | string | 'auto' | 'content';
minWidth?: number | string;
maxWidth?: number | string;
minHeight?: number | string;
maxHeight?: number | string;
// Flexbox (as container)
flexDirection: 'row' | 'column';
justifyContent: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around';
alignItems: 'flex-start' | 'center' | 'flex-end' | 'stretch';
gap?: number;
flexWrap?: 'nowrap' | 'wrap';
// Flexbox (as child)
alignSelf?: 'auto' | 'flex-start' | 'center' | 'flex-end' | 'stretch';
flexGrow?: number;
flexShrink?: number;
}
// Generated CSS
function generateLayoutCSS(props: NoodlLayoutProps): string {
const styles: string[] = [];
// Position
if (props.position !== 'relative') {
styles.push(`position: ${props.position}`);
}
if (props.positionX !== undefined) {
styles.push(`left: ${formatValue(props.positionX)}`);
}
if (props.positionY !== undefined) {
styles.push(`top: ${formatValue(props.positionY)}`);
}
// Size
if (props.width !== undefined && props.width !== 'auto') {
styles.push(`width: ${formatValue(props.width)}`);
}
// ... etc
// Flexbox
styles.push(`display: flex`);
styles.push(`flex-direction: ${props.flexDirection}`);
styles.push(`justify-content: ${props.justifyContent}`);
styles.push(`align-items: ${props.alignItems}`);
if (props.gap) {
styles.push(`gap: ${props.gap}px`);
}
return styles.join(';\n');
}
```
### Appearance Properties
```typescript
interface NoodlAppearanceProps {
// Background
backgroundColor?: string;
backgroundImage?: string;
backgroundSize?: 'cover' | 'contain' | 'auto';
// Border
borderWidth?: number;
borderColor?: string;
borderStyle?: 'solid' | 'dashed' | 'dotted';
borderRadius?: number | string;
// Shadow
boxShadow?: string;
boxShadowEnabled?: boolean;
boxShadowX?: number;
boxShadowY?: number;
boxShadowBlur?: number;
boxShadowSpread?: number;
boxShadowColor?: string;
// Effects
opacity?: number;
transform?: string;
transformOrigin?: string;
filter?: string;
// Overflow
overflow?: 'visible' | 'hidden' | 'scroll' | 'auto';
overflowX?: 'visible' | 'hidden' | 'scroll' | 'auto';
overflowY?: 'visible' | 'hidden' | 'scroll' | 'auto';
}
function generateAppearanceCSS(props: NoodlAppearanceProps): string {
const styles: string[] = [];
// Background
if (props.backgroundColor) {
styles.push(`background-color: ${props.backgroundColor}`);
}
// Border
if (props.borderWidth) {
styles.push(`border: ${props.borderWidth}px ${props.borderStyle || 'solid'} ${props.borderColor || '#000'}`);
}
if (props.borderRadius) {
styles.push(`border-radius: ${formatValue(props.borderRadius)}`);
}
// Shadow
if (props.boxShadowEnabled) {
const shadow = `${props.boxShadowX || 0}px ${props.boxShadowY || 0}px ${props.boxShadowBlur || 0}px ${props.boxShadowSpread || 0}px ${props.boxShadowColor || 'rgba(0,0,0,0.2)'}`;
styles.push(`box-shadow: ${shadow}`);
}
// Opacity
if (props.opacity !== undefined && props.opacity !== 1) {
styles.push(`opacity: ${props.opacity}`);
}
return styles.join(';\n');
}
```
### Typography Properties
```typescript
interface NoodlTypographyProps {
fontFamily?: string;
fontSize?: number | string;
fontWeight?: number | string;
fontStyle?: 'normal' | 'italic';
textAlign?: 'left' | 'center' | 'right' | 'justify';
lineHeight?: number | string;
letterSpacing?: number | string;
textDecoration?: 'none' | 'underline' | 'line-through';
textTransform?: 'none' | 'uppercase' | 'lowercase' | 'capitalize';
color?: string;
}
function generateTypographyCSS(props: NoodlTypographyProps): string {
const styles: string[] = [];
if (props.fontFamily) styles.push(`font-family: ${props.fontFamily}`);
if (props.fontSize) styles.push(`font-size: ${formatValue(props.fontSize)}`);
if (props.fontWeight) styles.push(`font-weight: ${props.fontWeight}`);
if (props.color) styles.push(`color: ${props.color}`);
// ... etc
return styles.join(';\n');
}
```
---
## Component Generation
### Basic Group Component
**Noodl Node:**
```json
{
"id": "group-1",
"type": "Group",
"parameters": {
"backgroundColor": "#ffffff",
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 12,
"paddingBottom": 12,
"borderRadius": 8,
"flexDirection": "column",
"gap": 8
},
"children": ["text-1", "button-1"]
}
```
**Generated Component:**
```tsx
// components/Card.tsx
import styles from './Card.module.css';
interface CardProps {
children?: React.ReactNode;
}
export function Card({ children }: CardProps) {
return (
<div className={styles.root}>
{children}
</div>
);
}
```
**Generated CSS:**
```css
/* components/Card.module.css */
.root {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 16px;
background-color: #ffffff;
border-radius: 8px;
}
```
### Text Node
**Noodl Node:**
```json
{
"id": "text-1",
"type": "Text",
"parameters": {
"text": "Hello World",
"fontSize": 18,
"fontWeight": 600,
"color": "#333333"
}
}
```
**Generated Code:**
```tsx
// Inline in parent component
<span className={styles.text1}>Hello World</span>
// Or with dynamic text binding
<span className={styles.text1}>{titleVar.get()}</span>
```
**Generated CSS:**
```css
.text1 {
font-size: 18px;
font-weight: 600;
color: #333333;
}
```
### Button with States
**Noodl Node:**
```json
{
"id": "button-1",
"type": "Button",
"parameters": {
"label": "Click Me",
"backgroundColor": "#3b82f6",
"hoverBackgroundColor": "#2563eb",
"pressedBackgroundColor": "#1d4ed8",
"disabledBackgroundColor": "#94a3b8",
"borderRadius": 6,
"paddingLeft": 16,
"paddingRight": 16,
"paddingTop": 8,
"paddingBottom": 8
},
"stateParameters": {
"hover": { "backgroundColor": "#2563eb" },
"pressed": { "backgroundColor": "#1d4ed8" },
"disabled": { "backgroundColor": "#94a3b8", "opacity": 0.6 }
}
}
```
**Generated Component:**
```tsx
// components/PrimaryButton.tsx
import { ButtonHTMLAttributes } from 'react';
import styles from './PrimaryButton.module.css';
interface PrimaryButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
label?: string;
}
export function PrimaryButton({
label = 'Click Me',
children,
className,
...props
}: PrimaryButtonProps) {
return (
<button
className={`${styles.root} ${className || ''}`}
{...props}
>
{children || label}
</button>
);
}
```
**Generated CSS:**
```css
/* components/PrimaryButton.module.css */
.root {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.15s ease;
}
.root:hover:not(:disabled) {
background-color: #2563eb;
}
.root:active:not(:disabled) {
background-color: #1d4ed8;
}
.root:disabled {
background-color: #94a3b8;
opacity: 0.6;
cursor: not-allowed;
}
```
### Repeater (For Each)
**Noodl Node:**
```json
{
"id": "repeater-1",
"type": "For Each",
"parameters": {
"items": "{{users}}"
},
"templateComponent": "UserCard",
"children": []
}
```
**Generated Code:**
```tsx
import { useArray, RepeaterItemProvider } from '@nodegx/core';
import { usersArray } from '../stores/arrays';
import { UserCard } from './UserCard';
export function UserList() {
const users = useArray(usersArray);
return (
<div className={styles.root}>
{users.map((user, index) => (
<RepeaterItemProvider
key={user.id || index}
item={user}
index={index}
itemId={`user_${user.id || index}`}
>
<UserCard />
</RepeaterItemProvider>
))}
</div>
);
}
```
### Component Children
**Noodl Node Structure:**
```
MyWrapper
├── Header
├── [Component Children] ← Slot for children
└── Footer
```
**Generated Code:**
```tsx
// components/MyWrapper.tsx
import styles from './MyWrapper.module.css';
interface MyWrapperProps {
children?: React.ReactNode;
}
export function MyWrapper({ children }: MyWrapperProps) {
return (
<div className={styles.root}>
<Header />
<div className={styles.content}>
{children}
</div>
<Footer />
</div>
);
}
```
---
## Visual States Handling
### State Transitions
Noodl supports animated transitions between visual states. For basic hover/pressed states, we use CSS transitions. For complex state machines, we use the `@nodegx/core` state machine.
**CSS Approach (simple states):**
```css
.button {
background-color: #3b82f6;
transition:
background-color 0.2s ease,
transform 0.15s ease,
opacity 0.2s ease;
}
.button:hover {
background-color: #2563eb;
transform: scale(1.02);
}
.button:active {
background-color: #1d4ed8;
transform: scale(0.98);
}
```
**State Machine Approach (complex states):**
```tsx
import { useStateMachine, useStateValues, createStateMachine } from '@nodegx/core';
// Define state machine with values for each state
const cardState = createStateMachine({
states: ['idle', 'hover', 'expanded', 'loading'],
initial: 'idle',
values: {
idle: { scale: 1, opacity: 1, height: 100 },
hover: { scale: 1.02, opacity: 1, height: 100 },
expanded: { scale: 1, opacity: 1, height: 300 },
loading: { scale: 1, opacity: 0.7, height: 100 }
}
});
export function Card() {
const [state, goTo] = useStateMachine(cardState);
const values = useStateValues(cardState);
return (
<div
className={styles.root}
style={{
transform: `scale(${values.scale})`,
opacity: values.opacity,
height: values.height
}}
onMouseEnter={() => state === 'idle' && goTo('hover')}
onMouseLeave={() => state === 'hover' && goTo('idle')}
onClick={() => goTo('expanded')}
>
{/* content */}
</div>
);
}
```
---
## Input Handling
### Controlled Components
All form inputs are generated as controlled components:
```tsx
import { useVariable } from '@nodegx/core';
import { searchQueryVar } from '../stores/variables';
export function SearchInput() {
const [query, setQuery] = useVariable(searchQueryVar);
return (
<input
type="text"
className={styles.input}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
```
### Event Connections
Noodl connections from visual node events become props or inline handlers:
**Click → Signal:**
```tsx
import { onButtonClick } from '../logic/handlers';
<button onClick={() => onButtonClick.send()}>
Click Me
</button>
```
**Click → Function:**
```tsx
import { handleSubmit } from '../logic/formHandlers';
<button onClick={handleSubmit}>
Submit
</button>
```
**Click → Navigate:**
```tsx
import { useNavigate } from 'react-router-dom';
export function NavButton() {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/dashboard')}>
Go to Dashboard
</button>
);
}
```
---
## Image Handling
### Static Images
```tsx
// Image in assets folder
<img
src="/assets/logo.png"
alt="Logo"
className={styles.logo}
/>
```
### Dynamic Images
```tsx
// From variable or object
const user = useObject(currentUser);
<img
src={user.avatar || '/assets/default-avatar.png'}
alt={user.name}
className={styles.avatar}
/>
```
### Loading States
```tsx
import { useState } from 'react';
export function LazyImage({ src, alt, className }: LazyImageProps) {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(false);
return (
<div className={`${styles.imageContainer} ${className}`}>
{!loaded && !error && (
<div className={styles.skeleton} />
)}
{error && (
<div className={styles.errorPlaceholder}>
Failed to load
</div>
)}
<img
src={src}
alt={alt}
className={`${styles.image} ${loaded ? styles.visible : styles.hidden}`}
onLoad={() => setLoaded(true)}
onError={() => setError(true)}
/>
</div>
);
}
```
---
## Code Generation Algorithm
```typescript
interface GenerateVisualNodeOptions {
node: NoodlNode;
componentName: string;
outputDir: string;
cssMode: 'modules' | 'tailwind' | 'inline';
}
async function generateVisualComponent(options: GenerateVisualNodeOptions) {
const { node, componentName, outputDir, cssMode } = options;
// 1. Analyze node and children
const analysis = analyzeVisualNode(node);
// 2. Determine if this needs a separate component file
const needsSeparateFile =
analysis.hasChildren ||
analysis.hasStateLogic ||
analysis.hasEventHandlers ||
analysis.isReusable;
// 3. Generate styles
const styles = generateStyles(node, cssMode);
// 4. Generate component code
const componentCode = generateComponentCode({
node,
componentName,
styles,
analysis
});
// 5. Write files
if (needsSeparateFile) {
await writeFile(`${outputDir}/${componentName}.tsx`, componentCode);
if (cssMode === 'modules') {
await writeFile(`${outputDir}/${componentName}.module.css`, styles.css);
}
}
return {
componentName,
inlineCode: needsSeparateFile ? null : componentCode,
imports: analysis.imports
};
}
```
---
## Testing Checklist
### Visual Parity Tests
- [ ] Group renders with correct flexbox layout
- [ ] Text displays with correct typography
- [ ] Image loads and displays correctly
- [ ] Button states (hover, pressed, disabled) work
- [ ] Repeater renders all items
- [ ] Component Children slot works
- [ ] Nested components render correctly
### Style Tests
- [ ] All spacing values (margin, padding, gap) correct
- [ ] Border radius renders correctly
- [ ] Box shadows render correctly
- [ ] Colors match exactly
- [ ] Responsive units (%,vh,vw) work
- [ ] CSS transitions animate smoothly
### Interaction Tests
- [ ] Click handlers fire correctly
- [ ] Form inputs are controlled
- [ ] Mouse enter/leave events work
- [ ] Focus states display correctly
- [ ] Keyboard navigation works
---
## Success Criteria
1. **Visual Match** - Exported app looks identical to Noodl preview
2. **Clean Code** - Generated components are readable and maintainable
3. **Proper Typing** - Full TypeScript types for all props
4. **Accessibility** - Proper ARIA attributes, semantic HTML
5. **Performance** - No unnecessary re-renders, proper memoization

View File

@@ -0,0 +1,832 @@
# CODE-003: State Store Generator
## Overview
The State Store Generator creates the reactive state management layer from Noodl's Variable, Object, and Array nodes. It also handles Component Object, Parent Component Object, and Repeater Object patterns. These stores are the backbone of application state in exported code.
**Estimated Effort:** 1-2 weeks
**Priority:** HIGH
**Dependencies:** CODE-001 (@nodegx/core)
**Blocks:** CODE-004 (Logic Node Generator)
---
## Store Types
### 1. Variables (Global Reactive Values)
**Noodl Pattern:**
- Variable nodes create named global values
- Set Variable nodes update them
- Any component can read/write
**Generated Structure:**
```
stores/
├── variables.ts ← All Variable definitions
└── index.ts ← Re-exports everything
```
### 2. Objects (Reactive Key-Value Stores)
**Noodl Pattern:**
- Object nodes read from a named object by ID
- Set Object Properties nodes update them
- Can have dynamic property names
**Generated Structure:**
```
stores/
├── objects.ts ← All Object definitions
└── index.ts
```
### 3. Arrays (Reactive Lists)
**Noodl Pattern:**
- Array nodes read from a named array by ID
- Insert Into Array, Remove From Array modify them
- Static Array nodes are just constants
**Generated Structure:**
```
stores/
├── arrays.ts ← Reactive Array definitions
├── staticArrays.ts ← Constant array data
└── index.ts
```
---
## Analysis Phase
Before generating stores, we need to analyze the entire project:
```typescript
interface StoreAnalysis {
variables: Map<string, VariableInfo>;
objects: Map<string, ObjectInfo>;
arrays: Map<string, ArrayInfo>;
staticArrays: Map<string, StaticArrayInfo>;
componentStores: Map<string, ComponentStoreInfo>;
}
interface VariableInfo {
name: string;
initialValue: any;
type: 'string' | 'number' | 'boolean' | 'color' | 'any';
usedInComponents: string[];
setByComponents: string[];
}
interface ObjectInfo {
id: string;
properties: Map<string, PropertyInfo>;
usedInComponents: string[];
}
interface ArrayInfo {
id: string;
itemType: 'object' | 'primitive' | 'mixed';
sampleItem?: any;
usedInComponents: string[];
}
function analyzeProjectStores(project: NoodlProject): StoreAnalysis {
const analysis: StoreAnalysis = {
variables: new Map(),
objects: new Map(),
arrays: new Map(),
staticArrays: new Map(),
componentStores: new Map()
};
// Scan all components for Variable nodes
for (const component of project.components) {
for (const node of component.nodes) {
switch (node.type) {
case 'Variable':
case 'String':
case 'Number':
case 'Boolean':
case 'Color':
analyzeVariableNode(node, component.name, analysis);
break;
case 'Set Variable':
analyzeSetVariableNode(node, component.name, analysis);
break;
case 'Object':
analyzeObjectNode(node, component.name, analysis);
break;
case 'Set Object Properties':
analyzeSetObjectNode(node, component.name, analysis);
break;
case 'Array':
analyzeArrayNode(node, component.name, analysis);
break;
case 'Static Array':
analyzeStaticArrayNode(node, component.name, analysis);
break;
case 'net.noodl.ComponentObject':
case 'Component State':
analyzeComponentStateNode(node, component.name, analysis);
break;
}
}
}
return analysis;
}
```
---
## Variable Generation
### Single Variable Nodes
**Noodl Variable Node:**
```json
{
"type": "Variable",
"parameters": {
"name": "isLoggedIn",
"value": false
}
}
```
**Generated Code:**
```typescript
// stores/variables.ts
import { createVariable } from '@nodegx/core';
/**
* User authentication status
* @used-in LoginForm, Header, ProfilePage
*/
export const isLoggedInVar = createVariable('isLoggedIn', false);
```
### Typed Variable Nodes
**Noodl String Node:**
```json
{
"type": "String",
"parameters": {
"name": "searchQuery",
"value": ""
}
}
```
**Generated Code:**
```typescript
// stores/variables.ts
import { createVariable } from '@nodegx/core';
export const searchQueryVar = createVariable<string>('searchQuery', '');
export const itemCountVar = createVariable<number>('itemCount', 0);
export const isDarkModeVar = createVariable<boolean>('isDarkMode', false);
export const primaryColorVar = createVariable<string>('primaryColor', '#3b82f6');
```
### Complete Variables File
```typescript
// stores/variables.ts
import { createVariable, Variable } from '@nodegx/core';
// ============================================
// Authentication
// ============================================
export const isLoggedInVar = createVariable<boolean>('isLoggedIn', false);
export const currentUserIdVar = createVariable<string>('currentUserId', '');
export const authTokenVar = createVariable<string>('authToken', '');
// ============================================
// UI State
// ============================================
export const isDarkModeVar = createVariable<boolean>('isDarkMode', false);
export const sidebarOpenVar = createVariable<boolean>('sidebarOpen', true);
export const activeTabVar = createVariable<string>('activeTab', 'home');
// ============================================
// Search & Filters
// ============================================
export const searchQueryVar = createVariable<string>('searchQuery', '');
export const filterCategoryVar = createVariable<string>('filterCategory', 'all');
export const sortOrderVar = createVariable<'asc' | 'desc'>('sortOrder', 'desc');
// ============================================
// Form State
// ============================================
export const formEmailVar = createVariable<string>('formEmail', '');
export const formPasswordVar = createVariable<string>('formPassword', '');
export const formErrorVar = createVariable<string>('formError', '');
// ============================================
// Type-safe Variable Registry (optional)
// ============================================
export const variables = {
isLoggedIn: isLoggedInVar,
currentUserId: currentUserIdVar,
authToken: authTokenVar,
isDarkMode: isDarkModeVar,
sidebarOpen: sidebarOpenVar,
activeTab: activeTabVar,
searchQuery: searchQueryVar,
filterCategory: filterCategoryVar,
sortOrder: sortOrderVar,
formEmail: formEmailVar,
formPassword: formPasswordVar,
formError: formErrorVar,
} as const;
export type VariableName = keyof typeof variables;
```
---
## Object Generation
### Object Node Analysis
**Noodl Object Node:**
```json
{
"type": "Object",
"parameters": {
"idSource": "explicit",
"objectId": "currentUser"
},
"dynamicports": [
{ "name": "name", "type": "string" },
{ "name": "email", "type": "string" },
{ "name": "avatar", "type": "string" },
{ "name": "role", "type": "string" }
]
}
```
**Generated Code:**
```typescript
// stores/objects.ts
import { createObject, ReactiveObject } from '@nodegx/core';
// ============================================
// User Objects
// ============================================
export interface CurrentUser {
id: string;
name: string;
email: string;
avatar: string;
role: 'admin' | 'user' | 'guest';
}
export const currentUserObj = createObject<CurrentUser>('currentUser', {
id: '',
name: '',
email: '',
avatar: '',
role: 'guest'
});
// ============================================
// Settings Objects
// ============================================
export interface AppSettings {
theme: 'light' | 'dark' | 'system';
language: string;
notifications: boolean;
autoSave: boolean;
}
export const appSettingsObj = createObject<AppSettings>('appSettings', {
theme: 'system',
language: 'en',
notifications: true,
autoSave: true
});
// ============================================
// Form Data Objects
// ============================================
export interface ContactForm {
name: string;
email: string;
subject: string;
message: string;
}
export const contactFormObj = createObject<ContactForm>('contactForm', {
name: '',
email: '',
subject: '',
message: ''
});
```
### Set Object Properties Generation
**Noodl Set Object Properties Node:**
```json
{
"type": "Set Object Properties",
"parameters": {
"idSource": "explicit",
"objectId": "currentUser"
},
"connections": [
{ "from": "loginResult.name", "to": "name" },
{ "from": "loginResult.email", "to": "email" }
]
}
```
**Generated Code:**
```typescript
// In component or logic file
import { currentUserObj } from '../stores/objects';
function updateCurrentUser(data: { name: string; email: string }) {
currentUserObj.set('name', data.name);
currentUserObj.set('email', data.email);
// Or batch update
currentUserObj.setProperties({
name: data.name,
email: data.email
});
}
```
---
## Array Generation
### Reactive Arrays
**Noodl Array Node:**
```json
{
"type": "Array",
"parameters": {
"idSource": "explicit",
"arrayId": "todoItems"
}
}
```
**Generated Code:**
```typescript
// stores/arrays.ts
import { createArray, ReactiveArray } from '@nodegx/core';
// ============================================
// Todo Items
// ============================================
export interface TodoItem {
id: string;
text: string;
completed: boolean;
createdAt: string;
}
export const todoItemsArray = createArray<TodoItem>('todoItems', []);
// ============================================
// Messages
// ============================================
export interface Message {
id: string;
content: string;
sender: string;
timestamp: string;
read: boolean;
}
export const messagesArray = createArray<Message>('messages', []);
// ============================================
// Search Results
// ============================================
export interface SearchResult {
id: string;
title: string;
description: string;
url: string;
score: number;
}
export const searchResultsArray = createArray<SearchResult>('searchResults', []);
```
### Static Arrays
**Noodl Static Array Node:**
```json
{
"type": "Static Array",
"parameters": {
"items": [
{ "label": "Home", "path": "/", "icon": "home" },
{ "label": "Products", "path": "/products", "icon": "box" },
{ "label": "About", "path": "/about", "icon": "info" },
{ "label": "Contact", "path": "/contact", "icon": "mail" }
]
}
}
```
**Generated Code:**
```typescript
// stores/staticArrays.ts
// ============================================
// Navigation Items
// ============================================
export interface NavItem {
label: string;
path: string;
icon: string;
}
export const navigationItems: NavItem[] = [
{ label: 'Home', path: '/', icon: 'home' },
{ label: 'Products', path: '/products', icon: 'box' },
{ label: 'About', path: '/about', icon: 'info' },
{ label: 'Contact', path: '/contact', icon: 'mail' }
];
// ============================================
// Dropdown Options
// ============================================
export interface SelectOption {
value: string;
label: string;
}
export const countryOptions: SelectOption[] = [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'ca', label: 'Canada' },
{ value: 'au', label: 'Australia' }
];
export const categoryOptions: SelectOption[] = [
{ value: 'all', label: 'All Categories' },
{ value: 'electronics', label: 'Electronics' },
{ value: 'clothing', label: 'Clothing' },
{ value: 'books', label: 'Books' }
];
```
### Array Manipulation
**Insert Into Array:**
```typescript
import { todoItemsArray } from '../stores/arrays';
function addTodo(text: string) {
todoItemsArray.push({
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date().toISOString()
});
}
```
**Remove From Array:**
```typescript
import { todoItemsArray } from '../stores/arrays';
function removeTodo(id: string) {
todoItemsArray.remove(item => item.id === id);
}
function clearCompleted() {
const current = todoItemsArray.get();
todoItemsArray.set(current.filter(item => !item.completed));
}
```
**Array Filter Node:**
```typescript
// In component
import { useArray, useFilteredArray } from '@nodegx/core';
import { todoItemsArray } from '../stores/arrays';
import { filterStatusVar } from '../stores/variables';
function TodoList() {
const [filterStatus] = useVariable(filterStatusVar);
const filteredTodos = useFilteredArray(todoItemsArray, (item) => {
if (filterStatus === 'all') return true;
if (filterStatus === 'active') return !item.completed;
if (filterStatus === 'completed') return item.completed;
return true;
});
return (
<ul>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
```
---
## Component Object Generation
### Component Object (Component-Scoped State)
**Noodl Component Object Node:**
- Creates state scoped to a component instance
- Each instance of the component has its own state
- Accessed via `Component.Object` in Function nodes
**Generated Code:**
```typescript
// components/Counter.tsx
import { ComponentStoreProvider, useComponentStore, useSetComponentStore } from '@nodegx/core';
interface CounterState {
count: number;
step: number;
}
function CounterInner() {
const state = useComponentStore<CounterState>();
const { set } = useSetComponentStore<CounterState>();
return (
<div>
<span>{state.count}</span>
<button onClick={() => set('count', state.count + state.step)}>
+{state.step}
</button>
</div>
);
}
export function Counter({ initialCount = 0, step = 1 }) {
return (
<ComponentStoreProvider initialState={{ count: initialCount, step }}>
<CounterInner />
</ComponentStoreProvider>
);
}
```
### Parent Component Object
**Noodl Parent Component Object Node:**
- Accesses the Component Object of the visual parent
- Used for child-to-parent communication
**Generated Code:**
```typescript
// components/ListItem.tsx
import { useParentComponentStore, useSetComponentStore } from '@nodegx/core';
interface ListState {
selectedId: string | null;
}
function ListItem({ id, label }: { id: string; label: string }) {
const parentState = useParentComponentStore<ListState>();
const isSelected = parentState?.selectedId === id;
// To update parent, we need to communicate via events or callbacks
// Since Parent Component Object is read-only from children
return (
<div className={isSelected ? 'selected' : ''}>
{label}
</div>
);
}
```
### Repeater Object (For Each Item)
**Noodl Repeater Object Node:**
- Inside a Repeater/For Each, accesses the current item
- Each iteration gets its own item context
**Generated Code:**
```typescript
// components/UserList.tsx
import { useArray, RepeaterItemProvider, useRepeaterItem } from '@nodegx/core';
import { usersArray } from '../stores/arrays';
function UserCard() {
const user = useRepeaterItem<User>();
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
export function UserList() {
const users = useArray(usersArray);
return (
<div className="user-list">
{users.map((user, index) => (
<RepeaterItemProvider
key={user.id}
item={user}
index={index}
itemId={`user_${user.id}`}
>
<UserCard />
</RepeaterItemProvider>
))}
</div>
);
}
```
---
## Store Index File
```typescript
// stores/index.ts
// Variables
export * from './variables';
// Objects
export * from './objects';
// Arrays
export * from './arrays';
// Static data
export * from './staticArrays';
// Re-export primitives for convenience
export {
useVariable,
useObject,
useArray,
useComponentStore,
useParentComponentStore,
useRepeaterItem
} from '@nodegx/core';
```
---
## Type Inference
The generator should infer types from:
1. **Explicit type nodes** (String, Number, Boolean)
2. **Initial values** in parameters
3. **Connected node outputs** (if source has type info)
4. **Property panel selections** (enums, colors)
```typescript
function inferVariableType(node: NoodlNode): string {
// Check explicit type nodes
if (node.type === 'String') return 'string';
if (node.type === 'Number') return 'number';
if (node.type === 'Boolean') return 'boolean';
if (node.type === 'Color') return 'string'; // Colors are strings
// Check initial value
const value = node.parameters.value;
if (value !== undefined) {
if (typeof value === 'string') return 'string';
if (typeof value === 'number') return 'number';
if (typeof value === 'boolean') return 'boolean';
if (Array.isArray(value)) return 'any[]';
if (typeof value === 'object') return 'Record<string, any>';
}
// Default to any
return 'any';
}
function inferObjectType(
objectId: string,
nodes: NoodlNode[],
connections: NoodlConnection[]
): string {
const properties: Map<string, string> = new Map();
// Find all Object nodes with this ID
const objectNodes = nodes.filter(
n => n.type === 'Object' && n.parameters.objectId === objectId
);
// Collect properties from dynamic ports
for (const node of objectNodes) {
if (node.dynamicports) {
for (const port of node.dynamicports) {
properties.set(port.name, inferPortType(port));
}
}
}
// Find Set Object Properties nodes
const setNodes = nodes.filter(
n => n.type === 'Set Object Properties' && n.parameters.objectId === objectId
);
// Collect more properties
for (const node of setNodes) {
if (node.dynamicports) {
for (const port of node.dynamicports) {
properties.set(port.name, inferPortType(port));
}
}
}
// Generate interface
const props = Array.from(properties.entries())
.map(([name, type]) => ` ${name}: ${type};`)
.join('\n');
return `{\n${props}\n}`;
}
```
---
## Testing Checklist
### Variable Tests
- [ ] Variables created with correct initial values
- [ ] Type inference works for all types
- [ ] Set Variable updates propagate
- [ ] Multiple components can read same variable
- [ ] Variables persist across navigation
### Object Tests
- [ ] Objects created with all properties
- [ ] Property types correctly inferred
- [ ] Set Object Properties updates work
- [ ] Dynamic properties handled
- [ ] Object ID sources work (explicit, from variable, from repeater)
### Array Tests
- [ ] Arrays created with correct types
- [ ] Static arrays are constant
- [ ] Insert Into Array adds items
- [ ] Remove From Array removes items
- [ ] Array Filter works reactively
- [ ] Repeater iterates correctly
### Component Store Tests
- [ ] Component Object scoped to instance
- [ ] Parent Component Object reads parent
- [ ] Repeater Object provides item
- [ ] Multiple instances have separate state
---
## Success Criteria
1. **All stores discovered** - No missing variables/objects/arrays
2. **Types inferred** - TypeScript types are accurate
3. **Reactivity works** - Updates propagate correctly
4. **Clean organization** - Logical file structure
5. **Good DX** - Easy to use in components

View File

@@ -0,0 +1,751 @@
# CODE-004: Logic Node Generator
## Overview
The Logic Node Generator transforms Noodl's Function nodes, Expression nodes, and logic nodes (Condition, Switch, And/Or/Not, etc.) into clean JavaScript/TypeScript code. This is one of the more complex aspects of code export because it requires understanding the data flow graph and generating appropriate code patterns.
**Estimated Effort:** 2-3 weeks
**Priority:** HIGH
**Dependencies:** CODE-001 (@nodegx/core), CODE-003 (State Store Generator)
**Blocks:** CODE-006 (Project Scaffolding)
---
## Function Node Transformation
### Noodl Function Node Structure
In Noodl, Function nodes contain JavaScript that interacts with:
- `Inputs` object - values from connected inputs
- `Outputs` object - set values to send to outputs
- `Outputs.signalName()` - send a signal
```javascript
// Noodl Function Node code
const doubled = Inputs.value * 2;
Outputs.result = doubled;
if (doubled > 100) {
Outputs.exceeded(); // Signal
}
```
### Transformation Strategy
**Option 1: Pure Function (preferred when possible)**
```typescript
// logic/mathUtils.ts
export function doubleValue(value: number): { result: number; exceeded: boolean } {
const doubled = value * 2;
return {
result: doubled,
exceeded: doubled > 100
};
}
```
**Option 2: Side-effect Function (when updating stores)**
```typescript
// logic/userActions.ts
import { createSignal, setVariable } from '@nodegx/core';
import { userNameVar, isLoggedInVar } from '../stores/variables';
export const onLoginSuccess = createSignal('onLoginSuccess');
export const onLoginFailure = createSignal('onLoginFailure');
export async function handleLogin(email: string, password: string): Promise<void> {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
if (!response.ok) throw new Error('Login failed');
const user = await response.json();
// Set variables (equivalent to Outputs.userName = user.name)
setVariable(userNameVar, user.name);
setVariable(isLoggedInVar, true);
// Send signal (equivalent to Outputs.success())
onLoginSuccess.send();
} catch (error) {
onLoginFailure.send();
}
}
```
**Option 3: Hook Function (when needing React context)**
```typescript
// hooks/useFormValidation.ts
import { useMemo } from 'react';
export function useFormValidation(email: string, password: string) {
return useMemo(() => {
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const passwordValid = password.length >= 8;
return {
emailValid,
passwordValid,
formValid: emailValid && passwordValid,
emailError: emailValid ? null : 'Invalid email address',
passwordError: passwordValid ? null : 'Password must be at least 8 characters'
};
}, [email, password]);
}
```
---
## Input/Output Mapping
### Inputs Transformation
| Noodl Pattern | Generated Code |
|---------------|----------------|
| `Inputs.value` | Function parameter |
| `Inputs["my value"]` | Function parameter (camelCased) |
| Dynamic inputs | Destructured object parameter |
```javascript
// Noodl code
const sum = Inputs.a + Inputs.b + Inputs["extra value"];
// Generated
export function calculateSum(a: number, b: number, extraValue: number): number {
return a + b + extraValue;
}
```
### Outputs Transformation
| Noodl Pattern | Generated Code |
|---------------|----------------|
| `Outputs.result = value` | Return value |
| `Outputs.signal()` | Signal.send() |
| Multiple outputs | Return object |
| Async outputs | Promise or callback |
```javascript
// Noodl code
Outputs.sum = Inputs.a + Inputs.b;
Outputs.product = Inputs.a * Inputs.b;
if (Outputs.sum > 100) {
Outputs.overflow();
}
// Generated
import { createSignal } from '@nodegx/core';
export const onOverflow = createSignal('onOverflow');
export function calculate(a: number, b: number): { sum: number; product: number } {
const sum = a + b;
const product = a * b;
if (sum > 100) {
onOverflow.send();
}
return { sum, product };
}
```
---
## Expression Node Transformation
### Simple Expressions
```javascript
// Noodl Expression
Inputs.price * Inputs.quantity * (1 - Inputs.discount)
// Generated (inline)
const total = price * quantity * (1 - discount);
// Or with useMemo if used in render
const total = useMemo(
() => price * quantity * (1 - discount),
[price, quantity, discount]
);
```
### Expressions with Noodl Globals
```javascript
// Noodl Expression
Noodl.Variables.taxRate * Inputs.subtotal
// Generated
import { useVariable } from '@nodegx/core';
import { taxRateVar } from '../stores/variables';
// In component
const [taxRate] = useVariable(taxRateVar);
const tax = taxRate * subtotal;
```
### Complex Expressions
```javascript
// Noodl Expression
Noodl.Variables.isLoggedIn
? `Welcome, ${Noodl.Objects.currentUser.name}!`
: "Please log in"
// Generated
import { useVariable, useObject } from '@nodegx/core';
import { isLoggedInVar } from '../stores/variables';
import { currentUserObj } from '../stores/objects';
// In component
const [isLoggedIn] = useVariable(isLoggedInVar);
const currentUser = useObject(currentUserObj);
const greeting = isLoggedIn
? `Welcome, ${currentUser.name}!`
: "Please log in";
```
---
## Logic Nodes Transformation
### Condition Node
```
┌──────────┐
│ Condition│
│ value ─○─┼──▶ True Path
│ │──▶ False Path
└──────────┘
```
**Generated Code:**
```typescript
// As inline conditional
{value ? <TrueComponent /> : <FalseComponent />}
// As conditional render
if (condition) {
return <TrueComponent />;
}
return <FalseComponent />;
// As signal routing
if (condition) {
onTruePath.send();
} else {
onFalsePath.send();
}
```
### Switch Node
```
┌──────────┐
│ Switch │
│ value ─○─┼──▶ case "a"
│ │──▶ case "b"
│ │──▶ case "c"
│ │──▶ default
└──────────┘
```
**Generated Code:**
```typescript
// As switch statement
function handleSwitch(value: string) {
switch (value) {
case 'a':
handleCaseA();
break;
case 'b':
handleCaseB();
break;
case 'c':
handleCaseC();
break;
default:
handleDefault();
}
}
// As object lookup (often cleaner)
const handlers: Record<string, () => void> = {
a: handleCaseA,
b: handleCaseB,
c: handleCaseC
};
(handlers[value] || handleDefault)();
// As component mapping
const components: Record<string, React.ComponentType> = {
a: ComponentA,
b: ComponentB,
c: ComponentC
};
const Component = components[value] || DefaultComponent;
return <Component />;
```
### Boolean Logic Nodes (And, Or, Not)
```
┌───┐ ┌───┐
│ A │──┐ │ │
└───┘ ├────▶│AND│──▶ Result
┌───┐ │ │ │
│ B │──┘ └───┘
└───┘
```
**Generated Code:**
```typescript
// Simple cases - inline operators
const result = a && b;
const result = a || b;
const result = !a;
// Complex cases - named function
function checkConditions(
isLoggedIn: boolean,
hasPermission: boolean,
isEnabled: boolean
): boolean {
return isLoggedIn && hasPermission && isEnabled;
}
// As useMemo when dependent on state
const canProceed = useMemo(
() => isLoggedIn && hasPermission && isEnabled,
[isLoggedIn, hasPermission, isEnabled]
);
```
### Inverter Node
```typescript
// Simply negates the input
const inverted = !value;
```
---
## States Node Transformation
The States node is a simple state machine. See CODE-001 for the `createStateMachine` primitive.
**Noodl States Node:**
```json
{
"type": "States",
"parameters": {
"states": ["idle", "loading", "success", "error"],
"startState": "idle",
"values": {
"idle": { "opacity": 1, "message": "" },
"loading": { "opacity": 0.5, "message": "Loading..." },
"success": { "opacity": 1, "message": "Done!" },
"error": { "opacity": 1, "message": "Error occurred" }
}
}
}
```
**Generated Code:**
```typescript
// stores/stateMachines.ts
import { createStateMachine } from '@nodegx/core';
export type FormState = 'idle' | 'loading' | 'success' | 'error';
export const formStateMachine = createStateMachine<FormState>({
states: ['idle', 'loading', 'success', 'error'],
initial: 'idle',
values: {
idle: { opacity: 1, message: '' },
loading: { opacity: 0.5, message: 'Loading...' },
success: { opacity: 1, message: 'Done!' },
error: { opacity: 1, message: 'Error occurred' }
}
});
// In component
import { useStateMachine, useStateValues } from '@nodegx/core';
import { formStateMachine } from '../stores/stateMachines';
function SubmitButton() {
const [state, goTo] = useStateMachine(formStateMachine);
const values = useStateValues(formStateMachine);
const handleClick = async () => {
goTo('loading');
try {
await submitForm();
goTo('success');
} catch {
goTo('error');
}
};
return (
<button
onClick={handleClick}
disabled={state === 'loading'}
style={{ opacity: values.opacity }}
>
{state === 'loading' ? values.message : 'Submit'}
</button>
);
}
```
---
## Timing Nodes
### Delay Node
```typescript
// Noodl: Delay node with 500ms
// Generated:
import { useCallback, useRef, useEffect } from 'react';
function useDelay(callback: () => void, ms: number) {
const timeoutRef = useRef<NodeJS.Timeout>();
const trigger = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(callback, ms);
}, [callback, ms]);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return trigger;
}
// Usage
const delayedAction = useDelay(() => {
console.log('Delayed!');
}, 500);
```
### Debounce Node
```typescript
// hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebouncedValue<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
export function useDebouncedCallback<T extends (...args: any[]) => any>(
callback: T,
delay: number
): T {
const callbackRef = useRef(callback);
callbackRef.current = callback;
const timeoutRef = useRef<NodeJS.Timeout>();
return useCallback((...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callbackRef.current(...args);
}, delay);
}, [delay]) as T;
}
```
### Counter Node
```typescript
// stores/counters.ts (or inline)
import { createVariable } from '@nodegx/core';
export const clickCounter = createVariable('clickCounter', 0);
// Usage
import { useVariable } from '@nodegx/core';
function Counter() {
const [count, setCount] = useVariable(clickCounter);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<div>
<span>{count}</span>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
<button onClick={reset}>Reset</button>
</div>
);
}
```
---
## Data Transformation Nodes
### String Format Node
```typescript
// Noodl String Format: "Hello, {name}! You have {count} messages."
// Generated:
function formatGreeting(name: string, count: number): string {
return `Hello, ${name}! You have ${count} messages.`;
}
```
### Date/Time Nodes
```typescript
// Date Format node
import { format, parseISO } from 'date-fns';
function formatDate(date: string | Date, formatString: string): string {
const dateObj = typeof date === 'string' ? parseISO(date) : date;
return format(dateObj, formatString);
}
// Example usage
formatDate('2024-01-15', 'MMM d, yyyy'); // "Jan 15, 2024"
```
### Number Format Node
```typescript
function formatNumber(
value: number,
options: {
decimals?: number;
thousandsSeparator?: boolean;
prefix?: string;
suffix?: string;
} = {}
): string {
const { decimals = 2, thousandsSeparator = true, prefix = '', suffix = '' } = options;
let formatted = value.toFixed(decimals);
if (thousandsSeparator) {
formatted = parseFloat(formatted).toLocaleString('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
}
return `${prefix}${formatted}${suffix}`;
}
// Examples
formatNumber(1234.5, { prefix: '$' }); // "$1,234.50"
formatNumber(0.15, { suffix: '%', decimals: 0 }); // "15%"
```
---
## Connection Flow Analysis
The code generator must analyze the data flow graph to determine:
1. **Execution Order** - Which nodes depend on which
2. **Reactivity Boundaries** - Where to use hooks vs pure functions
3. **Side Effect Isolation** - Keep side effects in event handlers
```typescript
interface ConnectionAnalysis {
// Nodes that feed into this node
dependencies: string[];
// Nodes that consume this node's output
dependents: string[];
// Whether this node has side effects
hasSideEffects: boolean;
// Whether this is part of a reactive chain
isReactive: boolean;
// Suggested generation pattern
pattern: 'inline' | 'function' | 'hook' | 'effect';
}
function analyzeConnectionFlow(
nodes: NoodlNode[],
connections: NoodlConnection[]
): Map<string, ConnectionAnalysis> {
const analysis = new Map<string, ConnectionAnalysis>();
for (const node of nodes) {
// Find all connections to this node
const incomingConnections = connections.filter(c => c.targetId === node.id);
const outgoingConnections = connections.filter(c => c.sourceId === node.id);
const dependencies = [...new Set(incomingConnections.map(c => c.sourceId))];
const dependents = [...new Set(outgoingConnections.map(c => c.targetId))];
// Determine if this has side effects
const hasSideEffects =
node.type === 'Function' && containsSideEffects(node.parameters.code) ||
node.type.includes('Set') ||
node.type.includes('Send') ||
node.type.includes('Navigate');
// Determine if reactive (depends on Variables/Objects/Arrays)
const isReactive = dependencies.some(depId => {
const depNode = nodes.find(n => n.id === depId);
return depNode && isReactiveNode(depNode);
});
// Suggest pattern
let pattern: 'inline' | 'function' | 'hook' | 'effect' = 'inline';
if (hasSideEffects) {
pattern = 'effect';
} else if (isReactive) {
pattern = 'hook';
} else if (node.type === 'Function' || dependencies.length > 2) {
pattern = 'function';
}
analysis.set(node.id, {
dependencies,
dependents,
hasSideEffects,
isReactive,
pattern
});
}
return analysis;
}
```
---
## Code Generation Algorithm
```typescript
async function generateLogicCode(
node: NoodlNode,
connections: NoodlConnection[],
analysis: ConnectionAnalysis,
outputDir: string
): Promise<GeneratedFile[]> {
const files: GeneratedFile[] = [];
switch (node.type) {
case 'Function':
case 'Javascript2':
files.push(...generateFunctionNode(node, analysis));
break;
case 'Expression':
files.push(...generateExpressionNode(node, analysis));
break;
case 'Condition':
files.push(...generateConditionNode(node, analysis));
break;
case 'Switch':
files.push(...generateSwitchNode(node, analysis));
break;
case 'States':
files.push(...generateStatesNode(node, analysis));
break;
case 'And':
case 'Or':
case 'Not':
// Usually inlined, but generate helper if complex
if (analysis.dependents.length > 1) {
files.push(...generateBooleanLogicNode(node, analysis));
}
break;
case 'Delay':
case 'Debounce':
files.push(...generateTimingNode(node, analysis));
break;
}
return files;
}
```
---
## Testing Checklist
### Function Node Tests
- [ ] Simple function transforms correctly
- [ ] Multiple inputs handled
- [ ] Multiple outputs return object
- [ ] Signals generate createSignal
- [ ] Async functions preserve async/await
- [ ] Error handling preserved
### Expression Tests
- [ ] Math expressions evaluate correctly
- [ ] String templates work
- [ ] Noodl.Variables access works
- [ ] Noodl.Objects access works
- [ ] Complex ternaries work
### Logic Node Tests
- [ ] Condition branches correctly
- [ ] Switch cases all handled
- [ ] Boolean operators combine correctly
- [ ] States machine transitions work
- [ ] Timing nodes delay/debounce correctly
### Integration Tests
- [ ] Data flows through connected nodes
- [ ] Reactive updates propagate
- [ ] Side effects trigger correctly
- [ ] No circular dependencies generated
---
## Success Criteria
1. **Behavioral Parity** - Logic executes identically to Noodl runtime
2. **Clean Code** - Generated functions are readable and well-named
3. **Type Safety** - Proper TypeScript types inferred/generated
4. **Testable** - Generated functions can be unit tested
5. **No Runtime Errors** - No undefined references or type mismatches

View File

@@ -0,0 +1,689 @@
# CODE-005: Event System Generator
## Overview
The Event System Generator transforms Noodl's Send Event and Receive Event nodes into a clean event-driven architecture. It handles global events, component-scoped events, and various propagation modes (parent, children, siblings).
**Estimated Effort:** 1-2 weeks
**Priority:** MEDIUM
**Dependencies:** CODE-001 (@nodegx/core)
**Blocks:** CODE-006 (Project Scaffolding)
---
## Noodl Event Model
### Event Propagation Modes
Noodl supports several propagation modes:
| Mode | Description | Use Case |
|------|-------------|----------|
| **Global** | All Receive Event nodes with matching channel | App-wide notifications |
| **Parent** | Only parent component hierarchy | Child-to-parent communication |
| **Children** | Only child components | Parent-to-child broadcast |
| **Siblings** | Only sibling components | Peer communication |
### Send Event Node
```json
{
"type": "Send Event",
"parameters": {
"channelName": "userLoggedIn",
"sendMode": "global"
},
"dynamicports": [
{ "name": "userId", "plug": "input" },
{ "name": "userName", "plug": "input" }
]
}
```
### Receive Event Node
```json
{
"type": "Receive Event",
"parameters": {
"channelName": "userLoggedIn"
},
"dynamicports": [
{ "name": "userId", "plug": "output" },
{ "name": "userName", "plug": "output" }
]
}
```
---
## Event Analysis
```typescript
interface EventChannelInfo {
name: string;
propagation: 'global' | 'parent' | 'children' | 'siblings';
dataShape: Record<string, string>; // property name -> type
senders: Array<{
componentName: string;
nodeId: string;
}>;
receivers: Array<{
componentName: string;
nodeId: string;
}>;
}
function analyzeEventChannels(project: NoodlProject): Map<string, EventChannelInfo> {
const channels = new Map<string, EventChannelInfo>();
for (const component of project.components) {
for (const node of component.nodes) {
if (node.type === 'Send Event') {
const channelName = node.parameters.channelName;
if (!channels.has(channelName)) {
channels.set(channelName, {
name: channelName,
propagation: node.parameters.sendMode || 'global',
dataShape: {},
senders: [],
receivers: []
});
}
const channel = channels.get(channelName)!;
channel.senders.push({
componentName: component.name,
nodeId: node.id
});
// Collect data shape from dynamic ports
for (const port of node.dynamicports || []) {
if (port.plug === 'input') {
channel.dataShape[port.name] = inferPortType(port);
}
}
}
if (node.type === 'Receive Event' || node.type === 'Event Receiver') {
const channelName = node.parameters.channelName;
if (!channels.has(channelName)) {
channels.set(channelName, {
name: channelName,
propagation: 'global',
dataShape: {},
senders: [],
receivers: []
});
}
const channel = channels.get(channelName)!;
channel.receivers.push({
componentName: component.name,
nodeId: node.id
});
// Collect data shape from dynamic ports
for (const port of node.dynamicports || []) {
if (port.plug === 'output') {
channel.dataShape[port.name] = inferPortType(port);
}
}
}
}
}
return channels;
}
```
---
## Generated Event Definitions
### Events Type File
```typescript
// events/types.ts
/**
* Event data types for type-safe event handling
* Auto-generated from Noodl project
*/
export interface UserLoggedInEvent {
userId: string;
userName: string;
timestamp?: number;
}
export interface ItemAddedEvent {
itemId: string;
itemName: string;
category: string;
}
export interface FormSubmittedEvent {
formId: string;
data: Record<string, any>;
isValid: boolean;
}
export interface NavigationEvent {
from: string;
to: string;
params?: Record<string, string>;
}
export interface NotificationEvent {
type: 'success' | 'error' | 'warning' | 'info';
message: string;
duration?: number;
}
// Union type for all events (useful for logging/debugging)
export type AppEvent =
| { channel: 'userLoggedIn'; data: UserLoggedInEvent }
| { channel: 'itemAdded'; data: ItemAddedEvent }
| { channel: 'formSubmitted'; data: FormSubmittedEvent }
| { channel: 'navigation'; data: NavigationEvent }
| { channel: 'notification'; data: NotificationEvent };
```
### Events Channel File
```typescript
// events/channels.ts
import { createSignal } from '@nodegx/core';
import type {
UserLoggedInEvent,
ItemAddedEvent,
FormSubmittedEvent,
NavigationEvent,
NotificationEvent
} from './types';
// ============================================
// Global Event Channels
// ============================================
/**
* Authentication Events
*/
export const userLoggedIn = createEventChannel<UserLoggedInEvent>('userLoggedIn');
export const userLoggedOut = createEventChannel<void>('userLoggedOut');
/**
* Data Events
*/
export const itemAdded = createEventChannel<ItemAddedEvent>('itemAdded');
export const itemRemoved = createEventChannel<{ itemId: string }>('itemRemoved');
export const dataRefresh = createEventChannel<void>('dataRefresh');
/**
* Form Events
*/
export const formSubmitted = createEventChannel<FormSubmittedEvent>('formSubmitted');
export const formReset = createEventChannel<{ formId: string }>('formReset');
/**
* Navigation Events
*/
export const navigation = createEventChannel<NavigationEvent>('navigation');
/**
* UI Events
*/
export const notification = createEventChannel<NotificationEvent>('notification');
export const modalOpened = createEventChannel<{ modalId: string }>('modalOpened');
export const modalClosed = createEventChannel<{ modalId: string }>('modalClosed');
// ============================================
// Helper: Typed Event Channel
// ============================================
interface EventChannel<T> {
send: (data: T) => void;
subscribe: (handler: (data: T) => void) => () => void;
}
function createEventChannel<T>(name: string): EventChannel<T> {
const handlers = new Set<(data: T) => void>();
return {
send(data: T) {
handlers.forEach(h => h(data));
},
subscribe(handler: (data: T) => void) {
handlers.add(handler);
return () => handlers.delete(handler);
}
};
}
```
### Events Hook File
```typescript
// events/hooks.ts
import { useEffect, useRef } from 'react';
import type {
UserLoggedInEvent,
ItemAddedEvent,
FormSubmittedEvent,
NavigationEvent,
NotificationEvent
} from './types';
import * as channels from './channels';
/**
* Hook for receiving userLoggedIn events
*/
export function useUserLoggedIn(handler: (data: UserLoggedInEvent) => void) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
return channels.userLoggedIn.subscribe((data) => handlerRef.current(data));
}, []);
}
/**
* Hook for receiving itemAdded events
*/
export function useItemAdded(handler: (data: ItemAddedEvent) => void) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
return channels.itemAdded.subscribe((data) => handlerRef.current(data));
}, []);
}
/**
* Hook for receiving notification events
*/
export function useNotification(handler: (data: NotificationEvent) => void) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
return channels.notification.subscribe((data) => handlerRef.current(data));
}, []);
}
// Generic hook for any event
export function useEventChannel<T>(
channel: { subscribe: (handler: (data: T) => void) => () => void },
handler: (data: T) => void
) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
return channel.subscribe((data) => handlerRef.current(data));
}, [channel]);
}
```
---
## Global Events Usage
### Sending Events
**Noodl:**
```
[Button] → click → [Send Event: "itemAdded"]
├─ itemId ← "item-123"
├─ itemName ← "New Item"
└─ category ← "electronics"
```
**Generated:**
```typescript
// In component
import { itemAdded } from '../events/channels';
function AddItemButton() {
const handleClick = () => {
itemAdded.send({
itemId: 'item-123',
itemName: 'New Item',
category: 'electronics'
});
};
return <button onClick={handleClick}>Add Item</button>;
}
```
### Receiving Events
**Noodl:**
```
[Receive Event: "itemAdded"]
├─ itemId → [Text] display
├─ itemName → [Variable] set
└─ received → [Animation] trigger
```
**Generated:**
```typescript
// In component
import { useItemAdded } from '../events/hooks';
import { setVariable } from '@nodegx/core';
import { latestItemNameVar } from '../stores/variables';
function ItemNotification() {
const [notification, setNotification] = useState<ItemAddedEvent | null>(null);
useItemAdded((data) => {
setNotification(data);
setVariable(latestItemNameVar, data.itemName);
// Clear after animation
setTimeout(() => setNotification(null), 3000);
});
if (!notification) return null;
return (
<div className="notification">
Added: {notification.itemName}
</div>
);
}
```
---
## Scoped Events (Parent/Children/Siblings)
For non-global propagation modes, we use React context.
### Component Event Provider
```typescript
// events/ComponentEventContext.tsx
import { createContext, useContext, useRef, useCallback, ReactNode } from 'react';
interface ScopedEventHandlers {
[channel: string]: Array<(data: any) => void>;
}
interface ComponentEventContextValue {
// Send event to parent
sendToParent: (channel: string, data: any) => void;
// Send event to children
sendToChildren: (channel: string, data: any) => void;
// Send event to siblings
sendToSiblings: (channel: string, data: any) => void;
// Register this component's handlers
registerHandler: (channel: string, handler: (data: any) => void) => () => void;
// Register child component
registerChild: (handlers: ScopedEventHandlers) => () => void;
}
const ComponentEventContext = createContext<ComponentEventContextValue | null>(null);
export function ComponentEventProvider({
children,
onEvent
}: {
children: ReactNode;
onEvent?: (channel: string, data: any) => void;
}) {
const parentContext = useContext(ComponentEventContext);
const childHandlersRef = useRef<Set<ScopedEventHandlers>>(new Set());
const localHandlersRef = useRef<ScopedEventHandlers>({});
const registerHandler = useCallback((channel: string, handler: (data: any) => void) => {
if (!localHandlersRef.current[channel]) {
localHandlersRef.current[channel] = [];
}
localHandlersRef.current[channel].push(handler);
return () => {
const handlers = localHandlersRef.current[channel];
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
};
}, []);
const registerChild = useCallback((handlers: ScopedEventHandlers) => {
childHandlersRef.current.add(handlers);
return () => childHandlersRef.current.delete(handlers);
}, []);
const sendToParent = useCallback((channel: string, data: any) => {
// Local handler
onEvent?.(channel, data);
// Propagate up
parentContext?.sendToParent(channel, data);
}, [parentContext, onEvent]);
const sendToChildren = useCallback((channel: string, data: any) => {
childHandlersRef.current.forEach(childHandlers => {
const handlers = childHandlers[channel];
handlers?.forEach(h => h(data));
});
}, []);
const sendToSiblings = useCallback((channel: string, data: any) => {
// Siblings are other children of our parent
parentContext?.sendToChildren(channel, data);
}, [parentContext]);
const value: ComponentEventContextValue = {
sendToParent,
sendToChildren,
sendToSiblings,
registerHandler,
registerChild
};
return (
<ComponentEventContext.Provider value={value}>
{children}
</ComponentEventContext.Provider>
);
}
// Hook for sending scoped events
export function useScopedEventSender() {
const context = useContext(ComponentEventContext);
return {
sendToParent: context?.sendToParent ?? (() => {}),
sendToChildren: context?.sendToChildren ?? (() => {}),
sendToSiblings: context?.sendToSiblings ?? (() => {})
};
}
// Hook for receiving scoped events
export function useScopedEvent(
channel: string,
handler: (data: any) => void
) {
const context = useContext(ComponentEventContext);
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
if (!context) return;
return context.registerHandler(channel, (data) => handlerRef.current(data));
}, [context, channel]);
}
```
### Usage: Parent-to-Child Event
**Noodl:**
```
[Parent Component]
├─ [Button] → click → [Send Event: "refresh" mode="children"]
└─ [Child Component]
└─ [Receive Event: "refresh"] → [Fetch Data]
```
**Generated:**
```typescript
// Parent component
import { ComponentEventProvider, useScopedEventSender } from '../events/ComponentEventContext';
function ParentComponent() {
const { sendToChildren } = useScopedEventSender();
return (
<ComponentEventProvider>
<button onClick={() => sendToChildren('refresh', {})}>
Refresh All
</button>
<ChildComponent />
<ChildComponent />
</ComponentEventProvider>
);
}
// Child component
import { useScopedEvent } from '../events/ComponentEventContext';
function ChildComponent() {
const [data, setData] = useState([]);
useScopedEvent('refresh', () => {
fetchData().then(setData);
});
return <div>{/* render data */}</div>;
}
```
### Usage: Child-to-Parent Event
**Noodl:**
```
[Parent Component]
├─ [Receive Event: "itemSelected"] → [Variable: selectedId]
└─ [Child Component]
└─ [Button] → click → [Send Event: "itemSelected" mode="parent"]
└─ itemId ← props.id
```
**Generated:**
```typescript
// Parent component
function ParentComponent() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const handleItemSelected = (data: { itemId: string }) => {
setSelectedId(data.itemId);
};
return (
<ComponentEventProvider onEvent={(channel, data) => {
if (channel === 'itemSelected') {
handleItemSelected(data);
}
}}>
<ItemList />
{selectedId && <ItemDetail id={selectedId} />}
</ComponentEventProvider>
);
}
// Child component
function ItemList() {
const items = useArray(itemsArray);
return (
<div>
{items.map(item => (
<ItemRow key={item.id} item={item} />
))}
</div>
);
}
function ItemRow({ item }: { item: Item }) {
const { sendToParent } = useScopedEventSender();
return (
<div onClick={() => sendToParent('itemSelected', { itemId: item.id })}>
{item.name}
</div>
);
}
```
---
## Event Index File
```typescript
// events/index.ts
// Types
export * from './types';
// Global event channels
export * from './channels';
// Hooks for global events
export * from './hooks';
// Scoped events (parent/children/siblings)
export {
ComponentEventProvider,
useScopedEventSender,
useScopedEvent
} from './ComponentEventContext';
// Re-export from @nodegx/core for convenience
export { sendEvent, useEvent, events } from '@nodegx/core';
```
---
## Testing Checklist
### Global Events
- [ ] Send Event broadcasts to all receivers
- [ ] Receive Event hooks fire correctly
- [ ] Event data is passed correctly
- [ ] Multiple receivers all get notified
- [ ] No memory leaks on unmount
### Scoped Events
- [ ] Parent mode reaches parent only
- [ ] Children mode reaches children only
- [ ] Siblings mode reaches siblings only
- [ ] Nested components work correctly
- [ ] Context providers don't break rendering
### Type Safety
- [ ] Event data is typed correctly
- [ ] TypeScript catches wrong event names
- [ ] TypeScript catches wrong data shapes
---
## Success Criteria
1. **All channels discovered** - Every Send/Receive Event pair found
2. **Type safety** - Full TypeScript support for event data
3. **Propagation parity** - All modes work as in Noodl
4. **Clean API** - Easy to send and receive events
5. **No leaks** - Subscriptions cleaned up properly

View File

@@ -0,0 +1,834 @@
# CODE-006: Project Scaffolding Generator
## Overview
The Project Scaffolding Generator creates the complete React project structure from a Noodl project, including routing, entry point, build configuration, and package dependencies. This pulls together all the generated code into a runnable application.
**Estimated Effort:** 1-2 weeks
**Priority:** HIGH
**Dependencies:** CODE-001 through CODE-005
**Blocks:** CODE-007 (CLI & Integration)
---
## Output Project Structure
```
my-app/
├── src/
│ ├── components/ # Generated React components
│ │ ├── Layout/
│ │ │ ├── Header.tsx
│ │ │ ├── Header.module.css
│ │ │ ├── Footer.tsx
│ │ │ └── Sidebar.tsx
│ │ ├── Pages/
│ │ │ ├── HomePage.tsx
│ │ │ ├── ProductsPage.tsx
│ │ │ └── ContactPage.tsx
│ │ └── UI/
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ └── Modal.tsx
│ │
│ ├── stores/ # State management
│ │ ├── variables.ts
│ │ ├── objects.ts
│ │ ├── arrays.ts
│ │ ├── staticArrays.ts
│ │ └── index.ts
│ │
│ ├── logic/ # Function node code
│ │ ├── auth.ts
│ │ ├── api.ts
│ │ ├── validation.ts
│ │ └── index.ts
│ │
│ ├── events/ # Event channels
│ │ ├── types.ts
│ │ ├── channels.ts
│ │ ├── hooks.ts
│ │ └── index.ts
│ │
│ ├── hooks/ # Custom React hooks
│ │ ├── useDebounce.ts
│ │ ├── useLocalStorage.ts
│ │ └── index.ts
│ │
│ ├── styles/ # Global styles
│ │ ├── globals.css
│ │ ├── variables.css
│ │ └── reset.css
│ │
│ ├── assets/ # Copied from Noodl project
│ │ ├── images/
│ │ ├── fonts/
│ │ └── icons/
│ │
│ ├── App.tsx # Root component with routing
│ ├── main.tsx # Entry point
│ └── vite-env.d.ts # Vite types
├── public/
│ ├── favicon.ico
│ └── robots.txt
├── package.json
├── tsconfig.json
├── vite.config.ts
├── .eslintrc.cjs
├── .prettierrc
├── .gitignore
└── README.md
```
---
## Routing Generation
### Page Router Analysis
**Noodl Page Router Node:**
```json
{
"type": "Page Router",
"parameters": {
"pages": [
{ "name": "Home", "path": "/", "component": "HomePage" },
{ "name": "Products", "path": "/products", "component": "ProductsPage" },
{ "name": "Product Detail", "path": "/products/:id", "component": "ProductDetailPage" },
{ "name": "Contact", "path": "/contact", "component": "ContactPage" }
],
"notFoundPage": "NotFoundPage"
}
}
```
### Generated Router
```tsx
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Layout } from './components/Layout/Layout';
import { HomePage } from './components/Pages/HomePage';
import { ProductsPage } from './components/Pages/ProductsPage';
import { ProductDetailPage } from './components/Pages/ProductDetailPage';
import { ContactPage } from './components/Pages/ContactPage';
import { NotFoundPage } from './components/Pages/NotFoundPage';
export function App() {
return (
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/products/:id" element={<ProductDetailPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Layout>
</BrowserRouter>
);
}
```
### Navigate Node Handling
**Noodl Navigate Node:**
```json
{
"type": "Navigate",
"parameters": {
"target": "/products",
"openInNewTab": false
}
}
```
**Generated Code:**
```tsx
import { useNavigate } from 'react-router-dom';
function NavigateButton() {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/products')}>
View Products
</button>
);
}
```
### Dynamic Routes with Parameters
**Noodl:**
```
[Navigate]
├─ target: "/products/{productId}"
└─ productId ← selectedProduct.id
```
**Generated:**
```tsx
const navigate = useNavigate();
const productId = selectedProduct.id;
<button onClick={() => navigate(`/products/${productId}`)}>
View Details
</button>
```
### Route Parameters in Page Components
**Noodl Page Inputs:**
```json
{
"type": "Page Inputs",
"dynamicports": [
{ "name": "id", "type": "string" }
]
}
```
**Generated:**
```tsx
import { useParams } from 'react-router-dom';
function ProductDetailPage() {
const { id } = useParams<{ id: string }>();
// Use id to fetch product data
useEffect(() => {
if (id) {
fetchProduct(id);
}
}, [id]);
return (/* ... */);
}
```
---
## Entry Point Generation
### main.tsx
```tsx
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './App';
// Global styles
import './styles/reset.css';
import './styles/variables.css';
import './styles/globals.css';
// Initialize stores (if needed for SSR-compatible hydration)
import './stores';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
```
### index.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<!-- From Noodl project settings -->
<title>My App</title>
<meta name="description" content="Built with Nodegx" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
```
---
## Build Configuration
### package.json
```json
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@nodegx/core": "^0.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^6.22.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitejs/plugin-react": "^4.2.0",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"prettier": "^3.2.0",
"typescript": "^5.3.0",
"vite": "^5.1.0"
}
}
```
### vite.config.ts
```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@stores': path.resolve(__dirname, './src/stores'),
'@logic': path.resolve(__dirname, './src/logic'),
'@events': path.resolve(__dirname, './src/events'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@assets': path.resolve(__dirname, './src/assets'),
},
},
build: {
target: 'esnext',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
nodegx: ['@nodegx/core'],
},
},
},
},
server: {
port: 3000,
open: true,
},
});
```
### tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@stores/*": ["./src/stores/*"],
"@logic/*": ["./src/logic/*"],
"@events/*": ["./src/events/*"],
"@hooks/*": ["./src/hooks/*"],
"@assets/*": ["./src/assets/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
```
### tsconfig.node.json
```json
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
```
---
## Global Styles Generation
### reset.css
```css
/* src/styles/reset.css */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
padding: 0;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
#root {
isolation: isolate;
min-height: 100vh;
}
```
### variables.css (from Noodl Project Settings)
```css
/* src/styles/variables.css */
:root {
/* Colors - extracted from Noodl project */
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-secondary: #64748b;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-background: #ffffff;
--color-surface: #f8fafc;
--color-text: #1e293b;
--color-text-muted: #64748b;
--color-border: #e2e8f0;
/* Typography */
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-size-xs: 12px;
--font-size-sm: 14px;
--font-size-base: 16px;
--font-size-lg: 18px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
--font-size-3xl: 30px;
/* Spacing */
--spacing-1: 4px;
--spacing-2: 8px;
--spacing-3: 12px;
--spacing-4: 16px;
--spacing-5: 20px;
--spacing-6: 24px;
--spacing-8: 32px;
--spacing-10: 40px;
--spacing-12: 48px;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 200ms ease;
--transition-slow: 300ms ease;
}
/* Dark mode (if enabled in Noodl project) */
@media (prefers-color-scheme: dark) {
:root {
--color-background: #0f172a;
--color-surface: #1e293b;
--color-text: #f1f5f9;
--color-text-muted: #94a3b8;
--color-border: #334155;
}
}
```
### globals.css
```css
/* src/styles/globals.css */
body {
font-family: var(--font-family);
font-size: var(--font-size-base);
color: var(--color-text);
background-color: var(--color-background);
}
/* Focus styles */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Button base styles */
button {
cursor: pointer;
border: none;
background: none;
}
/* Link base styles */
a {
color: var(--color-primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Scrollbar styling (optional) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-surface);
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-muted);
}
```
---
## Asset Copying
### Asset Types
| Noodl Location | Output Location | Handling |
|----------------|-----------------|----------|
| `/assets/images/` | `/src/assets/images/` | Direct copy |
| `/assets/fonts/` | `/src/assets/fonts/` | Copy + @font-face |
| `/assets/icons/` | `/src/assets/icons/` | Copy or SVG component |
| `/noodl_modules/` | N/A | Dependencies → npm |
### Image References
Update image paths in generated components:
```tsx
// Before (Noodl)
<Image src="/assets/images/hero.jpg" />
// After (Generated)
<img src="/src/assets/images/hero.jpg" alt="" />
// Or with import (better for bundling)
import heroImage from '@assets/images/hero.jpg';
<img src={heroImage} alt="" />
```
### Font Loading
```css
/* src/assets/fonts/fonts.css */
@font-face {
font-family: 'CustomFont';
src: url('./CustomFont-Regular.woff2') format('woff2'),
url('./CustomFont-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'CustomFont';
src: url('./CustomFont-Bold.woff2') format('woff2'),
url('./CustomFont-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
```
---
## README Generation
```markdown
<!-- README.md -->
# My App
This application was exported from Nodegx.
## Getting Started
### Prerequisites
- Node.js 18+
- npm or yarn
### Installation
\`\`\`bash
npm install
\`\`\`
### Development
\`\`\`bash
npm run dev
\`\`\`
Open [http://localhost:3000](http://localhost:3000) in your browser.
### Build
\`\`\`bash
npm run build
\`\`\`
The build output will be in the \`dist\` folder.
### Preview Production Build
\`\`\`bash
npm run preview
\`\`\`
## Project Structure
\`\`\`
src/
├── components/ # React components
├── stores/ # State management (@nodegx/core)
├── logic/ # Business logic functions
├── events/ # Event channels
├── hooks/ # Custom React hooks
├── styles/ # Global styles
└── assets/ # Images, fonts, icons
\`\`\`
## Technologies
- React 19
- TypeScript
- Vite
- React Router
- @nodegx/core (reactive primitives)
## Notes
This code was automatically generated. Some manual adjustments may be needed for:
- API integrations (see \`// TODO\` comments in \`logic/\`)
- Authentication setup
- Environment variables
## License
[Your License]
```
---
## Export Report
After export, generate a summary report:
```typescript
interface ExportReport {
success: boolean;
outputDir: string;
stats: {
components: number;
pages: number;
stores: {
variables: number;
objects: number;
arrays: number;
};
events: number;
functions: number;
};
warnings: ExportWarning[];
todos: ExportTodo[];
nextSteps: string[];
}
interface ExportWarning {
type: 'unsupported-node' | 'complex-logic' | 'external-dependency';
message: string;
location?: string;
}
interface ExportTodo {
file: string;
line: number;
description: string;
}
```
```
┌──────────────────────────────────────────────────────────────┐
│ Export Complete ✅ │
├──────────────────────────────────────────────────────────────┤
│ │
│ Output: ./my-app-export/ │
│ │
│ Statistics: │
│ ────────────────────────────────────── │
│ Components: 23 │
│ Pages: 5 │
│ Variables: 12 │
│ Objects: 4 │
│ Arrays: 3 │
│ Event Channels: 8 │
│ Logic Functions: 15 │
│ │
│ ⚠️ Warnings (2): │
│ ────────────────────────────────────── │
│ • Cloud Function node not supported - see logic/api.ts │
│ • Query Records node not supported - see logic/db.ts │
│ │
│ Next Steps: │
│ ────────────────────────────────────── │
│ 1. cd my-app-export && npm install │
│ 2. Review TODO comments (3 found) │
│ 3. Set up environment variables │
│ 4. npm run dev │
│ │
│ 📖 See README.md for full documentation │
│ │
└──────────────────────────────────────────────────────────────┘
```
---
## Testing Checklist
### Project Structure
- [ ] All folders created correctly
- [ ] Files in correct locations
- [ ] Imports resolve correctly
- [ ] No circular dependencies
### Build
- [ ] `npm install` succeeds
- [ ] `npm run dev` starts dev server
- [ ] `npm run build` produces bundle
- [ ] `npm run preview` serves production build
- [ ] TypeScript compiles without errors
- [ ] ESLint passes
### Routing
- [ ] All routes accessible
- [ ] Route parameters work
- [ ] Navigation works
- [ ] 404 page shows for unknown routes
- [ ] Browser back/forward works
### Assets
- [ ] Images load correctly
- [ ] Fonts load correctly
- [ ] Icons display correctly
---
## Success Criteria
1. **Runnable** - `npm install && npm run dev` works first try
2. **Complete** - All components, stores, events present
3. **Clean** - No TypeScript or ESLint errors
4. **Documented** - README explains setup and structure
5. **Modern** - Uses current best practices (Vite, ESM, etc.)

View File

@@ -0,0 +1,228 @@
# CODE-007: CLI & Editor Integration
## Overview
The CLI & Editor Integration task provides the command-line interface for exporting Noodl projects to React code, and integrates the export functionality into the editor UI.
**Estimated Effort:** 1-2 weeks
**Priority:** MEDIUM
**Dependencies:** CODE-001 through CODE-006
**Blocks:** None (final integration task)
---
## CLI Tool
### Package: @nodegx/cli
```bash
# Installation
npm install -g @nodegx/cli
# Usage
nodegx export ./my-project --output ./my-app
nodegx export ./my-project --output ./my-app --typescript
nodegx export ./my-project --output ./my-app --css-mode tailwind
```
### CLI Options
| Option | Description | Default |
| --------------- | --------------------------------------------- | ---------- |
| `--output, -o` | Output directory | `./export` |
| `--typescript` | Generate TypeScript | `true` |
| `--css-mode` | CSS approach: `modules`, `tailwind`, `inline` | `modules` |
| `--clean` | Clean output directory first | `false` |
| `--verbose, -v` | Verbose output | `false` |
| `--dry-run` | Preview without writing files | `false` |
### CLI Implementation
```typescript
// packages/nodegx-cli/src/index.ts
import { Command } from 'commander';
import { exportProject } from './export';
const program = new Command();
program.name('nodegx').description('Export Noodl projects to React applications').version('0.1.0');
program
.command('export <project-path>')
.description('Export a Noodl project to React code')
.option('-o, --output <dir>', 'Output directory', './export')
.option('--typescript', 'Generate TypeScript', true)
.option('--css-mode <mode>', 'CSS mode: modules, tailwind, inline', 'modules')
.option('--clean', 'Clean output directory first', false)
.option('-v, --verbose', 'Verbose output', false)
.option('--dry-run', 'Preview without writing files', false)
.action(async (projectPath, options) => {
await exportProject(projectPath, options);
});
program.parse();
```
---
## Editor Integration
### Export Menu Item
Add "Export to React" option in File menu:
```typescript
// In editor menu configuration
{
label: 'Export to React...',
click: () => {
showExportDialog();
}
}
```
### Export Dialog UI
```tsx
// components/ExportDialog.tsx
interface ExportDialogProps {
projectPath: string;
onExport: (options: ExportOptions) => void;
onCancel: () => void;
}
function ExportDialog({ projectPath, onExport, onCancel }: ExportDialogProps) {
const [outputDir, setOutputDir] = useState('./export');
const [cssMode, setCssMode] = useState<'modules' | 'tailwind' | 'inline'>('modules');
const [useTypeScript, setUseTypeScript] = useState(true);
return (
<Dialog title="Export to React">
<FormField label="Output Directory">
<DirectoryPicker value={outputDir} onChange={setOutputDir} />
</FormField>
<FormField label="CSS Mode">
<Select value={cssMode} onChange={setCssMode}>
<Option value="modules">CSS Modules</Option>
<Option value="tailwind">Tailwind CSS</Option>
<Option value="inline">Inline Styles</Option>
</Select>
</FormField>
<FormField label="TypeScript">
<Checkbox checked={useTypeScript} onChange={setUseTypeScript} />
</FormField>
<DialogFooter>
<Button onClick={onCancel}>Cancel</Button>
<Button primary onClick={() => onExport({ outputDir, cssMode, useTypeScript })}>
Export
</Button>
</DialogFooter>
</Dialog>
);
}
```
### Progress Indicator
Show export progress:
```tsx
function ExportProgress({ status, progress, currentFile }: ExportProgressProps) {
return (
<div className={styles.progress}>
<ProgressBar value={progress} max={100} />
<span className={styles.status}>{status}</span>
{currentFile && <span className={styles.currentFile}>Processing: {currentFile}</span>}
</div>
);
}
```
---
## Export Workflow
### 1. Project Analysis
```typescript
async function analyzeProject(projectPath: string): Promise<ProjectAnalysis> {
const project = await loadProject(projectPath);
return {
components: analyzeComponents(project),
stores: analyzeStores(project),
events: analyzeEvents(project),
routes: analyzeRoutes(project),
assets: analyzeAssets(project)
};
}
```
### 2. Code Generation
```typescript
async function generateCode(analysis: ProjectAnalysis, options: ExportOptions): Promise<GeneratedFiles> {
const files: GeneratedFiles = {};
// Generate stores
files['src/stores/variables.ts'] = generateVariables(analysis.stores);
files['src/stores/objects.ts'] = generateObjects(analysis.stores);
files['src/stores/arrays.ts'] = generateArrays(analysis.stores);
// Generate components
for (const component of analysis.components) {
const code = generateComponent(component, options);
files[`src/components/${component.name}.tsx`] = code;
}
// Generate events
files['src/events/channels.ts'] = generateEventChannels(analysis.events);
// Generate routing
files['src/App.tsx'] = generateApp(analysis.routes);
return files;
}
```
### 3. File Output
```typescript
async function writeFiles(files: GeneratedFiles, outputDir: string, options: ExportOptions): Promise<void> {
if (options.clean) {
await fs.rm(outputDir, { recursive: true, force: true });
}
await fs.mkdir(outputDir, { recursive: true });
for (const [path, content] of Object.entries(files)) {
const fullPath = join(outputDir, path);
await fs.mkdir(dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, content, 'utf-8');
}
}
```
---
## Testing Checklist
- [ ] CLI parses all options correctly
- [ ] Export creates valid project structure
- [ ] Editor dialog shows correct options
- [ ] Progress updates during export
- [ ] Error handling shows helpful messages
- [ ] Generated project builds without errors
---
## Success Criteria
1. **CLI works standalone** - Export works without editor
2. **Editor integration seamless** - One-click export from menu
3. **Clear feedback** - Progress and errors well-communicated
4. **Generated code runs** - `npm install && npm run dev` succeeds

View File

@@ -0,0 +1,672 @@
# CODE-008: Node Comments Export
## Overview
Export user-added node comments as code comments in the generated React application. This preserves documentation and context that developers add to their visual graphs, making the exported code more maintainable and self-documenting.
**Estimated Effort:** 2-3 days
**Priority:** MEDIUM (quality-of-life enhancement)
**Dependencies:** CODE-002 through CODE-005 (generators must exist)
**Blocks:** None
---
## Feature Description
Noodl now supports adding comments to individual nodes via a comment button. These plain-text comments explain what a node does, why it's configured a certain way, or document business logic. When exporting to React code, these comments should be preserved as actual code comments.
### User Value
1. **Documentation survives export** - Notes added during visual development aren't lost
2. **Onboarding** - New developers reading exported code understand intent
3. **Maintenance** - Future modifications are guided by original context
4. **Audit trail** - Business logic explanations remain with the code
---
## Comment Sources
### Node-Level Comments (Primary)
Each node stores its comment in the `metadata.comment` field. This is the comment button feature you can click on each node:
**Source:** `NodeGraphNode.ts`
```typescript
// Get the comment text for this node
getComment(): string | undefined {
return this.metadata?.comment;
}
// Check if this node has a comment
hasComment(): boolean {
return !!this.metadata?.comment?.trim();
}
// Set or clear the comment for this node
setComment(comment: string | undefined, args?: { undo?: any; label?: any }) {
if (!this.metadata) this.metadata = {};
this.metadata.comment = comment?.trim() || undefined;
this.notifyListeners('commentChanged', { comment: this.metadata.comment });
}
```
**Serialized JSON Structure:**
```json
{
"id": "function-123",
"type": "Function",
"metadata": {
"comment": "Calculates shipping cost based on weight and destination zone. Uses tiered pricing from the 2024 rate card."
},
"parameters": {
"code": "..."
}
}
```
### Canvas Comments (Separate System)
The floating comment boxes on the canvas are a **different system** managed by `CommentsModel`. These are stored at the component level, not attached to nodes:
```json
{
"components": [{
"name": "MyComponent",
"nodes": [...],
"comments": [
{
"id": "comment-abc",
"text": "This section handles authentication",
"x": 100,
"y": 200,
"width": 300,
"height": 150,
"fill": "transparent",
"color": "logic"
}
]
}]
}
```
**For code export, we focus on node-level comments (`metadata.comment`) since they're directly associated with specific code constructs.**
### Component-Level Comments (Future)
Components themselves could have description metadata:
```json
{
"name": "CheckoutForm",
"metadata": {
"description": "Multi-step checkout flow. Handles payment validation, address verification, and order submission."
},
"nodes": [...]
}
```
### Connection Comments (Future)
Potentially, connections between nodes could also have comments explaining data flow:
```json
{
"sourceId": "api-result",
"sourcePort": "items",
"targetId": "repeater",
"targetPort": "items",
"metadata": {
"comment": "Filtered products matching search criteria"
}
}
```
---
## Output Formats
### Function/Logic Files
```typescript
// logic/calculateShipping.ts
/**
* Calculates shipping cost based on weight and destination zone.
* Uses tiered pricing from the 2024 rate card.
*/
export function calculateShipping(weight: number, zone: string): number {
// ... implementation
}
```
### Component Files
```tsx
// components/CheckoutForm.tsx
/**
* Multi-step checkout flow.
* Handles payment validation, address verification, and order submission.
*/
export function CheckoutForm() {
// ...
}
```
### Inline Comments for Complex Logic
When a node's comment explains a specific piece of logic:
```tsx
function OrderSummary() {
const items = useArray(cartItemsArray);
// Apply member discount if user has active subscription
// (Business rule: 15% off for Premium, 10% for Basic)
const discount = useMemo(() => {
if (membershipLevel === 'premium') return 0.15;
if (membershipLevel === 'basic') return 0.10;
return 0;
}, [membershipLevel]);
// ...
}
```
### Store Comments
```typescript
// stores/variables.ts
/**
* Current user's membership level.
* Determines discount rates and feature access.
* Set during login, cleared on logout.
*/
export const membershipLevelVar = createVariable<'none' | 'basic' | 'premium'>('membershipLevel', 'none');
```
### Event Channel Comments
```typescript
// events/channels.ts
/**
* Fired when user completes checkout successfully.
* Triggers order confirmation email and inventory update.
*/
export const orderCompleted = createEventChannel<OrderCompletedEvent>('orderCompleted');
```
---
## Implementation
### Step 1: Extract Comments During Analysis
```typescript
interface NodeWithMetadata {
id: string;
type: string;
metadata?: {
comment?: string;
};
label?: string;
parameters?: Record<string, any>;
}
interface CommentInfo {
nodeId: string;
nodeType: string;
nodeName?: string;
comment: string;
placement: 'jsdoc' | 'inline' | 'block';
}
function extractNodeComments(nodes: NodeWithMetadata[]): Map<string, CommentInfo> {
const comments = new Map<string, CommentInfo>();
for (const node of nodes) {
// Comments are stored in node.metadata.comment
const comment = node.metadata?.comment;
if (comment && comment.trim()) {
comments.set(node.id, {
nodeId: node.id,
nodeType: node.type,
nodeName: node.label || node.parameters?.name,
comment: comment.trim(),
placement: determineCommentPlacement(node)
});
}
}
return comments;
}
function determineCommentPlacement(node: NodeWithMetadata): 'jsdoc' | 'inline' | 'block' {
// JSDoc for functions, components, exports
if (['Function', 'Javascript2', 'Component'].includes(node.type)) {
return 'jsdoc';
}
// JSDoc for state stores (Variables, Objects, Arrays)
if (['Variable', 'String', 'Number', 'Boolean', 'Object', 'Array'].includes(node.type)) {
return 'jsdoc';
}
// Inline for simple expressions, conditions
if (['Expression', 'Condition', 'Switch'].includes(node.type)) {
return 'inline';
}
// Block comments for complex logic nodes
return 'block';
}
```
### Step 2: Format Comments
```typescript
/**
* Format a comment as JSDoc
*/
function formatAsJSDoc(comment: string): string {
const lines = comment.split('\n');
if (lines.length === 1) {
return `/** ${comment} */`;
}
return [
'/**',
...lines.map(line => ` * ${line}`),
' */'
].join('\n');
}
/**
* Format a comment as inline
*/
function formatAsInline(comment: string): string {
// Single line
if (!comment.includes('\n') && comment.length < 80) {
return `// ${comment}`;
}
// Multi-line
return comment.split('\n').map(line => `// ${line}`).join('\n');
}
/**
* Format a comment as block
*/
function formatAsBlock(comment: string): string {
const lines = comment.split('\n');
if (lines.length === 1) {
return `/* ${comment} */`;
}
return [
'/*',
...lines.map(line => ` * ${line}`),
' */'
].join('\n');
}
```
### Step 3: Inject Comments During Generation
```typescript
// In function generator
function generateFunctionNode(
node: NoodlNode,
comments: Map<string, CommentInfo>
): string {
const comment = comments.get(node.id);
const functionCode = generateFunctionCode(node);
if (comment) {
const formattedComment = formatAsJSDoc(comment.comment);
return `${formattedComment}\n${functionCode}`;
}
return functionCode;
}
// In component generator
function generateComponent(
component: NoodlComponent,
nodeComments: Map<string, CommentInfo>
): string {
let code = '';
// Component-level comment
if (component.comment) {
code += formatAsJSDoc(component.comment) + '\n';
}
code += `export function ${component.name}() {\n`;
// Generate body with inline comments for relevant nodes
for (const node of component.nodes) {
const comment = nodeComments.get(node.id);
if (comment && comment.placement === 'inline') {
code += ` ${formatAsInline(comment.comment)}\n`;
}
code += generateNodeCode(node);
}
code += '}\n';
return code;
}
```
### Step 4: Handle Special Cases
#### Long Comments (Wrap at 80 chars)
```typescript
function wrapComment(comment: string, maxWidth: number = 80): string[] {
const words = comment.split(' ');
const lines: string[] = [];
let currentLine = '';
for (const word of words) {
if (currentLine.length + word.length + 1 > maxWidth) {
lines.push(currentLine.trim());
currentLine = word;
} else {
currentLine += (currentLine ? ' ' : '') + word;
}
}
if (currentLine) {
lines.push(currentLine.trim());
}
return lines;
}
```
#### Comments with Code References
```typescript
// If comment mentions node names, try to update references
function updateCommentReferences(
comment: string,
nodeNameMap: Map<string, string> // old name -> generated name
): string {
let updated = comment;
for (const [oldName, newName] of nodeNameMap) {
// Replace references like "the Calculate Total node" with "calculateTotal()"
const pattern = new RegExp(`\\b${escapeRegex(oldName)}\\b`, 'gi');
updated = updated.replace(pattern, `\`${newName}\``);
}
return updated;
}
```
#### Comments with TODO/FIXME/NOTE
```typescript
function enhanceComment(comment: string): string {
// Detect and format special markers
const markers = ['TODO', 'FIXME', 'NOTE', 'HACK', 'XXX', 'BUG'];
for (const marker of markers) {
if (comment.toUpperCase().startsWith(marker)) {
// Already has marker, keep as-is
return comment;
}
}
return comment;
}
```
---
## Examples
### Example 1: Function Node with Comment
**Noodl Node JSON:**
```json
{
"id": "function-123",
"type": "Function",
"label": "validateCard",
"metadata": {
"comment": "Validates credit card using Luhn algorithm. Returns true if valid."
},
"parameters": {
"code": "// user code here..."
}
}
```
**Visual (in editor):**
```
┌─────────────────────────────────────────┐
│ 💬 "Validates credit card using Luhn │
│ algorithm. Returns true if valid." │
├─────────────────────────────────────────┤
│ Function │
│ name: validateCard │
│─○ cardNumber │──○ isValid
└─────────────────────────────────────────┘
```
**Generated:**
```typescript
// logic/validateCard.ts
/**
* Validates credit card using Luhn algorithm.
* Returns true if valid.
*/
export function validateCard(cardNumber: string): boolean {
// Luhn algorithm implementation
const digits = cardNumber.replace(/\D/g, '');
let sum = 0;
let isEven = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = parseInt(digits[i], 10);
if (isEven) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
```
### Example 2: Variable with Comment
**Noodl Node JSON:**
```json
{
"id": "var-timeout",
"type": "Number",
"metadata": {
"comment": "Session timeout in milliseconds. Default 30 min. Configurable via admin settings."
},
"parameters": {
"name": "sessionTimeout",
"value": 1800000
}
}
```
**Visual (in editor):**
```
┌─────────────────────────────────────────┐
│ 💬 "Session timeout in milliseconds. │
│ Default 30 min. Configurable via │
│ admin settings." │
├─────────────────────────────────────────┤
│ Variable │
│ name: sessionTimeout │
│ value: 1800000 │
└─────────────────────────────────────────┘
```
**Generated:**
```typescript
// stores/variables.ts
/**
* Session timeout in milliseconds.
* Default 30 min. Configurable via admin settings.
*/
export const sessionTimeoutVar = createVariable<number>('sessionTimeout', 1800000);
```
### Example 3: Component with Multiple Commented Nodes
**Noodl:**
```
PaymentForm Component
💬 "Handles credit card and PayPal payments. Integrates with Stripe."
├── CardInput
│ 💬 "Auto-formats as user types (XXXX-XXXX-XXXX-XXXX)"
├── Condition (isPayPal)
│ 💬 "PayPal flow redirects to external site"
└── Submit Button
💬 "Disabled until form validates"
```
**Generated:**
```tsx
// components/PaymentForm.tsx
/**
* Handles credit card and PayPal payments.
* Integrates with Stripe.
*/
export function PaymentForm() {
const [paymentMethod] = useVariable(paymentMethodVar);
const [isValid] = useVariable(formValidVar);
return (
<div className={styles.form}>
{/* Auto-formats as user types (XXXX-XXXX-XXXX-XXXX) */}
<CardInput />
{/* PayPal flow redirects to external site */}
{paymentMethod === 'paypal' ? (
<PayPalRedirect />
) : (
<CardFields />
)}
{/* Disabled until form validates */}
<button disabled={!isValid}>
Submit Payment
</button>
</div>
);
}
```
### Example 4: Event Channel with Comment
**Noodl:**
```
┌─────────────────────────────────────────┐
│ 💬 "Broadcast when cart changes. │
│ Listeners: Header badge, Checkout │
│ button, Analytics tracker" │
├─────────────────────────────────────────┤
│ Send Event │
│ channel: cartUpdated │
│─○ itemCount │
│─○ totalPrice │
└─────────────────────────────────────────┘
```
**Generated:**
```typescript
// events/channels.ts
/**
* Broadcast when cart changes.
* Listeners: Header badge, Checkout button, Analytics tracker
*/
export const cartUpdated = createEventChannel<{
itemCount: number;
totalPrice: number;
}>('cartUpdated');
```
---
## Testing Checklist
### Comment Extraction
- [ ] Single-line comments extracted
- [ ] Multi-line comments extracted
- [ ] Empty/whitespace-only comments ignored
- [ ] Special characters in comments escaped properly
- [ ] Unicode characters preserved
### Comment Formatting
- [ ] JSDoc format correct for functions/components
- [ ] Inline comments on single line when short
- [ ] Multi-line inline comments formatted correctly
- [ ] Block comments formatted correctly
- [ ] Line wrapping at 80 characters
### Comment Placement
- [ ] Function comments above function declaration
- [ ] Component comments above component
- [ ] Variable comments above variable
- [ ] Inline logic comments at correct position
- [ ] Event channel comments preserved
### Edge Cases
- [ ] Comments with code snippets (backticks)
- [ ] Comments with URLs
- [ ] Comments with special markers (TODO, FIXME)
- [ ] Comments referencing other node names
- [ ] Very long comments (500+ characters)
---
## Success Criteria
1. **No comment loss** - Every node comment appears in generated code
2. **Correct placement** - Comments appear near relevant code
3. **Proper formatting** - Valid JSDoc/inline/block syntax
4. **Readability** - Comments enhance, not clutter, the code
5. **Accuracy** - Comment content unchanged (except formatting)
---
## Future Enhancements
1. **Comment Categories** - Support for `@param`, `@returns`, `@example` in function comments
2. **Automatic Documentation** - Generate README sections from component comments
3. **Comment Validation** - Warn about outdated comments referencing removed nodes
4. **Markdown Support** - Preserve markdown formatting in JSDoc
5. **Connection Comments** - Comments on wires explaining data flow

View File

@@ -0,0 +1,281 @@
# CODE-EXPORT: React Code Export System
## Overview
A comprehensive code export system that transforms Nodegx (Noodl) projects into clean, maintainable React 19 applications. Unlike a simple "eject with TODOs" approach, this system generates **fully functional code** by including a small companion library (`@nodegx/core`) that provides Noodl-like reactive primitives.
**Phase:** Future (Post Phase 3)
**Total Estimated Effort:** 12-16 weeks
**Strategic Value:** Very High - eliminates vendor lock-in concern
---
## Philosophy: The Companion Library Approach
### The Problem with Pure Code Export
A naive code export faces a fundamental paradigm mismatch:
| Noodl Model | React Model | Challenge |
|-------------|-------------|-----------|
| Push-based signals | Pull-based rendering | Signals → useEffect chains |
| Global Variables | Component state | Cross-component sync |
| Observable Objects | Plain objects | Change detection |
| Event propagation | Props/callbacks | Parent/child/sibling events |
| Visual states | CSS + useState | Animation transitions |
Attempting to mechanically translate every pattern results in either:
- **Unreadable code** (nested useEffect chains)
- **TODO comments** (giving up on hard parts)
### The Solution: @nodegx/core
Instead of fighting React's model, we provide a **tiny runtime library (~8KB)** that preserves Noodl's mental model while generating idiomatic code:
```
┌─────────────────────────────────────────────────────────────────┐
│ project.json │
│ (Noodl Node Graph) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Code Generator │
│ • Analyze component graph │
│ • Identify state boundaries │
│ • Generate React components │
│ • Preserve Function node code │
│ • Wire up reactive primitives │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Generated Project │
│ │
│ my-app/ │
│ ├── src/ │
│ │ ├── components/ ← Clean React components │
│ │ ├── stores/ ← From Variables/Objects/Arrays │
│ │ ├── logic/ ← Extracted Function node code │
│ │ ├── events/ ← Event channel definitions │
│ │ └── App.tsx ← Root with routing │
│ ├── package.json ← Depends on @nodegx/core │
│ └── vite.config.ts ← Modern build setup │
└─────────────────────────────────────────────────────────────────┘
```
---
## Task Series
| Task | Name | Effort | Description |
|------|------|--------|-------------|
| CODE-001 | @nodegx/core Library | 2-3 weeks | Companion runtime library |
| CODE-002 | Visual Node Generator | 1-2 weeks | UI components + styling |
| CODE-003 | State Store Generator | 1-2 weeks | Variables, Objects, Arrays |
| CODE-004 | Logic Node Generator | 2-3 weeks | Functions, Expressions, Logic |
| CODE-005 | Event System Generator | 1-2 weeks | Send/Receive Event, Component scope |
| CODE-006 | Project Scaffolding | 1-2 weeks | Routing, entry point, build config |
| CODE-007 | CLI & Integration | 1-2 weeks | Export command, editor integration |
**Total: 12-16 weeks**
---
## Noodl Feature → Generated Code Mapping
### Visual Nodes
| Noodl Node | Generated Code |
|------------|----------------|
| Group | `<div>` with Flexbox/CSS |
| Text | `<span>` / `<p>` with text binding |
| Image | `<img>` with src binding |
| Button | `<button>` with onClick |
| TextInput | `<input>` with onChange + controlled value |
| Checkbox | `<input type="checkbox">` |
| Repeater | `{array.map(item => <Component key={item.id} />)}` |
| Page Router | React Router `<Routes>` + `<Route>` |
| Page | Route component |
| Component Children | `{children}` prop |
### State & Data Nodes
| Noodl Node | Generated Code |
|------------|----------------|
| Variable | `createVariable()` from @nodegx/core |
| Set Variable | `variable.set(value)` |
| Object | `createObject()` with Proxy |
| Set Object Properties | `object.set(key, value)` |
| Array | `createArray()` reactive array |
| Static Array | Plain `const array = [...]` |
| Insert Into Array | `array.push()` / `array.insert()` |
| Remove From Array | `array.remove()` / `array.filter()` |
| Array Filter | `useArrayFilter(array, predicate)` |
| Array Map | `useArrayMap(array, transform)` |
### Logic Nodes
| Noodl Node | Generated Code |
|------------|----------------|
| Function | Extracted function in `/logic/` |
| Expression | Inline expression or `useMemo` |
| Condition | Ternary or `if` statement |
| Switch | `switch` statement or object lookup |
| And / Or / Not | `&&` / `||` / `!` operators |
| States | State machine using `createStateMachine()` |
| Delay | `setTimeout` wrapped in cleanup |
| Debounce | `useDebouncedValue()` hook |
### Event & Communication Nodes
| Noodl Node | Generated Code |
|------------|----------------|
| Send Event | `events.emit(channel, data)` |
| Receive Event | `useEvent(channel, handler)` |
| Component Inputs | Component props |
| Component Outputs | Callback props |
| Navigate | `useNavigate()` from React Router |
### Component Scope Nodes
| Noodl Node | Generated Code |
|------------|----------------|
| Component Object | `useComponentStore()` hook |
| Parent Component Object | `useParentStore()` with context |
| Repeater Object | `item` from map callback |
| For Each Item | `item` from map callback |
---
## Architecture Decision Records
### ADR-001: Companion Library vs Pure Export
**Decision:** Include @nodegx/core companion library
**Rationale:**
- Preserves Noodl's mental model (easier for users to understand)
- Generates cleaner, more maintainable code
- Avoids useEffect spaghetti
- Library is small (~8KB) and tree-shakeable
- Enables future multi-framework support (same primitives, different renderers)
**Trade-offs:**
- Still has a "runtime" dependency
- Not 100% "pure React"
### ADR-002: Code Generator Architecture
**Decision:** AST-based generation with templates
**Approach:**
1. Parse project.json into intermediate representation (IR)
2. Analyze component boundaries and dependencies
3. Generate TypeScript AST using ts-morph
4. Apply formatting with Prettier
5. Write files to output directory
**Why AST over string templates:**
- Type-safe code generation
- Automatic import management
- Easier to maintain and extend
- Better handling of edge cases
### ADR-003: Styling Approach
**Decision:** CSS Modules by default, Tailwind optional
**Options considered:**
- Inline styles (what Noodl uses internally)
- CSS Modules (clean separation)
- Tailwind CSS (utility-first)
- Styled Components (CSS-in-JS)
**Rationale:**
- CSS Modules work everywhere, no build config needed
- Easy to migrate to other approaches
- Tailwind can be enabled as an option for users who prefer it
---
## Success Criteria
### Functional Requirements
1. **Visual Parity** - Exported app looks identical to Noodl preview
2. **Behavioral Parity** - All interactions work the same
3. **Data Flow Parity** - State changes propagate correctly
4. **Event Parity** - Events trigger correct handlers
### Code Quality Requirements
1. **Readable** - A React developer can understand the code
2. **Maintainable** - Code follows React best practices
3. **Typed** - Full TypeScript with proper types
4. **Formatted** - Consistent code style (Prettier)
5. **Organized** - Logical file structure
### Performance Requirements
1. **Bundle Size** - @nodegx/core adds < 10KB gzipped
2. **Runtime Performance** - No worse than hand-written React
3. **Build Time** - Export completes in < 30 seconds for typical project
---
## Out of Scope (Phase 1)
The following are explicitly NOT included in the initial implementation:
1. **Database/Cloud nodes** - Query Records, Cloud Functions (placeholder stubs)
2. **Round-trip editing** - Cannot re-import exported code
3. **Framework targets** - Only React 19 initially
4. **Native targets** - No React Native export yet
5. **SSR/SSG** - Client-side only initially
---
## Related Documents
- [CODE-001: @nodegx/core Library](./CODE-001-nodegx-core-library.md)
- [CODE-002: Visual Node Generator](./CODE-002-visual-node-generator.md)
- [CODE-003: State Store Generator](./CODE-003-state-store-generator.md)
- [CODE-004: Logic Node Generator](./CODE-004-logic-node-generator.md)
- [CODE-005: Event System Generator](./CODE-005-event-system-generator.md)
- [CODE-006: Project Scaffolding](./CODE-006-project-scaffolding.md)
---
## Appendix: What This Enables
### Use Cases
1. **Prototype → Production Handoff**
- Build MVP in Nodegx
- Validate with users
- Export for engineering team
2. **Outgrowing Low-Code**
- Project needs custom functionality
- Export and continue in code
3. **Learning Tool**
- See how visual designs become code
- Learn React patterns
4. **Component Libraries**
- Build UI components visually
- Export for use in other projects
5. **Hybrid Development**
- Design system in Nodegx
- Export components
- Use in larger codebase
### Strategic Benefits
1. **Eliminates vendor lock-in** - Users can always leave
2. **Builds trust** - Transparent about what Nodegx does
3. **Enables enterprise adoption** - IT teams can audit code
4. **Creates evangelists** - Exported code spreads Nodegx patterns

View File

@@ -0,0 +1,999 @@
# CODE-REFERENCE: Noodl Node Export Reference
## Overview
This document provides a comprehensive reference for how each Noodl-specific node type transforms into generated React code. This is the definitive guide for implementing the code generator.
---
## Table of Contents
1. [State Nodes](#state-nodes)
2. [Component Scope Nodes](#component-scope-nodes)
3. [Event Nodes](#event-nodes)
4. [Logic Nodes](#logic-nodes)
5. [Visual Container Nodes](#visual-container-nodes)
6. [Data Manipulation Nodes](#data-manipulation-nodes)
7. [Navigation Nodes](#navigation-nodes)
8. [Utility Nodes](#utility-nodes)
---
## State Nodes
### Variable / String / Number / Boolean / Color
**Purpose:** Global reactive values that can be read/written from anywhere.
**Noodl Pattern:**
```
┌──────────────┐
│ Variable │
│ name: "foo" │──○ Value
│ value: 123 │
└──────────────┘
```
**Generated Code:**
```typescript
// stores/variables.ts
import { createVariable } from '@nodegx/core';
export const fooVar = createVariable<number>('foo', 123);
// Usage in component
import { useVariable } from '@nodegx/core';
import { fooVar } from '../stores/variables';
function MyComponent() {
const [foo, setFoo] = useVariable(fooVar);
return <span>{foo}</span>;
}
// Usage in logic
import { getVariable, setVariable } from '@nodegx/core';
import { fooVar } from '../stores/variables';
function updateFoo(newValue: number) {
setVariable(fooVar, newValue);
}
```
---
### Set Variable
**Purpose:** Updates a Variable's value.
**Noodl Pattern:**
```
┌───────────────────┐
│ Set Variable │
│ name: "foo" │
│─○ Value │
│─○ Do │──○ Done
└───────────────────┘
```
**Generated Code:**
```typescript
// Inline in component
import { fooVar } from '../stores/variables';
<button onClick={() => fooVar.set(newValue)}>
Update
</button>
// Or via hook
const [, setFoo] = useVariable(fooVar);
<button onClick={() => setFoo(newValue)}>
Update
</button>
```
---
### Object
**Purpose:** Read properties from a reactive object by ID.
**Noodl Pattern:**
```
┌────────────────┐
│ Object │
│ id: "user" │──○ name
│ │──○ email
│ │──○ avatar
└────────────────┘
```
**Generated Code:**
```typescript
// stores/objects.ts
import { createObject } from '@nodegx/core';
export interface User {
name: string;
email: string;
avatar: string;
}
export const userObj = createObject<User>('user', {
name: '',
email: '',
avatar: ''
});
// Usage in component
import { useObject } from '@nodegx/core';
import { userObj } from '../stores/objects';
function UserProfile() {
const user = useObject(userObj);
return (
<div>
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
```
---
### Set Object Properties
**Purpose:** Updates properties on an Object.
**Noodl Pattern:**
```
┌──────────────────────┐
│ Set Object Props │
│ id: "user" │
│─○ name │
│─○ email │
│─○ Do │──○ Done
└──────────────────────┘
```
**Generated Code:**
```typescript
import { userObj } from '../stores/objects';
// Single property
userObj.set('name', 'John Doe');
// Multiple properties
userObj.setProperties({
name: 'John Doe',
email: 'john@example.com'
});
// Replace entire object
userObj.setAll({
name: 'John Doe',
email: 'john@example.com',
avatar: '/avatars/john.jpg'
});
```
---
### Array
**Purpose:** Read from a reactive array by ID.
**Noodl Pattern:**
```
┌──────────────┐
│ Array │
│ id: "items" │──○ Items
│ │──○ Count
└──────────────┘
```
**Generated Code:**
```typescript
// stores/arrays.ts
import { createArray } from '@nodegx/core';
export interface Item {
id: string;
name: string;
price: number;
}
export const itemsArray = createArray<Item>('items', []);
// Usage in component
import { useArray } from '@nodegx/core';
import { itemsArray } from '../stores/arrays';
function ItemList() {
const items = useArray(itemsArray);
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// Get count
const count = itemsArray.length;
```
---
### Static Array
**Purpose:** Constant array data (not reactive).
**Noodl Pattern:**
```
┌─────────────────────┐
│ Static Array │
│ items: [{...},...] │──○ Items
│ │──○ Count
└─────────────────────┘
```
**Generated Code:**
```typescript
// stores/staticArrays.ts
export interface MenuItem {
label: string;
path: string;
icon: string;
}
export const menuItems: MenuItem[] = [
{ label: 'Home', path: '/', icon: 'home' },
{ label: 'Products', path: '/products', icon: 'box' },
{ label: 'Contact', path: '/contact', icon: 'mail' }
];
// Usage in component
import { menuItems } from '../stores/staticArrays';
function NavMenu() {
return (
<nav>
{menuItems.map(item => (
<NavLink key={item.path} to={item.path}>
<Icon name={item.icon} />
{item.label}
</NavLink>
))}
</nav>
);
}
```
---
### States
**Purpose:** Simple state machine with named states and optional values per state.
**Noodl Pattern:**
```
┌──────────────────────┐
│ States │
│ states: [idle, │
│ loading, success, │
│ error] │
│ start: idle │
│─○ To Idle │
│─○ To Loading │
│─○ To Success │
│─○ To Error │──○ State
│ │──○ At Idle
│ │──○ At Loading
│ │──○ opacity (value)
└──────────────────────┘
```
**Generated Code:**
```typescript
// stores/stateMachines.ts
import { createStateMachine } from '@nodegx/core';
export type LoadingState = 'idle' | 'loading' | 'success' | 'error';
export const loadingStateMachine = createStateMachine<LoadingState>({
states: ['idle', 'loading', 'success', 'error'],
initial: 'idle',
values: {
idle: { opacity: 1, message: '' },
loading: { opacity: 0.5, message: 'Loading...' },
success: { opacity: 1, message: 'Complete!' },
error: { opacity: 1, message: 'Error occurred' }
}
});
// Usage in component
import { useStateMachine, useStateValues } from '@nodegx/core';
import { loadingStateMachine } from '../stores/stateMachines';
function LoadingButton() {
const [state, goTo] = useStateMachine(loadingStateMachine);
const values = useStateValues(loadingStateMachine);
const handleClick = async () => {
goTo('loading');
try {
await doSomething();
goTo('success');
} catch {
goTo('error');
}
};
return (
<button
onClick={handleClick}
style={{ opacity: values.opacity }}
disabled={state === 'loading'}
>
{values.message || 'Submit'}
</button>
);
}
```
---
## Component Scope Nodes
### Component Object (Component State)
**Purpose:** State scoped to a component instance. Each instance has its own state.
**Noodl Pattern:**
```
┌────────────────────┐
│ Component Object │──○ count
│ (Component State) │──○ isOpen
└────────────────────┘
```
**Runtime Behavior:**
- Created when component mounts
- Destroyed when component unmounts
- Isolated per instance
**Generated Code:**
```typescript
// components/Counter.tsx
import {
ComponentStoreProvider,
useComponentStore,
useSetComponentStore
} from '@nodegx/core';
interface CounterState {
count: number;
isOpen: boolean;
}
function CounterInner() {
const state = useComponentStore<CounterState>();
const { set } = useSetComponentStore<CounterState>();
return (
<div>
<span>{state.count}</span>
<button onClick={() => set('count', state.count + 1)}>+</button>
<button onClick={() => set('isOpen', !state.isOpen)}>Toggle</button>
</div>
);
}
export function Counter() {
return (
<ComponentStoreProvider<CounterState>
initialState={{ count: 0, isOpen: false }}
>
<CounterInner />
</ComponentStoreProvider>
);
}
```
---
### Parent Component Object
**Purpose:** Read the Component Object of the visual parent component.
**Noodl Pattern:**
```
┌──────────────────────────┐
│ Parent Component Object │──○ selectedId
│ │──○ isExpanded
└──────────────────────────┘
```
**Runtime Behavior:**
- Reads from parent's Component Object
- Read-only (cannot set parent's state directly)
- Updates when parent's state changes
**Generated Code:**
```typescript
// components/ListItem.tsx
import { useParentComponentStore } from '@nodegx/core';
interface ParentListState {
selectedId: string | null;
isExpanded: boolean;
}
function ListItem({ id, label }: { id: string; label: string }) {
const parentState = useParentComponentStore<ParentListState>();
const isSelected = parentState?.selectedId === id;
const showDetails = parentState?.isExpanded && isSelected;
return (
<div className={isSelected ? 'selected' : ''}>
<span>{label}</span>
{showDetails && <ItemDetails id={id} />}
</div>
);
}
```
---
### Component Children
**Purpose:** Slot where child components are rendered.
**Noodl Pattern:**
```
Card Component:
┌──────────────────────┐
│ ┌────────────────┐ │
│ │ Header │ │
│ └────────────────┘ │
│ ┌────────────────┐ │
│ │ [Component │ │
│ │ Children] │ │
│ └────────────────┘ │
│ ┌────────────────┐ │
│ │ Footer │ │
│ └────────────────┘ │
└──────────────────────┘
```
**Generated Code:**
```typescript
// components/Card.tsx
import styles from './Card.module.css';
interface CardProps {
children?: React.ReactNode;
title?: string;
}
export function Card({ children, title }: CardProps) {
return (
<div className={styles.card}>
<div className={styles.header}>
<h3>{title}</h3>
</div>
<div className={styles.content}>
{children}
</div>
<div className={styles.footer}>
<span>Card Footer</span>
</div>
</div>
);
}
// Usage
<Card title="My Card">
<p>This content goes in the children slot</p>
<Button>Click me</Button>
</Card>
```
---
### For Each / Repeater
**Purpose:** Iterate over an array, rendering a template component for each item.
**Noodl Pattern:**
```
┌───────────────────┐
│ For Each │
│─○ Items │
│ template: "Card" │
└───────────────────┘
```
**Generated Code:**
```typescript
// components/ItemList.tsx
import { useArray, RepeaterItemProvider } from '@nodegx/core';
import { itemsArray } from '../stores/arrays';
import { ItemCard } from './ItemCard';
export function ItemList() {
const items = useArray(itemsArray);
return (
<div className="item-list">
{items.map((item, index) => (
<RepeaterItemProvider
key={item.id}
item={item}
index={index}
itemId={`item_${item.id}`}
>
<ItemCard />
</RepeaterItemProvider>
))}
</div>
);
}
// ItemCard.tsx - the template component
import { useRepeaterItem, useRepeaterIndex } from '@nodegx/core';
interface Item {
id: string;
name: string;
price: number;
}
export function ItemCard() {
const item = useRepeaterItem<Item>();
const index = useRepeaterIndex();
return (
<div className="item-card">
<span className="index">#{index + 1}</span>
<h4>{item.name}</h4>
<span className="price">${item.price}</span>
</div>
);
}
```
---
### Repeater Object (For Each Item)
**Purpose:** Access the current item inside a For Each template.
**Noodl Pattern:**
```
Inside For Each template:
┌──────────────────┐
│ Repeater Object │──○ id
│ │──○ name
│ │──○ price
└──────────────────┘
```
**Generated Code:**
```typescript
import { useRepeaterItem } from '@nodegx/core';
function ProductCard() {
const product = useRepeaterItem<Product>();
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
```
---
## Event Nodes
### Send Event
**Purpose:** Broadcast an event with optional data.
**Noodl Pattern:**
```
┌─────────────────────┐
│ Send Event │
│ channel: "refresh" │
│ mode: "global" │
│─○ itemId │
│─○ Send │
└─────────────────────┘
```
**Generated Code (Global):**
```typescript
// events/channels.ts
import { createEventChannel } from '@nodegx/core';
interface RefreshEvent {
itemId: string;
}
export const refreshEvent = createEventChannel<RefreshEvent>('refresh');
// Sending
import { refreshEvent } from '../events/channels';
function handleSend(itemId: string) {
refreshEvent.send({ itemId });
}
<button onClick={() => handleSend('123')}>Refresh</button>
```
**Generated Code (Scoped - Parent/Children/Siblings):**
```typescript
import { useScopedEventSender } from '../events/ComponentEventContext';
function ChildComponent() {
const { sendToParent } = useScopedEventSender();
return (
<button onClick={() => sendToParent('itemSelected', { itemId: '123' })}>
Select
</button>
);
}
```
---
### Receive Event
**Purpose:** Listen for events on a channel.
**Noodl Pattern:**
```
┌─────────────────────┐
│ Receive Event │
│ channel: "refresh" │──○ itemId
│ │──○ Received
└─────────────────────┘
```
**Generated Code (Global):**
```typescript
// Using generated hook
import { useRefreshEvent } from '../events/hooks';
function DataPanel() {
useRefreshEvent((data) => {
console.log('Refresh requested for:', data.itemId);
fetchData(data.itemId);
});
return <div>...</div>;
}
// Or using generic hook
import { useEventChannel } from '../events/hooks';
import { refreshEvent } from '../events/channels';
function DataPanel() {
useEventChannel(refreshEvent, (data) => {
fetchData(data.itemId);
});
return <div>...</div>;
}
```
**Generated Code (Scoped):**
```typescript
import { useScopedEvent } from '../events/ComponentEventContext';
function ParentComponent() {
useScopedEvent('itemSelected', (data: { itemId: string }) => {
setSelectedId(data.itemId);
});
return (
<ComponentEventProvider>
<ItemList />
</ComponentEventProvider>
);
}
```
---
## Logic Nodes
### Function Node
**Purpose:** Custom JavaScript code with inputs/outputs.
**Noodl Pattern:**
```
┌─────────────────────┐
│ Function │
│─○ value │
│─○ multiplier │──○ result
│─○ Run │──○ done
│ │
│ Script: │
│ const result = │
│ Inputs.value * │
│ Inputs.multiplier;│
│ Outputs.result = │
│ result; │
│ Outputs.done(); │
└─────────────────────┘
```
**Generated Code:**
```typescript
// logic/mathFunctions.ts
import { createSignal } from '@nodegx/core';
export const onMultiplyDone = createSignal('onMultiplyDone');
export function multiply(value: number, multiplier: number): number {
const result = value * multiplier;
onMultiplyDone.send();
return result;
}
// Usage in component
import { multiply, onMultiplyDone } from '../logic/mathFunctions';
import { useSignal } from '@nodegx/core';
function Calculator() {
const [result, setResult] = useState(0);
useSignal(onMultiplyDone, () => {
console.log('Calculation complete!');
});
const handleCalculate = () => {
const newResult = multiply(value, multiplier);
setResult(newResult);
};
return <button onClick={handleCalculate}>Calculate</button>;
}
```
---
### Expression
**Purpose:** Evaluate a JavaScript expression reactively.
**Noodl Pattern:**
```
┌────────────────────────────────────┐
│ Expression │
│─○ a │
│─○ b │──○ result
│ │──○ isTrue
│ expression: (a + b) * 2 │──○ isFalse
└────────────────────────────────────┘
```
**Generated Code:**
```typescript
// Simple case - inline
const result = (a + b) * 2;
// With dependencies - useMemo
const result = useMemo(() => (a + b) * 2, [a, b]);
// Accessing Noodl globals
import { useVariable } from '@nodegx/core';
import { taxRateVar, subtotalVar } from '../stores/variables';
function TaxCalculator() {
const [taxRate] = useVariable(taxRateVar);
const [subtotal] = useVariable(subtotalVar);
const tax = useMemo(() => subtotal * taxRate, [subtotal, taxRate]);
return <span>Tax: ${tax.toFixed(2)}</span>;
}
```
---
### Condition
**Purpose:** Route execution based on boolean value.
**Generated Code:**
```typescript
// Visual conditional
{isLoggedIn ? <Dashboard /> : <LoginForm />}
// Logic conditional
if (isValid) {
onSuccess.send();
} else {
onFailure.send();
}
```
---
### Switch
**Purpose:** Route based on value matching cases.
**Generated Code:**
```typescript
// Component selection
const viewComponents = {
list: ListView,
grid: GridView,
table: TableView
};
const ViewComponent = viewComponents[viewMode] || ListView;
return <ViewComponent items={items} />;
// Action routing
switch (action) {
case 'save':
handleSave();
break;
case 'delete':
handleDelete();
break;
case 'cancel':
handleCancel();
break;
}
```
---
### And / Or / Not
**Purpose:** Boolean logic operations.
**Generated Code:**
```typescript
// And
const canSubmit = isValid && !isLoading && hasPermission;
// Or
const showWarning = hasErrors || isExpired;
// Not
const isDisabled = !isEnabled;
// Combined
const showContent = (isLoggedIn && hasAccess) || isAdmin;
```
---
## Navigation Nodes
### Page Router
**Purpose:** Define application routes.
**Generated Code:**
```typescript
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
export function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/products/:id" element={<ProductDetailPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
}
```
---
### Navigate
**Purpose:** Programmatic navigation.
**Generated Code:**
```typescript
import { useNavigate } from 'react-router-dom';
function NavButton({ to }: { to: string }) {
const navigate = useNavigate();
return (
<button onClick={() => navigate(to)}>
Go
</button>
);
}
// With parameters
const productId = '123';
navigate(`/products/${productId}`);
// With options
navigate('/login', { replace: true });
```
---
### Page Inputs / Page Outputs
**Purpose:** Pass data to/from pages via route parameters.
**Generated Code:**
```typescript
// Page Inputs (route parameters)
import { useParams, useSearchParams } from 'react-router-dom';
function ProductPage() {
// URL params (/products/:id)
const { id } = useParams<{ id: string }>();
// Query params (/products?category=electronics)
const [searchParams] = useSearchParams();
const category = searchParams.get('category');
return <div>Product {id} in {category}</div>;
}
// Page Outputs (navigate with state)
navigate('/checkout', { state: { cartItems } });
// Read in target page
import { useLocation } from 'react-router-dom';
const { state } = useLocation();
const cartItems = state?.cartItems;
```
---
## Summary: Quick Reference Table
| Noodl Node | @nodegx/core Primitive | Generated Pattern |
|------------|------------------------|-------------------|
| Variable | `createVariable` | Store + `useVariable` hook |
| Object | `createObject` | Store + `useObject` hook |
| Array | `createArray` | Store + `useArray` hook |
| Static Array | N/A | Constant export |
| States | `createStateMachine` | Store + `useStateMachine` hook |
| Component Object | `ComponentStoreProvider` | Context provider wrapper |
| Parent Component Object | `useParentComponentStore` | Context consumer hook |
| Component Children | N/A | `{children}` prop |
| For Each / Repeater | `RepeaterItemProvider` | `.map()` with context |
| Repeater Object | `useRepeaterItem` | Context consumer hook |
| Send Event (global) | `events.emit` | Event channel |
| Send Event (scoped) | `useScopedEventSender` | Context-based events |
| Receive Event | `useEvent` / hooks | Event subscription |
| Function | N/A | Extracted function |
| Expression | N/A | Inline or `useMemo` |
| Condition | N/A | Ternary / `if` |
| Switch | N/A | `switch` / object lookup |
| And/Or/Not | N/A | `&&` / `||` / `!` |
| Navigate | `useNavigate` | React Router |
| Page Router | `<Routes>` | React Router |

View File

@@ -0,0 +1,70 @@
# Phase 7: Code Export - Progress Tracker
**Last Updated:** 2026-01-07
**Overall Status:** 🔴 Not Started
---
## Quick Summary
| Metric | Value |
| ------------ | ------ |
| Total Tasks | 8 |
| Completed | 0 |
| In Progress | 0 |
| Not Started | 8 |
| **Progress** | **0%** |
---
## Task Status
| Task | Name | Status | Effort | Notes |
| -------- | ---------------------- | -------------- | --------- | ----------------------------- |
| CODE-001 | @nodegx/core Library | 🔴 Not Started | 2-3 weeks | Companion runtime library |
| CODE-002 | Visual Node Generator | 🔴 Not Started | 1-2 weeks | UI components + styling |
| CODE-003 | State Store Generator | 🔴 Not Started | 1-2 weeks | Variables, Objects, Arrays |
| CODE-004 | Logic Node Generator | 🔴 Not Started | 2-3 weeks | Functions, Expressions, Logic |
| CODE-005 | Event System Generator | 🔴 Not Started | 1-2 weeks | Send/Receive Event, Scope |
| CODE-006 | Project Scaffolding | 🔴 Not Started | 1-2 weeks | Routing, entry point, build |
| CODE-007 | CLI & Integration | 🔴 Not Started | 1-2 weeks | Export command, editor UI |
| CODE-008 | Node Comments Export | 🔴 Not Started | 1 week | Export node comments to code |
---
## Status Legend
- 🔴 **Not Started** - Work has not begun
- 🟡 **In Progress** - Actively being worked on
- 🟢 **Complete** - Finished and verified
---
## Recent Updates
| Date | Update |
| ---------- | ----------------------------------------------- |
| 2026-01-07 | Updated PROGRESS.md to list all 8 defined tasks |
| 2026-01-07 | Renumbered from Phase 6 to Phase 7 |
---
## Dependencies
Depends on: Phase 2 (React Migration), Phase 10 (Project Structure)
---
## Documentation
- [CODE-EXPORT-overview.md](./CODE-EXPORT-overview.md) - Full system overview
- [CODE-REFERENCE-noodl-nodes.md](./CODE-REFERENCE-noodl-nodes.md) - Node mapping reference
- Individual task specs: CODE-001 through CODE-008
---
## Notes
This phase enables exporting Noodl projects as standalone React 19 applications. Uses a companion library approach (@nodegx/core) to preserve Noodl's mental model while generating idiomatic React code.
**Estimated Total Effort:** 12-16 weeks