Tasks completed to update Storybook and Typescript versions. Please see phase-1-summary.md for details

This commit is contained in:
Richard Osborne
2025-12-08 16:19:56 +01:00
parent ef1ffdd593
commit e927df760f
117 changed files with 8853 additions and 4913 deletions

View File

@@ -0,0 +1,424 @@
# Code Export: Why It's Hard and What We Can Do Instead
## The Question Everyone Asks
"Can I export my Noodl project as a regular React codebase?"
It's one of the most common feature requests, and for good reason. The appeal is obvious:
- **No vendor lock-in** - Know you can leave anytime
- **Developer handoff** - Give your codebase to a React team
- **Standard tooling** - Use React DevTools, any bundler, any hosting
- **Smaller bundles** - Ship React code, not JSON + interpreter
- **Peace of mind** - Your work isn't trapped in a proprietary format
We hear you. This document explains why full code export is genuinely difficult, and proposes a practical alternative that delivers most of the value.
## How Noodl Actually Works
To understand why code export is hard, you need to understand what Noodl is doing under the hood.
When you build in Noodl, you're not writing React code—you're creating a **graph of nodes and connections**. This graph is saved as JSON and interpreted at runtime:
```
Your Noodl Project What Gets Deployed
┌─────────────────┐ ┌─────────────────┐
│ │ │ project.json │ (your node graphs)
│ Visual Editor │ ──────▶ │ + │
│ (Node Graphs) │ │ noodl-runtime │ (interprets the JSON)
│ │ │ + │
└─────────────────┘ │ react.js │ (renders the UI)
└─────────────────┘
```
The runtime reads your JSON and dynamically creates React components, wires up connections, and executes logic. This is powerful and flexible, but it means there's no "React code" to export—just data that describes what the code should do.
**Code export would mean building a compiler** that transforms this graph representation into equivalent React source code.
## What Makes This Hard
### The Easy Parts
Some Noodl concepts translate cleanly to React:
| Noodl | React | Difficulty |
|-------|-------|------------|
| Group, Text, Image nodes | `<div>`, `<span>`, `<img>` | Straightforward |
| Component hierarchy | Component tree | Straightforward |
| Props passed between components | React props | Straightforward |
| Basic styling | CSS/Tailwind classes | Straightforward |
| Repeater node | `array.map()` | Moderate |
| Page Router | React Router | Moderate |
| States (hover, pressed, etc.) | `useState` + event handlers | Moderate |
If Noodl were purely a UI builder, code export would be very achievable.
### The Hard Parts
The challenge is Noodl's **logic and data flow system**. This is where the visual programming model diverges significantly from how React thinks.
#### The Signal System
In Noodl, you connect outputs to inputs, and "signals" flow through the graph:
```
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Button │────▶│ Counter │────▶│ Text │
│ Click ○─┼────▶│─○ Add │ │─○ Value │
└─────────┘ │ Count ○┼────▶│ │
└─────────┘ └─────────┘
```
When Button emits "Click", Counter receives "Add", increments, and emits "Count", which Text receives as "Value".
This is intuitive in the visual editor. But what's the React equivalent?
```jsx
// Option A: useEffect chains (gets messy fast)
function MyComponent() {
const [clicked, setClicked] = useState(false);
const [count, setCount] = useState(0);
useEffect(() => {
if (clicked) {
setCount(c => c + 1);
setClicked(false); // reset the "signal"
}
}, [clicked]);
return (
<>
<button onClick={() => setClicked(true)}>Add</button>
<span>{count}</span>
</>
);
}
// Option B: Direct handlers (loses the graph-like flow)
function MyComponent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Add</button>
<span>{count}</span>
</>
);
}
```
Option B is cleaner, but it's a **complete restructuring** of how the logic is expressed. The compiler would need to understand the *intent* of your node graph, not just translate it mechanically.
Now imagine this with 50 nodes, branching conditions, and signals that trigger other signals. The generated code either becomes an unreadable mess of `useEffect` chains, or requires sophisticated analysis to restructure into idiomatic React.
#### Logic Nodes
Noodl has nodes like And, Or, Switch, Condition, Expression. These operate on the signal/value flow model:
```
┌─────────┐
│ Value A │──┐ ┌─────────┐
└─────────┘ ├────▶│ And │────▶ Result
┌─────────┐ │ └─────────┘
│ Value B │──┘
└─────────┘
```
In React, this might be:
- A derived value: `const result = valueA && valueB`
- A `useMemo`: `useMemo(() => valueA && valueB, [valueA, valueB])`
- Part of render logic: `{valueA && valueB && <Thing />}`
The "right" choice depends on context. A compiler would need to analyze the entire graph to decide.
#### Function Nodes (Custom JavaScript)
When you write custom JavaScript in Noodl, you're writing code that interacts with Noodl's runtime APIs:
```javascript
// Inside a Noodl Function node
define({
inputs: { value: 'number' },
outputs: { doubled: 'number' },
run() {
this.outputs.doubled = this.inputs.value * 2;
}
});
```
This code assumes Noodl's execution model. Translating it to a React hook or component requires understanding what `this.inputs`, `this.outputs`, and `run()` mean in the broader context.
#### Database and Cloud Nodes
Nodes like Query Records, Create Record, and Cloud Function are deeply integrated with Noodl's backend services. They handle:
- Authentication state
- Caching
- Optimistic updates
- Error handling
- Retry logic
Exporting these as code would mean either:
- Generating a lot of boilerplate API code
- Requiring a companion library (at which point, you still have a "runtime")
### The Maintenance Problem
Even if we built a compiler, we'd now have **two systems that must behave identically**:
1. The runtime (interprets JSON in the browser)
2. The compiler (generates React code)
Every bug fix, every new feature, every edge case would need to be implemented twice and tested for parity. This is a significant ongoing maintenance burden.
## What We Propose Instead: The "Eject" Feature
Rather than promising perfect code export, we're considering an **"Eject" feature** that's honest about its limitations but still genuinely useful.
### The Concept
Export your project as a React codebase with:
-**Clean, readable code** for all UI components
-**Proper React patterns** (hooks, components, props)
-**Extracted styles** (CSS modules or Tailwind)
-**Project structure** (routing, file organization)
- ⚠️ **TODO comments** for logic that needs manual implementation
- ⚠️ **Placeholder functions** for database operations
### What It Would Look Like
Your Noodl component:
```
┌─────────────────────────────────────────┐
│ UserCard │
├─────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌────────────┐ │
│ │ Image │ │ Text │ │ Button │ │
│ │ avatar │ │ name │ │ "Edit" │ │
│ └─────────┘ └─────────┘ └──────┬─────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ Function │ │
│ │ editUser │ │
│ └───────────┘ │
└─────────────────────────────────────────┘
```
Exported as:
```jsx
// components/UserCard/UserCard.jsx
import React from 'react';
import styles from './UserCard.module.css';
export function UserCard({ avatar, name, userId }) {
const handleEdit = () => {
// TODO: Implement edit logic
// Original Noodl Function node contained:
// ─────────────────────────────────────
// this.outputs.navigate = `/users/${this.inputs.userId}/edit`;
// ─────────────────────────────────────
console.warn('UserCard.handleEdit: Not yet implemented');
};
return (
<div className={styles.userCard}>
<img
src={avatar}
alt={name}
className={styles.avatar}
/>
<span className={styles.name}>{name}</span>
<button
onClick={handleEdit}
className={styles.editButton}
>
Edit
</button>
</div>
);
}
```
```css
/* components/UserCard/UserCard.module.css */
.userCard {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
}
.name {
flex: 1;
font-weight: 500;
}
.editButton {
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
```
### Database Operations
For database nodes, we'd generate a clear interface:
```jsx
// services/api.js
/**
* Auto-generated API service
* TODO: Implement these functions with your backend of choice
*/
export const api = {
/**
* Fetches users from the database
*
* Original Noodl Query:
* Collection: Users
* Filter: { role: 'admin' }
* Sort: createdAt (descending)
* Limit: 20
*/
async getUsers() {
// TODO: Implement with your API
// Example with fetch:
// return fetch('/api/users?role=admin&limit=20').then(r => r.json());
throw new Error('api.getUsers: Not implemented');
},
/**
* Creates a new user record
*
* Original Noodl fields:
* - name (string)
* - email (string)
* - role (string)
*/
async createUser(data) {
// TODO: Implement with your API
throw new Error('api.createUser: Not implemented');
},
};
```
### Export Report
After export, you'd receive a report:
```
┌──────────────────────────────────────────────────────────────┐
│ Export Complete │
├──────────────────────────────────────────────────────────────┤
│ │
│ ✅ Exported successfully to: ./my-app-export/ │
│ │
│ Summary: │
│ ──────────────────────────────────────────────────────── │
│ Components exported: 23 │
│ Styles extracted: 23 │
│ Routes configured: 5 │
│ │
│ ⚠️ Manual work required: │
│ ──────────────────────────────────────────────────────── │
│ Function nodes: 7 (see TODO comments) │
│ Database operations: 12 (see services/api.js) │
│ Cloud functions: 3 (see services/cloud.js) │
│ │
│ Next steps: │
│ 1. Run: cd my-app-export && npm install │
│ 2. Search for "TODO" comments in your editor │
│ 3. Implement the placeholder functions │
│ 4. Run: npm run dev │
│ │
│ 📖 Full guide: docs.opennoodl.com/guides/code-export │
│ │
└──────────────────────────────────────────────────────────────┘
```
## Who Is This For?
The Eject feature would be valuable for:
### Prototyping → Production Handoff
Build your MVP in Noodl, validate with users, then hand the codebase to your engineering team for production development.
### Outgrowing Low-Code
Your project has become complex enough that you need full code control. Export what you have and continue in a traditional development environment.
### Learning Tool
See how your visual designs translate to React code. Great for designers learning to code or developers understanding React patterns.
### Component Libraries
Build UI components visually in Noodl, export them for use in other React projects.
## What This Is NOT
To be completely clear:
-**Not round-trip** - You cannot re-import exported code back into Noodl
-**Not "zero effort"** - You'll need a developer to complete the TODOs
-**Not production-ready** - The exported code is a starting point, not a finished product
-**Not a replacement for Noodl** - If you want visual development, keep using Noodl!
## Comparison: Full Export vs. Eject
| Aspect | Full Code Export | Eject Feature |
|--------|------------------|---------------|
| Development effort | 6-12 months | 4-6 weeks |
| UI components | ✅ Complete | ✅ Complete |
| Styling | ✅ Complete | ✅ Complete |
| Routing | ✅ Complete | ✅ Complete |
| Simple logic | ✅ Complete | ⚠️ Best-effort |
| Complex logic | ✅ Complete | 📝 TODO comments |
| Database operations | ✅ Complete | 📝 Placeholder stubs |
| Code quality | Varies (could be messy) | Clean (humans finish it) |
| Maintenance burden | High (two systems) | Low (one-time export) |
| Honesty | Promises a lot | Clear expectations |
## The Bottom Line
We could spend a year building a compiler that produces questionable code for edge cases, or we could spend a few weeks building an export tool that's honest about what it can and can't do.
The Eject feature acknowledges that:
1. Visual development and code development are different paradigms
2. The best code is written by humans who understand the context
3. Getting 80% of the way there is genuinely useful
4. Clear documentation beats magic that sometimes fails
We think this approach respects both your time and your intelligence.
## We Want Your Input
This feature is in the planning stage. We'd love to hear from you:
- Would the Eject feature be useful for your workflow?
- What would you use it for? (Handoff? Learning? Components?)
- What's the minimum viable version that would help you?
- Are there specific node types you'd want prioritized?
Join the discussion: [Community Link]
---
*This document reflects our current thinking and is subject to change based on community feedback and technical discoveries.*

