Files
famapp/docs/tasks/04-module-loader.md
T
ginnoir b89690a9f2 Initial scaffold: tooling, plan, task briefs
- pnpm 10 workspace + TypeScript strict + ESLint flat + Prettier
- CLAUDE.md as canonical brief
- docs/tasks/ — 22 task briefs broken out by phase for sub-sessions
- docs/decisions/ — ADR scaffold

Implements task 01 (repo-init).
2026-05-06 00:05:50 -05:00

2.6 KiB

04 — Module loader & registry

Goal

Implement the module system that everything else hangs off: each module declares a manifest, the loader composes them at startup, and core services iterate the registry instead of hardcoding entity types.

Why

This is the load-bearing extensibility piece. Get this wrong and every later module needs core changes.

Depends on

  • 03 (DB)

Scope

Module manifest type (src/modules/_core/module.ts)

export type ModuleManifest = {
  id: string; // "calendar", "lists", "notes"
  name: string; // human-readable
  nav?: { href: string; label: string; icon?: string };
  entities: EntityTypeRegistration[]; // see below
  dashboardWidgets?: DashboardWidget[];
  quickAdds?: QuickAddAction[];
};

export type EntityTypeRegistration = {
  type: string; // "calendar.event", "lists.item", "notes.note"
  label: { singular: string; plural: string };
  share?: ShareCapabilities; // null = not shareable
  reminder?: ReminderCapabilities; // null = not remindable
  search?: SearchAdapter; // null = not searchable
  resolveUrl: (id: string) => string; // canonical app URL
  loadForShare?: (id: string) => Promise<unknown>; // payload for /s/<token>
};

Registry (src/modules/_core/registry.ts)

  • registerModule(manifest) and getRegistry() returning frozen views.
  • getEntityType(type) lookup.
  • Import-time side-effect free: modules are listed and registered explicitly in src/modules/index.ts.

Loader (src/modules/index.ts)

  • Imports each module's manifest and calls registerModule. Order is deterministic.
  • Stub modules for now: calendar, lists, notes each export an empty-but-valid manifest so the loader has something to register.

Wiring

  • Root layout reads the registry to render nav.
  • A throwaway /debug/registry page (dev-only) dumps the loaded registry as JSON.

Out of scope

  • Real implementations of share / reminder / search adapters (later phases).
  • Dynamic plugin loading from disk. Modules are statically imported.

Acceptance criteria

  • src/modules/index.ts registers three stub modules.
  • Nav renders entries from the registry, not from a hardcoded list.
  • /debug/registry shows all three module manifests in dev.
  • Adding a fourth stub module only requires creating its folder + adding one line to src/modules/index.ts.
  • All types exported from _core so other modules can import without circulars.

Notes

  • Frozen objects (Object.freeze) on registry exposure are cheap insurance against accidental mutation.
  • Resist the urge to make this a fancy DI container. A plain map is enough.