Initial Commit
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# `define-plugin` — author-facing plugin DX
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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-array `requires` so `ctx.<cap>` is narrowed exactly to the declared ids.
|
||||
- Synchronous validation for the two failure modes the type system cannot catch: empty `name`, empty `version`, duplicate id.
|
||||
- Frozen output record — safe to hash for the rollback journal.
|
||||
- Lightweight `DefinePluginError` (plain `Error` subclass) — no `SuaveplanError` dependency on the hot author-side path.
|
||||
- Type aliases `PluginContextOf<P>` / `RequestContextOf<P>` derive the per-hook context shape from the module record.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user