Plugins in data/plugins are loaded at startup and can add a tab with a read-only node board, and the demo plugin shows every asset as a node (#113)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a390e52ff
commit
57a97ba94f
22 files changed
+1153
-57
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import { Widget } from '../../ui';
|
||||
import { NodeBoard } from './NodeBoard';
|
||||
|
||||
/** The API version this editor offers. A plugin names the one it was written for in `meta.api`. */
|
||||
export const PLUGIN_API_VERSION = 1;
|
||||
|
||||
export interface PluginAsset {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
/** What the editor lends the API – narrow, so the API can be exercised without an editor. */
|
||||
export interface PluginHost {
|
||||
assets(): PluginAsset[];
|
||||
/** Called after every change to the model; returns the unsubscribe. */
|
||||
onProjectChange(listener: () => void): () => void;
|
||||
/** Whether a tab of this id exists already, built in or added by a plugin. */
|
||||
hasTab(id: string): boolean;
|
||||
addTab(id: string, label: string, content: Widget): void;
|
||||
}
|
||||
|
||||
/** A change of the asset list, as `project.on('change')` sees it: ids, names and types in order. */
|
||||
function assetSignature(assets: readonly PluginAsset[]): string {
|
||||
return JSON.stringify(assets.map((a) => [a.id, a.name, a.type]));
|
||||
}
|
||||
|
||||
function requireString(value: unknown, what: string): string {
|
||||
if (typeof value !== 'string' || value === '') throw new TypeError(`${what} must be a non-empty string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `api` a plugin's `activate` receives, version 1: read the assets and hear when they change,
|
||||
* add a tab, and build its content from `api.ui`. Every plugin gets its own, so what it does can
|
||||
* be told apart in the console.
|
||||
*/
|
||||
export function createPluginApi(pluginId: string, host: PluginHost) {
|
||||
const project = Object.freeze({
|
||||
assets: (): PluginAsset[] => host.assets().map(({ id, name, type }) => ({ id, name, type })),
|
||||
/**
|
||||
* Only a change a plugin can see calls back: the model changes on every step of a drag, the
|
||||
* asset list almost never. A listener that throws is reported and stays subscribed.
|
||||
*/
|
||||
on(event: string, listener: () => void): () => void {
|
||||
if (event !== 'change') throw new Error(`Unknown project event "${event}" – API v1 knows "change"`);
|
||||
if (typeof listener !== 'function') throw new TypeError('project.on needs a listener function');
|
||||
let seen = assetSignature(host.assets());
|
||||
return host.onProjectChange(() => {
|
||||
const next = assetSignature(host.assets());
|
||||
if (next === seen) return;
|
||||
seen = next;
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
console.error(`[plugin ${pluginId}] change listener failed:`, error);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const tabs = Object.freeze({
|
||||
/** False, with a warning, when the id is taken – by a built-in tab or an earlier plugin. */
|
||||
add(tab: { id: unknown; label: unknown; create: unknown }): boolean {
|
||||
const id = requireString(tab.id, 'tabs.add: id');
|
||||
const label = requireString(tab.label, 'tabs.add: label');
|
||||
if (typeof tab.create !== 'function') throw new TypeError('tabs.add: create must be a function');
|
||||
if (host.hasTab(id)) {
|
||||
console.warn(`[plugin ${pluginId}] a tab "${id}" exists already – not added`);
|
||||
return false;
|
||||
}
|
||||
const content: unknown = (tab.create as () => unknown)();
|
||||
if (!(content instanceof Widget))
|
||||
throw new TypeError(`tabs.add: create must return a widget from api.ui (tab "${id}")`);
|
||||
host.addTab(id, label, content);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return Object.freeze({
|
||||
version: PLUGIN_API_VERSION,
|
||||
project,
|
||||
tabs,
|
||||
ui: Object.freeze({ NodeBoard }),
|
||||
});
|
||||
}
|
||||
|
||||
export type PluginApi = ReturnType<typeof createPluginApi>;
|
||||
Reference in new issue
Block a user