Make the module navigation capability an optional context member
CI / verify (pull_request) Successful in 1m11s
CI / infrastructure-smoke (pull_request) Successful in 42s
CI / package-release (pull_request) Successful in 48s
CI / migration-smoke (pull_request) Successful in 2m27s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 52s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 3m35s
CI / verify (pull_request) Successful in 1m11s
CI / infrastructure-smoke (pull_request) Successful in 42s
CI / package-release (pull_request) Successful in 48s
CI / migration-smoke (pull_request) Successful in 2m27s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 52s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 3m35s
ModuleUiHostContext.navigation is now `navigation?: ModuleUiNavigation | undefined`, so the public context change is source-compatible: a context built before navigation existed (without the member) still compiles unchanged, as 0.x patch releases require. The Dashboard supplies navigation; hosts without addressable pages, including the display host, omit the member; modules feature-detect it. Existing hosts' tests are restored to their pre-PR shape, and a module-ui-sdk test proves a pre-navigation context still satisfies ModuleUiHostContext (and fails if the member is ever made required). Runtime behaviour, manifest v8/v9 compatibility, and security rules are unchanged. ADR 0063 and the module development guide say so. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ae360ef318
commit
c78f1318b4
@@ -108,11 +108,13 @@ export interface ModuleUiHostContext {
|
||||
* Where this module is and how it moves between its own pages, and to its Settings page
|
||||
* (ADR 0063). See {@link ModuleUiNavigation}.
|
||||
*
|
||||
* `undefined` where the host has no addressable pages for it — the full-viewport display host,
|
||||
* which shows one screen, and any standalone host without routes. A module that offers
|
||||
* navigation checks for it, exactly as it checks `display`.
|
||||
* **Optional**, so adding it broke no existing host: a context built before navigation existed
|
||||
* is still a complete context. A host with addressable module pages — Ekklesia's Dashboard —
|
||||
* supplies it; a host without them — the full-viewport display host, which shows one screen, or a
|
||||
* standalone host without routes — omits it (or sets it to `undefined`). A module feature-detects
|
||||
* it: `context.navigation?.navigate("timelines")`.
|
||||
*/
|
||||
readonly navigation: ModuleUiNavigation | undefined;
|
||||
readonly navigation?: ModuleUiNavigation | undefined;
|
||||
}
|
||||
|
||||
export interface ModuleUiDisplayLocation {
|
||||
|
||||
@@ -188,7 +188,6 @@ describe("the same module UI artifact hosted by two different hosts", () => {
|
||||
contributions: NO_MODULE_UI_CONTRIBUTIONS,
|
||||
display: undefined,
|
||||
moduleId,
|
||||
navigation: undefined,
|
||||
api,
|
||||
principal: { kind: "anonymous" },
|
||||
permissions: { has: () => canPublish },
|
||||
@@ -215,7 +214,6 @@ describe("the same module UI artifact hosted by two different hosts", () => {
|
||||
contributions: NO_MODULE_UI_CONTRIBUTIONS,
|
||||
display: { url: "https://announcements.example.org/display" },
|
||||
moduleId,
|
||||
navigation: undefined,
|
||||
api,
|
||||
principal: { kind: "anonymous" },
|
||||
permissions: { has: () => canPublish },
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type ModuleUiHostContext,
|
||||
type ModuleUiNavigation,
|
||||
ModuleUiNavigationError,
|
||||
NO_MODULE_UI_CONTRIBUTIONS,
|
||||
} from "../src/index.js";
|
||||
|
||||
/** The navigation capability's public surface (ADR 0063). Its behaviour is the host's: the
|
||||
@@ -22,11 +23,26 @@ describe("module UI navigation contract", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("makes navigation a context member a host without pages leaves undefined", () => {
|
||||
// A type-level contract: a context may say it has no navigation, as the display host does, and
|
||||
// a module reads it through an optional check.
|
||||
const describe = (context: Pick<ModuleUiHostContext, "navigation">): string =>
|
||||
context.navigation?.location().path ?? "no navigation";
|
||||
it("keeps a context built before navigation existed a complete, valid context", () => {
|
||||
// Exactly the seven members every host supplied before ADR 0063, with no `navigation` member at
|
||||
// all. This must keep compiling unchanged: `tsc` over this file is the compatibility proof, and
|
||||
// `satisfies` makes the check exact rather than structural-by-accident.
|
||||
const preNavigationContext = {
|
||||
api: { request: async () => new Response(null, { status: 204 }) },
|
||||
contributions: NO_MODULE_UI_CONTRIBUTIONS,
|
||||
display: undefined,
|
||||
moduleId: "announcements",
|
||||
permissions: { has: () => false },
|
||||
principal: { kind: "anonymous" },
|
||||
realtime: { subscribe: () => ({ close: () => undefined }) },
|
||||
} satisfies ModuleUiHostContext;
|
||||
const asContext: ModuleUiHostContext = preNavigationContext;
|
||||
|
||||
expect("navigation" in asContext).toBe(false);
|
||||
expect(asContext.navigation?.location().path).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lets a host supply navigation, omit it, or set it to undefined, and a module feature-detect it", () => {
|
||||
const navigation: ModuleUiNavigation = {
|
||||
link: () => ({ href: "/m/x", onClick: () => undefined }),
|
||||
location: () => ({ page: "x", path: "x" }),
|
||||
@@ -34,7 +50,14 @@ describe("module UI navigation contract", () => {
|
||||
settings: undefined,
|
||||
subscribe: () => () => undefined,
|
||||
};
|
||||
expect(describe({ navigation: undefined })).toBe("no navigation");
|
||||
expect(describe({ navigation })).toBe("x");
|
||||
const where = (context: Pick<ModuleUiHostContext, "navigation">): string =>
|
||||
context.navigation?.location().path ?? "no navigation";
|
||||
|
||||
expect(where({ navigation })).toBe("x");
|
||||
expect(where({})).toBe("no navigation");
|
||||
expect(where({ navigation: undefined })).toBe("no navigation");
|
||||
// Present, it must be the real capability — not any object.
|
||||
// @ts-expect-error a navigation member must satisfy ModuleUiNavigation
|
||||
expect(where({ navigation: { location: () => "x" } })).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user