# 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 { 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 ? : } // As conditional render if (condition) { return ; } return ; // 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 void> = { a: handleCaseA, b: handleCaseB, c: handleCaseC }; (handlers[value] || handleDefault)(); // As component mapping const components: Record = { a: ComponentA, b: ComponentB, c: ComponentC }; const Component = components[value] || DefaultComponent; return ; ``` ### 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({ 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 ( ); } ``` --- ## 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(); 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(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 any>( callback: T, delay: number ): T { const callbackRef = useRef(callback); callbackRef.current = callback; const timeoutRef = useRef(); return useCallback((...args: Parameters) => { 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 (
{count}
); } ``` --- ## 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 { const analysis = new Map(); 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 { 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