dorfteich/docs/de/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

172 lines
7.5 KiB
Markdown

# Entwicklerhandbuch — Dorfteich erweitern
_Englisches Original: [docs/developer/extending.md](../../developer/extending.md)_
Zwei Wege, Dorfteich mehr beizubringen: ein **Plugin** schreiben (kein
Fork, kein Redeploy, konstruktionsbedingt sicher) oder am **Kern**
mitarbeiten. Beginne mit einem Plugin, außer du musst ändern, wie das
Produkt selbst funktioniert.
## Ein Plugin schreiben
Lies einmal
[`docs/architecture/plugin-architecture.md`](../../architecture/plugin-architecture.md)
(englisch) — das ist der Vertrag. Die Kurzfassung:
Ein Plugin ist ein ZIP mit einer `manifest.json`, einem einzelnen
ES-Modul-Bundle `plugin.js`, optional `styles.css`, `i18n/*.json` und
`assets/…`. Es steuert einen oder mehrere **Erweiterungspunkte** bei:
| Typ | Du baust | Beispiel |
| -------------- | -------------------------------------------------------------- | ---------------------- |
| `sectionStyle` | benannte CSS-Stile für Inhaltsabschnitte (ganz ohne Code) | `section-styles-basic` |
| `pageTool` | ein Nur-Lese-Widget im Seiten-Werkzeuge-Panel | `toc`, `page-index` |
| `block` | ein eigener Editor-Block mit eigenen Daten und Bearbeitungs-UI | `mermaid`, `drawio` |
### Die Sandbox — was dein Code kann und was nicht
Dein `plugin.js` läuft in einem `<iframe sandbox="allow-scripts">` mit
opakem Origin und strikter CSP: **keine Cookies, kein Storage, kein
Eltern-DOM, Netzwerk/Frames nur zu deinen eigenen gebündelten Assets** —
nie zur API oder einem externen Host. Alles andere läuft über die
typisierte RPC des SDK, ausgeführt mit den Berechtigungen der
_betrachtenden Person_:
```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: Bearbeitungs-UI; persistieren via host.blockData.setData(...) */
},
});
```
Fähigkeiten, die du im Manifest unter `permissions` deklarieren kannst,
und was sie freischalten: `readCurrentPage` (Gliederung/Inhalt/
Metadaten), `readPond` (Seitenlisten + Inhalte), `readBlock`
(Block-Lesezugriffe über Seiten hinweg), `blockData`
(`getData`/`setData` deines Blocks — Schreibzugriffe werden normale
Dokumentänderungen, repliziert und versioniert), `ui` (`resize`,
`openPage`, `toast`, `scrollToHeading`,
`enterFullscreen`/`exitFullscreen`). Aufrufe außerhalb des deklarierten
Satzes werden zur Laufzeit abgelehnt.
### Block-Plugins in drei Sätzen
Der Host mountet deinen Frame pro Block und ruft `render` (Ansicht)
oder `edit` (die Person hat den Bearbeiten-Knopf des Blocks gedrückt).
Persistiere `{ …deineDaten }` via `host.blockData.setData` — die Frames
der Mitarbeitenden rendern live neu, wenn sich die Daten unter ihnen
ändern. Lege neben deinen Quelldaten einen statischen Schnappschuss ab
(z. B. einen `svg`-String): Exporte und die öffentliche Ansicht zeigen
ihn über die `fallback`-Mechanik des Manifests, ohne je Plugin-Code
auszuführen.
### Gebündelte Apps und Vollbild
Ein Plugin darf eine ganze Unter-Anwendung als Assets mitbringen und in
einem Kind-iframe seines eigenen Asset-Pfads ausführen — so bettet das
`drawio`-Plugin den echten draw.io-Editor ein. Das `excalidraw`-Plugin
zeigt die andere Spielart derselben Idee: eine npm-React-Bibliothek,
per esbuild direkt in die `plugin.js` gebündelt und im Frame selbst im
Vollbild gemountet, mit ihren Laufzeit-Assets (Fonts, Sprachen) im ZIP.
Kombiniere das mit `host.ui.enterFullscreen()` für Editoren, die den
ganzen Bildschirm brauchen. Größen-Limits: 64 MiB ZIP, 256 MiB
entpackt.
### Entwickeln und ausliefern
Die Referenz-Plugins unter `packages/plugins/` sind die Vorlagen —
kopiere das nächstliegende. Jedes hat ein `build.mjs`, das
`src/plugin.ts` mit esbuild bündelt und das installierbare ZIP nach
`dist/` packt:
```sh
cd packages/plugins/<dein-plugin>
pnpm build # → dist/<id>-<version>.zip
```
Installiere das ZIP über Admin → Plugins (oder die Dropzone), öffne die
gesandboxte Vorschau unter `/admin/plugins/<id>/preview`, iteriere. Das
Install-Gate validiert Struktur, Manifest, Größe und CSS-Scoping und
lehnt mit einem präzisen Fehlercode ab. Ein Update veröffentlichen =
gleiche ID, höhere Version.
Konventionen, die im Review durchgesetzt werden: UI-Strings über die
`i18n/`-Dateien des Plugins in **beiden** Sprachen `de` und `en`; der
Fallback muss in einem gedruckten Dokument Sinn ergeben.
## Am Kern arbeiten
### Der Stack auf einen Blick
TypeScript-Monorepo (pnpm-Workspaces): `apps/api` (NestJS + Prisma,
PostgreSQL), `apps/collab` (Hocuspocus/Yjs-Echtzeit-Server), `apps/web`
(React + Vite + TipTap), `apps/backup` (Backup-Sidecar),
`packages/shared` (Typen, Schemas, Editor-Schema, i18n-Kataloge),
`packages/plugin-sdk`, `packages/plugins/*`.
Architektur-Entscheidungen liegen in
[`docs/architecture/adr/`](../../architecture/adr/) — lies das
zutreffende ADR, bevor du ein Subsystem anfasst; `docs/architecture/`
hat die Tiefenbohrungen (Berechtigungen, Datenmodell,
Echtzeit-Zusammenarbeit, Sicherheit, Plugins; alles englisch).
### Eine Entwicklungsumgebung bekommen
```sh
pnpm install
cd deploy/compose && cp .env.example .env
# Voll containerisierter Dev-Stack (Hot Reload; der erste Start installiert die Abhängigkeiten):
docker compose -f docker-compose.yml -f compose.dev.yml up
# → web http://localhost:5173, api :3001, db :5434
```
Schnellstes Feedback: nur die Datenbank in Docker, web/api nativ — das
genaue Rezept steht am Kopf von
[`deploy/compose/compose.dev.yml`](../../../deploy/compose/compose.dev.yml).
Fixture-Nutzer/-Teiche einspielen mit
`pnpm --filter @dorfteich/api db:seed` (Fixture-Passwort: siehe
`apps/api/prisma/seed.ts`).
### Die Gates, die jede Änderung bestehen muss
```sh
pnpm lint # ESLint + Prettier — keine Pipes, die Exit-Codes verschlucken
pnpm typecheck
pnpm test # überall vitest; DB-gestützte Suiten brauchen TEST_DATABASE_URL
pnpm i18n:check # jeder UI-String in de UND en
```
DB-gestützte Tests laufen gegen die Compose-Dev-Datenbank:
`TEST_DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich pnpm test`.
Playwright-e2e-Packs liegen in `apps/web/e2e/` und laufen gegen einen
lokal geseedeten Stack (das genaue Rezept: `.gitea/workflows/ci.yml`).
### Hausregeln, die du vor deinem ersten PR kennen solltest
- **Berechtigungen**: Beantworte eine Zugriffsfrage nie außerhalb des
`PermissionService`/der Routen-Dekoratoren; verweigertes Lesen ist
404, verweigertes Schreiben auf Lesbarem ist 403
([permissions.md](../../architecture/permissions.md)).
- **i18n**: keine hart codierten UI-Strings; Keys nach
`packages/shared/i18n/{de,en}/…` (ADR 0012).
- **Keine Anfragen an Dritte** aus dem Produkt, niemals — Schriften,
Editoren, alles wird selbst gehostet ausgeliefert (ADR 0016 setzt den
Präzedenzfall).
- **Migrationen**: additiv und innerhalb eines Minor-Releases
umkehrbar; das QA-Gate der Release-Pipeline spielt ein Upgrade vom
vorherigen Release gegen echte Daten nach.
- **Verträge dokumentieren**: Alles, was zwei Services teilen
(Status-Dateien, NOTIFY-Kanäle, Wire-Typen), lebt in
`packages/shared` mit einem Kommentar, wer liest und wer schreibt.