diff --git a/deno.json b/deno.json index 5cef24dc..3b08278b 100644 --- a/deno.json +++ b/deno.json @@ -5,7 +5,7 @@ "author": "zumbrunn (https://zumbrunn.com)", "minimumDependencyAge": { "age": "P1D", - "exclude": ["jsr:@dune/core", "jsr:@dune/theme-api"] + "exclude": ["jsr:@dune/core", "jsr:@dune/plugin-api", "jsr:@dune/theme-api"] }, "exports": { ".": "./src/mod.ts", @@ -57,6 +57,7 @@ "./watchdog": "./src/runtime/watchdog.ts" }, "imports": { + "@dune/plugin-api": "jsr:@dune/plugin-api@^1", "@dune/theme-api": "jsr:@dune/theme-api@^1", "fresh": "jsr:@fresh/core@^2", "preact": "npm:preact@^10", diff --git a/tests/contract/plugin_api_test.ts b/tests/contract/plugin_api_test.ts new file mode 100644 index 00000000..a53d0cf6 --- /dev/null +++ b/tests/contract/plugin_api_test.ts @@ -0,0 +1,197 @@ +/** + * `@dune/plugin-api` is a leaf package that redeclares the plugin contract. + * These tests keep core and the package from drifting apart. They are mostly + * compile-time: `deno check` is the assertion. + * + * Two directions (plugin-api extraction plan, decision 10): + * - what core hands a plugin is assignable to the package's types; + * - what a plugin hands in (an engine, an editor manager, a record) is + * accepted by core. + * Types that are identical on both sides are checked both ways. + */ + +import { assertEquals } from "@std/assert"; +import type * as P from "@dune/plugin-api"; +import { isDuneError as pluginIsDuneError } from "@dune/plugin-api"; +import type { PageIndex } from "../../src/content/types.ts"; +import type { + ContentApi, + ContentSearchResult, + PagesOptions, + ResolvedPage, + TaxonomyTerm, +} from "../../src/content/api.ts"; +import type { + FacetCounts, + InjectedSearchRecord, + SearchEngine, + SearchEngineCreateContext, + SearchFilter, + SearchOptions, + SearchRecordsCollectContext, + SearchResult, +} from "../../src/search/engine.ts"; +import type { StorageAdapter, StorageEntry, StorageStat, WatchEvent } from "../../src/storage/types.ts"; +import type { Submission, SubmissionFile, SubmissionMeta, SubmissionStatus } from "../../src/forms/submissions.ts"; +import type { BlueprintField } from "../../src/blueprints/types.ts"; +import type { EditorIdentity, InlineEditManager, InlineEditRequestContext } from "../../src/inline-edit/types.ts"; +import type { ScheduledAction } from "../../src/workflow/types.ts"; +import type { PluginApp, PluginRouteContext } from "../../src/plugin-api/app.ts"; +import type { PluginServices } from "../../src/plugin-api/services.ts"; +import type { User, UserCreate } from "../../src/auth/types.ts"; +import type { UserStore } from "../../src/auth/user-store.ts"; +import type { Session } from "../../src/session/types.ts"; +import type { SessionManager } from "../../src/session/mod.ts"; +import type { HistoryEngine } from "../../src/history/engine.ts"; +import type { ContentDiff, ContentRevision } from "../../src/workflow/types.ts"; +import type { WorkflowEngine } from "../../src/workflow/engine.ts"; +import type { Scheduler } from "../../src/workflow/scheduler.ts"; +import type { StagingEngine } from "../../src/staging/engine.ts"; +import type { RateLimiter } from "../../src/security/rate-limit.ts"; +import type { RateLimitStore } from "../../src/security/rate-limit-store.ts"; +import type { Logger } from "../../src/core/logger.ts"; +import type { DuneConfig } from "../../src/config/types.ts"; +import type { DuneEngine } from "../../src/core/engine.ts"; +import type { Page } from "../../src/content/types.ts"; +import type { SearchManager } from "../../src/search/manager.ts"; +import type { FlexEngine } from "../../src/flex/engine.ts"; +import type { SubmissionManager } from "../../src/forms/submissions.ts"; +import type { SectionRegistry } from "../../src/sections/registry.ts"; +import type { MetricsCollector } from "../../src/metrics/mod.ts"; +import type { JobScheduler } from "../../src/jobs/mod.ts"; +import type { DuneAuthSystem } from "../../src/auth/authz.ts"; +import type { ThemeLoader } from "../../src/themes/loader.ts"; +import type { RouteResolver } from "../../src/routing/resolver.ts"; +import type { PluginRuntime } from "../../src/plugin-api/runtime.ts"; +import type { + DunePlugin, + FormSubmitPayload, + HookContext, + HookEvent, + HookRegistry, + MountApi, + PageMovePayload, + PluginApi, + PublicRouteRegistration, + ResponseTransformContext, + SystemActionPayload, +} from "../../src/hooks/types.ts"; +import type { + AuthenticateResult, + Capabilities, + CapabilityContext, + CapabilityKey, + CapabilityReader, + CapabilityRegistrar, +} from "../../src/plugin-api/capabilities.ts"; +import { isDuneError as coreIsDuneError } from "../../src/plugin-api/errors.ts"; +import { capabilityKey as coreCapabilityKey } from "../../src/plugin-api/capabilities.ts"; + +// Both directions: identical on both sides. +function same(..._ok: [A] extends [B] ? ([B] extends [A] ? [] : [never]) : [never]): void {} +// Core -> package only: core hands out something wider. +function handsOut(): void {} +// Package -> core only: a plugin supplies it and core consumes it. +function takesIn(): void {} + +// --- what core hands a plugin --------------------------------------------- +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); + +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); + +// --- what a plugin hands in ----------------------------------------------- +takesIn(); +takesIn(); +takesIn(); +same(); +same(); + +// --- identity: a provider supplies its own stores and cores consumes them - +handsOut(); +takesIn(); +handsOut(); +handsOut(); +handsOut(); +same(); +same(); +same(); + +// --- identical both ways --------------------------------------------------- +same(); +same(); +same(); +same(); +same(); +same(); +same(); +same(); +same(); +same(); +same(); +same(); +same(); +same(); +same(); +same(); + +// --- slice 2b: config, runtime, hooks, plugin object, capabilities --------- +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); +handsOut(); + +// What a plugin supplies and core consumes. +takesIn(); +takesIn(); +takesIn(); +// Extension points a plugin owns are typed keys, not entries in a shared interface. +same, P.CapabilityKey>(); + +same(); +same(); +same(); +same(); +same(); + +Deno.test("isDuneError behaves the same in the package and in core", () => { + const err = { code: "ssrf_blocked" }; + for (const code of ["ssrf_blocked", "body_too_large", "weak_password", "duplicate_email"] as const) { + assertEquals(pluginIsDuneError(err, code), coreIsDuneError(err, code)); + } + assertEquals(pluginIsDuneError(new Error("x"), "ssrf_blocked"), false); +}); + +Deno.test("a capability key made by the package is the key core reads", async () => { + const { capabilityKey: pluginCapabilityKey } = await import("@dune/plugin-api"); + assertEquals(pluginCapabilityKey("bar.actions"), coreCapabilityKey("bar.actions")); +});