Front-end Engineering Lab
PatternsArchitecture Patterns

Plugin Architecture

Build extensible systems that users can customize with plugins

Plugin Architecture

A plugin architecture allows users to extend your application without modifying the core. Used by VS Code, Figma, WordPress, and Stripe.

🎯 The Concept

Core Application (Your code)

Plugin System (Hook points)

Plugins (User code)

Core stays stable, plugins add features

📊 Benefits

BenefitDescription
ExtensibilityUsers add features without core changes
ModularityFeatures are isolated and optional
MaintainabilityCore remains simple and stable
CommunityUsers build and share plugins
CustomizationEach user gets what they need

🔧 Basic Plugin System

Simple Plugin Interface

interface Plugin {
  name: string;
  version: string;
  init(): void;
  destroy?(): void;
}

class PluginManager {
  private plugins: Map<string, Plugin> = new Map();
  
  register(plugin: Plugin): void {
    if (this.plugins.has(plugin.name)) {
      throw new Error(`Plugin "${plugin.name}" already registered`);
    }
    
    this.plugins.set(plugin.name, plugin);
    plugin.init();
    
    console.log(`Plugin "${plugin.name}" v${plugin.version} loaded`);
  }
  
  unregister(name: string): void {
    const plugin = this.plugins.get(name);
    
    if (plugin) {
      plugin.destroy?.();
      this.plugins.delete(name);
      console.log(`Plugin "${name}" unloaded`);
    }
  }
  
  getPlugin(name: string): Plugin | undefined {
    return this.plugins.get(name);
  }
  
  getAllPlugins(): Plugin[] {
    return Array.from(this.plugins.values());
  }
}

// Usage
const manager = new PluginManager();

const themePlugin: Plugin = {
  name: 'dark-theme',
  version: '1.0.0',
  init() {
    document.body.classList.add('dark-theme');
  },
  destroy() {
    document.body.classList.remove('dark-theme');
  }
};

manager.register(themePlugin);

🪝 Hook System (VS Code Style)

Event-Based Hooks

type HookCallback = (...args: unknown[]) => void;

class HookSystem {
  private hooks = new Map<string, HookCallback[]>();
  
  // Register hook
  registerHook(name: string): void {
    if (!this.hooks.has(name)) {
      this.hooks.set(name, []);
    }
  }
  
  // Add listener to hook
  on(name: string, callback: HookCallback): () => void {
    if (!this.hooks.has(name)) {
      this.registerHook(name);
    }
    
    this.hooks.get(name)!.push(callback);
    
    // Return unsubscribe function
    return () => {
      const callbacks = this.hooks.get(name);
      if (callbacks) {
        const index = callbacks.indexOf(callback);
        if (index > -1) {
          callbacks.splice(index, 1);
        }
      }
    };
  }
  
  // Trigger hook
  emit(name: string, ...args: unknown[]): void {
    const callbacks = this.hooks.get(name);
    if (callbacks) {
      callbacks.forEach(callback => callback(...args));
    }
  }
  
  // Get all hooks
  getHooks(): string[] {
    return Array.from(this.hooks.keys());
  }
}

// Core application
class Application {
  private hooks = new HookSystem();
  
  constructor() {
    // Register available hooks
    this.hooks.registerHook('app:init');
    this.hooks.registerHook('app:ready');
    this.hooks.registerHook('file:open');
    this.hooks.registerHook('file:save');
  }
  
  init(): void {
    this.hooks.emit('app:init');
    // ... initialization logic
    this.hooks.emit('app:ready');
  }
  
  openFile(path: string): void {
    this.hooks.emit('file:open', path);
    // ... file opening logic
  }
  
  saveFile(path: string, content: string): void {
    this.hooks.emit('file:save', path, content);
    // ... file saving logic
  }
  
  // Expose hook system to plugins
  getHooks(): HookSystem {
    return this.hooks;
  }
}

