Web Loom logo
Plugin BookAppendices and Reference Material
Appendices

Appendices and Reference Material

Comprehensive resources for plugin development.

Appendices

The chapters of this book have introduced a lot of patterns, interfaces, and conventions in the context of specific problems. This section collects the most important of them in one place: canonical type definitions you can copy into a real project, concrete plugin examples for each major framework, build configurations for the three most common bundlers, and condensed checklists for security and performance. Treat it as a reference you return to rather than a section you read once.


Appendix A: Complete TypeScript Type Definitions

These are the core contracts we have referenced throughout the book. They form the boundary between the host application and the plugin ecosystem — everything a plugin can do is expressed through these interfaces, and everything the host is obligated to provide is declared here as well.

/**
 * ===============================
 * 1. Core Concepts & Contracts
 * ===============================
 */
 
export interface PluginManifest {
  id: string;
  name: string;
  version: string;
  description?: string;
  author?: string;
  entry: string;
  icon?: string;
  permissions?: string[];
  routes?: PluginRouteDefinition[];
  menuItems?: PluginMenuItem[];
  widgets?: PluginWidgetDefinition[];
  metadata?: Record<string, unknown>;
}
 
export interface PluginModule {
  init?: (sdk: PluginSDK) => void | Promise<void>;
  mount?: (sdk: PluginSDK) => void | Promise<void>;
  unmount?: (sdk: PluginSDK) => void | Promise<void>;
}
 
export interface PluginRouteDefinition<TComponent = unknown> {
  path: string;
  component: TComponent;
  title?: string;
}
 
export interface PluginMenuItem {
  label: string;
  icon?: string;
  action: string | (() => void);
  tooltip?: string;
  order?: number;
}
 
export interface PluginWidgetDefinition<TComponent = unknown> {
  id: string;
  title: string;
  render: TComponent;
  size?: 'small' | 'medium' | 'large';
}
 
/**
 * ===============================
 * 2. Core App Responsibilities
 * ===============================
 */
 
export interface PluginRegistry {
  register: (pluginDef: PluginDefinition) => void;
  unregister: (pluginId: string) => void;
  get: (pluginId: string) => PluginDefinition | undefined;
  getAll: () => PluginDefinition[];
}
 
export interface PluginDefinition {
  manifest: PluginManifest;
  module: PluginModule;
}
 
export interface PluginLoader {
  loadFromManifest: (manifest: PluginManifest) => Promise<PluginDefinition>;
  loadFromUrl: (manifestUrl: string) => Promise<PluginDefinition>;
}
 
/**
 * ===============================
 * 3. Plugin SDK (Public API)
 * ===============================
 */
 
export interface PluginSDK {
  routes: {
    add: (route: PluginRouteDefinition) => void;
    remove: (path: string) => void;
  };
  menus: {
    addItem: (item: PluginMenuItem) => void;
    removeItem: (label: string) => void;
  };
  widgets: {
    add: (widget: PluginWidgetDefinition) => void;
    remove: (id: string) => void;
  };
  events: {
    on: (event: string, handler: (payload?: unknown) => void) => void;
    off: (event: string, handler: (payload?: unknown) => void) => void;
    emit: (event: string, payload?: unknown) => void;
  };
  ui: {
    showModal: (content: unknown, options?: { title?: string }) => void;
    showToast: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => void;
  };
  services: {
    apiClient: {
      get: <T>(url: string, params?: Record<string, unknown>) => Promise<T>;
      post: <T>(url: string, body: unknown) => Promise<T>;
    };
    auth: {
      getUser: () => Promise<{ id: string; name: string; roles: string[] }>;
      hasRole: (role: string) => boolean;
    };
    storage: {
      get: <T>(key: string) => T | undefined;
      set: <T>(key: string, value: T) => void;
      remove: (key: string) => void;
    };
  };
  plugin: {
    id: string;
    manifest: PluginManifest;
  };
}

Appendix B: Example Plugin Implementations

