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,95 @@
|
||||
import { PLUGIN_API_VERSION } from './pluginApi';
|
||||
|
||||
export interface PluginMeta {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
api: number;
|
||||
}
|
||||
|
||||
/** What became of one file in `data/plugins/`; the check scripts and a later settings list read it. */
|
||||
export interface PluginRecord {
|
||||
file: string;
|
||||
status: 'active' | 'failed';
|
||||
meta: PluginMeta | null;
|
||||
/** Why it is not active. */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PluginLoaderDeps {
|
||||
/** The module a file holds. */
|
||||
load(file: string): Promise<unknown>;
|
||||
/** A fresh API for a plugin that passed the checks. */
|
||||
api(meta: PluginMeta): unknown;
|
||||
/** One line for the user about a plugin that could not be activated. */
|
||||
report(message: string): void;
|
||||
}
|
||||
|
||||
function messageOf(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
/** The module's `meta` as the loader needs it, or why it cannot be used. */
|
||||
export function readMeta(module: unknown): PluginMeta | string {
|
||||
const exports = (typeof module === 'object' && module !== null ? module : {}) as Record<string, unknown>;
|
||||
const meta = exports.meta;
|
||||
if (typeof meta !== 'object' || meta === null) return 'exports no meta';
|
||||
const { id, name, version, api } = meta as Record<string, unknown>;
|
||||
if (typeof id !== 'string' || id === '') return 'meta.id must be a non-empty string';
|
||||
if (typeof exports.activate !== 'function') return 'exports no activate function';
|
||||
if (api !== PLUGIN_API_VERSION)
|
||||
return `needs plugin API ${String(api)}, this editor offers ${PLUGIN_API_VERSION}`;
|
||||
return {
|
||||
id,
|
||||
name: typeof name === 'string' && name !== '' ? name : id,
|
||||
version: typeof version === 'string' ? version : '',
|
||||
api,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and activates every plugin, one after the other in file-name order, so that what an
|
||||
* earlier one registers (a tab id, say) is settled before the next runs. Nothing a plugin does
|
||||
* wrong stops the others or the editor: a file that does not load, a module that is not a plugin,
|
||||
* an API version this editor does not offer, a second plugin of the same id and an `activate`
|
||||
* that throws each leave a failed record and a report.
|
||||
*/
|
||||
export async function activatePlugins(
|
||||
files: readonly string[],
|
||||
deps: PluginLoaderDeps,
|
||||
): Promise<PluginRecord[]> {
|
||||
const records: PluginRecord[] = [];
|
||||
const ids = new Set<string>();
|
||||
for (const file of [...files].sort()) {
|
||||
const fail = (meta: PluginMeta | null, error: string) => {
|
||||
records.push({ file, status: 'failed', meta, error });
|
||||
deps.report(`Plugin ${file} not activated: ${error}`);
|
||||
};
|
||||
let module: unknown;
|
||||
try {
|
||||
module = await deps.load(file);
|
||||
} catch (error) {
|
||||
fail(null, `does not load (${messageOf(error)})`);
|
||||
continue;
|
||||
}
|
||||
const meta = readMeta(module);
|
||||
if (typeof meta === 'string') {
|
||||
fail(null, meta);
|
||||
continue;
|
||||
}
|
||||
if (ids.has(meta.id)) {
|
||||
fail(meta, `a plugin with the id "${meta.id}" is active already`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const activate = (module as { activate: (api: unknown) => unknown }).activate;
|
||||
await activate(deps.api(meta));
|
||||
} catch (error) {
|
||||
fail(meta, `activate failed (${messageOf(error)})`);
|
||||
continue;
|
||||
}
|
||||
ids.add(meta.id);
|
||||
records.push({ file, status: 'active', meta });
|
||||
}
|
||||
return records;
|
||||
}
|
||||
Reference in new issue
Block a user