dorfteich/docs/developer/extending.md
Claude Fable 5 2fe9374f16
All checks were successful
CI / Import/export fidelity gate (pull_request) Successful in 54s
CI / Lint, typecheck, test (pull_request) Successful in 4m25s
CD / Build and push images (push) Successful in 17s
Release / Build release images and notes (push) Successful in 1m18s
CD / Smoke tests against Test (push) Successful in 1m18s
CI / Build container images (push) Has been skipped
CI / Import/export fidelity gate (push) Successful in 59s
CI / Build container images (pull_request) Successful in 3m49s
CI / Auth e2e pack (pull_request) Successful in 6m47s
CD / Deploy to Test (push) Successful in 11s
CD / Promote to Int (push) Successful in 11s
Release / Release-candidate operations QA (push) Successful in 45s
Prod deploy / Deploy the released images to Prod (push) Successful in 15s
CI / Lint, typecheck, test (push) Successful in 4m36s
CI / Auth e2e pack (push) Successful in 6m54s
Doku: Excalidraw als sechstes Standard-Plugin ergänzt
- Tutorial K19: neue Sektion „Excalidraw — Skizzen wie von Hand" mit
  Nadias Bühnenplan-Beispiel (konzerte-live); Intro fünf→sechs Plugins.
- Tutorial K18: fünf→sechs Standard-Plugins.
- Site-Admin-Guide (en+de): Referenzliste + Build-Namensliste +
  Build-Hinweis (~16-MiB-ZIP aus npm).
- plugin-architecture.md: Referenz-Eintrag excalidraw (npm-Library-
  Spielart des Bundled-App-Pfads, {scene, svg}).
- developer/extending (en+de): Excalidraw als zweite Bundled-App-Variante.

Holt die in #136 zugesagten Doku-Ergänzungen nach. Screenshot für K19
folgt nach dem CSP-Deploy (damit die Handschrift korrekt rendert).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 22:27:17 +02:00

160 lines
6.9 KiB
Markdown

