TASK-009: Replace Monaco Code Editor in Expression/Function/Script Nodes
Overview
Replace the broken Monaco code editor in Expression, Function, and Script nodes with a lightweight, custom React-based JavaScript editor that works reliably in Electron.
Critical Requirement: 100% backward compatible - All existing projects must load their code without any data loss or connection loss.
Problem Statement
Current State
- Monaco is broken in Electron - Web worker loading failures flood the console
- Expression nodes don't work - Users can't type or see their code
- Function/Script nodes at risk - Same Monaco dependency, likely same issues
- User trust at stake - Every Noodl project has Expression/Function/Script nodes
Error Symptoms
Error: Unexpected usage
at EditorSimpleWorker.loadForeignModule
Cannot use import statement outside a module
Why Monaco Fails
Monaco relies on web workers for TypeScript/JavaScript language services. In Electron's CommonJS environment, the worker module loading is broken. TASK-008 encountered the same issue with JSON editing and solved it by ditching Monaco entirely.
Solution Design
Approach: Custom React-Based Editor
Following TASK-008's successful pattern, build a simple, reliable code editor without Monaco:
- Textarea-based - No complex dependencies
- Validation on blur - Catch syntax errors without real-time overhead
- Line numbers - Essential for debugging
- Format button - Basic code prettification
- No syntax highlighting - Keeps it simple and performant
Why This Will Work
- Proven Pattern - TASK-008 already did this successfully for JSON
- Electron Compatible - No web workers, no module loading issues
- Lightweight - Fast, reliable, maintainable
- Backward Compatible - Reads/writes same string format as before
Critical Safety Requirements
1. Data Preservation (ABSOLUTE PRIORITY)
The new editor MUST:
- Read code from the exact same model property:
model.getParameter('code') - Write code to the exact same model property:
model.setParameter('code', value) - Support all existing code without any transformation
- Handle multiline strings, special characters, Unicode, etc.
Test criteria:
// Before migration:
const existingCode = model.getParameter('code'); // "return a + b;"
// After migration (with new editor):
const loadedCode = model.getParameter('code'); // MUST BE: "return a + b;"
// Identity test:
expect(loadedCode).toBe(existingCode); // MUST PASS
2. Connection Preservation (CRITICAL)
Node connections are NOT stored in the editor - they're in the node definition and graph model.
- Inputs/outputs defined by node configuration, not editor
- Editor only edits the code string
- Changing editor UI cannot affect connections
Test criteria:
- Open project with Expression nodes that have connections
- Verify all input/output connections are visible
- Edit code in new editor
- Close and reopen project
- Verify all connections still intact
3. No Data Migration Required
Key insight: The editor is just a UI component for editing a string property.
// Old Monaco editor:
<MonacoEditor
value={model.getParameter('code')}
onChange={(value) => model.setParameter('code', value)}
/>
// New custom editor:
<JavaScriptEditor
value={model.getParameter('code')}
onChange={(value) => model.setParameter('code', value)}
/>
Same input, same output, just different UI.
Technical Implementation
Component Structure
packages/noodl-core-ui/src/components/
└── code-editor/
├── JavaScriptEditor.tsx # Main editor component
├── JavaScriptEditor.module.scss
├── index.ts
│
├── components/
│ ├── LineNumbers.tsx # Line number gutter
│ ├── ValidationBar.tsx # Error/warning display
│ └── CodeTextarea.tsx # Textarea with enhancements
│
└── utils/
├── jsValidator.ts # Syntax validation (try/catch eval)
├── jsFormatter.ts # Simple indentation
└── types.ts # TypeScript definitions
API Design
interface JavaScriptEditorProps {
/** Code value (string) */
value: string;
/** Called when code changes */
onChange: (value: string) => void;
/** Called on save (Cmd+S) */
onSave?: (value: string) => void;
/** Validation mode */
validationType?: 'expression' | 'function' | 'script';
/** Read-only mode */
disabled?: boolean;
/** Height */
height?: number | string;
/** Placeholder text */
placeholder?: string;
}
// Usage in Expression node:
<JavaScriptEditor
value={model.getParameter('code')}
onChange={(code) => model.setParameter('code', code)}
onSave={(code) => model.setParameter('code', code)}
validationType="expression"
height="200px"
/>;
Validation Strategy
Expression nodes: Validate as JavaScript expression
function validateExpression(code) {
try {
// Try to eval as expression (in isolated context)
new Function('return (' + code + ')');
return { valid: true };
} catch (err) {
return {
valid: false,
error: err.message,
suggestion: 'Check for syntax errors in your expression'
};
}
}
Function nodes: Validate as function body
function validateFunction(code) {
try {
new Function(code);
return { valid: true };
} catch (err) {
return {
valid: false,
error: err.message,
line: extractLineNumber(err)
};
}
}
Script nodes: Same as function validation
Integration Strategy
Phase 1: Expression Nodes (HIGHEST PRIORITY)
Why Expression first:
- Most commonly used (every project has them)
- Simpler validation (single expression)
- Least risky to change
Integration steps:
- Create JavaScriptEditor component
- Find where Expression nodes use Monaco
- Replace Monaco import with JavaScriptEditor import
- Test with existing projects (NO data migration needed)
- Verify all connections work
Safety checkpoint:
- Load 10 real Noodl projects
- Open every Expression node
- Verify code loads correctly
- Verify connections intact
- Edit and save
- Reopen - verify changes persisted
Phase 2: Function Nodes (PROCEED WITH CAUTION)
Why Function second:
- Less common than Expression
- More complex (multiple statements)
- Users likely have critical business logic here
Integration steps:
- Use same JavaScriptEditor component
- Change validation mode to 'function'
- Test extensively with real-world Function nodes
- Verify input/output definitions preserved
Safety checkpoint:
- Test with Functions that have:
- Multiple inputs/outputs
- Complex logic
- Dependencies on other nodes
- Async operations
Phase 3: Script Nodes (MOST CAREFUL)
Why Script last:
- Can contain any JavaScript
- May have side effects
- Least used (gives us time to perfect editor)
Integration steps:
- Use same JavaScriptEditor component
- Validation mode: 'script'
- Test with real Script nodes from projects
- Ensure lifecycle hooks preserved
Subtasks
Phase 1: Core JavaScript Editor (2-3 days)
- CODE-001: Create JavaScriptEditor component structure
- CODE-002: Implement CodeTextarea with line numbers
- CODE-003: Add syntax validation (expression mode)
- CODE-004: Add ValidationBar with error display
- CODE-005: Add format/indent button
- CODE-006: Add keyboard shortcuts (Cmd+S)
Phase 2: Expression Node Integration (1-2 days)
- CODE-007: Locate Expression node Monaco usage
- CODE-008: Replace Monaco with JavaScriptEditor
- CODE-009: Test with 10 real projects (data preservation)
- CODE-010: Test with various expression patterns
- CODE-011: Verify connections preserved
Phase 3: Function Node Integration (1-2 days)
- CODE-012: Add function validation mode
- CODE-013: Replace Monaco in Function nodes
- CODE-014: Test with real Function nodes
- CODE-015: Verify input/output preservation
Phase 4: Script Node Integration (1 day)
- CODE-016: Add script validation mode
- CODE-017: Replace Monaco in Script nodes
- CODE-018: Test with real Script nodes
- CODE-019: Final integration testing
Phase 5: Cleanup (1 day)
- CODE-020: Remove Monaco dependencies (if unused elsewhere)
- CODE-021: Add Storybook stories
- CODE-022: Documentation and migration notes
Data Safety Testing Protocol
For Each Node Type (Expression, Function, Script):
Test 1: Load Existing Code
- Open project created before migration
- Click on node to open code editor
- ✅ Code appears exactly as saved
- ✅ No garbling, no loss, no transformation
Test 2: Connection Preservation
- Open node with multiple input/output connections
- Verify connections visible in graph
- Open code editor
- Edit code
- Close editor
- ✅ All connections still intact
Test 3: Save and Reload
- Edit code in new editor
- Save
- Close project
- Reopen project
- ✅ Code changes persisted correctly
Test 4: Special Characters
- Test with code containing:
- Multiline strings
- Unicode characters
- Special symbols (`, ", ', \n, etc.)
- Comments with special chars
- ✅ All characters preserved
Test 5: Large Code
- Test with Function/Script containing 100+ lines
- ✅ Loads quickly
- ✅ Edits smoothly
- ✅ Saves correctly
Acceptance Criteria
Functional
- ✅ Expression, Function, and Script nodes can edit code without Monaco
- ✅ Syntax errors are caught and displayed clearly
- ✅ Line numbers help locate errors
- ✅ Format button improves readability
- ✅ Keyboard shortcuts work (Cmd+S to save)
Safety (CRITICAL)
- ✅ All existing projects load their code correctly
- ✅ No data loss when opening/editing/saving
- ✅ All input/output connections preserved
- ✅ Code with special characters works
- ✅ Multiline code works
Performance
- ✅ Editor opens instantly (no Monaco load time)
- ✅ No console errors (no web worker issues)
- ✅ Typing is smooth and responsive
User Experience
- ✅ Clear error messages when validation fails
- ✅ Visual feedback for valid/invalid code
- ✅ Works reliably in Electron
Dependencies
- React 19 (existing)
- No new npm packages required (pure React)
- Remove monaco-editor dependency (if unused elsewhere)
Design Tokens
Use existing Noodl design tokens:
--theme-color-bg-2for editor background--theme-color-bg-3for line numbers gutter--theme-font-monofor monospace font--theme-color-errorfor error state--theme-color-successfor valid state
Migration Notes for Users
No user action required!
- Your code will load automatically
- All connections will work
- No project updates needed
- Just opens faster and more reliably
Known Limitations
No Syntax Highlighting
Reason: Keeping it simple and reliable
Mitigation: Line numbers and indentation help readability
Basic Validation Only
Reason: Can't run full JavaScript parser without complex dependencies
Mitigation: Catches most common errors (missing brackets, quotes, etc.)
No Autocomplete
Reason: Would require Monaco-like complexity
Mitigation: Users can reference documentation; experienced users type without autocomplete
Future Enhancements
- Syntax highlighting via simple tokenizer (not Monaco)
- Basic autocomplete for common patterns
- Code snippets library
- AI-assisted code suggestions
- Search/replace within editor
- Multiple tabs for large scripts
Related Tasks
- TASK-008: JSON Editor (same pattern, proven approach)
- TASK-006B: Expression rendering fixes (data model understanding)
Priority: HIGH (Expression nodes are broken right now) Risk Level: Medium (mitigated by careful testing) Estimated Effort: 7-10 days Critical Success Factor: Zero data loss
Emergency Rollback Plan
If critical issues discovered after deployment:
- Revert PR - Go back to Monaco (even if broken)
- Communicate - Tell users to not edit code until fixed
- Fix Quickly - Address specific issue
- Re-deploy - With fix applied
Safety net: Git history preserves everything. No permanent data loss possible.