// Plugin using hooks
class AutoSavePlugin implements Plugin {
  name = 'auto-save';
  version = '1.0.0';
  private unsubscribe?: () => void;
  
  init(app: Application): void {
    const hooks = app.getHooks();
    
    // Listen to file changes
    this.unsubscribe = hooks.on('file:save', (path, content) => {
      console.log(`Auto-save: Saving ${path}`);
      localStorage.setItem(path, content);
    });
  }
  
  destroy(): void {
    this.unsubscribe?.();
  }
}

🔌 Advanced: Middleware Pattern (Redux/Stripe Style)

type Middleware<T = unknown> = (
  context: T,
  next: () => Promise<void>
) => Promise<void>;

class MiddlewareChain<T = unknown> {
  private middlewares: Middleware<T>[] = [];
  
  use(middleware: Middleware<T>): void {
    this.middlewares.push(middleware);
  }
  
  async execute(context: T): Promise<void> {
    let index = 0;
    
    const next = async (): Promise<void> => {
      if (index >= this.middlewares.length) {
        return;
      }
      
      const middleware = this.middlewares[index++];
      await middleware(context, next);
    };
    
    await next();
  }
}

// Usage in payment system
interface PaymentContext {
  amount: number;
  currency: string;
  customerId: string;
  metadata?: Record<string, unknown>;
}

const paymentPipeline = new MiddlewareChain<PaymentContext>();

// Plugin 1: Logging
paymentPipeline.use(async (context, next) => {
  console.log(`Processing payment: $${context.amount}`);
  await next();
  console.log('Payment processed');
});

// Plugin 2: Fraud detection
paymentPipeline.use(async (context, next) => {
  if (context.amount > 10000) {
    console.log('Flagging for fraud review');
    context.metadata = { ...context.metadata, flagged: true };
  }
  await next();
});

// Plugin 3: Currency conversion
paymentPipeline.use(async (context, next) => {
  if (context.currency !== 'USD') {
    console.log(`Converting ${context.currency} to USD`);
    context.amount = convertToUSD(context.amount, context.currency);
    context.currency = 'USD';
  }
  await next();
});

// Execute pipeline
await paymentPipeline.execute({
  amount: 15000,
  currency: 'EUR',
  customerId: '123'
});

🎨 Plugin API Design

Well-Designed Plugin API

interface PluginAPI {
  // App info
  version: string;
  
  // UI extensions
  registerCommand(command: Command): void;
  registerMenuItem(item: MenuItem): void;
  registerPanel(panel: Panel): void;
  
  // Data access
  getState(): AppState;
  setState(state: Partial<AppState>): void;
  
  // Events
  on(event: string, callback: Function): () => void;
  emit(event: string, data?: unknown): void;
  
  // Storage
  storage: {
    get<T>(key: string): T | null;
    set<T>(key: string, value: T): void;
    remove(key: string): void;
  };
  
  // Notifications
  showNotification(message: string, type: 'info' | 'error' | 'success'): void;
}

// Plugin using the API
class TodoPlugin implements Plugin {
  name = 'todo-list';
  version = '1.0.0';
  
  init(api: PluginAPI): void {
    // Register command
    api.registerCommand({
      id: 'todo.add',
      name: 'Add Todo',
      execute: () => {
        const todo = prompt('Enter todo:');
        if (todo) {
          this.addTodo(api, todo);
        }
      }
    });
    
    // Register menu item
    api.registerMenuItem({
      id: 'todo-menu',
      label: 'Todos',
      location: 'sidebar',
      onClick: () => this.showTodoPanel(api)
    });
    
    // Listen to events
    api.on('app:ready', () => {
      this.loadTodos(api);
    });
  }
  
  private addTodo(api: PluginAPI, text: string): void {
    const todos = api.storage.get('todos') || [];
    todos.push({ id: Date.now(), text, done: false });
    api.storage.set('todos', todos);
    api.showNotification('Todo added', 'success');
    api.emit('todos:changed', todos);
  }
  
