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)
game-maker / gate (push) Successful in 1m38s
game-maker / suite (push) Successful in 5m43s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-17 19:00:03 +02:00
1 parent 2a390e52ff
commit 57a97ba94f
22 files changed
+1153 -57

No files matched your search

+95
View File
@@ -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;
}