mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 23:02:56 +01:00
Finished component sidebar updates, with one small bug remaining and documented
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
```
|
||||
@@ -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
334
dev-docs/tasks/phase-5-multi-target-deployment/README.md
Normal file
334
dev-docs/tasks/phase-5-multi-target-deployment/README.md
Normal 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)
|
||||
Reference in New Issue
Block a user