ADR-004: Monorepo mit Bun Workspaces

Warum Owlat ein Monorepo mit Bun Workspaces und Turborepo einsetzt — für schnelle Installationen, atomare paketübergreifende Änderungen und gecachte CI.

  • Status: Angenommen
  • Datum: 2024-06-15

Kontext

Owlat besteht aus mehreren miteinander verbundenen Teilen: einem Nuxt-Frontend, einem Convex-Backend, einer Dokumentationsseite, einer E-Mail-Builder-Komponente, einer E-Mail-Renderer-Bibliothek, gemeinsamen Typen und veröffentlichbaren SDKs. Diese Packages teilen sich Typen und Utilities in großem Umfang — so hängen etwa sowohl der E-Mail-Builder als auch der E-Mail-Renderer für die Block-Typdefinitionen von @owlat/shared ab.

Die wesentlichen Optionen:

  1. Getrennte Repositories — jedes Package in einem eigenen Repo, gemeinsamer Code über veröffentlichte npm-Packages. Saubere Grenzen, aber langsame Iteration: Die Änderung eines gemeinsamen Typs erfordert Veröffentlichen, Versionsanhebungen und das Aktualisieren der Consumer.
  2. Monorepo mit npm-/pnpm-Workspaces — Code an einem Ort, Abhängigkeiten per Symlink. Gut etabliert, aber langsamere Installationen und komplexeres Tooling.
  3. Monorepo mit Bun Workspaces — dasselbe Workspace-Modell, aber mit Buns schneller Installation, nativer TypeScript-Ausführung und Turbo für die Task-Orchestrierung.

Entscheidung

Ein Monorepo mit Bun Workspaces und Turborepo für die Task-Orchestrierung einsetzen. Alle Apps und Packages liegen in einem einzigen Repository unter apps/ und packages/.

Struktur:

apps/web        — @owlat/web (Nuxt frontend)
apps/api        — @owlat/api (Convex backend)
apps/docs       — @owlat/docs (Nuxt + @nuxt/content)
packages/shared — @owlat/shared (types, utilities)
packages/email-builder   — @owlat/email-builder
packages/email-renderer  — @owlat/email-renderer
packages/sdk-js          — @owlat/sdk-js
packages/ui              — @owlat/ui

Dies ist ein repräsentativer Ausschnitt; das Repo enthält darüber hinaus weitere Apps (apps/mta, apps/marketing, apps/imap, apps/updater, apps/setup-cli und mehr) sowie weitere Packages. Zu beachten: packages/sdk-java ist ein Java-SDK und wird explizit aus den Bun Workspaces ausgenommen (!packages/sdk-java in workspaces.packages der Root-package.json).

Konsequenzen

Ermöglicht:

  • Atomare paketübergreifende Änderungen — eine Typänderung in shared und ihre Consumer gehen in einem einzigen PR raus
  • Schnelle Installationen — Buns Linker ist bei der Workspace-Auflösung deutlich schneller als npm/pnpm
  • Turbo-Caching — Build-Ausgaben werden gecacht (der Task build in turbo.json deklariert outputs), sodass bun run build unveränderte Packages überspringt. Tests laufen ungecacht: Der Task test setzt "cache": false.
  • Eine einzige CI-Pipeline — .github/workflows/test.yml (Workflow-Name „CI“) führt alle Workspace-Tests aus, über eine Matrix pro Workspace, die bun run --cwd <workspace> test:coverage für jeden Workspace unabhängig aufruft
  • Gemeinsames Tooling — eine oxlintrc.json, eine oxfmtrc.json, eine Turbo-Konfiguration
  • Changesets für die SDK-Veröffentlichung — @changesets/cli verwaltet die Versionierung der veröffentlichbaren Packages

Abwägungen:

  • Alle Mitwirkenden benötigen eine Bun-Installation (nicht nur Node)
  • Große bun.lock-Datei, die bei Abhängigkeitsänderungen Merge-Konflikte verursachen kann
  • Turbo fügt eine Build-Orchestrierungsschicht hinzu, die neue Mitwirkende verstehen müssen
  • Die CI führt alle Workspace-Prüfungen aus, selbst bei Änderungen an einem einzelnen Package. Build-Caching hilft, doch Tests sind ungecacht; der Test-Job dämpft die Kosten mit einer Matrix pro Workspace in .github/workflows/test.yml, sodass Workspaces parallel statt sequenziell laufen