import { Hono } from "hono"; import type { FlowKind, FeedDefinition } from "../lib/layer-types"; export type { FeedDefinition } from "../lib/layer-types"; import type { SyncServer } from "../server/local-first/sync-server"; // ── Module Scoping ── export type ModuleScope = 'space' | 'global'; export interface ModuleScoping { /** Whether the module's data lives per-space or globally by default */ defaultScope: ModuleScope; /** Whether space owners can override the default scope */ userConfigurable: boolean; } // ── Lifecycle Context ── export interface SpaceLifecycleContext { spaceSlug: string; ownerDID: string | null; enabledModules: string[]; syncServer: SyncServer; } // ── Doc Schema (for Automerge document types a module manages) ── export interface DocSchema { /** Document ID pattern, e.g. '{space}:notes:notebooks:{notebookId}' */ pattern: string; /** Human-readable description */ description: string; /** Factory to create a fresh empty document */ init: () => T; } /** A browsable content type that a module produces. */ export interface OutputPath { /** URL segment: "notebooks" */ path: string; /** Display name: "Notebooks" */ name: string; /** Emoji: "📓" */ icon: string; /** Short description: "Rich-text collaborative notebooks" */ description: string; } /** * The contract every rSpace module must implement. * * A module is a self-contained feature area (books, pubs, cart, canvas, etc.) * that exposes Hono routes and metadata. The shell mounts these routes under * `/:space/{moduleId}` in unified mode. In standalone mode, the module's own * `standalone.ts` mounts them at the root with a minimal shell. */ export interface RSpaceModule { /** Short identifier used in URLs: 'books', 'pubs', 'cart', 'canvas', etc. */ id: string; /** Human-readable name: 'rBooks', 'rPubs', 'rCart', etc. */ name: string; /** Emoji or SVG string for the app switcher */ icon: string; /** One-line description */ description: string; /** Mountable Hono sub-app. Routes are relative to the mount point. */ routes: Hono; // ── Scoping & Schema ── /** How this module's data is scoped (space vs global) */ scoping: ModuleScoping; /** Automerge document schemas this module manages */ docSchemas?: DocSchema[]; // ── Lifecycle hooks ── /** Called once at server startup (register schemas, init DB, etc.) */ onInit?: (ctx: { syncServer: SyncServer }) => Promise; /** Called when a new space is created */ onSpaceCreate?: (ctx: SpaceLifecycleContext) => Promise; /** Called when a space is deleted */ onSpaceDelete?: (ctx: SpaceLifecycleContext) => Promise; /** Called when this module is enabled for a space */ onSpaceEnable?: (ctx: SpaceLifecycleContext) => Promise; /** Called when this module is disabled for a space */ onSpaceDisable?: (ctx: SpaceLifecycleContext) => Promise; // ── Display & routing ── /** Optional: standalone domain for this module (e.g. 'rbooks.online') */ standaloneDomain?: string; /** Feeds this module exposes to other layers */ feeds?: FeedDefinition[]; /** Feed kinds this module can consume from other layers */ acceptsFeeds?: FlowKind[]; /** If true, module is hidden from app switcher (still has routes) */ hidden?: boolean; /** Browsable content types this module produces */ outputPaths?: OutputPath[]; /** Optional: render rich landing page body HTML */ landingPage?: () => string; /** Optional: external app to embed via iframe when ?view=app */ externalApp?: { url: string; name: string; }; } /** Registry of all loaded modules */ const modules = new Map(); export function registerModule(mod: RSpaceModule): void { modules.set(mod.id, mod); } export function getModule(id: string): RSpaceModule | undefined { return modules.get(id); } export function getAllModules(): RSpaceModule[] { return Array.from(modules.values()); } /** Metadata exposed to the client for the app switcher and tab bar */ export interface ModuleInfo { id: string; name: string; icon: string; description: string; scoping: ModuleScoping; standaloneDomain?: string; feeds?: FeedDefinition[]; acceptsFeeds?: FlowKind[]; hidden?: boolean; hasLandingPage?: boolean; externalApp?: { url: string; name: string; }; outputPaths?: OutputPath[]; } export function getModuleInfoList(): ModuleInfo[] { return getAllModules() .filter((m) => !m.hidden) .map((m) => ({ id: m.id, name: m.name, icon: m.icon, description: m.description, scoping: m.scoping, ...(m.standaloneDomain ? { standaloneDomain: m.standaloneDomain } : {}), ...(m.feeds ? { feeds: m.feeds } : {}), ...(m.acceptsFeeds ? { acceptsFeeds: m.acceptsFeeds } : {}), ...(m.landingPage ? { hasLandingPage: true } : {}), ...(m.externalApp ? { externalApp: m.externalApp } : {}), ...(m.outputPaths ? { outputPaths: m.outputPaths } : {}), })); }