View File

@@ -0,0 +1,382 @@
# Multi-Project Support Scoping Document
## Executive Summary
This document scopes the feature request to enable OpenNoodl to have multiple projects open simultaneously. Two approaches are analyzed: multi-project within a single Electron app, and multiple Electron app instances.
**Recommendation:** Start with **Option B (Multiple Electron Instances)** as Phase 1 due to significantly lower complexity and risk. Consider Option A as a future enhancement if user demand warrants the investment.
---
## Current Architecture Analysis
### Key Findings
The codebase has several architectural patterns that make multi-project support challenging:
#### 1. Singleton Pattern Throughout
```typescript
// ProjectModel is a strict singleton
public static get instance() {
return ProjectModel._instance;
}
public static set instance(project: ProjectModel | undefined) {
// Only one project at a time...
}
```
This pattern is referenced extensively across the codebase:
- `ProjectModel.instance` - Core project data
- `NodeLibrary.instance` - Node definitions (registers/unregisters per project)
- `CloudService.instance` - Cloud backend per project
- `ViewerConnection.instance` - Single preview connection
- `SidebarModel.instance`, `UndoQueue.instance`, etc.
#### 2. Router Enforces Single Project
The router explicitly disposes the old project when switching:
```typescript
route(args: AppRouteOptions) {
if (ProjectModel.instance && ProjectModel.instance !== args.project) {
ProjectModel.instance.dispose();
// ...
ProjectModel.instance = undefined;
}
}
```
#### 3. IPC Assumes Single Project
Main process IPC events like `project-opened` and `project-closed` assume one active project:
```javascript
ipcMain.on('project-opened', (e, newProjectName) => {
projectName = newProjectName; // Single name tracked
// ...
});
```
#### 4. Viewer Window is Tightly Coupled
The viewer window is a child of the main window with direct IPC communication assuming a single project context.
---
## Option A: Multi-Project Within Single Electron App
### Description
Transform the architecture to support multiple projects open as tabs or panels within a single application window.
### Required Changes
#### Phase A1: Core Architecture Refactoring
| Component | Current State | Required Change | Complexity |
|-----------|--------------|-----------------|------------|
| `ProjectModel` | Singleton | Registry with active project tracking | 🔴 High |
| `NodeLibrary` | Singleton with project registration | Per-project library instances | 🔴 High |
| `EventDispatcher` | Global events | Project-scoped events | 🟡 Medium |
| `UndoQueue` | Singleton | Per-project undo stacks | 🟡 Medium |
| `Router` | Single route | Multi-route or tab system | 🔴 High |
| `ViewerConnection` | Single connection | Connection pool by project | 🟡 Medium |
#### Phase A2: UI/UX Development
- Tab bar or project switcher component
- Visual indicators for active project
- Window management (detach projects to separate windows)
- Cross-project drag & drop considerations
#### Phase A3: Resource Management
- Memory management for multiple loaded projects
- Preview server port allocation per project
- Cloud service connection pooling
- File watcher consolidation
### Effort Estimate
| Phase | Estimated Time | Risk Level |
|-------|---------------|------------|
| A1: Core Architecture | 8-12 weeks | 🔴 High |
| A2: UI/UX | 3-4 weeks | 🟡 Medium |
| A3: Resource Management | 2-3 weeks | 🟡 Medium |
| Testing & Stabilization | 3-4 weeks | 🔴 High |
| **Total** | **16-23 weeks** | **High** |
### Risks
1. **Regression Risk**: Touching ProjectModel singleton affects nearly every feature
2. **Memory Pressure**: Multiple full projects in RAM
3. **State Isolation**: Ensuring complete isolation between projects
4. **Performance**: Managing multiple preview servers
5. **Complexity Explosion**: Every new feature must consider multi-project context
### Benefits
- Single dock icon / application instance
- Potential for cross-project features (copy/paste between projects)
- Professional multi-document interface
- Shared resources (single node library load)
---
## Option B: Multiple Electron App Instances
### Description
Allow multiple independent Electron app instances, each with its own project. Minimal code changes required.
### Required Changes
#### Phase B1: Enable Multi-Instance
```javascript
// Current: Single instance lock (likely present)
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
}
// Change to: Allow multiple instances
// Simply remove or conditionally bypass the single-instance lock
```
#### Phase B2: Instance Isolation
| Component | Change Required | Complexity |
|-----------|----------------|------------|
| Single-instance lock | Remove or make conditional | 🟢 Low |
| Preview server ports | Dynamic port allocation | 🟢 Low |
| UDP broadcast | Include instance ID | 🟢 Low |
| Window bounds storage | Per-project storage key | 🟢 Low |
| Design tool import server | Instance-aware | 🟡 Medium |
#### Phase B3: UX Polish (Optional)
- Menu item: "Open Project in New Window"
- Keyboard shortcut support
- Recent projects list per instance awareness
### Implementation Details
**Port Allocation:**
```javascript
// Instead of fixed port:
// const port = Config.PreviewServer.port;
// Use dynamic allocation:
const server = net.createServer();
server.listen(0); // OS assigns available port
const port = server.address().port;
```
**Window Bounds:**
```javascript
// Key by project directory or ID
const boundsKey = `windowBounds_${projectId}`;
jsonstorage.get(boundsKey, (bounds) => { /* ... */ });
```
### Effort Estimate
| Phase | Estimated Time | Risk Level |
|-------|---------------|------------|
| B1: Multi-Instance | 1-2 days | 🟢 Low |
| B2: Instance Isolation | 3-5 days | 🟢 Low |
| B3: UX Polish | 3-5 days | 🟢 Low |
| Testing | 2-3 days | 🟢 Low |
| **Total** | **2-3 weeks** | **Low** |
### Risks
1. **Multiple dock icons**: May confuse some users
2. **Memory duplication**: Each instance loads full editor
3. **No cross-project features**: Can't drag nodes between projects
4. **OS Integration**: May complicate app bundling/signing
### Benefits
- Minimal code changes
- Complete isolation (no state bleed)
- Each project has dedicated resources
- Can close one project without affecting others
- Already supported pattern in many apps (VS Code, terminal apps)
---
## Comparison Matrix
| Criteria | Option A (Single App) | Option B (Multi-Instance) |
|----------|----------------------|---------------------------|
| Development Time | 16-23 weeks | 2-3 weeks |
| Risk Level | 🔴 High | 🟢 Low |
| Code Changes | Extensive refactoring | Minimal, isolated changes |
| Memory Usage | Shared (more efficient) | Duplicated (less efficient) |
| UX Polish | Professional tabbed interface | Multiple windows/dock icons |
| Cross-Project Features | Possible | Not possible |
| Isolation | Requires careful engineering | Automatic |
| Maintenance Burden | Higher (ongoing complexity) | Lower |
---
## Recommendation
### Phase 1: Multiple Electron Instances (Option B)
**Timeline: 2-3 weeks**
Start here because:
- Low risk, high value
- Validates user need before major investment
- Can ship quickly and gather feedback
- Doesn't block future Option A work
### Phase 2 (Future): Evaluate Single-App Approach
**Timeline: After 6+ months of Phase 1 feedback**
Consider Option A if:
- Users strongly request tabbed interface
- Cross-project features become a priority
- Memory usage becomes a significant concern
- User feedback indicates multiple windows is problematic
---
## Implementation Plan for Option B
### Week 1: Core Multi-Instance Support
**Day 1-2: Single Instance Lock**
- [ ] Locate and understand current single-instance handling
- [ ] Add configuration flag `allowMultipleInstances`
- [ ] Test launching multiple instances manually
**Day 3-4: Port Allocation**
- [ ] Modify preview server to use dynamic ports
- [ ] Update ViewerConnection to handle dynamic ports
- [ ] Test multiple instances with different projects
**Day 5: Basic Testing**
- [ ] Test simultaneous editing of different projects
- [ ] Verify no state leakage between instances
- [ ] Check cloud service isolation
### Week 2: Polish & Edge Cases
**Day 1-2: Storage Isolation**
- [ ] Key window bounds by project ID/path
- [ ] Handle recent projects list updates
- [ ] UDP broadcast instance differentiation
**Day 3-4: UX Improvements**
- [ ] Add "Open in New Window" to project context menu
- [ ] Keyboard shortcut for opening new instance
- [ ] Window title includes project name prominently
**Day 5: Documentation & Testing**
- [ ] Update user documentation
- [ ] Edge case testing (same project in two instances)
- [ ] Memory and performance profiling
### Week 3: Buffer & Release
- [ ] Bug fixes from testing
- [ ] Final QA pass
- [ ] Release notes preparation
- [ ] User feedback collection setup
---
## Files to Modify (Option B)
### Critical Path
1. `packages/noodl-editor/src/main/main.js` - Single instance lock, port config
2. `packages/noodl-editor/src/main/src/preview-server.js` (or equivalent) - Dynamic ports
### Supporting Changes
3. `packages/noodl-editor/src/main/src/StorageApi.js` - Keyed storage
4. `packages/noodl-editor/src/editor/src/pages/ProjectsPage/` - "Open in New Window" option
5. UDP multicast function in main.js - Instance awareness
---
## Open Questions
1. **Same project in multiple instances?**
- Recommend: Block with friendly message, or warn about conflicts
2. **Instance limit?**
- Recommend: No hard limit initially, monitor memory usage
3. **macOS app icon behavior?**
- Each instance shows in dock; standard behavior for multi-window apps
4. **File locking?**
- Noodl already handles project.json locking - verify behavior with multiple instances
---
## Appendix: Code Snippets
### Current Single-Instance Pattern (Likely)
```javascript
// main.js - probable current implementation
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Focus existing window when second instance attempted
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
}
});
}
```
### Proposed Multi-Instance Support
```javascript
// main.js - proposed modification
const allowMultipleInstances = true; // Could be a setting
if (!allowMultipleInstances) {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
}
app.on('second-instance', (event, commandLine, workingDirectory) => {
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
}
});
}
// Rest of initialization continues for each instance...
```
### Dynamic Port Allocation
```javascript
const net = require('net');
function findAvailablePort(startPort = 8574) {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(startPort, () => {
const port = server.address().port;
server.close(() => resolve(port));
});
server.on('error', () => {
// Port in use, try next
resolve(findAvailablePort(startPort + 1));
});
});
}
```