These four examples implement the same conceptual plugin — a widget that registers itself on mount and removes itself on unmount — across the four most common integration targets. The pattern is identical in each case; only the component authoring syntax changes.

React Plugin Example

// manifest.json
{
  "id": "widget.sales-chart",
  "name": "Sales Chart Widget",
  "version": "1.0.0",
  "entry": "/plugins/sales-chart/index.js",
  "permissions": ["api:read", "ui:dashboard"]
}
 
// index.tsx
import React from 'react';
 
interface SalesData {
  month: string;
  revenue: number;
}
 
const SalesChart: React.FC = () => {
  const [data, setData] = React.useState<SalesData[]>([]);
 
  React.useEffect(() => {
    // Fetch is performed through the SDK's apiClient in a real implementation.
    // The component receives data via props or a context bridge.
  }, []);
 
  return (
    <div className="sales-chart-widget">
      {data.map(({ month, revenue }) => (
        <div key={month}>{month}: {revenue}</div>
      ))}
    </div>
  );
};
 
export const module: PluginModule = {
  mount(sdk) {
    sdk.widgets.add({
      id: 'sales-chart',
      title: 'Sales Chart',
      render: SalesChart,
    });
  },
  unmount(sdk) {
    sdk.widgets.remove('sales-chart');
  },
};

Vue Plugin Example

// index.ts
import { defineComponent, h, ref } from 'vue';
 
const HelloWidget = defineComponent({
  name: 'HelloWidget',
  setup() {
    const message = ref('Hello from Vue widget!');
    return { message };
  },
  render() {
    return h('div', { class: 'hello-widget' }, this.message);
  },
});
 
export const module: PluginModule = {
  mount(sdk) {
    sdk.widgets.add({
      id: 'hello-widget',
      title: 'Hello Widget',
      render: HelloWidget,
    });
  },
  unmount(sdk) {
    sdk.widgets.remove('hello-widget');
  },
};

Angular Plugin Example

import { Component } from '@angular/core';
 
@Component({
  selector: 'my-plugin',
  template: `<h3>Hello from Angular plugin!</h3>`,
})
export class MyPluginComponent {}
 
export const module: PluginModule = {
  mount(sdk) {
    sdk.widgets.add({
      id: 'angular-widget',
      title: 'Angular Widget',
      render: MyPluginComponent,
    });
  },
  unmount(sdk) {
    sdk.widgets.remove('angular-widget');
  },
};

Vanilla JavaScript Plugin Example

Vanilla plugins follow the same mount/unmount contract. Because there is no component abstraction, the plugin creates DOM elements directly. This is the lowest-level integration point and requires the most manual cleanup.

let container;
 
export const module = {
  mount(sdk) {
    container = document.createElement('div');
    container.className = 'vanilla-widget';
    container.textContent = 'Hello from Vanilla JS!';
 
    sdk.widgets.add({
      id: 'vanilla-widget',
      title: 'Vanilla Widget',
      render: container,
    });
  },
  unmount(sdk) {
    sdk.widgets.remove('vanilla-widget');
    container = undefined;
  },
};

Appendix C: Build Configuration Examples

Plugins must be compiled to a format the host can load dynamically. The three configurations below produce UMD and ES module outputs suitable for remote loading. All three assume an entry point at src/index.ts and output to dist/.

Webpack Configuration

module.exports = {
  entry: './src/index.ts',
  output: {
    filename: 'plugin.js',
    libraryTarget: 'umd',
    globalObject: 'this',
  },
  resolve: { extensions: ['.ts', '.tsx', '.js'] },
  module: {
    rules: [{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }],
  },
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
};

Vite Configuration

import { defineConfig } from 'vite';
 
export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.ts',
      formats: ['es', 'umd'],
      name: 'Plugin',
    },
    rollupOptions: {
      // Exclude framework dependencies — the host provides them.
      external: ['react', 'react-dom', 'vue'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
          vue: 'Vue',
        },
      },
    },
  },
});

Rollup Configuration

import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
 
