3.7 KiB
define-plugin — author-facing plugin DX
(no submodules)
Purpose
definePlugin is the canonical entry point plugin authors call to declare their name / version / requires / isolation / trustLevel plus optional lifecycle hooks (install, start, stop, uninstall, health). It threads the literal-array requires through to the PluginContext<Caps> and RequestContext<Caps> so undeclared capability access becomes a compile-time TypeScript error — the type system, not a runtime guard, is what enforces the capability surface.
The runtime work is light: validate name / version are non-empty, reject duplicate ids inside requires, and return a frozen record. The heavy lifting (manifest cross-check, capability resolution, FSM hand-off) lives in the host's lifecycle driver; definePlugin is the type-safety + ergonomics layer.
Features
const Caps extends ReadonlyArray<CapabilityId>generic — captures literal-arrayrequiressoctx.<cap>is narrowed exactly to the declared ids.- Synchronous validation for the two failure modes the type system cannot catch: empty
name, emptyversion, duplicate id. - Frozen output record — safe to hash for the rollback journal.
- Lightweight
DefinePluginError(plainErrorsubclass) — noSuaveplanErrordependency on the hot author-side path. - Type aliases
PluginContextOf<P>/RequestContextOf<P>derive the per-hook context shape from the module record.
Usage: Basic
import { definePlugin } from "@suaveplan/api-host/define-plugin";
export default definePlugin({
name: "billing",
version: "1.2.3",
requires: ["db", "auth.session", "logger"] as const,
isolation: "trusted",
trustLevel: "first-party",
async start(ctx) {
ctx.capabilities.logger.info("billing: started");
},
});
Usage: Advanced
Derive request-context type for downstream router wiring:
import {
definePlugin,
type RequestContextOf,
} from "@suaveplan/api-host/define-plugin";
const plugin = definePlugin({
name: "billing",
version: "1.2.3",
requires: ["db", "auth.session"] as const,
isolation: "isolated",
trustLevel: "third-party",
async install(ctx) {
// ctx.capabilities.db is `AdminDatabaseLike` here.
await ctx.capabilities.db.execute("INSERT INTO ...");
},
async start(ctx) {
ctx.capabilities.db; // ok
// ctx.capabilities.events; // <- compile error: "events" not declared
},
});
type Req = RequestContextOf<typeof plugin>;
// Req.capabilities.db is `TenantScopedDatabaseLike` here.
API Reference
definePlugin<const Caps>(spec): PluginModule<Caps>— validates + freezes.DefinePluginSpec<Caps>— input shape with optional lifecycle hooks.PluginModule<Caps>—Readonly<DefinePluginSpec<Caps>>.PluginContextOf<P>/RequestContextOf<P>— per-module derived context types.DefinePluginError—code∈DEFINE_PLUGIN.INVALID_NAME|DEFINE_PLUGIN.INVALID_VERSION|DEFINE_PLUGIN.DUPLICATE_CAPABILITY.
Implementation Notes
The const Caps extends ReadonlyArray<CapabilityId> constraint relies on TypeScript 5.0+ const generic inference — callers MUST write requires: ["db"] as const (or satisfies CapabilityRequirements) for the narrowing to work. Without as const, TypeScript widens the tuple to string[] and the capability map degrades to the full CapabilityId union. The validation is intentionally minimal: deeper manifest-vs-module cross-check is the host loader's job (Chunk B+) so author errors surface in two places — fast / type-only here, slow / runtime + manifest-aware there. The frozen output ensures the rollback journal can content-hash a stable representation.