# Phase D: Chrome Extension Target ## Overview Chrome extensions are web applications with special capabilities - they can inject content into other websites, run persistent background scripts, and access browser APIs. Since Noodl already exports HTML/CSS/JS, the core export is straightforward. The complexity lies in handling the unique execution contexts and APIs. **Timeline:** 2-3 weeks **Priority:** Lowest (niche use case, but quick win) **Prerequisites:** Phase E (Target System Core) ## Value Proposition - Build browser extensions without learning the extension API - Visual node-based approach to content injection - Hot-reload development workflow - Package for Chrome Web Store distribution ## Chrome Extension Architecture ### Extension Components ``` my-extension/ ├── manifest.json # Extension configuration ├── popup/ # Toolbar popup (standard web page) │ ├── popup.html │ ├── popup.js │ └── popup.css ├── background/ # Service worker (persistent logic) │ └── service-worker.js ├── content/ # Scripts injected into web pages │ └── content-script.js ├── options/ # Settings page │ ├── options.html │ └── options.js └── assets/ └── icons/ ``` ### Manifest V3 (Required for Chrome) ```json { "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "description": "Built with Noodl", "action": { "default_popup": "popup/popup.html", "default_icon": { "16": "assets/icons/icon16.png", "48": "assets/icons/icon48.png", "128": "assets/icons/icon128.png" } }, "background": { "service_worker": "background/service-worker.js" }, "content_scripts": [{ "matches": [""], "js": ["content/content-script.js"], "css": ["content/content-styles.css"] }], "permissions": [ "storage", "tabs", "activeTab" ], "host_permissions": [ "https://*.example.com/*" ] } ``` ## Execution Contexts Chrome extensions have three distinct execution contexts, each with different capabilities: ### 1. Popup Context (Standard Noodl) The popup is a standard HTML page that appears when clicking the extension icon. This is the most familiar context - essentially a small web app. **Characteristics:** - Standard DOM access - Runs when popup is open, terminates when closed - No persistent state (use chrome.storage) - Can communicate with background and content scripts **Noodl Mapping:** Default Noodl web export works here directly. ### 2. Background Context (Service Worker) A service worker that runs persistently (with limitations in MV3). Handles events, manages state, coordinates between contexts. **Characteristics:** - No DOM access - Event-driven (wakes on events, sleeps when idle) - Has access to most chrome.* APIs - Can communicate with all other contexts **Noodl Mapping:** Requires special "Background" components that compile differently - no visual nodes, only logic nodes. ### 3. Content Script Context (Injected) Scripts injected into web pages. Can read/modify the page DOM but run in an isolated environment. **Characteristics:** - Full DOM access to the host page - Isolated JavaScript context (can't access page's JS) - Limited chrome.* API access - Must communicate with background for most operations **Noodl Mapping:** Special "Content Script" components that inject into pages. ## Node Definitions ### Storage Nodes #### Storage Get Node Access extension storage (synced across devices or local only). ```typescript interface StorageGetNode { category: 'Chrome Extension'; displayName: 'Storage Get'; docs: 'Retrieves a value from extension storage'; inputs: { get: Signal; // Trigger retrieval key: string; // Storage key storageArea: 'sync' | 'local'; // sync = across devices, local = this browser defaultValue: any; // Value if key doesn't exist }; outputs: { value: any; // Retrieved value found: boolean; // Whether key existed fetched: Signal; // Fires when retrieval completes failed: Signal; // Fires on error error: string; // Error message }; } ``` **Implementation:** ```typescript chrome.storage[storageArea].get([key], (result) => { if (chrome.runtime.lastError) { outputs.error = chrome.runtime.lastError.message; outputs.failed(); } else { outputs.value = result[key] ?? defaultValue; outputs.found = key in result; outputs.fetched(); } }); ``` #### Storage Set Node ```typescript interface StorageSetNode { category: 'Chrome Extension'; displayName: 'Storage Set'; inputs: { set: Signal; key: string; value: any; storageArea: 'sync' | 'local'; }; outputs: { saved: Signal; failed: Signal; error: string; }; } ``` #### Storage Watch Node ```typescript interface StorageWatchNode { category: 'Chrome Extension'; displayName: 'Storage Watch'; docs: 'Watches for changes to storage values'; inputs: { key: string; // Key to watch (empty = all keys) storageArea: 'sync' | 'local' | 'both'; }; outputs: { changedKey: string; // Which key changed oldValue: any; // Previous value newValue: any; // New value changed: Signal; // Fires on any change }; } ``` ### Tab Nodes #### Get Active Tab Node ```typescript interface GetActiveTabNode { category: 'Chrome Extension'; displayName: 'Get Active Tab'; inputs: { get: Signal; }; outputs: { tabId: number; windowId: number; url: string; title: string; favIconUrl: string; incognito: boolean; pinned: boolean; fetched: Signal; failed: Signal; }; } ``` **Implementation:** ```typescript chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (tabs[0]) { const tab = tabs[0]; outputs.tabId = tab.id; outputs.url = tab.url; outputs.title = tab.title; // ... other properties outputs.fetched(); } }); ``` #### Query Tabs Node ```typescript interface QueryTabsNode { category: 'Chrome Extension'; displayName: 'Query Tabs'; inputs: { query: Signal; url: string; // URL pattern (supports wildcards) title: string; // Title pattern currentWindow: boolean; active: boolean; pinned: boolean; audible: boolean; }; outputs: { tabs: Tab[]; // Matching tabs count: number; // Number of matches fetched: Signal; failed: Signal; }; } ``` #### Create Tab Node ```typescript interface CreateTabNode { category: 'Chrome Extension'; displayName: 'Create Tab'; inputs: { create: Signal; url: string; active: boolean; // Whether to focus the tab pinned: boolean; index: number; // Position in tab bar (-1 = end) windowId: number; // Target window (-1 = current) }; outputs: { tabId: number; created: Signal; failed: Signal; }; } ``` #### Update Tab Node ```typescript interface UpdateTabNode { category: 'Chrome Extension'; displayName: 'Update Tab'; inputs: { update: Signal; tabId: number; // Target tab (-1 = active tab) url: string; active: boolean; pinned: boolean; muted: boolean; }; outputs: { updated: Signal; failed: Signal; }; } ``` #### Close Tab Node ```typescript interface CloseTabNode { category: 'Chrome Extension'; displayName: 'Close Tab'; inputs: { close: Signal; tabId: number; // Tab to close (-1 = active tab) }; outputs: { closed: Signal; failed: Signal; }; } ``` #### Tab Events Node ```typescript interface TabEventsNode { category: 'Chrome Extension'; displayName: 'Tab Events'; docs: 'Listen for tab lifecycle events'; inputs: { // Configuration only, always listening }; outputs: { // Created createdTabId: number; onCreated: Signal; // Updated updatedTabId: number; updatedUrl: string; updatedTitle: string; updatedStatus: 'loading' | 'complete'; onUpdated: Signal; // Activated activatedTabId: number; onActivated: Signal; // Removed removedTabId: number; onRemoved: Signal; }; } ``` ### Messaging Nodes #### Send Message Node Cross-context communication within the extension. ```typescript interface SendMessageNode { category: 'Chrome Extension'; displayName: 'Send Message'; docs: 'Send message to other parts of the extension'; inputs: { send: Signal; target: 'background' | 'popup' | 'content' | 'tab'; tabId: number; // Required if target is 'tab' or 'content' action: string; // Message type identifier data: object; // Message payload }; outputs: { response: any; // Response from recipient sent: Signal; responded: Signal; failed: Signal; error: string; }; } ``` **Implementation:** ```typescript // To background or popup if (target === 'background' || target === 'popup') { chrome.runtime.sendMessage({ action, data }, (response) => { outputs.response = response; outputs.responded(); }); } // To content script in specific tab if (target === 'content' || target === 'tab') { chrome.tabs.sendMessage(tabId, { action, data }, (response) => { outputs.response = response; outputs.responded(); }); } ``` #### Receive Message Node ```typescript interface ReceiveMessageNode { category: 'Chrome Extension'; displayName: 'Receive Message'; inputs: { actions: string[]; // Filter by action type (empty = all) }; outputs: { action: string; // Received action type data: any; // Message payload senderId: number; // Sender tab ID (if from content script) senderUrl: string; // Sender URL received: Signal; // Response mechanism respond: Signal; // Trigger to send response responseData: any; // Data to send back }; } ``` ### Content Script Nodes #### Inject Script Node Programmatically inject scripts into pages. ```typescript interface InjectScriptNode { category: 'Chrome Extension'; displayName: 'Inject Script'; contextWarning: 'Background/Popup only'; inputs: { inject: Signal; tabId: number; // Target tab (-1 = active) scriptId: string; // Reference to content script component allFrames: boolean; // Inject into all frames }; outputs: { injected: Signal; failed: Signal; error: string; }; } ``` #### Inject CSS Node ```typescript interface InjectCSSNode { category: 'Chrome Extension'; displayName: 'Inject CSS'; contextWarning: 'Background/Popup only'; inputs: { inject: Signal; tabId: number; css: string; // CSS code to inject allFrames: boolean; }; outputs: { injected: Signal; failed: Signal; }; } ``` #### Page DOM Access Node For content scripts - interact with host page DOM. ```typescript interface PageDOMNode { category: 'Chrome Extension'; displayName: 'Page DOM'; contextWarning: 'Content Script only'; inputs: { query: Signal; selector: string; // CSS selector action: 'get' | 'set' | 'click' | 'getAttribute' | 'setAttribute'; property: string; // For get/set (innerHTML, textContent, value, etc.) value: string; // For set/setAttribute attributeName: string; // For getAttribute/setAttribute }; outputs: { element: HTMLElement; // Found element (first match) elements: HTMLElement[]; // All matches count: number; // Number of matches value: string; // Retrieved value success: Signal; notFound: Signal; failed: Signal; }; } ``` ### Context Menu Node ```typescript interface ContextMenuNode { category: 'Chrome Extension'; displayName: 'Context Menu'; contextWarning: 'Background only'; inputs: { create: Signal; id: string; title: string; contexts: ('page' | 'selection' | 'link' | 'image' | 'all')[]; parentId: string; // For submenus enabled: boolean; visible: boolean; }; outputs: { menuInfo: { menuItemId: string; selectionText: string; linkUrl: string; srcUrl: string; pageUrl: string; }; clicked: Signal; created: Signal; failed: Signal; }; } ``` ### Badge Node Control the extension icon badge. ```typescript interface BadgeNode { category: 'Chrome Extension'; displayName: 'Badge'; inputs: { update: Signal; text: string; // Badge text (4 chars max) backgroundColor: string; // Hex color textColor: string; // Hex color tabId: number; // Per-tab badge (-1 = global) }; outputs: { updated: Signal; failed: Signal; }; } ``` ### Notification Node ```typescript interface ExtensionNotificationNode { category: 'Chrome Extension'; displayName: 'Notification'; inputs: { show: Signal; clear: Signal; id: string; // For updating/clearing type: 'basic' | 'image' | 'list' | 'progress'; title: string; message: string; iconUrl: string; imageUrl: string; // For 'image' type items: { title: string; message: string }[]; // For 'list' type progress: number; // For 'progress' type (0-100) buttons: { title: string }[]; // Max 2 buttons requireInteraction: boolean; }; outputs: { notificationId: string; shown: Signal; clicked: Signal; buttonIndex: number; // Which button was clicked buttonClicked: Signal; closed: Signal; failed: Signal; }; } ``` ### Alarms Node Schedule recurring or one-time events. ```typescript interface AlarmNode { category: 'Chrome Extension'; displayName: 'Alarm'; contextWarning: 'Background only'; inputs: { create: Signal; clear: Signal; name: string; delayMinutes: number; // When to first fire periodMinutes: number; // Repeat interval (0 = one-time) }; outputs: { alarmName: string; created: Signal; fired: Signal; cleared: Signal; failed: Signal; }; } ``` ### Permissions Node Request additional permissions at runtime. ```typescript interface PermissionsNode { category: 'Chrome Extension'; displayName: 'Request Permissions'; inputs: { request: Signal; check: Signal; permissions: string[]; // e.g., ['bookmarks', 'history'] origins: string[]; // e.g., ['https://example.com/*'] }; outputs: { granted: boolean; requestGranted: Signal; requestDenied: Signal; checked: Signal; }; } ``` ## Preview Mode ### Extension Preview Challenges Unlike web apps, extensions can't simply run in a browser tab. They need to be loaded as unpacked extensions. ### Preview Approach 1. **Popup Preview (Default)** - Simulate popup dimensions in Noodl preview (400px × 600px max) - Mock chrome.* APIs where possible - Show "extension-only" features with warning badges 2. **Live Extension Preview** - "Load as Extension" button exports to temp folder - Opens `chrome://extensions` with instructions - Developer enables "Developer mode" and loads unpacked - Changes in Noodl trigger reload (via extension reload API) ### Mock Chrome APIs For popup preview, we mock chrome.* APIs: ```typescript const mockChrome = { storage: { local: createMockStorage('local'), sync: createMockStorage('sync'), }, tabs: { query: async () => [{ id: 1, url: 'https://example.com', title: 'Mock Tab' }], create: async () => ({ id: 2 }), update: async () => ({}), }, runtime: { sendMessage: async (msg) => { console.log('[Mock] Message sent:', msg); return { mocked: true }; }, onMessage: { addListener: (cb) => { console.log('[Mock] Message listener added'); } } } }; function createMockStorage(area: string) { const storage = new Map(); return { get: (keys, cb) => { const result = {}; (Array.isArray(keys) ? keys : [keys]).forEach(k => { result[k] = storage.get(k); }); cb(result); }, set: (items, cb) => { Object.entries(items).forEach(([k, v]) => storage.set(k, v)); cb?.(); } }; } // Inject mock in preview mode if (isNoodlPreview) { window.chrome = mockChrome; } ``` ### Extension-Specific Preview UI ``` ┌─────────────────────────────────────────────────────────────┐ │ Preview [Popup ▾] [📦] │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────┐ │ │ │ │ │ │ │ Popup Preview │ ⚠️ Extension APIs │ │ │ (400 × 600) │ are mocked │ │ │ │ │ │ │ │ [Load as Extension] │ │ │ │ │ │ └─────────────────────────────────┘ │ │ │ │ Context: Popup | Content Script | Background │ └─────────────────────────────────────────────────────────────┘ ``` ## Export Pipeline ### Export Dialog ``` ┌─────────────────────────────────────────────────────────────┐ │ Export Chrome Extension [×] │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Extension Name: [My Extension________________] │ │ Version: [1.0.0____] │ │ Description: [Built with Noodl______________] │ │ │ │ ── Icons ────────────────────────────────────────────── │ │ 16×16: [icon16.png] [Browse] │ │ 48×48: [icon48.png] [Browse] │ │ 128×128:[icon128.png][Browse] │ │ │ │ ── Permissions ──────────────────────────────────────── │ │ ☑ Storage (used by Storage nodes) │ │ ☑ Tabs (used by Tab nodes) │ │ ☐ Bookmarks │ │ ☐ History │ │ ☐ Downloads │ │ │ │ ── Host Permissions ─────────────────────────────────── │ │ Pattern: [https://example.com/*_________] [+ Add] │ │ • https://example.com/* [×] │ │ │ │ ── Content Scripts ──────────────────────────────────── │ │ Component: [ContentOverlay ▾] │ │ URL Match: [______________] │ │ Run At: [Document End ▾] │ │ │ │ │ │ [Export to Folder] [Package .zip] │ └─────────────────────────────────────────────────────────────┘ ``` ### Generated Structure ``` my-extension/ ├── manifest.json ├── popup/ │ ├── popup.html │ ├── popup.js # Bundled Noodl runtime │ └── popup.css ├── background/ │ └── service-worker.js # From Background components ├── content/ │ ├── content-script.js # From Content Script components │ └── content-styles.css ├── assets/ │ └── icons/ │ ├── icon16.png │ ├── icon48.png │ └── icon128.png └── _locales/ # If internationalization used └── en/ └── messages.json ``` ### Manifest Generation ```typescript function generateManifest(config: ExtensionConfig): ManifestV3 { const manifest: ManifestV3 = { manifest_version: 3, name: config.name, version: config.version, description: config.description, action: { default_popup: 'popup/popup.html', default_icon: config.icons, }, permissions: detectPermissions(config.usedNodes), host_permissions: config.hostPermissions, }; // Add background if Background components exist if (config.hasBackgroundComponents) { manifest.background = { service_worker: 'background/service-worker.js', }; } // Add content scripts if configured if (config.contentScripts.length > 0) { manifest.content_scripts = config.contentScripts.map(cs => ({ matches: cs.matches, js: [`content/${cs.componentId}.js`], css: cs.hasStyles ? [`content/${cs.componentId}.css`] : undefined, run_at: cs.runAt, })); } return manifest; } function detectPermissions(usedNodes: string[]): string[] { const permissions = new Set(); const nodePermissionMap = { 'StorageGet': 'storage', 'StorageSet': 'storage', 'StorageWatch': 'storage', 'GetActiveTab': 'tabs', 'QueryTabs': 'tabs', 'CreateTab': 'tabs', 'TabEvents': 'tabs', 'Alarm': 'alarms', 'ContextMenu': 'contextMenus', 'Notification': 'notifications', 'Bookmarks': 'bookmarks', 'History': 'history', }; usedNodes.forEach(node => { if (nodePermissionMap[node]) { permissions.add(nodePermissionMap[node]); } }); return Array.from(permissions); } ``` ### Component Type Handling Different Noodl components compile to different extension contexts: ```typescript interface ExtensionComponent { id: string; name: string; context: 'popup' | 'background' | 'content'; // Popup: Default, runs in popup HTML // Background: Compiles to service worker // Content: Compiles to content script } function compileExtension(project: Project): ExtensionBundle { const components = project.getComponents(); const popup = components.filter(c => c.extensionContext === 'popup'); const background = components.filter(c => c.extensionContext === 'background'); const content = components.filter(c => c.extensionContext === 'content'); return { popup: { html: generatePopupHTML(popup), js: bundleComponents(popup, { includeDOM: true }), css: extractStyles(popup), }, background: { js: bundleComponents(background, { includeDOM: false, wrapAsServiceWorker: true }), }, content: content.map(c => ({ id: c.id, js: bundleComponents([c], { includeDOM: true, isolatedExecution: true }), css: extractStyles([c]), })), }; } ``` ## Security Considerations ### Permission Minimization - Auto-detect permissions from used nodes - Warn users about broad permissions (e.g., ``) - Explain each permission in UI ### Host Permission Patterns ```typescript const hostPermissionWarnings = { '': 'Allows access to ALL websites. Consider using specific patterns.', '*://*/*': 'Same as - very broad access.', 'http://*/*': 'Warning: HTTP sites are insecure.', }; function validateHostPermission(pattern: string): ValidationResult { if (pattern in hostPermissionWarnings) { return { valid: true, warning: hostPermissionWarnings[pattern] }; } // Validate pattern format const validPattern = /^(https?|ftp|\*):\/\/(\*|\*?\.[^\/\*]+|\[^\/\*]+)\/(.*)?$/; if (!validPattern.test(pattern)) { return { valid: false, error: 'Invalid URL pattern format' }; } return { valid: true }; } ``` ### Content Script Isolation Content scripts run in an isolated world - they can't access the page's JavaScript but can access the DOM. This is a security feature. ```typescript // Content scripts CAN: document.querySelector('#element').textContent = 'Modified'; // Content scripts CANNOT: window.pageGlobalVariable; // undefined myPageFunction(); // undefined ``` ### Chrome Web Store Requirements For distribution, extensions must comply with: - Single purpose policy - Privacy policy requirement - Minimal permissions - Clear description of functionality Generate compliance checklist in export: ``` □ Extension has a single, clear purpose □ All permissions are necessary and explained □ Privacy policy URL provided (if using user data) □ Description accurately reflects functionality □ Icons are appropriate and not misleading ``` ## Implementation Phases ### Phase 1: Extension Export Foundation (3-4 days) **Goal:** Generate valid extension structure from popup components **Tasks:** 1. Create manifest.json generator 2. Bundle popup components to extension format 3. Export to folder functionality 4. Chrome extension loading instructions UI **Files to Create:** ``` packages/noodl-editor/src/editor/src/services/ └── export/ └── ExtensionExporter.ts packages/noodl-editor/src/editor/src/views/ └── ExportExtensionDialog/ ├── ExportExtensionDialog.tsx └── ExportExtensionDialog.module.css ``` **Verification:** - [ ] Exported extension loads in Chrome - [ ] Popup displays correctly - [ ] Manifest contains correct metadata ### Phase 2: Extension-Specific Nodes (1 week) **Goal:** Implement core chrome.* API nodes **Tasks:** 1. Storage nodes (Get, Set, Watch) 2. Tab nodes (Query, Create, Update, Close, Events) 3. Messaging nodes (Send, Receive) 4. Badge node 5. Context Menu node 6. Notification node **Files to Create:** ``` packages/noodl-runtime/src/nodes/ └── chrome-extension/ ├── storage-get.ts ├── storage-set.ts ├── storage-watch.ts ├── tabs-query.ts ├── tabs-create.ts ├── tabs-events.ts ├── messaging-send.ts ├── messaging-receive.ts ├── badge.ts ├── context-menu.ts └── notification.ts ``` **Verification:** - [ ] Storage persists across popup opens - [ ] Tab operations work correctly - [ ] Messaging between contexts works - [ ] All nodes have proper error handling ### Phase 3: Preview Mode (3-4 days) **Goal:** Mock chrome.* APIs for in-editor preview **Tasks:** 1. Create mock chrome API implementation 2. Popup dimension simulation 3. Extension context indicator 4. "Load as Extension" quick action **Files to Create:** ``` packages/noodl-runtime/src/ └── mocks/ └── chrome-api-mock.ts packages/noodl-editor/src/editor/src/views/viewer/ └── ExtensionPreviewFrame.tsx ``` **Verification:** - [ ] Preview shows popup at correct dimensions - [ ] Mock storage persists during session - [ ] Clear indicators for mocked functionality ### Phase 4: Background & Content Scripts (4-5 days) **Goal:** Support all extension contexts **Tasks:** 1. Component context selector UI 2. Background component compilation (no DOM) 3. Content script compilation 4. Cross-context communication testing 5. Content script injection nodes **Verification:** - [ ] Background service worker runs correctly - [ ] Content scripts inject into pages - [ ] Messaging works across all contexts - [ ] DOM nodes disabled in background components ## Dependencies ### From Target System Core (Phase E) - Target compatibility system (nodes marked as extension-only) - Build-time validation (prevent incompatible nodes) - Context selector UI pattern ### External Dependencies None required - Chrome extension APIs are built into the browser. ## Risk Assessment | Risk | Likelihood | Impact | Mitigation | |------|------------|--------|------------| | Manifest V3 complexity | Medium | Medium | Start with popup-only, add contexts incrementally | | Service worker limitations | High | Low | Document MV3 restrictions clearly | | Chrome Web Store review | Low | High | Generate compliance checklist | | Cross-context messaging bugs | Medium | Medium | Comprehensive testing suite | | Preview fidelity | High | Low | Clear "mocked" indicators | ## Success Metrics **MVP Success:** - Popup-only extensions export and load correctly - Storage and Tab nodes work - Users can submit to Chrome Web Store **Full Success:** - All three contexts supported - Full node library (15+ nodes) - Live reload development workflow - One-click packaging for store submission ## Related Documentation - [Chrome Extension Manifest V3 Migration](https://developer.chrome.com/docs/extensions/mv3/intro/) - [Chrome Extension API Reference](https://developer.chrome.com/docs/extensions/reference/) - [Content Scripts Documentation](https://developer.chrome.com/docs/extensions/mv3/content_scripts/)