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 directfetch/XMLHttpRequestfrom 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
unmountand 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/mountexecution. 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
DeduplicatingApiClientpattern 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.