// types/plugin.ts
interface PixelCraftPlugin {
id: string;
name: string;
version: string;
author: string;
description: string;
permissions: PluginPermission[];
// Lifecycle
activate(api: PluginAPI): void;
deactivate(): void;
}
type PluginPermission =
| 'canvas:read'
| 'canvas:write'
| 'toolbar:add'
| 'events:subscribe'
| 'storage:read'
| 'storage:write';
interface PluginAPI {
// Canvas access
getImageData(): ImageData;
setImageData(data: ImageData): void;
getCanvasSize(): {
width: number; height: number };
// UI extension
addToolbarButton(btn: {
id: string;
label: string;
icon: string;
onClick: () => void;
}): void;
addFilterPanel(panel: {
id: string;
label: string;
render: (
container: HTMLElement
) => void;
}): void;
// Events
on(event: PluginEvent,
handler: Function): void;
off(event: PluginEvent,
handler: Function): void;
// Storage (per-plugin, sandboxed)
storage: {
get(key: string): Promise<any>;
set(key: string, value: any):
Promise<void>;
};
}
type PluginEvent =
| 'image:loaded'
| 'filter:before-apply'
| 'filter:after-apply'
| 'export:before'
| 'tool:changed';
// lib/pluginManager.ts
class PluginManager {
private plugins =
new Map<string, PluginInstance>();
private eventBus =
new EventEmitter();
async install(
pluginUrl: string
): Promise<void> {
// Dynamic import from URL
const module =
await import(
/* webpackIgnore: true */
pluginUrl);
const plugin: PixelCraftPlugin =
module.default;
// Validate required fields
if (!plugin.id || !plugin.name
|| !plugin.activate)
throw new Error(
'Invalid plugin structure');
// Create sandboxed API
const api = this.createSandboxedAPI(
plugin.id,
plugin.permissions);
// Activate
plugin.activate(api);
this.plugins.set(plugin.id, {
plugin,
api,
enabled: true,
});
console.log(
`✅ Plugin installed: `
+ `${plugin.name} v${
plugin.version}`);
}
async uninstall(
pluginId: string
): void {
const instance =
this.plugins.get(pluginId);
if (!instance) return;
instance.plugin.deactivate();
this.plugins.delete(pluginId);
}
emit(event: PluginEvent,
data?: any) {
this.eventBus.emit(event, data);
}
}
// lib/pluginManager.ts (continued)
private createSandboxedAPI(
pluginId: string,
permissions: PluginPermission[]
): PluginAPI {
const guard = (
perm: PluginPermission,
action: string
) => {
if (!permissions.includes(perm))
throw new Error(
`Plugin "${pluginId}" ` +
`lacks permission: ${perm} ` +
`for action: ${action}`);
};
return {
getImageData: () => {
guard('canvas:read',
'getImageData');
// Return a COPY, not the
// original — plugins can't
// corrupt state accidentally
const orig = getCanvasImageData();
return new ImageData(
new Uint8ClampedArray(
orig.data),
orig.width, orig.height);
},
setImageData: (data) => {
guard('canvas:write',
'setImageData');
pushHistory(`Plugin: ${pluginId}`);
applyImageData(data);
},
addToolbarButton: (btn) => {
guard('toolbar:add',
'addToolbarButton');
registerPluginButton(
pluginId, btn);
},
on: (event, handler) => {
guard('events:subscribe', 'on');
this.eventBus.on(event, handler);
},
storage: {
get: async (key) => {
guard('storage:read',
'storage.get');
return getPluginStorage(
pluginId, key);
},
set: async (key, value) => {
guard('storage:write',
'storage.set');
return setPluginStorage(
pluginId, key, value);
},
},
} as PluginAPI;
}
// plugins/vintage-filter.ts
// Third-party plugin example
const VintagePlugin:
PixelCraftPlugin = {
id: 'vintage-filter',
name: 'Vintage Photo Filter',
version: '1.0.0',
author: 'Community',
description:
'Apply a warm vintage look',
permissions: [
'canvas:read', 'canvas:write',
'toolbar:add'],
activate(api) {
api.addToolbarButton({
id: 'vintage',
label: 'Vintage',
icon: '📷',
onClick: () => {
const img = api.getImageData();
const d = img.data;
for (let i = 0;
i < d.length; i += 4) {
// Warm sepia tone
const r = d[i], g = d[i+1],
b = d[i+2];
d[i] = Math.min(255,
r * 1.2 + 30); // red up
d[i+1] = Math.min(255,
g * 1.0 + 15); // green
d[i+2] = Math.max(0,
b * 0.8); // blue down
}
api.setImageData(img);
},
});
},
deactivate() {
// Cleanup: remove toolbar button
},
};
export default VintagePlugin;
function PluginSettings() {
const plugins =
usePluginStore(s => s.plugins);
return (
<section>
<h2>Plugins</h2>
{plugins.map(p => (
<div key={p.id}
className="plugin-card">
<div className="plugin-info">
<h3>{p.name}</h3>
<span>v{p.version}
by {p.author}</span>
<p>{p.description}</p>
<div className="perms">
{p.permissions.map(perm =>
<span key={perm}
className="perm-badge">
{perm}
</span>
)}
</div>
</div>
<button onClick={() =>
togglePlugin(p.id)}>
{p.enabled
? 'Disable' : 'Enable'}
</button>
</div>
))}
<button onClick={openInstallDialog}>
+ Install Plugin
</button>
</section>
);
}
git switch -c feature/PIXELCRAFT-103-plugin-system
git add src/lib/pluginManager.ts src/types/plugin.ts src/components/
git commit -m "Add plugin architecture with sandboxed API (PIXELCRAFT-103)"
git push origin feature/PIXELCRAFT-103-plugin-system
# PR → Review → Merge → Close ticket ✅
Plugin systems are the ultimate expression of the Open/Closed Principle.
Open for extension, closed for modification. Third parties add capabilities without changing your core. This is how the most successful software platforms scale.