Editor Plugins
The editor has a plugin system built on @web-engine-dev/editor-core's ExtensionRegistry. Plugins contribute to well-known extension points to add panels, toolbar actions, gizmos, component editors, context menus, and more.
Extension Registry
The ExtensionRegistry is a type-safe, signal-driven registry where plugins register contributions at named extension points. It tracks a version signal that increments on every change, enabling reactive UI updates.
import { ExtensionRegistry } from '@web-engine-dev/editor-core';
const registry = new ExtensionRegistry();
// Register a contribution
const unregister = registry.register('panels', {
id: 'my-panel',
title: 'My Panel',
defaultPosition: 'left',
create: (container) => {
container.textContent = 'Hello from my plugin!';
return { dispose: () => { container.textContent = ''; } };
},
}, 'my-plugin-id');
// Query all contributions at a point
const panels = registry.getAll('panels');
// Query contributions by plugin
const myExtensions = registry.getByPlugin('my-plugin-id');
// Remove all contributions from a plugin
registry.unregisterPlugin('my-plugin-id');
// Unregister a single contribution
unregister();Extension Points
The editor defines 15 extension points in the EditorExtensionPoints type map. Each point accepts a specific contribution interface.
panels
Register custom panels that appear in the dockview layout.
interface PanelContribution {
id: string;
title: string;
icon?: string;
defaultPosition?: 'left' | 'right' | 'bottom' | 'center';
create: (container: HTMLElement) => { dispose: () => void };
}componentEditors
Custom property editors for specific component types, replacing or augmenting the default inspector view.
interface ComponentEditorContribution {
componentId: number; // Component type ID
priority?: number; // Higher wins when multiple match
create: (container: HTMLElement, props: ComponentEditorProps) => {
dispose: () => void;
};
}toolbar
Actions added to the editor toolbar.
interface ToolbarContribution {
id: string;
label: string;
icon?: string;
section?: string;
execute: () => void;
isEnabled?: () => boolean;
}gizmos
Visual handles rendered in the viewport for specific component types.
interface GizmoContribution {
id: string;
name: string;
componentIds: number[];
create: () => { dispose: () => void };
}contextMenus
Additional items for context menus in specific locations.
interface ContextMenuContribution {
location: string; // 'hierarchy', 'viewport', 'inspector', etc.
items: Array<{
id: string;
label: string;
icon?: string;
action: () => void;
when?: string; // When-expression for visibility
}>;
}viewportOverlays
Overlays rendered on top of the viewport canvas.
interface ViewportOverlayContribution {
id: string;
name: string;
zOrder?: number;
create: (container: HTMLElement) => { dispose: () => void };
}assetImporters
Custom importers for file types not handled by built-in importers.
interface AssetImporterContribution {
extensions: string[]; // e.g., ['.png', '.jpg']
name?: string;
priority?: number; // Higher wins when multiple match
import: (file: File) => Promise<AssetData>;
}shortcuts
Register keyboard shortcuts that trigger commands.
interface ShortcutContribution {
shortcuts: Array<{
id: string;
keys: string; // e.g., 'Ctrl+S', 'Ctrl+Shift+Z'
command: string; // Command ID to execute
when?: string; // Activation context
label?: string;
}>;
}commands
Register commands that can be invoked from the command palette, menus, or shortcuts.
interface CommandContribution {
commands: Array<{
id: string;
title: string;
icon?: string;
category?: string;
execute: (...args: unknown[]) => void | Promise<void>;
isEnabled?: () => boolean;
}>;
}statusBar
Items displayed in the editor's status bar.
interface StatusBarContribution {
create: () => {
id: string;
text: string;
tooltip?: string;
icon?: string;
alignment: 'left' | 'right';
priority?: number;
onClick?: () => void;
};
}themes
Custom editor themes.
interface ThemeContribution {
themes: Array<{
id: string;
name: string;
base: 'dark' | 'light';
tokens: Record<string, string>;
}>;
}buildHooks
Hooks into the build pipeline for pre-build configuration and post-build processing.
interface BuildHookContribution {
preBuild?(context: BuildHookContext): Promise<void> | void;
postBuild?(context: BuildHookContext, result: BuildHookResult): Promise<void> | void;
}The BuildHookContext provides profileId, target, outputDir, development flag, and a mutable config object that hooks can modify.
assetPostProcessors
Process assets during the build pipeline.
interface AssetPostProcessorContribution {
readonly extensions: string[];
process(asset: BuildAsset, context: BuildHookContext): Promise<BuildAsset> | BuildAsset;
}inspectionProviders
Custom data providers for the inspector panel.
inspectionRenderers
Custom renderers for inspector sections.
Plugin Lifecycle
Plugins are managed through the editor's plugin infrastructure. Each plugin has a manifest that declares its identity and capabilities:
interface PluginManifest {
id: string;
name: string;
version: string;
description?: string;
author?: string;
editorVersion?: string;
dependencies?: Record<string, string>;
capabilities?: {
propertyEditors?: boolean;
gizmos?: boolean;
assetImporters?: boolean;
menus?: boolean;
toolbar?: boolean;
panels?: boolean;
components?: boolean;
systems?: boolean;
};
configSchema?: {
properties: Record<string, {
type: 'string' | 'number' | 'boolean';
default?: unknown;
description?: string;
}>;
};
}Plugin states flow through: unloaded -> loaded -> activated (or degraded / error).
The Plugin Manager panel displays all plugins with their current state and provides activate/deactivate controls.
Example: Complete Plugin
Here is a minimal plugin that registers a toolbar button and a command:
import type { ExtensionRegistry } from '@web-engine-dev/editor-core';
export function registerMyPlugin(registry: ExtensionRegistry): () => void {
const disposers: Array<() => void> = [];
// Register a command
disposers.push(
registry.register('commands', {
commands: [{
id: 'myPlugin.sayHello',
title: 'Say Hello',
category: 'My Plugin',
execute: () => {
console.log('Hello from my plugin!');
},
}],
}, 'my-plugin'),
);
// Register a toolbar button
disposers.push(
registry.register('toolbar', {
id: 'myPlugin.helloButton',
label: 'Hello',
icon: 'wand',
section: 'tools',
execute: () => {
console.log('Toolbar button clicked!');
},
}, 'my-plugin'),
);
// Return cleanup function
return () => {
for (const dispose of disposers) dispose();
};
}Querying Extensions
Use getAll() to retrieve all contributions at an extension point:
// Get all registered panels
const panels = registry.getAll('panels');
// Get all toolbar items
const toolbarItems = registry.getAll('toolbar');
// Get all commands for the command palette
const commands = registry.getAll('commands');The version signal on the registry increments on every registration change, so reactive frameworks can re-render when extensions are added or removed:
import { createEffect } from 'solid-js';
createEffect(() => {
// Re-runs whenever extensions change
registry.version();
const panels = registry.getAll('panels');
// Update UI...
});