From 02344d169c1bfaca07bf0f26a67a5cde429e1f61 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:16:07 +0100 Subject: [PATCH] feat: expand addon system documentation with rebuild requirements and runtime loading strategy --- specifications/addon-plugin-theme-concept.md | 64 +++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/specifications/addon-plugin-theme-concept.md b/specifications/addon-plugin-theme-concept.md index 801ee8c9..eaf457ad 100644 --- a/specifications/addon-plugin-theme-concept.md +++ b/specifications/addon-plugin-theme-concept.md @@ -34,6 +34,18 @@ This document describes a system that allows the JMAP Webmail application to be - **Aligned with existing architecture** — builds on Zustand stores, React context/providers, CSS variables, and the Next.js App Router patterns already in use. - **Incrementally adoptable** — the core app can ship without any addons; the addon system is a layer on top. +### Rebuild Requirements + +| Addon Type | Source | Rebuild Needed? | +|-----------|--------|----------------| +| Theme | Bundled (`/addons/themes/`) | **Yes** — included at build time | +| Theme | URL (remote) | **No** — CSS loaded via `` at runtime | +| Plugin | Bundled (`/addons/plugins/`) | **Yes** — included at build time | +| Plugin | URL (remote) | **No** — JS loaded at runtime (see §6.3) | +| Plugin | Local file (dev mode) | **No** — loaded at runtime | + +Bundled addons are compiled into the app's static assets during `next build`. Adding, removing, or updating a bundled addon requires a rebuild and redeploy. URL-based and local addons are fully runtime-loaded — users can install, enable, disable, and uninstall them without any rebuild. + ### Non-Goals (for v1) - Server-side plugin execution (all addons run client-side). @@ -389,6 +401,54 @@ interface Disposable { 5. **Activate**: Call `activate(ctx)` for each plugin, passing a scoped `PluginContext`. 6. **Ready**: Emit `app:ready` hook — plugins can now interact with stores. +### 6.3 Runtime Loading Strategy + +Next.js `import()` only resolves modules known at build time. To load plugins from arbitrary URLs at runtime without a rebuild, the addon system uses a **script-based module loader**: + +```ts +// lib/addon-loader.ts + +async function loadRemotePlugin(url: string): Promise { + // 1. Fetch the plugin's JS bundle as text + const response = await fetch(url); + if (!response.ok) throw new Error(`Failed to fetch plugin: ${response.status}`); + const code = await response.text(); + + // 2. Validate size limit (500 KB default) + if (code.length > MAX_PLUGIN_SIZE) { + throw new Error(`Plugin exceeds size limit`); + } + + // 3. Create a scoped module environment + // The plugin receives a controlled `require` that only resolves + // allowed shared dependencies (React, Lucide icons, date-fns). + const module = { exports: {} as PluginModule }; + const scopedRequire = createScopedRequire(SHARED_DEPS); + const factory = new Function("module", "exports", "require", code); + factory(module, module.exports, scopedRequire); + + // 4. Validate the module exports the expected interface + if (typeof module.exports.activate !== "function") { + throw new Error(`Plugin does not export an activate() function`); + } + + return module.exports; +} + +// Shared dependencies exposed to plugins — avoids bundling duplicates +const SHARED_DEPS: Record = { + "react": React, + "react/jsx-runtime": jsxRuntime, + "lucide-react": lucideIcons, + "date-fns": dateFns, + "sonner": sonner, +}; +``` + +**Bundled addons** skip this loader — they are statically imported at build time via the generated `addon-registry.json`. + +**Theme CSS** (any source) is loaded by injecting a `` element — no special loader needed. + ### 6.2 Addon Manager Store A new Zustand store manages addon state: @@ -934,4 +994,6 @@ export function activate(ctx: PluginContext) { 4. **Addon signing?** For URL-installed addons, a signature verification system would prevent tampering. Worth considering for v2. -5. **Shared dependencies?** Should plugins be able to declare peer dependencies on the host app's packages (React, date-fns, Lucide icons)? This would reduce bundle sizes but creates coupling. Recommend providing these as globals via the plugin runtime. +5. **Shared dependencies?** Should plugins be able to declare peer dependencies on the host app's packages (React, date-fns, Lucide icons)? This would reduce bundle sizes but creates coupling. Recommend providing these as globals via the plugin runtime. **(Addressed in §6.3 — shared deps are exposed via a scoped `require`.)** + +6. **Admin-controlled addon allowlist?** In multi-user deployments, should the server admin be able to restrict which addons can be installed (e.g., via an environment variable `ALLOWED_ADDON_IDS`)? This would prevent users from loading untrusted plugins in managed environments.