diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eb4092..fedf150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,11 +38,18 @@ Slice 2b (the plugin object, hooks, capabilities and the runtime): - `DunePlugin`, `PluginApi`, `MountApi`, `HookRegistry`, `HookContext`, `HookHandler`, `HookEvent` and the event payloads, `ResponseTransformContext`, - `ContentEditorPlugin`, `PublicRouteRegistration` and the admin registrations - (`AdminPageRegistration`, `AdminRouteRegistration`, `AdminBarAction`, - `AdminBarActionContext`). `HookHandler`, `setup`, `mount` and + `PublicRouteRegistration`. `HookHandler`, `setup`, `mount` and `transformResponse` are bivariant: core hands handlers a wider context than this package declares. +- Extension points belong to the plugin that reads them (#127). The admin's + `AdminPageRegistration`, `AdminRouteRegistration` and `ContentEditorPlugin` + (and the keys `admin.pages`, `admin.routes`, `admin.editor`) are declared by + `@dune/plugin-admin`; the bar's `BarAction`, `BarActionContext` and + `bar.actions` by `@dune/plugin-inline-edit`. They extend `Capabilities` and + `Contributions` by declaration merging, which this package supports and keys + by the consuming plugin's name (`admin.*`, `bar.*`); unprefixed names are the + contract's. `ResponseTransformContext` carries `capabilities` (the reader + `MountApi` offers) instead of `adminPrefix` and `barActions`. - Capabilities: `Capabilities` (slots), `Contributions`, `CapabilityContext`, `CapabilityFactory`, `CapabilityRegistrar`, `CapabilityReader`, `CapabilityProvider`, `RequestAuthenticator`, `ScheduledActionExecutor`. diff --git a/src/capabilities.ts b/src/capabilities.ts index aa8dd3b..2a21120 100644 --- a/src/capabilities.ts +++ b/src/capabilities.ts @@ -13,12 +13,6 @@ * @module */ -import type { AdminBarAction, AdminBarActionContext } from "./hooks.ts"; -import type { - AdminPageRegistration, - AdminRouteRegistration, - ContentEditorPlugin, -} from "./hooks.ts"; import type { DuneConfig } from "./config.ts"; import type { HistoryEngine } from "./history.ts"; import type { HookRegistry } from "./hooks.ts"; @@ -75,7 +69,25 @@ export interface AdminPrefix { readonly prefix: string; } -/** Slots: at most one active provider each. First-party keys only in 1.0. */ +/** + * Slots: at most one active provider each. What core itself consumes, or what + * every plugin may rely on. + * + * Open to declaration merging. A plugin that offers an extension point of its + * own declares the key next to its types, so this contract does not grow with + * every plugin's features: + * + * ```ts + * declare module "@dune/plugin-api" { + * interface Capabilities { "admin.editor": ContentEditorPlugin } + * } + * ``` + * + * The rule: **a key is prefixed with the name of the plugin that consumes it** + * (`admin.*` is the admin's, `bar.*` plugin-inline-edit's). Unprefixed + * namespaces (`editor`, `scheduler`, `admin.prefix`) belong to core, and core + * never reads a key it did not declare. + */ export interface Capabilities { /** Where the admin is mounted. */ "admin.prefix": AdminPrefix; @@ -85,19 +97,15 @@ export interface Capabilities { "scheduler.executor": ScheduledActionExecutor; /** Real-time inline editing. */ "editor.inline": InlineEditManager; - /** A replacement page editor. */ - "editor.content": ContentEditorPlugin; } -/** Contribution points: one item from each contributing plugin. */ -export interface Contributions { - /** An extra admin page. */ - "admin.pages": AdminPageRegistration; - /** A guarded admin route under the admin prefix. */ - "admin.routes": AdminRouteRegistration; - /** Buttons for the admin bar. */ - "bar.actions": (ctx: AdminBarActionContext) => AdminBarAction[]; -} +/** + * Contribution points: one item from each contributing plugin. Open to + * declaration merging under the same rule as {@link Capabilities}. The contract + * declares none: every point belongs to the plugin that reads it. + */ +// deno-lint-ignore no-empty-interface +export interface Contributions {} /** What a slot factory receives: core infrastructure that exists once every `setup()` has settled. */ export interface CapabilityContext { diff --git a/src/hooks.ts b/src/hooks.ts index 3ac3e67..d1d425b 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -146,58 +146,6 @@ export interface PublicRouteRegistration { island?: string; } -/** Context passed to a `bar.actions` contribution. */ -export interface AdminBarActionContext { - /** Content page matching the current URL, or null for non-content routes. */ - page: { - sourcePath: string; - route: string; - title: string | null; - template: string; - } | null; - /** Admin panel URL prefix (e.g. `"/admin"`). */ - adminPrefix: string; -} - -/** A button contributed to the shared admin bar through the `bar.actions` point. */ -export interface AdminBarAction { - /** Unique id; namespace by plugin name, e.g. `"pdf-export:download"`. */ - id: string; - label: string; - icon?: string; - /** Renders as a link when set; takes precedence over `onClick`. */ - href?: string; - /** Literal JavaScript run on click when `href` is not set. Trusted plugin content. */ - onClick?: string; -} - -/** A custom admin page contributed through the `admin.pages` point. */ -// deno-lint-ignore no-explicit-any -export interface AdminPageRegistration { - /** URL path relative to the admin prefix (must start with `/`). */ - path: string; - /** Human-readable nav label shown in the admin sidebar. */ - label: string; - icon?: string; - /** Admin permission required to view this page. */ - permission: string; - /** GET handler for the page. */ - handler(ctx: PluginRouteContext): Promise | Response; -} - -/** A guarded admin route contributed through the `admin.routes` point. */ -// deno-lint-ignore no-explicit-any -export interface AdminRouteRegistration { - /** HTTP method. Anything other than `GET` is subject to the CSRF check. */ - method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; - /** URL path relative to the admin prefix (must start with `/`). */ - path: string; - /** Admin permission required, including ones declared through `authzActions`. */ - permission: string; - /** Runs only after authentication, CSRF and permission checks pass. */ - handler(ctx: PluginRouteContext): Promise | Response; -} - /** * Plugin definition. Export it as the default export of your plugin module. * @@ -255,17 +203,6 @@ export interface DunePlugin { ): Promise | Response; } -/** Plugin-provided replacement for the built-in block editor (the `editor.content` slot). */ -export interface ContentEditorPlugin { - /** Handle `GET /admin/pages/edit?path=...`. */ - pageEditorHandler(ctx: PluginRouteContext): Response | Promise; - /** Optional WebSocket upgrade handler for real-time collaboration. */ - wsHandler?: ( - req: Request, - user: { id: string; name: string }, - ) => Response; -} - /** Context passed to {@link DunePlugin.transformResponse}. */ export interface ResponseTransformContext { req: Request; @@ -289,10 +226,13 @@ export interface ResponseTransformContext { title: string | null; template: string; } | null; - /** Admin panel URL prefix (e.g. `"/admin"`). */ - adminPrefix: string; - /** Bar buttons contributed through `bar.actions`, in plugin registration order. */ - barActions: readonly ((ctx: AdminBarActionContext) => AdminBarAction[])[]; + /** + * What plugins provide, as `MountApi.capabilities` offers it: slots such as + * `get("admin.prefix")` and any extension point a plugin declares, such as + * `list("bar.actions")`. Read per request, so a plugin module shared between + * sites keeps no state of its own. + */ + capabilities: import("./capabilities.ts").CapabilityReader; } /** What a plugin's `mount()` receives. */ diff --git a/src/mod.ts b/src/mod.ts index 901288e..3e51f9e 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -132,11 +132,6 @@ export type { ScheduledActionExecutor, } from "./capabilities.ts"; export type { - AdminBarAction, - AdminBarActionContext, - AdminPageRegistration, - AdminRouteRegistration, - ContentEditorPlugin, DunePlugin, FormSubmitPayload, HookContext,