  private showTodoPanel(api: PluginAPI): void {
    const todos = api.storage.get('todos') || [];
    api.registerPanel({
      id: 'todos',
      title: 'Todos',
      content: this.renderTodos(todos)
    });
  }
  
  private renderTodos(todos: Array<{ id: string; text: string; done: boolean }>): string {
    return todos.map(todo => `
      <div>
        <input type="checkbox" ${todo.done ? 'checked' : ''} />
        <span>${todo.text}</span>
      </div>
    `).join('');
  }
  
  private loadTodos(api: PluginAPI): void {
    const todos = api.storage.get('todos') || [];
    console.log(`Loaded ${todos.length} todos`);
  }
}

🔐 Sandboxed Plugins (Security)

// Safe plugin execution
class SandboxedPluginManager {
  private iframe?: HTMLIFrameElement;
  
  async loadPlugin(url: string): Promise<void> {
    // Create isolated iframe
    this.iframe = document.createElement('iframe');
    this.iframe.sandbox.add('allow-scripts');
    this.iframe.style.display = 'none';
    document.body.appendChild(this.iframe);
    
    // Load plugin code
    const response = await fetch(url);
    const code = await response.text();
    
    // Execute in sandbox
    const script = this.iframe.contentDocument!.createElement('script');
    script.textContent = code;
    this.iframe.contentDocument!.body.appendChild(script);
  }
  
  // Message passing for communication
  sendMessage(message: unknown): void {
    this.iframe?.contentWindow?.postMessage(message, '*');
  }
  
  onMessage(callback: (message: unknown) => void): void {
    window.addEventListener('message', (event) => {
      if (event.source === this.iframe?.contentWindow) {
        callback(event.data);
      }
    });
  }
}

🏗️ Real-World Examples

Figma Plugin System

// Figma provides restricted API to plugins
interface FigmaAPI {
  // Can access and modify canvas
  currentPage: {
    selection: Node[];
    findAll(filter?: (node: Node) => boolean): Node[];
  };
  
  // Can create UI
  showUI(html: string, options?: UIOptions): void;
  
  // Sandboxed communication
  ui: {
    postMessage(message: unknown): void;
    onmessage: (message: unknown) => void;
  };
  
  // No network access (security)
  // No file system access
  // No external APIs
}

// Plugin code
figma.showUI(__html__);

figma.ui.onmessage = (msg) => {
  if (msg.type === 'create-rectangle') {
    const rect = figma.createRectangle();
    rect.x = msg.x;
    rect.y = msg.y;
    figma.currentPage.appendChild(rect);
  }
};

VS Code Extension API

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  // Register command
  const disposable = vscode.commands.registerCommand(
    'extension.helloWorld',
    () => {
      vscode.window.showInformationMessage('Hello World!');
    }
  );
  
  context.subscriptions.push(disposable);
  
  // Register code action provider
  vscode.languages.registerCodeActionsProvider('typescript', {
    provideCodeActions() {
      return [/* code actions */];
    }
  });
}

Stripe Plugin System

// Stripe middleware for payment processing
stripe.use((req, next) => {
  console.log('Payment request:', req);
  return next();
});

stripe.use(fraudDetectionPlugin);
stripe.use(taxCalculationPlugin);
stripe.use(receiptEmailPlugin);

📚 Key Takeaways

  1. Define clear API - What plugins can and can't do
  2. Use hooks/events - Let plugins react to app events
  3. Middleware pattern - Chain plugins in order
  4. Sandbox for security - Limit plugin capabilities
  5. Version your API - Breaking changes = new version
  6. Document everything - Plugin developers need docs
  7. Test plugins - Provide testing utilities

When to use: Building platforms (VS Code, Figma), frameworks (Gatsby, Next.js), or products that need extensibility.

On this page