Phase E: Target System Core
Overview
The Target System Core is the foundational infrastructure that enables Noodl to support multiple deployment targets. It provides the underlying systems for target selection, node compatibility validation, conditional compilation, and target-aware UX across the editor.
Timeline: 2-3 weeks
Priority: Critical Foundation - Must complete before Phases B, C, D
Prerequisites: None (runs in parallel with Phase A)
Core Concept
Noodl's visual graph is inherently target-agnostic. Nodes define what happens, not where it runs. The Target System Core provides the infrastructure to:
- Let users select their deployment target(s)
- Show which nodes work on which targets
- Prevent invalid deployments at build time
- Enable target-specific runtime injection
Target Types
enum TargetType {
WEB = 'web', // Default - browser deployment
CAPACITOR = 'capacitor', // iOS/Android via Capacitor
ELECTRON = 'electron', // Desktop via Electron
EXTENSION = 'extension', // Chrome Extension
}
interface TargetDefinition {
type: TargetType;
displayName: string;
icon: string;
description: string;
runtimePackage: string; // e.g., 'noodl-runtime-capacitor'
previewSupported: boolean;
exportFormats: ExportFormat[];
}
const TARGETS: Record<TargetType, TargetDefinition> = {
[TargetType.WEB]: {
type: TargetType.WEB,
displayName: 'Web',
icon: '🌐',
description: 'Deploy as a web application',
runtimePackage: 'noodl-runtime',
previewSupported: true,
exportFormats: ['folder', 'netlify', 'vercel', 'cloudflare'],
},
[TargetType.CAPACITOR]: {
type: TargetType.CAPACITOR,
displayName: 'Mobile',
icon: '📱',
description: 'iOS and Android via Capacitor',
runtimePackage: 'noodl-runtime-capacitor',
previewSupported: true,
exportFormats: ['capacitor-project', 'ios-xcode', 'android-studio'],
},
[TargetType.ELECTRON]: {
type: TargetType.ELECTRON,
displayName: 'Desktop',
icon: '🖥️',
description: 'Windows, macOS, and Linux via Electron',
runtimePackage: 'noodl-runtime-electron',
previewSupported: true,
exportFormats: ['electron-project', 'dmg', 'exe', 'deb', 'appimage'],
},
[TargetType.EXTENSION]: {
type: TargetType.EXTENSION,
displayName: 'Browser Extension',
icon: '🧩',
description: 'Chrome extension',
runtimePackage: 'noodl-runtime',
previewSupported: false, // Requires loading as unpacked
exportFormats: ['extension-folder', 'extension-zip'],
},
};
Project Configuration Model
Target Configuration
interface ProjectTargetConfig {
primary: TargetType; // Main target (determines default palette)
enabled: TargetType[]; // All enabled targets
// Per-target configuration
web?: WebTargetConfig;
capacitor?: CapacitorTargetConfig;
electron?: ElectronTargetConfig;
extension?: ExtensionTargetConfig;
}
interface WebTargetConfig {
// Standard web config
}
interface CapacitorTargetConfig {
appId: string; // com.company.app
appName: string;
version: string;
ios?: {
teamId: string;
deploymentTarget: string;
};
android?: {
packageName: string;
minSdkVersion: number;
};
plugins: string[]; // Capacitor plugins to include
permissions: CapacitorPermission[]; // Auto-detected from nodes
}
interface ElectronTargetConfig {
appId: string;
appName: string;
version: string;
platforms: ('darwin' | 'win32' | 'linux')[];
permissions: {
fileSystem: boolean;
processExecution: boolean;
systemTray: boolean;
};
windowDefaults: {
width: number;
height: number;
minWidth?: number;
minHeight?: number;
};
}
interface ExtensionTargetConfig {
name: string;
version: string;
description: string;
permissions: string[];
hostPermissions: string[];
contexts: {
popup: boolean;
background: boolean;
contentScript: boolean;
};
contentScriptMatches: string[];
}
Storage in Project Metadata
// In project.json
{
"name": "My App",
"version": "1.0.0",
"targets": {
"primary": "capacitor",
"enabled": ["web", "capacitor"],
"capacitor": {
"appId": "com.mycompany.myapp",
"appName": "My App",
"version": "1.0.0"
}
},
// ... other project config
}
Node Compatibility System
Compatibility Declaration
Every node declares its target compatibility:
interface NodeDefinition {
name: string;
displayName: string;
category: string;
// Target compatibility
targetCompatibility: TargetType[] | 'all';
// Per-target implementation
implementations?: {
[key in TargetType]?: {
runtime: string; // Different runtime file
warnings?: string[]; // Limitations on this target
};
};
// ... other node properties
}
Compatibility Patterns
Universal Nodes (work everywhere):
{
name: 'Group',
targetCompatibility: 'all',
// Single implementation works on all targets
}
Platform-Specific Nodes (single target):
{
name: 'Camera Capture',
targetCompatibility: [TargetType.CAPACITOR],
// Only available when Capacitor is enabled
}
Multi-Platform with Different Implementations:
{
name: 'File Picker',
targetCompatibility: [TargetType.WEB, TargetType.ELECTRON, TargetType.CAPACITOR],
implementations: {
[TargetType.WEB]: {
runtime: 'file-picker-web.ts',
warnings: ['Limited to browser file picker API'],
},
[TargetType.ELECTRON]: {
runtime: 'file-picker-electron.ts',
// Full native file dialog
},
[TargetType.CAPACITOR]: {
runtime: 'file-picker-capacitor.ts',
warnings: ['On iOS, requires photo library permission'],
},
},
}
Graceful Degradation:
{
name: 'Haptic Feedback',
targetCompatibility: [TargetType.CAPACITOR, TargetType.WEB],
implementations: {
[TargetType.CAPACITOR]: {
runtime: 'haptic-capacitor.ts',
// Full haptic API
},
[TargetType.WEB]: {
runtime: 'haptic-web.ts',
warnings: ['Limited to Vibration API - may not work on all devices'],
},
},
}
Compatibility Registry
class NodeCompatibilityRegistry {
private static instance: NodeCompatibilityRegistry;
private registry: Map<string, TargetType[]> = new Map();
registerNode(nodeType: string, compatibility: TargetType[] | 'all'): void {
if (compatibility === 'all') {
this.registry.set(nodeType, Object.values(TargetType));
} else {
this.registry.set(nodeType, compatibility);
}
}
isCompatible(nodeType: string, target: TargetType): boolean {
const compatibility = this.registry.get(nodeType);
return compatibility?.includes(target) ?? false;
}
getCompatibleTargets(nodeType: string): TargetType[] {
return this.registry.get(nodeType) ?? [];
}
getNodesForTarget(target: TargetType): string[] {
return Array.from(this.registry.entries())
.filter(([_, targets]) => targets.includes(target))
.map(([nodeType, _]) => nodeType);
}
getIncompatibleNodes(usedNodes: string[], target: TargetType): string[] {
return usedNodes.filter(node => !this.isCompatible(node, target));
}
}
Node Palette Filtering
Dynamic Palette Based on Target
When a user selects their primary target, the node palette updates to show compatible nodes:
class NodePaletteService {
private currentTarget: TargetType;
private enabledTargets: TargetType[];
setTargets(primary: TargetType, enabled: TargetType[]): void {
this.currentTarget = primary;
this.enabledTargets = enabled;
this.refreshPalette();
}
getVisibleNodes(): NodeDefinition[] {
return allNodes.filter(node => {
// Node is visible if compatible with ANY enabled target
const compatibility = node.targetCompatibility === 'all'
? Object.values(TargetType)
: node.targetCompatibility;
return compatibility.some(t => this.enabledTargets.includes(t));
});
}
getNodeCategories(): Category[] {
const visibleNodes = this.getVisibleNodes();
// Group by category, add target-specific categories
const categories = new Map<string, NodeDefinition[]>();
visibleNodes.forEach(node => {
const existing = categories.get(node.category) ?? [];
categories.set(node.category, [...existing, node]);
});
// Add target-specific category headers
if (this.enabledTargets.includes(TargetType.CAPACITOR)) {
// Ensure "Mobile" category exists
}
if (this.enabledTargets.includes(TargetType.ELECTRON)) {
// Ensure "Desktop" category exists
}
return Array.from(categories.entries()).map(([name, nodes]) => ({
name,
nodes,
}));
}
}
Compatibility Badges in UI
Nodes display badges indicating their target compatibility:
┌──────────────────────────────────────────────────────────────┐
│ Node Palette [🔍] │
├──────────────────────────────────────────────────────────────┤
│ │
│ ▼ UI Elements │
│ ├── Text [🌐📱🖥️🧩] │
│ ├── Button [🌐📱🖥️🧩] │
│ ├── Image [🌐📱🖥️🧩] │
│ └── Video [🌐📱🖥️] │
│ │
│ ▼ Mobile │
│ ├── Camera Capture [📱] │
│ ├── Push Notifications [📱] │
│ ├── Haptic Feedback [📱🌐*] │
│ └── Geolocation [📱🌐] │
│ │
│ ▼ Desktop │
│ ├── Read File [🖥️] │
│ ├── Write File [🖥️] │
│ ├── Run Process [🖥️] │
│ └── System Tray [🖥️] │
│ │
│ ▼ Extension │
│ ├── Storage Get [🧩] │
│ ├── Tab Query [🧩] │
│ └── Send Message [🧩] │
│ │
│ * = Limited functionality on this target │
└──────────────────────────────────────────────────────────────┘
Badge Component
interface CompatibilityBadgeProps {
targets: TargetType[];
warnings?: Partial<Record<TargetType, string>>;
}
function CompatibilityBadge({ targets, warnings }: CompatibilityBadgeProps) {
return (
<div className="compatibility-badge">
{targets.map(target => (
<span
key={target}
className={`target-icon ${warnings?.[target] ? 'has-warning' : ''}`}
title={warnings?.[target] || TARGETS[target].displayName}
>
{TARGETS[target].icon}
{warnings?.[target] && <span className="warning-indicator">*</span>}
</span>
))}
</div>
);
}
Build-Time Validation
Validation Service
interface ValidationResult {
valid: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
}
interface ValidationError {
type: 'incompatible_node' | 'missing_config' | 'invalid_permission';
nodeId?: string;
nodeName?: string;
message: string;
target: TargetType;
}
interface ValidationWarning {
type: 'limited_functionality' | 'performance' | 'permission_broad';
nodeId?: string;
message: string;
target: TargetType;
}
class TargetValidationService {
validate(project: Project, target: TargetType): ValidationResult {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
// 1. Check all used nodes are compatible
const usedNodes = this.getUsedNodes(project);
usedNodes.forEach(usage => {
const compatibility = NodeCompatibilityRegistry.getInstance()
.getCompatibleTargets(usage.nodeType);
if (!compatibility.includes(target)) {
errors.push({
type: 'incompatible_node',
nodeId: usage.nodeId,
nodeName: usage.displayName,
message: `"${usage.displayName}" is not available on ${TARGETS[target].displayName}`,
target,
});
}
// Check for limited functionality warnings
const nodeDefinition = getNodeDefinition(usage.nodeType);
const targetWarnings = nodeDefinition.implementations?.[target]?.warnings;
if (targetWarnings) {
warnings.push({
type: 'limited_functionality',
nodeId: usage.nodeId,
message: targetWarnings.join('; '),
target,
});
}
});
// 2. Check target-specific configuration
const targetConfig = project.targets[target];
if (!targetConfig && target !== TargetType.WEB) {
errors.push({
type: 'missing_config',
message: `${TARGETS[target].displayName} target is not configured`,
target,
});
}
// 3. Validate permissions
const requiredPermissions = this.detectRequiredPermissions(usedNodes, target);
const configuredPermissions = targetConfig?.permissions ?? [];
requiredPermissions.forEach(perm => {
if (!configuredPermissions.includes(perm)) {
warnings.push({
type: 'permission_broad',
message: `Permission "${perm}" may be required for ${TARGETS[target].displayName}`,
target,
});
}
});
return {
valid: errors.length === 0,
errors,
warnings,
};
}
private getUsedNodes(project: Project): NodeUsage[] {
// Traverse all components and collect used nodes
const usages: NodeUsage[] = [];
project.components.forEach(component => {
component.nodes.forEach(node => {
usages.push({
nodeId: node.id,
nodeType: node.type,
displayName: node.displayName,
componentId: component.id,
});
});
});
return usages;
}
private detectRequiredPermissions(
usedNodes: NodeUsage[],
target: TargetType
): string[] {
const permissions = new Set<string>();
usedNodes.forEach(usage => {
const nodePerms = getNodePermissions(usage.nodeType, target);
nodePerms.forEach(p => permissions.add(p));
});
return Array.from(permissions);
}
}
Pre-Export Validation UI
┌──────────────────────────────────────────────────────────────┐
│ Export to Mobile [×] │
├──────────────────────────────────────────────────────────────┤
│ │
│ ⚠️ 2 issues found │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ❌ ERRORS (1) │ │
│ │ │ │
│ │ • "Read File" node is not available on Mobile │ │
│ │ Used in: SettingsScreen (line 47) │ │
│ │ [Go to Node] [Remove Node] [Use Alternative] │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ⚠️ WARNINGS (1) │ │
│ │ │ │
│ │ • "Haptic Feedback" has limited functionality on Web │ │
│ │ Vibration API may not work on all devices │ │
│ │ Used in: ButtonComponent │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ [Cancel] [Export with Warnings] │
└──────────────────────────────────────────────────────────────┘
Duplicate as Target Feature
When a user wants to adapt a component for a different target, the "Duplicate as Target" feature analyzes compatibility and suggests adaptations.
Analysis Engine
interface TargetAdaptationAnalysis {
compatible: boolean;
adaptations: Adaptation[];
blockers: Blocker[];
}
interface Adaptation {
nodeId: string;
nodeName: string;
adaptationType: 'replace' | 'remove' | 'configure';
// For 'replace'
replacement?: {
nodeType: string;
displayName: string;
reason: string;
};
// For 'configure'
configChanges?: {
property: string;
oldValue: any;
newValue: any;
reason: string;
}[];
// For 'remove'
removalReason?: string;
affectedConnections: string[];
}
interface Blocker {
nodeId: string;
nodeName: string;
reason: string;
suggestion?: string;
}
class TargetAdaptationService {
analyze(
component: Component,
sourceTarget: TargetType,
destTarget: TargetType
): TargetAdaptationAnalysis {
const adaptations: Adaptation[] = [];
const blockers: Blocker[] = [];
component.nodes.forEach(node => {
const sourceCompat = NodeCompatibilityRegistry.getInstance()
.isCompatible(node.type, sourceTarget);
const destCompat = NodeCompatibilityRegistry.getInstance()
.isCompatible(node.type, destTarget);
if (sourceCompat && !destCompat) {
// Node works on source but not dest - needs adaptation
const alternative = this.findAlternative(node.type, destTarget);
if (alternative) {
adaptations.push({
nodeId: node.id,
nodeName: node.displayName,
adaptationType: 'replace',
replacement: {
nodeType: alternative.nodeType,
displayName: alternative.displayName,
reason: alternative.reason,
},
});
} else if (this.isOptional(node)) {
adaptations.push({
nodeId: node.id,
nodeName: node.displayName,
adaptationType: 'remove',
removalReason: `No ${TARGETS[destTarget].displayName} equivalent`,
affectedConnections: this.getConnections(node),
});
} else {
blockers.push({
nodeId: node.id,
nodeName: node.displayName,
reason: `No ${TARGETS[destTarget].displayName} equivalent`,
suggestion: this.getSuggestion(node.type, destTarget),
});
}
}
});
return {
compatible: blockers.length === 0,
adaptations,
blockers,
};
}
private findAlternative(
nodeType: string,
target: TargetType
): { nodeType: string; displayName: string; reason: string } | null {
// Mapping of nodes to their alternatives on different targets
const alternatives: Record<string, Partial<Record<TargetType, string>>> = {
'ReadFile': {
[TargetType.CAPACITOR]: 'CapacitorFilesystem',
[TargetType.WEB]: null, // No alternative
},
'NativeFilePicker': {
[TargetType.WEB]: 'FileInput',
[TargetType.CAPACITOR]: 'CapacitorFilePicker',
},
'CameraCapture': {
[TargetType.WEB]: 'MediaDevicesCamera',
},
// ... more mappings
};
const alt = alternatives[nodeType]?.[target];
if (alt) {
const altDef = getNodeDefinition(alt);
return {
nodeType: alt,
displayName: altDef.displayName,
reason: `Equivalent functionality for ${TARGETS[target].displayName}`,
};
}
return null;
}
apply(
component: Component,
analysis: TargetAdaptationAnalysis
): Component {
const adapted = cloneComponent(component);
analysis.adaptations.forEach(adaptation => {
switch (adaptation.adaptationType) {
case 'replace':
this.replaceNode(adapted, adaptation.nodeId, adaptation.replacement!);
break;
case 'remove':
this.removeNode(adapted, adaptation.nodeId);
break;
case 'configure':
this.configureNode(adapted, adaptation.nodeId, adaptation.configChanges!);
break;
}
});
return adapted;
}
}
Adaptation UI
┌──────────────────────────────────────────────────────────────┐
│ Duplicate "FileManager" for Mobile [×] │
├──────────────────────────────────────────────────────────────┤
│ │
│ Analysis: 3 nodes need adaptation │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 🔄 REPLACEMENTS (2) │ │
│ │ │ │
│ │ ☑ "Read File" → "Capacitor Filesystem Read" │ │
│ │ Equivalent functionality for Mobile │ │
│ │ │ │
│ │ ☑ "Native File Picker" → "Capacitor File Picker" │ │
│ │ Equivalent functionality for Mobile │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 🗑️ REMOVALS (1) │ │
│ │ │ │
│ │ ☑ "Watch Directory" - No Mobile equivalent │ │
│ │ ⚠️ Will disconnect: onChange → UpdateList │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ New component name: [FileManager_Mobile________] │
│ │
│ [Cancel] [Create Copy] │
└──────────────────────────────────────────────────────────────┘
Target Selection UI
Project Creation Flow
┌──────────────────────────────────────────────────────────────┐
│ Create New Project [×] │
├──────────────────────────────────────────────────────────────┤
│ │
│ Project Name: [My App_________________________] │
│ │
│ ── Primary Target ───────────────────────────────────── │
│ What are you building? │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 🌐 │ │ 📱 │ │ 🖥️ │ │
│ │ Web │ │ Mobile │ │ Desktop │ │
│ │ │ │ (iOS/And) │ │ (Win/Mac) │ │
│ │ [Selected] │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ 🧩 │ │
│ │ Extension │ │
│ │ (Chrome) │ │
│ └─────────────┘ │
│ │
│ ℹ️ You can enable additional targets later in Settings │
│ │
│ [Cancel] [Create] │
└──────────────────────────────────────────────────────────────┘
Project Settings - Targets Panel
┌──────────────────────────────────────────────────────────────┐
│ Project Settings │
├──────────────────────────────────────────────────────────────┤
│ │
│ [General] [Targets] [Backends] [Version Control] │
│ ──────── │
│ │
│ ── Enabled Targets ──────────────────────────────────── │
│ │
│ ☑ 🌐 Web (Primary) [Configure] │
│ Standard web deployment │
│ │
│ ☑ 📱 Mobile [Configure] │
│ iOS and Android via Capacitor │
│ App ID: com.mycompany.myapp │
│ │
│ ☐ 🖥️ Desktop [Enable] │
│ Windows, macOS, Linux via Electron │
│ │
│ ☐ 🧩 Browser Extension [Enable] │
│ Chrome Extension │
│ │
│ ── Primary Target ───────────────────────────────────── │
│ [Web ▾] │
│ The primary target determines the default node palette │
│ │
│ [Save] │
└──────────────────────────────────────────────────────────────┘
Target Configuration Modals
Clicking "Configure" opens target-specific settings:
Capacitor Configuration:
┌──────────────────────────────────────────────────────────────┐
│ Mobile Target Settings [×] │
├──────────────────────────────────────────────────────────────┤
│ │
│ ── App Identity ─────────────────────────────────────── │
│ App ID: [com.mycompany.myapp_______] │
│ App Name: [My App__________________] │
│ Version: [1.0.0____] │
│ │
│ ── iOS Settings ─────────────────────────────────────── │
│ Team ID: [ABCD1234__] │
│ Deployment Target: [14.0 ▾] │
│ │
│ ── Android Settings ─────────────────────────────────── │
│ Min SDK Version: [24 ▾] │
│ │
│ ── Detected Permissions ─────────────────────────────── │
│ Based on nodes used in your project: │
│ ☑ Camera (Camera Capture node) │
│ ☑ Location (Geolocation node) │
│ ☑ Push Notifications (Push node) │
│ │
│ [Cancel] [Save] │
└──────────────────────────────────────────────────────────────┘
Runtime Injection
Conditional Runtime Loading
Different targets require different runtime packages:
class RuntimeInjector {
private target: TargetType;
constructor(target: TargetType) {
this.target = target;
}
getRuntime(): RuntimeBundle {
switch (this.target) {
case TargetType.WEB:
return {
core: 'noodl-runtime',
nodes: 'noodl-nodes-web',
};
case TargetType.CAPACITOR:
return {
core: 'noodl-runtime',
nodes: 'noodl-nodes-capacitor',
bridge: '@capacitor/core',
plugins: this.getCapacitorPlugins(),
};
case TargetType.ELECTRON:
return {
core: 'noodl-runtime',
nodes: 'noodl-nodes-electron',
preload: 'electron-preload',
};
case TargetType.EXTENSION:
return {
core: 'noodl-runtime',
nodes: 'noodl-nodes-extension',
};
}
}
private getCapacitorPlugins(): string[] {
// Based on nodes used in project
const usedNodes = ProjectModel.getInstance().getUsedNodeTypes();
const plugins: string[] = [];
if (usedNodes.includes('CameraCapture')) {
plugins.push('@capacitor/camera');
}
if (usedNodes.includes('Geolocation')) {
plugins.push('@capacitor/geolocation');
}
if (usedNodes.includes('PushNotifications')) {
plugins.push('@capacitor/push-notifications');
}
// ... more mappings
return plugins;
}
inject(bundle: RuntimeBundle): string {
// Generate import statements and initialization code
let code = `
import { Noodl } from '${bundle.core}';
import * as nodes from '${bundle.nodes}';
`;
if (bundle.bridge) {
code += `import { Capacitor } from '${bundle.bridge}';\n`;
}
if (bundle.plugins) {
bundle.plugins.forEach(plugin => {
const name = this.getPluginName(plugin);
code += `import { ${name} } from '${plugin}';\n`;
});
}
code += `
Noodl.registerNodes(nodes);
${this.target !== TargetType.WEB ? `Noodl.setTarget('${this.target}');` : ''}
`;
return code;
}
}
Preview Mode Runtime Selection
class PreviewService {
private target: TargetType;
setPreviewTarget(target: TargetType): void {
this.target = target;
this.reloadPreview();
}
getPreviewFrame(): string {
switch (this.target) {
case TargetType.WEB:
return 'standard-preview-frame';
case TargetType.CAPACITOR:
// Mobile dimensions, inject Capacitor bridge
return 'capacitor-preview-frame';
case TargetType.ELECTRON:
// Enable IPC bridge to main process
return 'electron-preview-frame';
case TargetType.EXTENSION:
// Popup dimensions, inject mock chrome API
return 'extension-preview-frame';
}
}
}
Preview Toolbar Target Selector
┌──────────────────────────────────────────────────────────────┐
│ Preview [Web ▾] [Device ▾] [↻] │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ││
│ │ Preview Content ││
│ │ ││
│ └─────────────────────────────────────────────────────────┘│
│ │
└──────────────────────────────────────────────────────────────┘
Target Dropdown:
┌───────────────┐
│ 🌐 Web │ ← Current
├───────────────┤
│ 📱 Mobile │
│ 🖥️ Desktop │
│ 🧩 Extension │
└───────────────┘
Implementation Phases
Phase 1: Target Configuration Model (3-4 days)
Goal: Implement project-level target configuration
Tasks:
- Define TypeScript interfaces for all target configs
- Add targets field to project.json schema
- Create TargetConfigService for CRUD operations
- Migrate existing projects (add default web target)
Files to Create:
packages/noodl-editor/src/editor/src/models/
├── TargetConfig.ts
├── TargetType.ts
└── targets/
├── WebTargetConfig.ts
├── CapacitorTargetConfig.ts
├── ElectronTargetConfig.ts
└── ExtensionTargetConfig.ts
packages/noodl-editor/src/editor/src/services/
└── TargetConfigService.ts
Verification:
- Target config saved/loaded correctly
- Default web target for new projects
- Migration works for existing projects
Phase 2: Node Compatibility System (4-5 days)
Goal: Implement node compatibility declaration and registry
Tasks:
- Add targetCompatibility to NodeDefinition
- Create NodeCompatibilityRegistry
- Add compatibility to all existing nodes (mark as 'all' for universal)
- Create compatibility badge component
Files to Create:
packages/noodl-editor/src/editor/src/models/
└── NodeCompatibility.ts
packages/noodl-editor/src/editor/src/services/
└── NodeCompatibilityRegistry.ts
packages/noodl-editor/src/editor/src/components/
└── CompatibilityBadge/
├── CompatibilityBadge.tsx
└── CompatibilityBadge.module.css
Verification:
- All nodes have compatibility declared
- Registry correctly filters nodes by target
- Badges render correctly in palette
Phase 3: Build-Time Validation (3-4 days)
Goal: Prevent deploying incompatible nodes
Tasks:
- Create TargetValidationService
- Integrate validation into export flow
- Create validation error UI
- Add "Go to Node" action for errors
Files to Create:
packages/noodl-editor/src/editor/src/services/
└── TargetValidationService.ts
packages/noodl-editor/src/editor/src/views/
└── ValidationErrorsPanel/
├── ValidationErrorsPanel.tsx
└── ValidationErrorsPanel.module.css
Verification:
- Incompatible nodes detected correctly
- Export blocked with clear error message
- "Go to Node" navigates to problem node
Phase 4: Target Selection UI (3-4 days)
Goal: User-facing target management
Tasks:
- Update project creation dialog with target selection
- Create Targets panel in Project Settings
- Create target configuration modals
- Add preview target selector
Files to Create:
packages/noodl-editor/src/editor/src/views/
├── CreateProjectDialog/
│ └── TargetSelector.tsx
├── ProjectSettings/
│ └── TargetsPanel/
│ ├── TargetsPanel.tsx
│ └── TargetConfigModal.tsx
└── PreviewToolbar/
└── TargetSelector.tsx
Verification:
- New projects prompt for target
- Targets panel shows enabled targets
- Configuration modals save correctly
- Preview switches targets
Phase 5: Duplicate as Target (2-3 days)
Goal: Intelligent component adaptation
Tasks:
- Create TargetAdaptationService
- Define node alternative mappings
- Create adaptation preview UI
- Implement apply logic
Files to Create:
packages/noodl-editor/src/editor/src/services/
└── TargetAdaptationService.ts
packages/noodl-editor/src/editor/src/views/
└── DuplicateAsTargetDialog/
├── DuplicateAsTargetDialog.tsx
└── AdaptationPreview.tsx
Verification:
- Analysis correctly identifies incompatibilities
- Alternatives suggested where available
- Apply creates working adapted component
Dependencies
External Packages
None for core target system - it's purely editor infrastructure.
Internal Dependencies
- ProjectModel (for project configuration)
- NodeLibrary (for node definitions)
- Compilation service (for validation integration)
- Export services (for build-time checks)
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Missing node compatibility | High | Medium | Default to 'all', refine over time |
| Complex migration | Medium | High | Thorough testing on real projects |
| UI complexity | Medium | Medium | Start with minimal UI, iterate |
| Alternative mappings incomplete | High | Low | Document as "best effort" |
Success Metrics
Phase Complete:
- All nodes have compatibility declared
- Target selection UI functional
- Build-time validation prevents bad deploys
- Duplicate as Target works for common cases
Integration Ready:
- Phases B, C, D can build on this foundation
- Preview mode switches targets correctly
- Export respects target configuration