Finished component sidebar updates, with one small bug remaining and documented

This commit is contained in:
Richard Osborne
2025-12-28 22:07:29 +01:00
parent 5f8ce8d667
commit fad9f1006d
193 changed files with 22245 additions and 506 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,891 @@
# Capacitor Mobile Target
**Phase ID:** PHASE-B
**Priority:** 🔴 High (Primary Target)
**Estimated Duration:** 4-5 weeks
**Status:** Planning
**Dependencies:** Phase A (BYOB Backend), Phase E (Target System Core)
**Last Updated:** 2025-12-28
## Executive Summary
Enable Noodl users to build native iOS and Android applications using Capacitor as the bridge between web technologies and native device capabilities. This is the highest-priority target due to massive user demand for mobile app development without native code knowledge.
## Value Proposition
| Current State | With Capacitor Target |
|--------------|----------------------|
| Export static web, manually wrap in Capacitor | One-click Capacitor project export |
| No mobile preview | Hot-reload preview on device/simulator |
| No native API access | Camera, GPS, Push, Haptics nodes |
| Manual bridge setup | Automatic Capacitor bridge injection |
| 30-60s iteration cycles | 1-2s hot-reload cycles |
## Technical Architecture
### How Capacitor Works
Capacitor is a cross-platform native runtime that allows web apps to run natively on iOS and Android with access to native device features through JavaScript bridges.
```
┌─────────────────────────────────────────────────────────────────────┐
│ CAPACITOR ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ YOUR NOODL APP │ │
│ │ (HTML, CSS, JavaScript - same as web export) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CAPACITOR BRIDGE │ │
│ │ window.Capacitor.Plugins.Camera.getPhoto() │ │
│ │ window.Capacitor.Plugins.Geolocation.getCurrentPosition() │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ iOS Native │ │ Android Native │ │ Web Fallback │ │
│ │ Swift/ObjC │ │ Kotlin/Java │ │ (PWA APIs) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Integration with Noodl
```
┌─────────────────────────────────────────────────────────────────────┐
│ NOODL EDITOR │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ VISUAL GRAPH │ │
│ │ [Button] ──onClick──▶ [Camera Capture] ──photo──▶ [Image] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CAPACITOR NODE LIBRARY │ │
│ │ Camera, Geolocation, Push, Haptics, StatusBar, etc. │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ PREVIEW │ │ EXPORT │ │ PREVIEW │ │
│ │ (Web) │ │ (Capacitor) │ │ (Device) │ │
│ │ │ │ │ │ │ │
│ │ Mock APIs │ │ Real build │ │ Hot-reload │ │
│ │ in browser │ │ to Xcode/ │ │ on physical │ │
│ │ │ │ Android │ │ device │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
## Preview Modes
### 1. Web Preview (Default)
Standard browser preview with mocked Capacitor APIs:
```typescript
// When in web preview mode, inject mock Capacitor
if (!window.Capacitor) {
window.Capacitor = {
isNativePlatform: () => false,
Plugins: {
Camera: {
async getPhoto(options: CameraOptions): Promise<Photo> {
// Use browser MediaDevices API
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// ... capture and return photo
}
},
Geolocation: {
async getCurrentPosition(): Promise<Position> {
// Use browser Geolocation API
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(pos) => resolve({
coords: {
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
accuracy: pos.coords.accuracy,
}
}),
reject
);
});
}
},
// ... other mocked plugins
}
};
}
```
**Limitations:**
- Push notifications won't work (no registration token)
- Some device-specific features unavailable
- Camera quality may differ from native
### 2. Capacitor Hot-Reload Preview
Connect physical device or simulator to Noodl's dev server:
```
┌─────────────────────────────────────────────────────────────────────┐
│ Preview Mode: [Web ▾] │
│ ├─ Web (Browser) │
│ ├─ iOS Simulator ◀ │
│ ├─ Android Emulator │
│ └─ Physical Device │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ⚠️ Capacitor Preview Setup Required │
│ │
│ To preview on iOS Simulator: │
│ │
│ 1. Install Xcode from the Mac App Store │
│ 2. Open Terminal and run: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ npx cap run ios --livereload-url=http://localhost:8574 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ 3. Select a simulator when prompted │
│ │
│ [Generate Capacitor Project] [Copy Command] [Open Documentation] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**How Hot-Reload Works:**
```
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Noodl Editor │◄───────▶│ Dev Server │◄───────▶│ iOS/Android │
│ │ WebSocket│ localhost:8574│ HTTP │ App in │
│ Make changes │─────────▶│ Serves app │─────────▶ WebView │
│ │ │ + Capacitor │ │ │
└───────────────┘ │ bridge │ │ Native APIs │
└───────────────┘ │ available │
└───────────────┘
```
1. Noodl's dev server already serves the preview app at `localhost:8574`
2. Capacitor app's WebView loads from this URL instead of bundled assets
3. When you make changes in Noodl, the WebView automatically refreshes
4. Native Capacitor plugins are available because you're running in a native app
### 3. Device Preview (Physical Device)
```
┌─────────────────────────────────────────────────────────────────────┐
│ Preview on Device [×] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CONNECTED DEVICES │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📱 Richard's iPhone 15 Pro ✓ Connected │ │
│ │ iOS 17.2 • USB Connected │ │
│ │ [Open App] │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ 📱 Pixel 8 ✓ Connected │ │
│ │ Android 14 • WiFi (192.168.1.42) │ │
│ │ [Open App] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ NETWORK PREVIEW (WiFi) │
│ Devices on the same network can connect to: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ http://192.168.1.100:8574 [Copy] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄│ │
│ │ █ █ █ █│ Scan with │
│ │ █ ███ █ █ ███ █│ Noodl Preview App │
│ │ █ █ █ █│ │
│ │ ▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀│ │
│ └─────────────────┘ │
│ │
│ [Download iOS Preview App] [Download Android Preview App] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
## Capacitor-Specific Nodes
### Core Plugin Nodes
#### Camera Node
```
┌─────────────────────────────────────────────────────────────────────┐
│ 📷 Camera Capture [📱] [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Capture ○──── (Signal - triggers capture) │
│ │
│ Source: [Camera ▾] │
│ ├─ Camera │
│ ├─ Photos Library │
│ └─ Prompt User │
│ │
│ Quality: [High ▾] │
│ ├─ Low (faster, smaller) │
│ ├─ Medium │
│ └─ High (best quality) │
│ │
│ Result Type: [Base64 ▾] │
│ ├─ Base64 (data URL) │
│ ├─ URI (file path) │
│ └─ Blob │
│ │
│ Direction: [Rear ▾] │
│ Correct Orientation: [✓] │
│ Allow Editing: [✓] │
│ Width: [1024 ] (0 = original) │
│ Height: [0 ] (0 = original) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Photo ────○ (String - base64/URI) │
│ Format ────○ (String - "jpeg"/"png"/"gif") │
│ Captured ────○ (Signal - on success) │
│ Failed ────○ (Signal - on error) │
│ Error ────○ (String - error message) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**Implementation:**
```typescript
// packages/noodl-runtime/src/nodes/capacitor/CameraNode.ts
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { createNodeDefinition } from '../../nodeDefinition';
export const CameraNode = createNodeDefinition({
name: 'Camera Capture',
category: 'Device & Platform',
color: 'purple',
inputs: {
capture: { type: 'signal', displayName: 'Capture' },
source: {
type: 'enum',
displayName: 'Source',
default: 'camera',
enums: [
{ value: 'camera', label: 'Camera' },
{ value: 'photos', label: 'Photos Library' },
{ value: 'prompt', label: 'Prompt User' },
],
},
quality: {
type: 'number',
displayName: 'Quality',
default: 90,
min: 1,
max: 100,
},
resultType: {
type: 'enum',
displayName: 'Result Type',
default: 'base64',
enums: [
{ value: 'base64', label: 'Base64' },
{ value: 'uri', label: 'URI' },
],
},
direction: {
type: 'enum',
displayName: 'Direction',
default: 'rear',
enums: [
{ value: 'rear', label: 'Rear' },
{ value: 'front', label: 'Front' },
],
},
allowEditing: { type: 'boolean', displayName: 'Allow Editing', default: false },
width: { type: 'number', displayName: 'Width', default: 0 },
height: { type: 'number', displayName: 'Height', default: 0 },
},
outputs: {
photo: { type: 'string', displayName: 'Photo' },
format: { type: 'string', displayName: 'Format' },
captured: { type: 'signal', displayName: 'Captured' },
failed: { type: 'signal', displayName: 'Failed' },
error: { type: 'string', displayName: 'Error' },
},
targetCompatibility: ['capacitor', 'electron', 'web'],
async execute(inputs, outputs, context) {
try {
const sourceMap = {
camera: CameraSource.Camera,
photos: CameraSource.Photos,
prompt: CameraSource.Prompt,
};
const resultTypeMap = {
base64: CameraResultType.Base64,
uri: CameraResultType.Uri,
};
const photo = await Camera.getPhoto({
source: sourceMap[inputs.source],
resultType: resultTypeMap[inputs.resultType],
quality: inputs.quality,
allowEditing: inputs.allowEditing,
width: inputs.width || undefined,
height: inputs.height || undefined,
direction: inputs.direction === 'front' ? 'FRONT' : 'REAR',
});
if (inputs.resultType === 'base64') {
outputs.photo = `data:image/${photo.format};base64,${photo.base64String}`;
} else {
outputs.photo = photo.webPath;
}
outputs.format = photo.format;
outputs.captured.trigger();
} catch (err) {
outputs.error = err.message;
outputs.failed.trigger();
}
},
});
```
#### Geolocation Node
```
┌─────────────────────────────────────────────────────────────────────┐
│ 📍 Geolocation [🌐] [📱] [🖥️] [🧩] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Get Position ○──── (Signal) │
│ Watch Position ○──── (Signal - start watching) │
│ Stop Watching ○──── (Signal) │
│ │
│ High Accuracy: [✓] (uses GPS, slower, more battery) │
│ Timeout (ms): [10000] │
│ Maximum Age (ms): [0 ] (0 = always fresh) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Latitude ────○ (Number) │
│ Longitude ────○ (Number) │
│ Accuracy ────○ (Number - meters) │
│ Altitude ────○ (Number - meters, may be null) │
│ Speed ────○ (Number - m/s, may be null) │
│ Heading ────○ (Number - degrees, may be null) │
│ Timestamp ────○ (Date) │
│ │
│ Position Updated ────○ (Signal) │
│ Error ────○ (Signal) │
│ Error Message ────○ (String) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
#### Push Notifications Node
```
┌─────────────────────────────────────────────────────────────────────┐
│ 🔔 Push Notifications [📱] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Register ○──── (Signal - request permission) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Token ────○ (String - device token) │
│ Registered ────○ (Signal) │
│ Registration Failed ────○ (Signal) │
│ │
│ Notification Received ────○ (Signal) │
│ Notification Title ────○ (String) │
│ Notification Body ────○ (String) │
│ Notification Data ────○ (Object - custom payload) │
│ Notification Action ────○ (String - action ID if tapped) │
│ │
│ Notification Tapped ────○ (Signal - user tapped notif) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
#### Additional Capacitor Nodes
| Node | Category | Description |
|------|----------|-------------|
| **Haptics** | Device | Trigger haptic feedback (impact, notification, selection) |
| **Status Bar** | UI | Control status bar appearance (style, color, visibility) |
| **Keyboard** | UI | Show/hide keyboard, get keyboard height |
| **Share** | Social | Native share sheet for content |
| **App Launcher** | System | Open other apps, URLs, settings |
| **Device Info** | System | Get device model, OS version, battery, etc. |
| **Network** | System | Get connection type, monitor connectivity |
| **Splash Screen** | UI | Control splash screen (hide, show) |
| **Local Notifications** | Notifications | Schedule local notifications |
| **Biometric Auth** | Security | Face ID, Touch ID, fingerprint auth |
| **Clipboard** | Utility | Read/write clipboard |
| **Browser** | Navigation | Open in-app browser |
| **App State** | Lifecycle | Monitor foreground/background state |
## Project Export
### Export Flow
```
┌─────────────────────────────────────────────────────────────────────┐
│ Export for Mobile [×] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ PLATFORM │
│ ┌───────────────────────────────┐ ┌───────────────────────────────┐ │
│ │ [Apple Logo] │ │ [Android Logo] │ │
│ │ │ │ │ │
│ │ iOS │ │ Android │ │
│ │ ✓ Selected │ │ ☐ │ │
│ └───────────────────────────────┘ └───────────────────────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ APP CONFIGURATION │
│ │
│ App ID: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ com.mycompany.myapp │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ⓘ Must match your App Store / Play Store app ID │
│ │
│ App Name: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ My Awesome App │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Version: Build Number: │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ 1.0.0 │ │ 1 │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ ▶ iOS Settings │
│ Team ID: [ABC123DEF4 ] │
│ Deployment Target: [iOS 14.0 ▾] │
│ │
│ ▶ Plugins │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ☑ @capacitor/camera (Used by: Camera Capture) │ │
│ │ ☑ @capacitor/geolocation (Used by: Geolocation) │ │
│ │ ☑ @capacitor/push-notifications (Used by: Push Notif...) │ │
│ │ ☐ @capacitor/haptics (Not used) │ │
│ │ ☐ @capacitor/share (Not used) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ▶ Permissions (auto-detected from node usage) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📷 Camera Access │ │
│ │ "This app needs camera access to take photos" │ │
│ │ │ │
│ │ 📍 Location When In Use │ │
│ │ "This app needs your location to show nearby places" │ │
│ │ │ │
│ │ 🔔 Push Notifications │ │
│ │ (Configured in Apple Developer Portal) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ OUTPUT │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ○ Generate Xcode/Android Studio Project │ │
│ │ Full native project ready for building and customization │ │
│ │ │ │
│ │ ● Generate Build-Ready Package RECOMMENDED │ │
│ │ Minimal project, just run `npx cap build` │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Export for iOS] │
└─────────────────────────────────────────────────────────────────────┘
```
### Generated Project Structure
```
my-app-capacitor/
├── package.json # Node.js package with Capacitor deps
├── capacitor.config.ts # Capacitor configuration
├── www/ # Built Noodl app (web assets)
│ ├── index.html
│ ├── main.js
│ ├── styles.css
│ └── assets/
├── ios/ # iOS Xcode project
│ ├── App/
│ │ ├── App.xcodeproj
│ │ ├── App/
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Info.plist # Permissions, app config
│ │ │ └── Assets.xcassets # App icons
│ │ └── Podfile # iOS dependencies
│ └── Pods/
├── android/ # Android Studio project
│ ├── app/
│ │ ├── build.gradle
│ │ ├── src/main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java/.../MainActivity.java
│ │ │ └── res/ # Icons, splash screens
│ └── gradle/
└── README.md # Build instructions
```
### capacitor.config.ts
```typescript
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.mycompany.myapp',
appName: 'My Awesome App',
webDir: 'www',
// Development: connect to Noodl dev server
server: process.env.NODE_ENV === 'development' ? {
url: 'http://localhost:8574',
cleartext: true,
} : undefined,
plugins: {
Camera: {
// iOS requires these privacy descriptions
},
PushNotifications: {
presentationOptions: ['badge', 'sound', 'alert'],
},
},
ios: {
// iOS-specific configuration
},
android: {
// Android-specific configuration
},
};
export default config;
```
## Implementation Phases
### Phase B.1: Capacitor Bridge Integration (1 week)
**Goal:** Enable Noodl preview to run inside Capacitor WebView with hot-reload.
**Tasks:**
- [ ] Detect when running in Capacitor context (`window.Capacitor.isNativePlatform()`)
- [ ] Inject Capacitor bridge scripts in preview HTML
- [ ] Configure dev server for external access (CORS, network binding)
- [ ] Create "Capacitor Preview Mode" toggle in editor
- [ ] Document Xcode/Android Studio setup for live-reload
**Files to Create/Modify:**
```
packages/noodl-editor/src/frames/viewer-frame/src/views/
├── viewer.js # Modify: Add Capacitor bridge injection
└── capacitorBridge.js # New: Capacitor-specific preview logic
packages/noodl-editor/src/editor/src/views/EditorTopbar/
└── PreviewModeSelector.tsx # New: Preview mode dropdown component
```
### Phase B.2: Core Capacitor Nodes (2 weeks)
**Goal:** Implement essential device capability nodes.
**Week 1: High-priority nodes**
- [ ] Camera Capture node
- [ ] Geolocation node
- [ ] Push Notifications node
- [ ] Haptics node
**Week 2: Secondary nodes**
- [ ] Share node
- [ ] Status Bar node
- [ ] Device Info node
- [ ] Network Status node
- [ ] App State node
**Files to Create:**
```
packages/noodl-runtime/src/nodes/capacitor/
├── index.ts # Export all Capacitor nodes
├── CameraNode.ts
├── GeolocationNode.ts
├── PushNotificationsNode.ts
├── HapticsNode.ts
├── ShareNode.ts
├── StatusBarNode.ts
├── DeviceInfoNode.ts
├── NetworkNode.ts
├── AppStateNode.ts
└── _mocks/ # Web fallback implementations
├── cameraMock.ts
├── geolocationMock.ts
└── ...
```
### Phase B.3: Simulator/Device Launch (1 week)
**Goal:** One-click launch to iOS Simulator or Android Emulator.
**Tasks:**
- [ ] Detect installed simulators (`xcrun simctl list`)
- [ ] Detect installed Android emulators (`emulator -list-avds`)
- [ ] Create simulator selection UI
- [ ] Implement launch command execution
- [ ] Display QR code for physical device connection
**Files to Create:**
```
packages/noodl-editor/src/editor/src/views/panels/
└── CapacitorPreview/
├── SimulatorList.tsx
├── DeviceConnection.tsx
└── QRCodeModal.tsx
```
### Phase B.4: Export Pipeline (1 week)
**Goal:** Generate production-ready Capacitor project.
**Tasks:**
- [ ] Build web assets to `www/` folder
- [ ] Generate `capacitor.config.ts`
- [ ] Generate `package.json` with correct dependencies
- [ ] Auto-detect required plugins from node usage
- [ ] Generate platform-specific permission strings
- [ ] Create iOS Xcode project scaffolding
- [ ] Create Android Studio project scaffolding
- [ ] Generate README with build instructions
**Files to Create:**
```
packages/noodl-editor/src/editor/src/export/
├── capacitor/
│ ├── CapacitorExporter.ts # Main export orchestrator
│ ├── configGenerator.ts # Generate capacitor.config.ts
│ ├── packageGenerator.ts # Generate package.json
│ ├── pluginDetector.ts # Detect required plugins from graph
│ ├── permissionGenerator.ts # Generate iOS Info.plist entries
│ ├── iosProjectGenerator.ts # Scaffold iOS project
│ └── androidProjectGenerator.ts # Scaffold Android project
└── templates/
└── capacitor/
├── package.json.template
├── capacitor.config.ts.template
└── ios/
└── Info.plist.template
```
## Web Fallback Behavior
When Capacitor nodes are used in web preview or web deployment:
| Node | Web Fallback Behavior |
|------|----------------------|
| **Camera** | Uses `navigator.mediaDevices.getUserMedia()` |
| **Geolocation** | Uses `navigator.geolocation` |
| **Push Notifications** | Shows warning, suggests Web Push API setup |
| **Haptics** | Uses `navigator.vibrate()` (limited support) |
| **Share** | Uses Web Share API (Chrome, Safari) |
| **Status Bar** | No-op (not applicable to web) |
| **Device Info** | Returns `navigator.userAgent` parsed info |
| **Network** | Uses `navigator.connection` |
```typescript
// Example: Camera fallback implementation
async function capturePhotoWeb(options: CameraOptions): Promise<Photo> {
// Check if we're in a native Capacitor context
if (window.Capacitor?.isNativePlatform()) {
return Camera.getPhoto(options);
}
// Web fallback using MediaDevices API
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: options.direction === 'front' ? 'user' : 'environment',
width: options.width || undefined,
height: options.height || undefined,
}
});
// Create video element to capture frame
const video = document.createElement('video');
video.srcObject = stream;
await video.play();
// Draw frame to canvas
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(video, 0, 0);
// Stop stream
stream.getTracks().forEach(track => track.stop());
// Convert to base64
const dataUrl = canvas.toDataURL('image/jpeg', options.quality / 100);
return {
base64String: dataUrl.split(',')[1],
format: 'jpeg',
webPath: dataUrl,
};
}
```
## Testing Strategy
### Unit Tests
```typescript
describe('CameraNode', () => {
describe('web fallback', () => {
it('should use MediaDevices API when not in native context', async () => {
const mockGetUserMedia = jest.fn().mockResolvedValue(mockStream);
navigator.mediaDevices.getUserMedia = mockGetUserMedia;
const node = new CameraNode();
node.setInput('source', 'camera');
await node.executeCapture();
expect(mockGetUserMedia).toHaveBeenCalledWith({
video: expect.any(Object),
});
});
});
describe('native context', () => {
beforeEach(() => {
window.Capacitor = { isNativePlatform: () => true };
});
it('should use Capacitor Camera plugin', async () => {
const mockGetPhoto = jest.fn().mockResolvedValue(mockPhoto);
Camera.getPhoto = mockGetPhoto;
const node = new CameraNode();
await node.executeCapture();
expect(mockGetPhoto).toHaveBeenCalled();
});
});
});
```
### Integration Tests
```typescript
describe('Capacitor Export', () => {
it('should detect required plugins from node graph', async () => {
const project = createTestProject([
{ type: 'Camera Capture' },
{ type: 'Geolocation' },
{ type: 'Text' }, // Not a Capacitor node
]);
const detector = new PluginDetector(project);
const plugins = detector.getRequiredPlugins();
expect(plugins).toEqual([
'@capacitor/camera',
'@capacitor/geolocation',
]);
});
it('should generate valid capacitor.config.ts', async () => {
const exporter = new CapacitorExporter(mockProject);
const config = await exporter.generateConfig();
expect(config).toContain("appId: 'com.test.app'");
expect(config).toContain("appName: 'Test App'");
});
});
```
### Manual Testing Checklist
- [ ] Create new Capacitor-target project
- [ ] Add Camera node, verify web fallback works
- [ ] Enable Capacitor preview mode
- [ ] Launch iOS Simulator with live-reload
- [ ] Take photo in simulator, verify it appears in Noodl
- [ ] Export project for iOS
- [ ] Open in Xcode, build successfully
- [ ] Run on physical iPhone, verify all features work
## Success Criteria
| Criteria | Target |
|----------|--------|
| Hot-reload latency | < 2 seconds from save to device update |
| Export time | < 30 seconds for complete Capacitor project |
| Xcode build success | First-time build succeeds without manual fixes |
| Plugin detection accuracy | 100% of used plugins detected automatically |
| Web fallback coverage | All Capacitor nodes have functional web fallbacks |
## Future Enhancements
### Phase B+ Features (Post-MVP)
1. **Capacitor Plugins Marketplace** - Browse and install community plugins
2. **Native UI Components** - Use platform-native UI (iOS UIKit, Android Material)
3. **Background Tasks** - Run code when app is backgrounded
4. **Deep Linking** - Handle custom URL schemes
5. **In-App Purchases** - Integrate with App Store / Play Store purchases
6. **App Store Deployment** - One-click submit to stores (via Fastlane)
### Advanced Native Integration
For users who need more native control:
```typescript
// Custom Capacitor Plugin Node
// Allows calling any Capacitor plugin method
interface CustomPluginNodeInputs {
pluginName: string; // e.g., "@capacitor/camera"
methodName: string; // e.g., "getPhoto"
options: object; // Method parameters
}
// This enables using ANY Capacitor plugin, even community ones
```

View File

@@ -0,0 +1,894 @@
# Electron Desktop Target
**Phase ID:** PHASE-C
**Priority:** 🟡 Medium (Second Priority)
**Estimated Duration:** 3-4 weeks
**Status:** Planning
**Dependencies:** Phase A (BYOB Backend), Phase E (Target System Core)
**Last Updated:** 2025-12-28
## Executive Summary
Enable Noodl users to build native desktop applications for Windows, macOS, and Linux using Electron. Desktop apps unlock capabilities impossible in web browsers: file system access, system processes, native dialogs, and offline-first operation.
## Strategic Advantage
Noodl's editor is already built on Electron (`packages/noodl-platform-electron/`), providing deep institutional knowledge of Electron patterns, IPC communication, and native integration.
## Value Proposition
| Capability | Web | Electron Desktop |
|------------|-----|------------------|
| File System Access | ❌ Limited (File API) | ✅ Full read/write/watch |
| Run System Processes | ❌ No | ✅ Spawn any executable (FFmpeg, Ollama, Python) |
| Native Dialogs | ❌ Browser dialogs | ✅ OS-native file pickers, alerts |
| System Tray | ❌ No | ✅ Tray icon with menu |
| Desktop Notifications | ⚠️ Limited | ✅ Native OS notifications |
| Offline Operation | ⚠️ PWA only | ✅ Full offline support |
| No CORS Restrictions | ❌ Blocked | ✅ Direct API access |
| Auto-Updates | ❌ No | ✅ Built-in updater |
## Use Cases
1. **Local AI Applications** - Run Ollama, LM Studio, or other local LLMs
2. **File Processing Tools** - Batch rename, image conversion, video encoding
3. **Developer Tools** - Code generators, project scaffolders, CLI wrappers
4. **Data Analysis** - Process local CSV/Excel files, generate reports
5. **Automation Tools** - File watchers, backup utilities, sync tools
6. **Kiosk Applications** - Point of sale, digital signage, information displays
## Technical Architecture
### Electron Process Model
```
┌─────────────────────────────────────────────────────────────────────┐
│ ELECTRON APP │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ MAIN PROCESS │ │
│ │ (Node.js - full system access) │ │
│ │ │ │
│ │ • File system operations (fs) │ │
│ │ • Spawn child processes (child_process) │ │
│ │ • System dialogs (dialog) │ │
│ │ • System tray (Tray) │ │
│ │ • Auto-updates (autoUpdater) │ │
│ │ • Native menus (Menu) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ IPC │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ RENDERER PROCESS │ │
│ │ (Chromium - your Noodl app) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ NOODL APP │ │ │
│ │ │ [File Read Node] ──▶ IPC ──▶ [Main Process] ──▶ fs.read │ │ │
│ │ │ [Run Process] ──▶ IPC ──▶ [Main Process] ──▶ spawn │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Preload Script (Bridge between renderer and main) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Security Model
Electron's security is critical. We use **context isolation** and **preload scripts**:
```typescript
// main.ts - Main process
const win = new BrowserWindow({
webPreferences: {
// Security settings
nodeIntegration: false, // Don't expose Node in renderer
contextIsolation: true, // Isolate preload from renderer
sandbox: true, // Sandbox renderer process
preload: path.join(__dirname, 'preload.js'),
}
});
// preload.ts - Secure bridge
const { contextBridge, ipcRenderer } = require('electron');
// Expose ONLY specific, validated APIs
contextBridge.exposeInMainWorld('electronAPI', {
// File operations (validated paths only)
readFile: (filePath: string) => {
// Validate path is within allowed directories
if (!isPathAllowed(filePath)) {
throw new Error('Access denied: path outside allowed directories');
}
return ipcRenderer.invoke('fs:readFile', filePath);
},
writeFile: (filePath: string, content: string) => {
if (!isPathAllowed(filePath)) {
throw new Error('Access denied');
}
return ipcRenderer.invoke('fs:writeFile', filePath, content);
},
// Dialog operations (safe - user-initiated)
showOpenDialog: (options) => ipcRenderer.invoke('dialog:open', options),
showSaveDialog: (options) => ipcRenderer.invoke('dialog:save', options),
// Process operations (controlled)
runProcess: (command: string, args: string[]) => {
// Only allow whitelisted commands
if (!isCommandAllowed(command)) {
throw new Error('Command not allowed');
}
return ipcRenderer.invoke('process:run', command, args);
},
});
```
## Electron-Specific Nodes
### File System Nodes
#### Read File
```
┌─────────────────────────────────────────────────────────────────────┐
│ 📄 Read File [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Read ○──── (Signal) │
│ │
│ File Path: [ ] [📁] │
│ Or connect from File Picker output │
│ │
│ Encoding: [UTF-8 ▾] │
│ ├─ UTF-8 │
│ ├─ ASCII │
│ ├─ Base64 │
│ └─ Binary (Buffer) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Content ────○ (String or Buffer) │
│ File Name ────○ (String) │
│ File Size ────○ (Number - bytes) │
│ Last Modified ────○ (Date) │
│ │
│ Success ────○ (Signal) │
│ Error ────○ (Signal) │
│ Error Message ────○ (String) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
#### Write File
```
┌─────────────────────────────────────────────────────────────────────┐
│ 💾 Write File [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Write ○──── (Signal) │
│ │
│ File Path: [ ] [📁] │
│ Content: ○──── (String or Buffer) │
│ │
│ Encoding: [UTF-8 ▾] │
│ Create Directories: [✓] (Create parent dirs if missing) │
│ Overwrite: [✓] (Overwrite if exists) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Written Path ────○ (String - full path) │
│ Success ────○ (Signal) │
│ Error ────○ (Signal) │
│ Error Message ────○ (String) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
#### Watch Directory
```
┌─────────────────────────────────────────────────────────────────────┐
│ 👁️ Watch Directory [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Start Watching ○──── (Signal) │
│ Stop Watching ○──── (Signal) │
│ │
│ Directory Path: [ ] [📁] │
│ │
│ Watch Subdirectories: [✓] │
│ File Filter: [*.* ] (glob pattern) │
│ Debounce (ms): [100 ] │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ File Path ────○ (String - changed file) │
│ Event Type ────○ (String - add/change/unlink) │
│ │
│ File Added ────○ (Signal) │
│ File Changed ────○ (Signal) │
│ File Removed ────○ (Signal) │
│ │
│ Is Watching ────○ (Boolean) │
│ Error ────○ (Signal) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
#### Native File Picker
```
┌─────────────────────────────────────────────────────────────────────┐
│ 📂 File Picker [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Open Picker ○──── (Signal) │
│ │
│ Mode: [Open File ▾] │
│ ├─ Open File │
│ ├─ Open Multiple Files │
│ ├─ Open Directory │
│ └─ Save File │
│ │
│ Title: [Select a file ] │
│ │
│ File Types: [Images ▾] [+ Add] │
│ ┌─────────────────────────────────────┐ │
│ │ Name: Images │ │
│ │ Extensions: jpg, jpeg, png, gif │ │
│ └─────────────────────────────────────┘ │
│ │
│ Default Path: [/Users/richard/Documents ] [📁] │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Selected Path ────○ (String) │
│ Selected Paths ────○ (Array - for multi-select) │
│ File Name ────○ (String - just filename) │
│ │
│ Selected ────○ (Signal - user made selection) │
│ Cancelled ────○ (Signal - user cancelled) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Process Nodes
#### Run Process
```
┌─────────────────────────────────────────────────────────────────────┐
│ ⚙️ Run Process [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Run ○──── (Signal) │
│ Kill ○──── (Signal - terminate process) │
│ │
│ Command: [ffmpeg ] │
│ │
│ Arguments: (Array of strings) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ [-i, input.mp4, -c:v, libx264, output.mp4] │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ Or connect from Array node │
│ │
│ Working Directory: [/Users/richard/projects] [📁] │
│ │
│ Environment Variables: (Object) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ { "PATH": "/usr/local/bin", "NODE_ENV": "production" } │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Shell: [✓] (Run in shell - enables pipes) │
│ Timeout (ms): [0 ] (0 = no timeout) │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ stdout ────○ (String - standard output) │
│ stderr ────○ (String - standard error) │
│ Exit Code ────○ (Number) │
│ │
│ On Output ────○ (Signal - fires on each line) │
│ Output Line ────○ (String - current output line) │
│ │
│ Started ────○ (Signal) │
│ Completed ────○ (Signal - exit code 0) │
│ Failed ────○ (Signal - non-zero exit) │
│ Is Running ────○ (Boolean) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**Implementation:**
```typescript
// packages/noodl-runtime/src/nodes/electron/RunProcessNode.ts
import { spawn, ChildProcess } from 'child_process';
import { createNodeDefinition } from '../../nodeDefinition';
export const RunProcessNode = createNodeDefinition({
name: 'Run Process',
category: 'System',
color: 'red',
inputs: {
run: { type: 'signal', displayName: 'Run' },
kill: { type: 'signal', displayName: 'Kill' },
command: { type: 'string', displayName: 'Command' },
args: { type: 'array', displayName: 'Arguments', default: [] },
cwd: { type: 'string', displayName: 'Working Directory' },
env: { type: 'object', displayName: 'Environment Variables' },
shell: { type: 'boolean', displayName: 'Shell', default: false },
timeout: { type: 'number', displayName: 'Timeout (ms)', default: 0 },
},
outputs: {
stdout: { type: 'string', displayName: 'stdout' },
stderr: { type: 'string', displayName: 'stderr' },
exitCode: { type: 'number', displayName: 'Exit Code' },
onOutput: { type: 'signal', displayName: 'On Output' },
outputLine: { type: 'string', displayName: 'Output Line' },
started: { type: 'signal', displayName: 'Started' },
completed: { type: 'signal', displayName: 'Completed' },
failed: { type: 'signal', displayName: 'Failed' },
isRunning: { type: 'boolean', displayName: 'Is Running' },
},
targetCompatibility: ['electron'],
state: {
process: null as ChildProcess | null,
stdoutBuffer: '',
stderrBuffer: '',
},
signalHandlers: {
run: async function(inputs, outputs, state) {
// Validate command against whitelist (security)
if (!await this.validateCommand(inputs.command)) {
outputs.stderr = 'Command not allowed by security policy';
outputs.failed.trigger();
return;
}
const options = {
cwd: inputs.cwd || process.cwd(),
env: { ...process.env, ...inputs.env },
shell: inputs.shell,
timeout: inputs.timeout || undefined,
};
state.stdoutBuffer = '';
state.stderrBuffer = '';
outputs.isRunning = true;
state.process = spawn(inputs.command, inputs.args, options);
outputs.started.trigger();
state.process.stdout?.on('data', (data) => {
const text = data.toString();
state.stdoutBuffer += text;
outputs.stdout = state.stdoutBuffer;
// Emit line-by-line
const lines = text.split('\n');
for (const line of lines) {
if (line.trim()) {
outputs.outputLine = line;
outputs.onOutput.trigger();
}
}
});
state.process.stderr?.on('data', (data) => {
state.stderrBuffer += data.toString();
outputs.stderr = state.stderrBuffer;
});
state.process.on('close', (code) => {
outputs.exitCode = code ?? -1;
outputs.isRunning = false;
state.process = null;
if (code === 0) {
outputs.completed.trigger();
} else {
outputs.failed.trigger();
}
});
state.process.on('error', (err) => {
outputs.stderr = err.message;
outputs.isRunning = false;
outputs.failed.trigger();
});
},
kill: function(inputs, outputs, state) {
if (state.process) {
state.process.kill();
outputs.isRunning = false;
}
},
},
});
```
### Window Nodes
#### Window Control
```
┌─────────────────────────────────────────────────────────────────────┐
│ 🪟 Window Control [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Minimize ○──── (Signal) │
│ Maximize ○──── (Signal) │
│ Restore ○──── (Signal) │
│ Close ○──── (Signal) │
│ Toggle Fullscreen ○──── (Signal) │
│ │
│ Set Size ○──── (Signal) │
│ Width: [800 ] │
│ Height: [600 ] │
│ │
│ Set Position ○──── (Signal) │
│ X: [100 ] │
│ Y: [100 ] │
│ │
│ Always On Top: [○] │
│ Resizable: [✓] │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Current Width ────○ (Number) │
│ Current Height ────○ (Number) │
│ Is Maximized ────○ (Boolean) │
│ Is Fullscreen ────○ (Boolean) │
│ Is Focused ────○ (Boolean) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
#### System Tray
```
┌─────────────────────────────────────────────────────────────────────┐
│ 🔔 System Tray [🖥️] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Show ○──── (Signal) │
│ Hide ○──── (Signal) │
│ │
│ Icon: [ ] [📁 Select Image] │
│ Tooltip: [My App ] │
│ │
│ Menu Items: (Array of menu items) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ [ │ │
│ │ { "label": "Show Window", "action": "show" }, │ │
│ │ { "type": "separator" }, │ │
│ │ { "label": "Settings", "action": "settings" }, │ │
│ │ { "type": "separator" }, │ │
│ │ { "label": "Quit", "action": "quit" } │ │
│ │ ] │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ OUTPUTS │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ Menu Action ────○ (String - action from clicked item) │
│ Clicked ────○ (Signal - tray icon clicked) │
│ Double Clicked ────○ (Signal) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Additional Electron Nodes
| Node | Category | Description |
|------|----------|-------------|
| **Native Notification** | System | OS-native notifications with actions |
| **Clipboard** | Utility | Read/write clipboard (text, images) |
| **Screen Info** | System | Get display info, cursor position |
| **Power Monitor** | System | Battery status, suspend/resume events |
| **App Info** | System | Get app version, paths, locale |
| **Protocol Handler** | System | Register custom URL protocols |
| **Auto Launch** | System | Start app on system boot |
| **Global Shortcut** | Input | Register system-wide hotkeys |
## Preview Mode
### Electron Preview in Editor
When Electron target is selected, preview can run with Node.js integration enabled:
```
┌─────────────────────────────────────────────────────────────────────┐
│ Preview Mode: [Electron (Desktop) ▾] │
│ ├─ Web (Browser) │
│ └─ Electron (Desktop) ◀ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ⚠️ ELECTRON PREVIEW MODE │
│ │
│ Preview is running with full desktop capabilities enabled. │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ✓ File system access enabled │ │
│ │ ✓ Process execution enabled │ │
│ │ ✓ Native dialogs enabled │ │
│ │ ✓ System tray available │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Security: Operations are sandboxed to your project directory │
│ │
│ [Switch to Web Preview] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
Since Noodl editor already runs in Electron, enabling desktop features in preview is straightforward:
```typescript
// In viewer.js when Electron preview mode is enabled
if (previewMode === 'electron') {
// Enable IPC bridge to main process
window.noodlElectronBridge = {
readFile: async (path) => ipcRenderer.invoke('fs:readFile', path),
writeFile: async (path, content) => ipcRenderer.invoke('fs:writeFile', path, content),
// ... other APIs
};
}
```
## Export Pipeline
### Export Dialog
```
┌─────────────────────────────────────────────────────────────────────┐
│ Export Desktop App [×] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ PLATFORM │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ [Apple] │ │ [Windows] │ │ [Linux] │ │
│ │ macOS │ │ Windows │ │ Linux │ │
│ │ ✓ Selected │ │ ✓ Selected │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ APP CONFIGURATION │
│ │
│ App Name: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ My Desktop App │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ App ID: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ com.mycompany.mydesktopapp │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Version: Build Number: │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ 1.0.0 │ │ 1 │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ ▶ macOS Settings │
│ Category: [Productivity ▾] │
│ Code Signing: [Developer ID ▾] │
│ Notarization: [✓] (Required for distribution) │
│ │
│ ▶ Windows Settings │
│ Code Signing: [None ▾] │
│ Installer Type: [NSIS ▾] │
│ │
│ ▶ App Icons │
│ ┌─────────────────┐ │
│ │ [App Icon] │ [📁 Select Icon] │
│ │ 512x512 PNG │ │
│ └─────────────────┘ │
│ ⓘ Will be converted to .icns (macOS) and .ico (Windows) │
│ │
│ ▶ Permissions │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ☑ File System Access (Required by: Read File, ...) │ │
│ │ ☑ Process Execution (Required by: Run Process) │ │
│ │ ☐ Camera Access │ │
│ │ ☐ Microphone Access │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ OUTPUT │
│ ● Installable Package (.dmg, .exe, .deb) RECOMMENDED │
│ ○ Portable App (zip folder) │
│ ○ Development Build (unpackaged, for testing) │
│ │
│ [Cancel] [Build for macOS] │
│ [Build for Windows] │
│ [Build All] │
└─────────────────────────────────────────────────────────────────────┘
```
### Generated Project Structure
```
my-app-electron/
├── package.json # Electron app dependencies
├── electron-builder.yml # Build configuration
├── src/
│ ├── main/
│ │ ├── main.ts # Main process entry
│ │ ├── preload.ts # Preload script (IPC bridge)
│ │ ├── ipc/
│ │ │ ├── fs.ts # File system handlers
│ │ │ ├── dialog.ts # Dialog handlers
│ │ │ ├── process.ts # Process handlers
│ │ │ └── index.ts
│ │ └── menu.ts # Application menu
│ └── renderer/
│ ├── index.html # App entry point
│ ├── main.js # Noodl runtime bundle
│ ├── styles.css
│ └── assets/
├── resources/
│ ├── icon.icns # macOS icon
│ ├── icon.ico # Windows icon
│ └── icon.png # Linux icon
└── dist/ # Build output
├── mac/
│ └── My App.dmg
└── win/
└── My App Setup.exe
```
## Implementation Phases
### Phase C.1: Electron Runtime Package (1 week)
**Goal:** Create separate runtime package for Electron-specific functionality.
**Tasks:**
- [ ] Create `packages/noodl-runtime-electron/` package
- [ ] Implement secure IPC bridge (preload.ts)
- [ ] Implement main process handlers (fs, dialog, process)
- [ ] Create sandbox validation utilities
- [ ] Set up security policy configuration
**Files to Create:**
```
packages/noodl-runtime-electron/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts
│ ├── main/
│ │ ├── main.ts # Main process entry
│ │ ├── preload.ts # Preload script
│ │ └── handlers/
│ │ ├── fs.ts
│ │ ├── dialog.ts
│ │ ├── process.ts
│ │ ├── window.ts
│ │ └── system.ts
│ ├── security/
│ │ ├── pathValidator.ts # Validate file paths
│ │ ├── commandWhitelist.ts # Allowed commands
│ │ └── permissions.ts
│ └── types.ts
└── test/
```
### Phase C.2: Electron-Specific Nodes (1 week)
**Goal:** Implement desktop capability nodes.
**Tasks:**
- [ ] File System nodes (Read, Write, Watch, Picker)
- [ ] Process nodes (Run, Kill)
- [ ] Window nodes (Control, Tray)
- [ ] System nodes (Notification, Clipboard, App Info)
- [ ] Register nodes in Electron target only
**Files to Create:**
```
packages/noodl-runtime/src/nodes/electron/
├── index.ts
├── fs/
│ ├── ReadFileNode.ts
│ ├── WriteFileNode.ts
│ ├── WatchDirectoryNode.ts
│ └── FilePickerNode.ts
├── process/
│ ├── RunProcessNode.ts
│ └── ProcessInfoNode.ts
├── window/
│ ├── WindowControlNode.ts
│ └── SystemTrayNode.ts
└── system/
├── NotificationNode.ts
├── ClipboardNode.ts
└── AppInfoNode.ts
```
### Phase C.3: Electron Preview Mode (3-4 days)
**Goal:** Enable desktop features in editor preview.
**Tasks:**
- [ ] Add Electron preview mode option
- [ ] Enable IPC bridge in preview window
- [ ] Create security sandbox for preview
- [ ] Add visual indicators for Electron mode
- [ ] Test all nodes in preview context
### Phase C.4: Electron Packaging (1 week)
**Goal:** Export production-ready desktop applications.
**Tasks:**
- [ ] Integrate electron-builder
- [ ] Generate main.ts from project configuration
- [ ] Generate preload.ts with used features
- [ ] Bundle Noodl app as renderer
- [ ] Configure code signing (macOS, Windows)
- [ ] Generate installer packages
- [ ] Create auto-update configuration
**Files to Create:**
```
packages/noodl-editor/src/editor/src/export/electron/
├── ElectronExporter.ts
├── mainGenerator.ts
├── preloadGenerator.ts
├── builderConfig.ts
├── iconConverter.ts
└── templates/
├── main.ts.template
├── preload.ts.template
└── electron-builder.yml.template
```
## Security Considerations
### Path Validation
All file system operations must validate paths:
```typescript
class PathValidator {
private allowedPaths: string[];
constructor(projectPath: string) {
this.allowedPaths = [
projectPath,
app.getPath('documents'),
app.getPath('downloads'),
app.getPath('temp'),
];
}
isPathAllowed(targetPath: string): boolean {
const resolved = path.resolve(targetPath);
// Check if path is within allowed directories
return this.allowedPaths.some(allowed =>
resolved.startsWith(path.resolve(allowed))
);
}
// Prevent path traversal attacks
sanitizePath(inputPath: string): string {
// Remove .. and normalize
return path.normalize(inputPath).replace(/\.\./g, '');
}
}
```
### Command Whitelist
Only allow specific commands to be executed:
```typescript
const ALLOWED_COMMANDS = [
// Media processing
'ffmpeg',
'ffprobe',
'imagemagick',
// AI/ML
'ollama',
'python',
'python3',
// Utilities
'git',
'npm',
'npx',
'node',
];
function isCommandAllowed(command: string): boolean {
const base = path.basename(command);
return ALLOWED_COMMANDS.includes(base);
}
```
### Permission System
```typescript
interface ElectronPermissions {
fileSystem: {
read: boolean;
write: boolean;
allowedPaths: string[];
};
process: {
execute: boolean;
allowedCommands: string[];
};
window: {
control: boolean;
tray: boolean;
};
system: {
notifications: boolean;
clipboard: boolean;
autoLaunch: boolean;
};
}
```
## Success Criteria
| Criteria | Target |
|----------|--------|
| Build time | < 2 minutes for production build |
| App size | < 150MB for minimal app |
| Startup time | < 3 seconds to first render |
| File operations | < 50ms overhead vs raw Node.js |
| All nodes tested | On macOS, Windows, Linux |
## Future Enhancements
1. **Native Node Modules** - Allow npm packages with native code
2. **Auto-Update System** - Built-in update mechanism
3. **Crash Reporting** - Integrate crash reporting service
4. **Hardware Access** - Serial ports, USB devices, Bluetooth
5. **Multiple Windows** - Open additional windows from nodes

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,334 @@
# Multi-Target Deployment Initiative
**Initiative ID:** INITIATIVE-001
**Priority:** HIGH
**Estimated Duration:** 16-22 weeks
**Status:** Planning
**Last Updated:** 2025-12-28
## Executive Summary
Transform Noodl from a web-only visual programming platform into a true multi-target development environment. Users will be able to build once and deploy to:
- **Web** (current) - Static sites, SPAs, PWAs
- **Mobile** (Capacitor) - iOS and Android native apps with device APIs
- **Desktop** (Electron) - Windows, macOS, Linux apps with file system access
- **Browser Extension** (Chrome) - Extensions with browser API access
Additionally, this initiative introduces **BYOB (Bring Your Own Backend)**, enabling users to connect to any BaaS platform (Directus, Supabase, Pocketbase, Firebase, etc.) rather than being locked to a single provider.
## Core Insight
Noodl's visual graph is already target-agnostic in principle. The nodes define *what* happens, not *where* it runs. Currently, tooling assumes web-only deployment, but the architecture naturally supports multiple targets through:
1. **Target-specific node libraries** (Capacitor Camera, Electron File System, etc.)
2. **Conditional runtime injection** (Capacitor bridge, Node.js integration)
3. **Target-aware export pipelines** (different build outputs per target)
## Strategic Value
| Target | Value Proposition |
|--------|-------------------|
| **Capacitor** | Mobile apps without learning Swift/Kotlin. Hot-reload preview. |
| **Electron** | Desktop apps with file access, offline capability, local AI (Ollama). |
| **Chrome Extension** | Browser tools, productivity extensions, content scripts. |
| **BYOB** | No vendor lock-in. Use existing infrastructure. Self-hosted options. |
## Project Architecture
### Current State
```
Graph Definition → Single Runtime (React/Web) → Single Deployment (Static Web)
```
### Target State
```
┌─→ Web Runtime ─────→ Static Web / PWA
Graph Definition → Target Adapter ──┼─→ Capacitor Runtime → iOS/Android App
├─→ Electron Runtime ─→ Desktop App
└─→ Extension Runtime → Chrome Extension
Backend Abstraction Layer
┌───────────┬───────────┬───────────┬───────────┐
│ Directus │ Supabase │ Pocketbase│ Custom │
└───────────┴───────────┴───────────┴───────────┘
```
## Target Selection System
### Philosophy
The graph itself is largely target-agnostic—it's the *nodes available* and *deployment output* that differ. This enables maximum flexibility while maintaining focused UX.
### User Experience Flow
#### 1. Project Creation
```
┌─────────────────────────────────────────────────────────────────────┐
│ Create New Project │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Project Name: [My App ] │
│ │
│ Primary Target: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ○ 🌐 Web Application │ │
│ │ Static sites, SPAs, PWAs. Deploy anywhere. │ │
│ │ │ │
│ │ ● 📱 Mobile App (Capacitor) RECOMMENDED │ │
│ │ iOS and Android. Access camera, GPS, push notifications. │ │
│ │ │ │
│ │ ○ 🖥️ Desktop App (Electron) │ │
│ │ Windows, macOS, Linux. File system, local processes. │ │
│ │ │ │
│ │ ○ 🧩 Browser Extension │ │
│ │ Chrome extensions. Browser APIs, content scripts. │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ⓘ You can add more targets later in Project Settings │
│ │
│ [Cancel] [Create Project] │
└─────────────────────────────────────────────────────────────────────┘
```
#### 2. Adding Additional Targets (Project Settings)
```
┌─────────────────────────────────────────────────────────────────────┐
│ Project Settings → Targets │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ PRIMARY TARGET │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 📱 Mobile App (Capacitor) [Change] │ │
│ │ Determines default node palette and preview mode │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ADDITIONAL TARGETS │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ☑ 🌐 Web Application │ │
│ │ ☐ 🖥️ Desktop App (Electron) [Configure...] │ │
│ │ ☐ 🧩 Browser Extension [Configure...] │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ NODE COMPATIBILITY │
│ ⚠️ 3 nodes in your project are incompatible with some targets: │
│ • Camera Capture - Only Capacitor, Electron │
│ • Push Notification - Only Capacitor │
│ • File System Read - Only Electron, Extension │
│ │
│ [View Compatibility Report] │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
#### 3. Node Palette with Compatibility Badges
```
┌─────────────────────────────────────────────────────────────────────┐
│ Node Palette [🔍 Search] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ▼ DEVICE & PLATFORM │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📷 Camera Capture [📱] [🖥️] │ │
│ │ 📍 Geolocation [🌐] [📱] [🖥️] [🧩] │ │
│ │ 🔔 Push Notification [📱] │ │
│ │ 📁 File System Read [🖥️] [🧩] │ │
│ │ 📁 File System Write [🖥️] │ │
│ │ ⚙️ Run Process [🖥️] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ▼ BROWSER EXTENSION │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 🗂️ Extension Storage [🧩] │ │
│ │ 📑 Browser Tabs [🧩] │ │
│ │ 💬 Content Script Message [🧩] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ▼ DATA & BACKEND │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📊 Query Records [🌐] [📱] [🖥️] [🧩] │ │
│ │ Create Record [🌐] [📱] [🖥️] [🧩] │ │
│ │ ✏️ Update Record [🌐] [📱] [🖥️] [🧩] │ │
│ │ 🌐 HTTP Request [🌐] [📱] [🖥️] [🧩] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Legend: [🌐] Web [📱] Mobile [🖥️] Desktop [🧩] Extension │
└─────────────────────────────────────────────────────────────────────┘
```
#### 4. Duplicate as Different Target
```
┌─────────────────────────────────────────────────────────────────────┐
│ Duplicate Project │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Source: My Mobile App (Capacitor) │
│ │
│ New Project Name: [My Mobile App - Web Version ] │
│ │
│ New Primary Target: │
│ ● 🌐 Web Application │
│ ○ 🖥️ Desktop App (Electron) │
│ ○ 🧩 Browser Extension │
│ │
│ COMPATIBILITY ANALYSIS │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ✅ 47 nodes are fully compatible │ │
│ │ ⚠️ 3 nodes need attention: │ │
│ │ │ │
│ │ 📷 Camera Capture │ │
│ │ └─ Will use browser MediaDevices API (reduced features) │ │
│ │ │ │
│ │ 🔔 Push Notification │ │
│ │ └─ Will use Web Push API (requires HTTPS, user permission) │ │
│ │ │ │
│ │ 📁 File System Read │ │
│ │ └─ ❌ Not available in web. Will be disabled. │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Duplicate with Adaptations] │
└─────────────────────────────────────────────────────────────────────┘
```
### Technical Implementation
```typescript
// Project configuration model
interface NoodlProject {
// Existing fields
name: string;
components: Component[];
settings: ProjectSettings;
// NEW: Target Configuration
targets: TargetConfiguration;
// NEW: Backend Configuration (BYOB)
backends: BackendConfig[];
activeBackendId: string;
}
interface TargetConfiguration {
// Primary target determines default node palette and preview mode
primary: TargetType;
// Additional enabled targets
enabled: TargetType[];
// Per-target settings
web?: WebTargetConfig;
capacitor?: CapacitorTargetConfig;
electron?: ElectronTargetConfig;
extension?: ExtensionTargetConfig;
}
type TargetType = 'web' | 'capacitor' | 'electron' | 'extension';
interface WebTargetConfig {
pwa: boolean;
serviceWorker: boolean;
baseUrl: string;
}
interface CapacitorTargetConfig {
appId: string; // com.example.myapp
appName: string;
platforms: ('ios' | 'android')[];
plugins: string[]; // Enabled Capacitor plugins
iosTeamId?: string;
androidKeystore?: string;
}
interface ElectronTargetConfig {
appId: string;
productName: string;
platforms: ('darwin' | 'win32' | 'linux')[];
nodeIntegration: boolean;
contextIsolation: boolean;
permissions: ElectronPermission[];
}
interface ExtensionTargetConfig {
manifestVersion: 3;
name: string;
permissions: string[]; // chrome.storage, chrome.tabs, etc.
hostPermissions: string[];
contentScripts?: ContentScriptConfig[];
serviceWorker?: boolean;
}
```
## Phase Overview
| Phase | Name | Duration | Priority | Dependencies |
|-------|------|----------|----------|--------------|
| **A** | [BYOB Backend System](../01-byob-backend/README.md) | 4-6 weeks | 🔴 Critical | HTTP Node |
| **B** | [Capacitor Mobile Target](../02-capacitor-mobile/README.md) | 4-5 weeks | 🔴 High | Phase A |
| **C** | [Electron Desktop Target](../03-electron-desktop/README.md) | 3-4 weeks | 🟡 Medium | Phase A |
| **D** | [Chrome Extension Target](../04-chrome-extension/README.md) | 2-3 weeks | 🟢 Lower | Phase A |
| **E** | [Target System Core](../05-target-system/README.md) | 2-3 weeks | 🔴 Critical | Before B,C,D |
### Recommended Execution Order
```
Phase E (Target System Core) ─┬─→ Phase B (Capacitor) ─┐
│ │
Phase A (BYOB Backend) ───────┼─→ Phase C (Electron) ──┼─→ Integration & Polish
│ │
└─→ Phase D (Extension) ─┘
```
**Rationale:**
1. **Phase A (BYOB) and Phase E (Target System)** can proceed in parallel as foundation
2. **Phase B (Capacitor)** is highest user priority - mobile apps unlock massive value
3. **Phase C (Electron)** leverages existing `noodl-platform-electron` knowledge
4. **Phase D (Extension)** is most niche but relatively quick to implement
## Success Criteria
### MVP Success (Phase A + B complete)
- [ ] Users can configure Directus/Supabase as backend without code
- [ ] Data nodes work with configured backend
- [ ] Schema introspection populates dropdowns
- [ ] Capacitor preview with hot-reload works
- [ ] Can export iOS/Android-ready Capacitor project
- [ ] Camera, Geolocation, and Push nodes work in Capacitor
### Full Success (All phases complete)
- [ ] All four deployment targets working
- [ ] Seamless target switching in project settings
- [ ] Compatibility badges on all nodes
- [ ] Build-time validation prevents incompatible deployments
- [ ] "Duplicate as Target" feature works with adaptation analysis
- [ ] Multiple backends configurable per project
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| Capacitor plugin complexity | Medium | High | Start with 3 core plugins, expand gradually |
| Backend abstraction leakiness | High | Medium | Accept platform-specific features won't abstract |
| Electron security concerns | Medium | High | Clear UI separation, sandboxed by default |
| Extension manifest v3 limitations | Medium | Medium | Document limitations, focus on popup use case |
| Scope creep | High | High | Strict phase gates, MVP-first approach |
## Related Documentation
- [BYOB Backend System](../01-byob-backend/README.md) - Extensive backend flexibility documentation
- [Capacitor Mobile Target](../02-capacitor-mobile/README.md) - Mobile app deployment
- [Electron Desktop Target](../03-electron-desktop/README.md) - Desktop app deployment
- [Chrome Extension Target](../04-chrome-extension/README.md) - Browser extension deployment
- [Target System Core](../05-target-system/README.md) - Node compatibility and target selection
## References
- Previous BYOB discussion: https://claude.ai/chat/905f39ae-973b-4c19-a3cc-6bf08befb513
- Existing deployment system: `packages/noodl-editor/src/editor/src/views/DeployPopup/`
- Existing preview system: `packages/noodl-editor/src/frames/viewer-frame/src/views/viewer.js`
- Platform abstraction: `packages/noodl-platform-electron/`
- TASK-002: Robust HTTP Node (prerequisite)