dorfteich/docs/developer/extending.md
Claude Fable 5 baebd79cc8
All checks were successful
CD / Build and push images (push) Successful in 1m10s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m6s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m35s
CI / Import/export fidelity gate (push) Successful in 48s
German translations of the seven user-facing docs under docs/de/
Mirrors the English tree (docs/de/{features.md,manual/*,developer/
extending.md}) so relative links between translated guides resolve
within the German set; links into untranslated areas (self-hosting,
architecture, deploy) point at the English files and say so. Every
quoted UI label matches the actual German interface strings. Each
pair of files cross-links the other language; English stays
authoritative when the two diverge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-12 19:28:50 +02:00

157 lines
6.7 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. 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.