# Developer guide — extending Dorfteich
_Deutsche Fassung: [docs/de/developer/extending.md](../de/developer/extending.md)_
Two ways to make Dorfteich do more: write a **plugin** (no fork, no
redeploy, safe by construction) or contribute to the **core**. Start
with a plugin unless you need to change how the product itself works.
## Writing a plugin
Read [`docs/architecture/plugin-architecture.md`](../architecture/plugin-architecture.md)
once — it is the contract. The short version:
A plugin is a ZIP with a `manifest.json`, a single ES-module bundle
`plugin.js`, optional `styles.css`, `i18n/*.json`, and `assets/…`. It
contributes one or more **extension points**:
| Type | You build | Example |
| -------------- | ------------------------------------------------------ | ---------------------- |
| `sectionStyle` | named CSS styles for content sections (no code at all) | `section-styles-basic` |
| `pageTool` | a read-only widget in the page-tools panel | `toc`, `page-index` |
| `block` | a custom editor block with its own data and edit UI | `mermaid`, `drawio` |
### The sandbox — what your code can and cannot do
Your `plugin.js` runs in an `<iframe sandbox="allow-scripts">` with an
opaque origin and a strict CSP: **no cookies, no storage, no parent DOM,
and network/frames only to your own bundled assets** — never to the api
or any external host. Everything else goes through the typed RPC the
SDK provides, executed with the _viewing user's_ permissions:
```ts
import { createPlugin, windowTransport } from '@dorfteich/plugin-sdk';
const { host } = createPlugin({
transport: windowTransport({
target: { postMessage: (m) => window.parent.postMessage(m, '*') },
source: window,
}),
onRender: async (ctx) => {
const outline = await host.readCurrentPage.getOutline();
document.body.textContent = outline.map((e) => e.text).join('\n');
void host.ui.resize(document.body.scrollHeight + 16);
},
onEdit: async (ctx) => {
/* block plugins: editing UI; persist via host.blockData.setData(...) */
},
});
```
Capabilities you may declare in the manifest `permissions` and what they
unlock: `readCurrentPage` (outline/content/meta), `readPond` (page
lists + contents), `readBlock` (cross-page block reads), `blockData`
(your block's `getData`/`setData` — writes become normal document
changes, replicated and versioned), `ui` (`resize`, `openPage`, `toast`,
`scrollToHeading`, `enterFullscreen`/`exitFullscreen`). Calls outside
the declared set are rejected at runtime.
### Block plugins in three sentences
The host mounts your frame per block and calls `render` (view) or
`edit` (the user pressed the block's edit button). Persist
`{ …yourData }` via `host.blockData.setData` — collaborators' frames
re-render live when the data changes under them. Store a static
snapshot (e.g. an `svg` string) alongside your source data: exports and
the public view show it through the manifest `fallback` machinery
without ever executing plugin code.
### Bundled apps and fullscreen
A plugin may ship an entire sub-application as assets and run it in a
child iframe of its own asset path — that is how the `drawio` plugin
embeds the real draw.io editor. The `excalidraw` plugin shows the other
flavor of the same idea: an npm React library bundled directly into
`plugin.js` (esbuild) and mounted fullscreen in the frame itself, with
its runtime assets (fonts, locales) shipped in the ZIP. Combine with
`host.ui.enterFullscreen()` for editors that need the whole screen.
Size limits: 64 MiB ZIP, 256 MiB unpacked.
### Developing and shipping
The reference plugins under `packages/plugins/` are the templates —
copy the closest one. Each has a `build.mjs` that bundles `src/plugin.ts`
with esbuild and packs the installable ZIP into `dist/`:
```sh
cd packages/plugins/<your-plugin>
pnpm build # → dist/<id>-<version>.zip
```
Install the ZIP via Admin → Plugins (or the dropzone), open the
sandboxed preview at `/admin/plugins/<id>/preview`, iterate. The install
gate validates structure, manifest, size and CSS scoping and rejects
with a precise error code. Publishing an update = same id, higher
version.
Conventions that will be enforced on review: UI strings via the plugin's
`i18n/` files in **both** `de` and `en`; the fallback must make sense in
a printed document.
## Working on the core
### Stack at a glance
TypeScript monorepo (pnpm workspaces): `apps/api` (NestJS + Prisma,
PostgreSQL), `apps/collab` (Hocuspocus/Yjs realtime server),
`apps/web` (React + Vite + TipTap), `apps/backup` (backup sidecar),
`packages/shared` (types, schemas, editor schema, i18n catalogs),
`packages/plugin-sdk`, `packages/plugins/*`. Architecture decisions live
in [`docs/architecture/adr/`](../architecture/adr/) — read the relevant
ADR before touching a subsystem; `docs/architecture/` has the deep dives
(permissions, data model, realtime collaboration, security, plugins).
### Getting a dev environment
```sh
pnpm install
cd deploy/compose && cp .env.example .env
# Full containerized dev stack (hot reload; first start installs deps):
docker compose -f docker-compose.yml -f compose.dev.yml up
# → web http://localhost:5173, api :3001, db :5434
```
Fastest feedback: run only the database in Docker and web/api natively —
the exact recipe is documented at the top of
[`deploy/compose/compose.dev.yml`](../../deploy/compose/compose.dev.yml).
Seed fixture users/ponds with `pnpm --filter @dorfteich/api db:seed`
(fixture password: see `apps/api/prisma/seed.ts`).
### The gates every change must pass
```sh
pnpm lint # ESLint + Prettier — no pipes that swallow exit codes
pnpm typecheck
pnpm test # vitest everywhere; DB-backed suites need TEST_DATABASE_URL
pnpm i18n:check # every UI string in de AND en
```
DB-backed tests run against the compose dev database:
`TEST_DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich pnpm test`.
Playwright e2e packs live in `apps/web/e2e/` and run against a seeded
local stack (see `.gitea/workflows/ci.yml` for the exact recipe).
### House rules worth knowing before your first PR
- **Permissions**: never answer an access question outside
`PermissionService`/the route decorators; denied reads are 404, denied
writes on readable things are 403
([permissions.md](../architecture/permissions.md)).
- **i18n**: no hard-coded UI strings; add keys to
`packages/shared/i18n/{de,en}/…` (ADR 0012).
- **No third-party requests** from the product, ever — fonts, editors,
everything ships self-hosted (ADR 0016 sets the precedent).
- **Migrations**: additive and reversible within one minor release; the
release pipeline's QA gate replays an upgrade from the previous
release against real data.
- **Document contracts**: anything two services share (status files,
NOTIFY channels, wire types) lives in `packages/shared` with a comment
saying who reads and who writes.