Add typed hook system for plugins

Replace invokeHook and the per plugin on* methods with AsyncHook
collections on the controller, host, instance, clusterioctl and web
control objects. Plugin entrypoints export a default function that
receives a load context and attaches handlers to the hooks it needs.
Class exports still load through fromContext with a deprecation
warning. The create templates and docs use the entrypoint function.

This is clusterio/clusterio#921 recreated on top of master after the
ESM migration and the PluginDeclaration permissions change.

Closes #730

Co-Authored-By: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
committed 2026-09-11 22:49:09 +00:00
1 parent 8c03ac15ab
commit 7122cbccf8
64 files changed
+1878 -1184

No files matched your search

+40 -10
View File
@@ -1,20 +1,50 @@
import assert from "node:assert/strict";
import * as mock from "../mock.js";
import * as host from "@clusterio/host";
describe("host/src/BaseHostPlugin", function() {
describe("class BaseHostPlugin", function() {
let hostPlugin;
it("should be constructible", async function() {
hostPlugin = new host.BaseHostPlugin({}, new mock.MockHost(), new mock.MockLogger());
await hostPlugin.init();
const info = { name: "test" };
let mockHost;
beforeEach(function() {
mockHost = new mock.MockHost();
});
it("should define defaults for hooks", async function() {
await hostPlugin.onHostConfigFieldChanged();
await hostPlugin.onMetrics();
await hostPlugin.onShutdown();
hostPlugin.onControllerConnectionEvent("connect");
await hostPlugin.onPrepareControllerDisconnect();
it("should be constructible", async function() {
const plugin = new host.BaseHostPlugin(info, mockHost, new mock.MockLogger());
await plugin.init();
assert.equal(mockHost.hooks.size, 0);
});
it("should be constructible from a load context", async function() {
const logger = new mock.MockLogger();
const plugin = host.BaseHostPlugin.fromContext({ plugin: info, host: mockHost, logger });
assert.equal(plugin.info, info);
assert.equal(plugin.host, mockHost);
assert.equal(plugin.logger, logger);
});
it("should attach overridden hooks", async function() {
let calls = [];
class HostPlugin extends host.BaseHostPlugin {
async onHostConfigFieldChanged(field, curr, prev) { calls.push(["config", field, curr, prev]); }
async onShutdown() { calls.push(["shutdown", this]); }
}
const plugin = new HostPlugin(info, mockHost, new mock.MockLogger());
assert.deepEqual([...mockHost.hooks.attached], ["test"]);
assert.equal(mockHost.hooks.metrics.size, 0);
await mockHost.hooks.hostConfigFieldChanged.invoke("host.name", "new", "old");
await mockHost.hooks.shutdown.invoke();
assert.deepEqual(calls, [["config", "host.name", "new", "old"], ["shutdown", plugin]]);
});
it("should detach hooks", async function() {
class HostPlugin extends host.BaseHostPlugin {
async onShutdown() { }
}
const plugin = new HostPlugin(info, mockHost, new mock.MockLogger());
assert.equal(mockHost.hooks.size, 1);
plugin.detachHooks();
assert.equal(mockHost.hooks.size, 0);
});
});
});