View File

@@ -0,0 +1,507 @@
# FUTURE: Native BaaS Integration Nodes
> **Document Type:** Future Project Scoping
> **Status:** Planning
> **Prerequisites:** TASK-002 (Robust HTTP Node)
> **Estimated Effort:** 2-4 weeks per BaaS
> **Priority:** High (post-HTTP node completion)
## Executive Summary
This document outlines the strategy for adding native Backend-as-a-Service (BaaS) integrations to OpenNoodl. The goal is to provide the same seamless "pick a table, see the fields" experience that Parse Server nodes currently offer, but for popular BaaS platforms that the community is asking for.
The key insight: **Noodl's Parse nodes demonstrate that schema-aware nodes dramatically improve the low-code experience.** When you select a table and immediately see all available fields as input/output ports, you eliminate the manual configuration that makes the current REST node painful.
## The Problem
**Community feedback:** "How do I hook up my backend?" is the #1 question from new Noodl users.
Current options:
1. **Parse Server nodes** - Great UX, but Parse isn't everyone's choice
2. **REST node** - Requires JavaScript scripting, intimidating for nocoders
3. **Function node** - Powerful but even more code-heavy
4. **AI-generated Function nodes** - Works but feels like a workaround
Users coming from other low-code platforms (n8n, Flutterflow, Retool) expect to see their backend in a dropdown and start building immediately.
## Strategic Approach
### Two-Track Strategy
**Track 1: Robust HTTP Node (TASK-002)**
- Foundation for any API integration
- Declarative, no-code configuration
- cURL import for quick setup
- The "escape hatch" that works with anything
**Track 2: Native BaaS Modules (This Document)**
- Schema-aware nodes for specific platforms
- Dropdown table selection → automatic field ports
- Visual query builders
- Authentication handled automatically
These tracks are complementary:
- HTTP Node = "You can connect to anything"
- BaaS Nodes = "Connecting to X is effortless"
### Module Architecture
Each BaaS integration ships as an installable **Noodl Module** (like MQTT or Material Icons):
```
noodl_modules/
├── supabase/
│ ├── manifest.json
│ ├── index.js
│ └── nodes/
│ ├── SupabaseConfig.js # Connection configuration
│ ├── SupabaseQuery.js # Read records
│ ├── SupabaseInsert.js # Create records
│ ├── SupabaseUpdate.js # Update records
│ ├── SupabaseDelete.js # Delete records
│ ├── SupabaseRealtime.js # Live subscriptions
│ └── SupabaseAuth.js # Authentication
├── pocketbase/
│ └── ...
└── directus/
└── ...
```
Benefits of module approach:
- Core Noodl stays lean
- Users opt-in to what they need
- Independent update cycles
- Community can contribute modules
- Easier to maintain
### Layered Implementation
```
┌─────────────────────────────────────────┐
│ BaaS Node (UX Layer) │ ← Table dropdown, field ports, visual filters
├─────────────────────────────────────────┤
│ BaaS Adapter (Logic Layer) │ ← Schema introspection, query translation
├─────────────────────────────────────────┤
│ HTTP Primitive (Transport Layer) │ ← Actual HTTP requests (from TASK-002)
└─────────────────────────────────────────┘
```
This means:
- One HTTP implementation to maintain
- BaaS modules are mostly "schema + translation"
- Debugging is easier (can inspect raw HTTP)
- HTTP node improvements benefit all BaaS modules
---
## BaaS Platform Analysis
### Priority 1: Supabase
**Why first:**
- Most requested by community
- Excellent schema introspection via PostgREST
- PostgreSQL is familiar and powerful
- Strong ecosystem and documentation
- Free tier makes it accessible
**Schema Introspection:**
```bash
# Supabase exposes OpenAPI spec at root
GET https://your-project.supabase.co/rest/v1/
# Returns full schema with tables, columns, types, relationships
```
**Node Set:**
| Node | Purpose | Key Features |
|------|---------|--------------|
| Supabase Config | Store connection | URL, anon key, service key |
| Query Records | SELECT | Table dropdown, column selection, filters, sorting, pagination |
| Insert Record | INSERT | Table dropdown, field inputs from schema |
| Update Record | UPDATE | Table dropdown, field inputs, row identifier |
| Delete Record | DELETE | Table dropdown, row identifier |
| Realtime Subscribe | Live data | Table + filter, outputs on change |
| Auth (Sign Up) | Create user | Email, password, metadata |
| Auth (Sign In) | Authenticate | Email/password, magic link, OAuth |
| Auth (User) | Current user | Session data, JWT |
| Storage Upload | File upload | Bucket selection, file input |
| Storage Download | File URL | Bucket, path → signed URL |
| RPC Call | Stored procedures | Function dropdown, parameter inputs |
**Technical Details:**
- Auth: Uses Supabase Auth (GoTrue)
- Realtime: WebSocket connection to Supabase Realtime
- Storage: S3-compatible API
- Query: PostgREST syntax (filters, operators, pagination)
**Estimated Effort:** 2-3 weeks
---
### Priority 2: Pocketbase
**Why second:**
- Growing rapidly in low-code community
- Simple, single-binary deployment
- Good schema API
- Simpler than Supabase (faster to implement)
- Self-hosting friendly
**Schema Introspection:**
```bash
# Pocketbase admin API returns collection schema
GET /api/collections
# Returns: name, type, schema (fields with types), options
```
**Node Set:**
| Node | Purpose | Key Features |
|------|---------|--------------|
| Pocketbase Config | Store connection | URL, admin credentials |
| List Records | Query | Collection dropdown, filter, sort, expand relations |
| View Record | Get one | Collection, record ID |
| Create Record | Insert | Collection dropdown, field inputs |
| Update Record | Modify | Collection, record ID, field inputs |
| Delete Record | Remove | Collection, record ID |
| Realtime Subscribe | Live data | Collection + filter |
| Auth | User management | Email/password, OAuth providers |
| File URL | Get file URL | Record, field name |
**Technical Details:**
- Simpler auth model than Supabase
- Built-in file handling per record
- Realtime via SSE (Server-Sent Events)
- Filter syntax is custom (not PostgREST)
**Estimated Effort:** 1.5-2 weeks
---
### Priority 3: Directus
**Why third:**
- Enterprise-focused, more complex
- Headless CMS capabilities
- Strong schema introspection
- GraphQL support
- Longer implementation due to complexity
**Schema Introspection:**
```bash
# Directus has comprehensive schema endpoint
GET /fields
GET /collections
GET /relations
# Returns detailed field metadata including UI hints
```
**Node Set:**
| Node | Purpose | Key Features |
|------|---------|--------------|
| Directus Config | Store connection | URL, access token |
| Get Items | Query | Collection dropdown, fields, filter, sort |
| Get Item | Single | Collection, ID |
| Create Item | Insert | Collection, field inputs |
| Update Item | Modify | Collection, ID, field inputs |
| Delete Item | Remove | Collection, ID |
| Assets | File handling | Upload, get URL |
| Auth | Authentication | Login, refresh, current user |
**Technical Details:**
- REST and GraphQL APIs available
- More complex permission model
- Richer field types (including custom)
- Flows/automation integration possible
**Estimated Effort:** 2-3 weeks
---
## Technical Deep Dive
### Schema Introspection Pattern
All BaaS modules follow this pattern:
```javascript
// 1. On config change, fetch schema
async function fetchSchema(config) {
const response = await fetch(`${config.url}/schema-endpoint`, {
headers: { 'Authorization': `Bearer ${config.apiKey}` }
});
return response.json();
}
// 2. Store schema in editor context
context.editorConnection.sendMetadata({
type: 'baas-schema',
provider: 'supabase',
tables: schema.definitions,
// Cache key for invalidation
hash: computeHash(schema)
});
// 3. Nodes consume schema for dynamic ports
function updatePorts(node, schema) {
const table = node.parameters.table;
const tableSchema = schema.tables[table];
if (!tableSchema) return;
const ports = [];
// Create input ports for each column
Object.entries(tableSchema.columns).forEach(([name, column]) => {
ports.push({
name: `field-${name}`,
displayName: name,
type: mapColumnType(column.type),
plug: 'input',
group: 'Fields'
});
});
// Create output ports
ports.push({
name: 'result',
displayName: 'Result',
type: 'array',
plug: 'output',
group: 'Results'
});
context.editorConnection.sendDynamicPorts(node.id, ports);
}
```
### Query Translation
Each BaaS has different filter syntax. The adapter translates from Noodl's visual filter format:
```javascript
// Noodl visual filter format (from QueryEditor)
const noodlFilter = {
combinator: 'and',
rules: [
{ property: 'status', operator: 'equalTo', value: 'active' },
{ property: 'created_at', operator: 'greaterThan', input: 'startDate' }
]
};
// Supabase (PostgREST) translation
function toSupabaseFilter(filter) {
return filter.rules.map(rule => {
switch(rule.operator) {
case 'equalTo': return `${rule.property}=eq.${rule.value}`;
case 'greaterThan': return `${rule.property}=gt.${rule.value}`;
// ... more operators
}
}).join('&');
}
// Pocketbase translation
function toPocketbaseFilter(filter) {
return filter.rules.map(rule => {
switch(rule.operator) {
case 'equalTo': return `${rule.property}="${rule.value}"`;
case 'greaterThan': return `${rule.property}>"${rule.value}"`;
// ... more operators
}
}).join(' && ');
}
```
### Authentication Flow
Each module handles auth internally:
```javascript
// Supabase example
const SupabaseConfig = {
name: 'Supabase Config',
category: 'Supabase',
inputs: {
projectUrl: { type: 'string', displayName: 'Project URL' },
anonKey: { type: 'string', displayName: 'Anon Key' },
// Service key for admin operations (optional)
serviceKey: { type: 'string', displayName: 'Service Key' }
},
// Store config globally for other nodes to access
methods: {
setConfig: function() {
this.context.globalStorage.set('supabase-config', {
url: this._internal.projectUrl,
anonKey: this._internal.anonKey,
serviceKey: this._internal.serviceKey
});
this.sendSignalOnOutput('configured');
}
}
};
// Other Supabase nodes retrieve config
const SupabaseQuery = {
methods: {
doQuery: async function() {
const config = this.context.globalStorage.get('supabase-config');
if (!config) throw new Error('Supabase not configured');
const response = await fetch(
`${config.url}/rest/v1/${this._internal.table}`,
{
headers: {
'apikey': config.anonKey,
'Authorization': `Bearer ${config.anonKey}`
}
}
);
// ... handle response
}
}
};
```
### Visual Filter Builder Integration
Reuse existing QueryEditor components with BaaS-specific schema:
```javascript
// In editor, when Supabase node is selected
const schema = getSupabaseSchema(node.parameters.table);
// Pass to QueryEditor
<QueryFilterEditor
schema={schema}
value={node.parameters.visualFilter}
onChange={(filter) => node.setParameter('visualFilter', filter)}
/>
```
The existing `QueryEditor` components from Parse integration can be reused:
- `QueryRuleEditPopup`
- `QuerySortingEditor`
- `RuleDropdown`, `RuleInput`
---
## Implementation Phases
### Phase 1: Foundation (TASK-002)
- Complete Robust HTTP Node
- Establish patterns for dynamic ports
- Create reusable editor components
### Phase 2: Supabase Module
**Week 1:**
- Schema introspection implementation
- Config node
- Query node with table dropdown
**Week 2:**
- Insert, Update, Delete nodes
- Visual filter builder integration
- Field-to-port mapping
**Week 3:**
- Realtime subscriptions
- Authentication nodes
- Storage nodes
- Documentation and examples
### Phase 3: Pocketbase Module
**Week 1-2:**
- Schema introspection
- Core CRUD nodes
- Realtime via SSE
- Authentication
- Documentation
### Phase 4: Directus Module
**Week 2-3:**
- Schema introspection (more complex)
- Core CRUD nodes
- Asset management
- Documentation
### Phase 5: Community & Iteration
- Publish module development guide
- Community feedback integration
- Additional BaaS based on demand (Firebase, Appwrite, etc.)
---
## Success Metrics
| Metric | Target |
|--------|--------|
| Time to first query | < 5 minutes (with Supabase account) |
| Lines of code to query | 0 (visual only) |
| Schema sync delay | < 2 seconds |
| Community satisfaction | Positive feedback in Discord |
| Module adoption | 50% of new projects using a BaaS module |
## Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| BaaS API changes | High | Version pin, monitor changelogs |
| Schema introspection rate limits | Medium | Cache aggressively, manual refresh |
| Complex filter translation | Medium | Start simple, iterate based on feedback |
| Module maintenance burden | Medium | Community contributions, shared patterns |
| Authentication complexity | High | Follow each BaaS's recommended patterns |
## Open Questions
1. **Should modules auto-detect connection issues?**
- e.g., "Can't reach Supabase - check your URL"
2. **How to handle schema changes?**
- Auto-refresh? Manual button? Both?
3. **Should we support multiple instances per BaaS?**
- e.g., "Supabase Production" vs "Supabase Staging"
4. **How to handle migrations?**
- If user changes BaaS provider, any tooling to help?
5. **GraphQL support for Directus/Supabase?**
- PostgREST is simpler, but GraphQL is more flexible
---
## References
### Supabase
- [PostgREST API](https://postgrest.org/en/stable/api.html)
- [Supabase JS Client](https://supabase.com/docs/reference/javascript)
- [Realtime Subscriptions](https://supabase.com/docs/guides/realtime)
- [Auth API](https://supabase.com/docs/guides/auth)
### Pocketbase
- [API Documentation](https://pocketbase.io/docs/api-records/)
- [JavaScript SDK](https://github.com/pocketbase/js-sdk)
- [Realtime via SSE](https://pocketbase.io/docs/realtime/)
### Directus
- [REST API Reference](https://docs.directus.io/reference/introduction.html)
- [SDK](https://docs.directus.io/guides/sdk/getting-started.html)
- [Authentication](https://docs.directus.io/reference/authentication.html)
### Noodl Internals
- [Module Creation Guide](/javascript/extending/create-lib.md)
- [Parse Nodes Implementation](/packages/noodl-runtime/src/nodes/std-library/data/)
- [Query Editor Components](/packages/noodl-editor/src/editor/src/views/panels/propertyeditor/components/QueryEditor/)
---
## Appendix: Community Quotes
> "I'm used to Flutterflow where I just pick Supabase and I'm done. In Noodl I have to figure out REST nodes and it's confusing." - Discord user
> "The Parse nodes are amazing, why can't we have that for other backends?" - Forum post
> "I tried using the Function node for Supabase but I'm not a developer, I don't know JavaScript." - New user feedback
> "If Noodl had native Supabase support I'd switch from Flutterflow tomorrow." - Potential user

View File

@@ -0,0 +1,596 @@
# Phase: Runtime React 19 Migration
## Overview
This phase modernizes the OpenNoodl runtime (the code that powers deployed/published projects) from React 17 to React 19. Unlike the editor migration, this directly affects end-user applications in production.
**Key Principle:** No one gets left behind. Users choose when to migrate, with comprehensive tooling to guide them.
## Goals
1. **Dual Runtime Support** - Allow users to deploy to either React 17 (legacy) or React 19 (modern) runtime
2. **Migration Detection System** - Automatically scan projects for React 19 incompatibilities
3. **Guided Migration** - Provide clear, actionable guidance for fixing compatibility issues
4. **Zero Breaking Changes for Passive Users** - Projects that don't explicitly opt-in continue working unchanged
## Architecture
### Dual Runtime System
```
┌─────────────────────────────────────────────────────────────┐
│ OpenNoodl Editor │
├─────────────────────────────────────────────────────────────┤
│ Deploy/Publish Dialog │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Runtime Version: [React 17 (Legacy) ▼] │ │
│ │ [React 19 (Modern) ] │ │
│ │ │ │
│ │ ⚠️ Migration Status: 2 issues detected │ │
│ │ [Run Migration Check] [View Details] │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ noodl-viewer-react │ │ noodl-viewer-react │
│ (React 17) │ │ (React 19) │
│ │ │ │
│ • Legacy lifecycle │ │ • Modern lifecycle │
│ • ReactDOM.render() │ │ • createRoot() │
│ • String refs support │ │ • Strict mode ready │
└─────────────────────────┘ └─────────────────────────┘
```
### Package Structure
```
packages/
├── noodl-viewer-react/
│ ├── src/
│ │ ├── index.js # Shared entry logic
│ │ ├── init-legacy.js # React 17 initialization
│ │ └── init-modern.js # React 19 initialization
│ ├── static/
│ │ ├── deploy/ # React 17 bundle (default)
│ │ └── deploy-react19/ # React 19 bundle
│ └── webpack-configs/
│ ├── webpack.deploy.legacy.js
│ └── webpack.deploy.modern.js
├── noodl-viewer-cloud/
│ └── [similar structure]
└── noodl-runtime/
└── src/
├── compat/
│ ├── react17-shims.js # Compatibility layer
│ └── react19-shims.js
└── migration/
├── detector.js # Incompatibility detection
└── reporter.js # Migration report generation
```
## Migration Detection System
### Detected Patterns
The migration system scans for the following incompatibilities:
#### Critical (Will Break)
| Pattern | Detection Method | Migration Path |
|---------|------------------|----------------|
| `componentWillMount` | AST scan of JS nodes | Convert to `constructor` or `componentDidMount` |
| `componentWillReceiveProps` | AST scan of JS nodes | Convert to `static getDerivedStateFromProps` or `componentDidUpdate` |
| `componentWillUpdate` | AST scan of JS nodes | Convert to `getSnapshotBeforeUpdate` + `componentDidUpdate` |
| `ReactDOM.render()` | String match in custom code | Convert to `createRoot().render()` |
| String refs (`ref="myRef"`) | Regex in JSX | Convert to `React.createRef()` or callback refs |
| `contextTypes` / `getChildContext` | AST scan | Convert to `React.createContext` |
| `createFactory()` | String match | Convert to JSX or `createElement` |
#### Warning (Deprecated but Functional)
| Pattern | Detection Method | Recommendation |
|---------|------------------|----------------|
| `defaultProps` on function components | AST scan | Use ES6 default parameters |
| `propTypes` | Import detection | Consider TypeScript or remove |
| `findDOMNode()` | String match | Use refs instead |
#### Info (Best Practice)
| Pattern | Detection Method | Recommendation |
|---------|------------------|----------------|
| Class components | AST scan | Consider converting to functional + hooks |
| `UNSAFE_` lifecycle methods | String match | Plan migration to modern patterns |
### Detection Implementation
```javascript
// packages/noodl-runtime/src/migration/detector.js
const CRITICAL_PATTERNS = [
{
id: 'componentWillMount',
pattern: /componentWillMount\s*\(/,
severity: 'critical',
title: 'componentWillMount is removed in React 19',
description: 'This lifecycle method has been removed. Move initialization logic to the constructor or componentDidMount.',
autoFixable: false,
documentation: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis',
migration: {
before: `componentWillMount() {\n this.setState({ data: fetchData() });\n}`,
after: `componentDidMount() {\n this.setState({ data: fetchData() });\n}`
}
},
{
id: 'componentWillReceiveProps',
pattern: /componentWillReceiveProps\s*\(/,
severity: 'critical',
title: 'componentWillReceiveProps is removed in React 19',
description: 'Use static getDerivedStateFromProps or componentDidUpdate instead.',
autoFixable: false,
documentation: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis',
migration: {
before: `componentWillReceiveProps(nextProps) {\n if (nextProps.id !== this.props.id) {\n this.setState({ data: null });\n }\n}`,
after: `static getDerivedStateFromProps(props, state) {\n if (props.id !== state.prevId) {\n return { data: null, prevId: props.id };\n }\n return null;\n}`
}
},
{
id: 'componentWillUpdate',
pattern: /componentWillUpdate\s*\(/,
severity: 'critical',
title: 'componentWillUpdate is removed in React 19',
description: 'Use getSnapshotBeforeUpdate with componentDidUpdate instead.',
autoFixable: false,
documentation: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis'
},
{
id: 'reactdom-render',
pattern: /ReactDOM\.render\s*\(/,
severity: 'critical',
title: 'ReactDOM.render is removed in React 19',
description: 'Use createRoot from react-dom/client instead.',
autoFixable: true,
migration: {
before: `import { render } from 'react-dom';\nrender(<App />, document.getElementById('root'));`,
after: `import { createRoot } from 'react-dom/client';\nconst root = createRoot(document.getElementById('root'));\nroot.render(<App />);`
}
},
{
id: 'string-refs',
pattern: /ref\s*=\s*["'][^"']+["']/,
severity: 'critical',
title: 'String refs are removed in React 19',
description: 'Use React.createRef() or callback refs instead.',
autoFixable: false,
migration: {
before: `<input ref="myInput" />`,
after: `// Using createRef:\nmyInputRef = React.createRef();\n<input ref={this.myInputRef} />\n\n// Using callback ref:\n<input ref={el => this.myInput = el} />`
}
},
{
id: 'legacy-context',
pattern: /contextTypes\s*=|getChildContext\s*\(/,
severity: 'critical',
title: 'Legacy Context API is removed in React 19',
description: 'Migrate to React.createContext and useContext.',
autoFixable: false,
documentation: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-legacy-context'
}
];
const WARNING_PATTERNS = [
{
id: 'defaultProps-function',
pattern: /\.defaultProps\s*=/,
severity: 'warning',
title: 'defaultProps on function components is deprecated',
description: 'Use ES6 default parameters instead. Class components still support defaultProps.',
autoFixable: true
},
{
id: 'propTypes',
pattern: /\.propTypes\s*=|from\s*['"]prop-types['"]/,
severity: 'warning',
title: 'PropTypes are removed from React',
description: 'Consider using TypeScript for type checking, or remove propTypes.',
autoFixable: false
}
];
class MigrationDetector {
constructor() {
this.patterns = [...CRITICAL_PATTERNS, ...WARNING_PATTERNS];
}
scanNode(nodeData) {
const issues = [];
const code = this.extractCode(nodeData);
if (!code) return issues;
for (const pattern of this.patterns) {
if (pattern.pattern.test(code)) {
issues.push({
...pattern,
nodeId: nodeData.id,
nodeName: nodeData.name || nodeData.type,
location: this.findLocation(code, pattern.pattern)
});
}
}
return issues;
}
scanProject(projectData) {
const report = {
timestamp: new Date().toISOString(),
projectName: projectData.name,
summary: {
critical: 0,
warning: 0,
info: 0,
canMigrate: true
},
issues: [],
affectedNodes: new Set()
};
// Scan all components
for (const component of projectData.components || []) {
for (const node of component.nodes || []) {
const nodeIssues = this.scanNode(node);
for (const issue of nodeIssues) {
report.issues.push({
...issue,
component: component.name
});
report.summary[issue.severity]++;
report.affectedNodes.add(node.id);
}
}
}
// Check custom modules
for (const module of projectData.modules || []) {
const moduleIssues = this.scanCustomModule(module);
report.issues.push(...moduleIssues);
}
report.summary.canMigrate = report.summary.critical === 0;
report.affectedNodes = Array.from(report.affectedNodes);
return report;
}
extractCode(nodeData) {
// Extract JavaScript code from various node types
if (nodeData.type === 'JavaScriptFunction' || nodeData.type === 'Javascript2') {
return nodeData.parameters?.code || nodeData.parameters?.Script || '';
}
if (nodeData.type === 'Expression') {
return nodeData.parameters?.expression || '';
}
// Custom React component nodes
if (nodeData.parameters?.reactComponent) {
return nodeData.parameters.reactComponent;
}
return '';
}
findLocation(code, pattern) {
const match = code.match(pattern);
if (!match) return null;
const lines = code.substring(0, match.index).split('\n');
return {
line: lines.length,
column: lines[lines.length - 1].length
};
}
}
module.exports = { MigrationDetector, CRITICAL_PATTERNS, WARNING_PATTERNS };
```
## User Interface
### Deploy Dialog Enhancement
```
┌──────────────────────────────────────────────────────────────────┐
│ Deploy Project │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Target: [Production Server ▼] │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Runtime Version │ │
│ │ │ │
│ │ ○ React 17 (Legacy) │ │
│ │ Stable, compatible with all existing code │ │
│ │ │ │
│ │ ● React 19 (Modern) ✨ Recommended │ │
│ │ Better performance, modern features, future-proof │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ ⚠️ Migration Check Results │ │
│ │ │ │
│ │ Found 2 issues that need attention: │ │
│ │ │ │
│ │ 🔴 CRITICAL (1) │ │
│ │ └─ MyCustomComponent: componentWillMount removed │ │
│ │ │ │
│ │ 🟡 WARNING (1) │ │
│ │ └─ UserCard: defaultProps deprecated │ │
│ │ │ │
│ │ [View Full Report] [How to Fix] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Critical issues must be resolved before deploying │ │
│ │ with React 19. You can still deploy with React 17. │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Deploy with React 17] [Fix Issues] │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### Migration Report Panel
```
┌──────────────────────────────────────────────────────────────────┐
│ Migration Report [×] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Project: My Awesome App │
│ Scanned: Dec 7, 2025 at 2:34 PM │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ 🔴 CRITICAL: componentWillMount removed │
│ ─────────────────────────────────────────────────────────── │
│ Location: Components/MyCustomComponent/Function Node │
│ │
│ This lifecycle method has been completely removed in React 19. │
│ Code using componentWillMount will throw an error at runtime. │
│ │
│ Your code: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ componentWillMount() { │ │
│ │ this.setState({ loading: true }); │ │
│ │ this.loadData(); │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ Recommended fix: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ constructor(props) { │ │
│ │ super(props); │ │
│ │ this.state = { loading: true }; │ │
│ │ } │ │
│ │ │ │
│ │ componentDidMount() { │ │
│ │ this.loadData(); │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ [Go to Node] [Copy Fix] [Learn More ↗] │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ 🟡 WARNING: defaultProps deprecated │
│ ─────────────────────────────────────────────────────────── │
│ Location: Components/UserCard/Function Node │
│ ... │
│ │
└──────────────────────────────────────────────────────────────────┘
```
## Implementation Phases
### Phase 1: Infrastructure (Week 1-2)
**Objective:** Set up dual-build system without changing default behavior
- [ ] Create separate webpack configs for React 17 and React 19 builds
- [ ] Set up `static/deploy-react19/` directory structure
- [ ] Create React 19 versions of bundled React files
- [ ] Update `noodl-viewer-react/static/deploy/index.json` to support version selection
- [ ] Add runtime version metadata to deploy manifest
**Success Criteria:**
- Both runtime versions build successfully
- Default deploy still uses React 17
- React 19 bundle available but not yet exposed in UI
### Phase 2: Migration Detection (Week 2-3)
**Objective:** Build scanning and reporting system
- [ ] Implement `MigrationDetector` class
- [ ] Create pattern definitions for all known incompatibilities
- [ ] Build project scanning logic
- [ ] Generate human-readable migration reports
- [ ] Add detection for custom React modules (external libs)
**Success Criteria:**
- Scanner identifies all critical patterns in test projects
- Reports clearly explain each issue with code examples
- Scanner handles edge cases (minified code, JSX variations)
### Phase 3: Editor Integration (Week 3-4)
**Objective:** Surface migration tools in the editor UI
- [ ] Add runtime version selector to Deploy dialog
- [ ] Integrate migration scanner with deploy workflow
- [ ] Create Migration Report panel component
- [ ] Add "Go to Node" navigation from report
- [ ] Show inline warnings in JavaScript node editor
**Success Criteria:**
- Users can select runtime version before deploy
- Migration check runs automatically when React 19 selected
- Clear UI prevents accidental broken deploys
### Phase 4: Runtime Compatibility Layer (Week 4-5)
**Objective:** Update internal runtime code for React 19
- [ ] Update `noodl-viewer-react` initialization to use `createRoot()`
- [ ] Update SSR package to use `hydrateRoot()`
- [ ] Migrate any internal `componentWillMount` usage
- [ ] Update `noodl-viewer-cloud` for React 19
- [ ] Test all built-in visual nodes with React 19
**Success Criteria:**
- All built-in Noodl nodes work with React 19
- SSR functions correctly with new APIs
- No regressions in React 17 runtime
### Phase 5: Documentation & Polish (Week 5-6)
**Objective:** Prepare for user adoption
- [ ] Write migration guide for end users
- [ ] Document all breaking changes with examples
- [ ] Create video walkthrough of migration process
- [ ] Add contextual help links in migration report
- [ ] Beta test with community projects
**Success Criteria:**
- Complete migration documentation
- At least 5 community projects successfully migrated
- No critical bugs in migration tooling
## Technical Considerations
### Build System Changes
```javascript
// webpack-configs/webpack.deploy.config.js
const REACT_VERSION = process.env.REACT_VERSION || '17';
module.exports = {
entry: `./src/init-react${REACT_VERSION}.js`,
output: {
path: path.resolve(__dirname, `../static/deploy${REACT_VERSION === '19' ? '-react19' : ''}`),
filename: 'noodl.deploy.js'
},
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
},
// ... rest of config
};
```
### Runtime Initialization (React 19)
```javascript
// src/init-react19.js
import { createRoot, hydrateRoot } from 'react-dom/client';
export function initializeApp(App, container, options = {}) {
if (options.hydrate && container.hasChildNodes()) {
return hydrateRoot(container, App);
}
const root = createRoot(container);
root.render(App);
return root;
}
export function unmountApp(root) {
root.unmount();
}
// Expose for runtime
window.NoodlReactInit = { initializeApp, unmountApp };
```
### Backwards Compatibility
```javascript
// src/compat/react-compat.js
// Shim for code that might reference old APIs
if (typeof ReactDOM !== 'undefined' && !ReactDOM.render) {
console.warn(
'[Noodl] ReactDOM.render is not available in React 19. ' +
'Please update your custom code to use createRoot instead.'
);
// Provide a helpful error instead of undefined function
ReactDOM.render = () => {
throw new Error(
'ReactDOM.render has been removed in React 19. ' +
'See migration guide: https://docs.opennoodl.com/migration/react19'
);
};
}
```
## Success Criteria
### Quantitative
- [ ] 100% of built-in Noodl nodes work on React 19
- [ ] Migration scanner detects >95% of incompatible patterns
- [ ] Build time increase <10% for dual-runtime support
- [ ] Zero regressions in React 17 runtime behavior
### Qualitative
- [ ] Users can confidently choose their runtime version
- [ ] Migration report provides actionable guidance
- [ ] No user is forced to migrate before they're ready
- [ ] Documentation covers all common migration scenarios
## Risks & Mitigations
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Custom modules with deep React dependencies | High | Medium | Provide detection + detailed migration docs |
| Third-party npm packages incompatible | Medium | Medium | Document known incompatible packages |
| SSR behavior differences between versions | High | Low | Extensive SSR testing suite |
| Build size increase from dual bundles | Low | High | Only ship selected version, not both |
| Community confusion about versions | Medium | Medium | Clear UI, documentation, and defaults |
## Future Considerations
### React 20+ Preparation
This dual-runtime architecture sets up a pattern for future React upgrades:
- Version selection UI is extensible
- Migration scanner patterns are configurable
- Build system supports arbitrary version targets
### Deprecation Timeline
```
v1.2.0 - React 19 available as opt-in (default: React 17)
v1.3.0 - React 19 becomes default (React 17 still available)
v1.4.0 - React 17 shows deprecation warning
v2.0.0 - React 17 support removed
```
## Related Documentation
- [React 19 Official Upgrade Guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide)
- [TASK-001: Dependency Updates & React 19 Migration (Editor)](./TASK-001-dependency-updates.md)
- [OpenNoodl Architecture Overview](./architecture/overview.md)
---
*Last Updated: December 7, 2025*
*Phase Owner: TBD*
*Estimated Duration: 6 weeks*