export default {
  input: 'src/index.ts',
  output: [
    { file: 'dist/plugin.esm.js', format: 'es' },
    { file: 'dist/plugin.umd.js', format: 'umd', name: 'Plugin', globals: { react: 'React' } },
  ],
  external: ['react', 'react-dom'],
  plugins: [nodeResolve(), typescript()],
};

Appendix D: Security Checklist

This checklist covers the key controls discussed in Chapter 9. It is intentionally terse — each item maps to a section in that chapter where the rationale and implementation details are explained.

Before loading a plugin:

  • [ ] Validate manifest schema against a strict JSON Schema definition before any code is executed.
  • [ ] Verify the plugin bundle's digital signature against the publisher's public key.
  • [ ] Check that all declared permissions are recognised and that none exceed what the installation context allows.
  • [ ] Inspect bundle size against the configured limit before fetching.

At runtime:

  • [ ] Execute plugin code inside a sandboxed boundary (iframe, Web Worker, or VM context).
  • [ ] Route all network requests through sdk.services.apiClient; block direct fetch/XMLHttpRequest from within the sandbox.
  • [ ] Enforce CSP headers that prevent inline scripts and restrict script sources to known origins.
  • [ ] Namespace all storage keys by plugin ID to prevent cross-plugin data access.
  • [ ] Restrict event bus subscriptions to events the plugin declared in its manifest.

At deactivation:

  • [ ] Call unmount and confirm all registered widgets, routes, and menu items have been removed.
  • [ ] Release the sandbox boundary (terminate the Worker, destroy the iframe).
  • [ ] Audit the event bus for any listeners the plugin failed to deregister.

Appendix E: Performance Benchmarking

Measuring plugin system performance requires instrumenting at multiple layers. A plugin that loads in 200ms on a developer machine may load in two seconds on a low-end device over a 4G connection. Establish baselines early and enforce them in CI.

What to measure:

  • Load time: Separate the manifest fetch, bundle fetch, parse time, and init/mount execution. Tracking these individually tells you which phase to optimise.
  • Memory usage: Record heap size before and after mounting a plugin. A plugin that never unmounts cleanly will show a growing heap over repeated activation cycles.
  • CPU time: Profile mount, event handler execution, and widget re-renders. Plugins that schedule work synchronously on the main thread will cause jank during host application interactions.
  • Network requests: Count API calls per plugin per page load. Deduplicate requests that multiple plugins make to the same endpoint using the DeduplicatingApiClient pattern from Chapter 14.
  • Bundle size: Enforce per-plugin size budgets (a reasonable starting point is 100KB gzipped for a full-featured plugin, 20KB for a utility plugin). Fail CI builds that exceed the budget.

Tools: Chrome DevTools Performance and Memory tabs, Lighthouse CI, WebPageTest for real-network simulation, performance.mark() and performance.measure() for custom instrumentation, Bundlephobia or vite-bundle-visualizer for static bundle analysis.


A Note on What Comes Next

The architecture patterns in this book are stable, but plugin systems are not a solved problem — they are a design space you navigate continuously. Every real deployment will surface constraints that no generic guide anticipated: a permission model that turns out to be too coarse, a sandbox that the team cannot afford to enforce across all plugins, a versioning policy that made sense until a critical plugin needed to ship a breaking change.

What matters is that you have a vocabulary for the tradeoffs now. Isolation versus performance. Flexibility versus governance. Centralised distribution versus team autonomy. The production systems referenced throughout — VS Code, Vite, Backstage, Kibana, Vendure, TinaCMS — each resolved these tradeoffs differently, and each resolution was shaped by the specific constraints of its ecosystem.

Start with the contracts. Define PluginManifest, PluginSDK, and PluginModule before you write a single plugin. Build the loader next, then the registry, then the SDK implementation. Add sandboxing when the threat model justifies it. Add a marketplace when the ecosystem justifies it. The temptation is to build everything upfront; the systems that last are the ones that added complexity only when a real problem demanded it.

Was this helpful?
Web Loom logo
Copyright © Web Loom. All rights reserved.