Monorepo mit JavaScript: Code-Organisation in der Praxis
In der modernen Webentwicklung mit JavaScript (Node.js) stellt sich immer häufiger die Frage: Wie organisiert man Code effizient, wenn man mehrere Pakete, Bibliotheken oder Microservices hat, die voneinander abhängen? Der traditionelle Ansatz mit separaten Repositories (Multirepo) kann zu Code-Duplizierung, Schwierigkeiten bei der Versionssynchronisation und langen CI/CD-Prozessen führen. Die Lösung ist ein Monorepo. In diesem Artikel schauen wir uns an, was ein Monorepo ist, warum man es braucht und wie man es in der Praxis mit JavaScript organisiert.
Was ist ein Monorepo und warum wird es benötigt?
Ein Monorepo ist ein Ansatz, bei dem der Code mehrerer Projekte (Pakete, Bibliotheken, Dienste) in einem einzigen Repository gespeichert wird. Es geht nicht nur darum, "alles in einen Ordner zu packen", sondern um eine durchdachte Architektur, die viele Probleme löst:
- Einheitliche Abhängigkeitsversionen: Sie vermeiden Situationen, in denen verschiedene Pakete unterschiedliche Versionen derselben Bibliothek verwenden.
- Einfaches Refactoring: Änderungen an einer gemeinsamen Bibliothek sind sofort in allen Projekten sichtbar, die sie verwenden.
- Vereinfachtes CI/CD: Ein Repository – eine Konfiguration für Build und Tests.
- Gemeinsame Codenutzung: Hilfsprogramme, Typen und Konfigurationen stehen allen Paketen ohne Veröffentlichung in npm zur Verfügung.
Allerdings ist ein Monorepo nicht für jeden geeignet. Es erfordert Disziplin im Team und gute Werkzeuge. Für JavaScript (Node.js) sind die wichtigsten Werkzeuge npm workspaces (integriert in npm 7+) und Lerna (ein fortschrittlicheres Werkzeug).
Einrichtung eines grundlegenden Monorepos mit npm workspaces
Beginnen wir mit einem einfachen Beispiel. Angenommen, wir haben ein Projekt, das aus zwei Paketen besteht: @myapp/core (Kern mit Geschäftslogik) und @myapp/server (Server mit Express).
Erstellen wir die Ordnerstruktur:
my-monorepo/├── packages/│ ├── core/│ │ ├── src/│ │ │ └── index.js│ │ ├── package.json│ │ └── ...│ └── server/│ ├── src/│ │ └── index.js│ ├── package.json│ └── ...├── package.json└── ...Das root package.json muss das Feld workspaces enthalten:
{ "name": "my-monorepo", "private": true, "workspaces": [ "packages/*" ], "scripts": { "start": "node packages/server/src/index.js", "test": "npm run test --workspaces" }}Nun in packages/core/package.json:
{ "name": "@myapp/core", "version": "1.0.0", "main": "src/index.js"}Und in packages/server/package.json fügen wir die Abhängigkeit zu core hinzu:
{ "name": "@myapp/server", "version": "1.0.0", "dependencies": { "@myapp/core": "*", "express": "^4.18.0" }}Nach der Ausführung von npm install im root-Verzeichnis erstellt npm Symlinks zwischen den Paketen. Jetzt können wir im Server-Code core direkt importieren:
// packages/server/src/index.jsconst express = require('express');const { calculate } = require('@myapp/core');
const app = express();app.get('/', (req, res) => { res.send(calculate(2, 3));});app.listen(3000);Dies ist ein grundlegendes Beispiel, zeigt aber bereits den Hauptvorteil: Änderungen in @myapp/core sind sofort in @myapp/server sichtbar, ohne dass eine Veröffentlichung in der Registry erforderlich ist.
Fortgeschrittene Organisation mit Lerna
Wenn die Anzahl der Pakete auf mehr als 5-10 steigt, können npm workspaces an ihre Grenzen stoßen. Hier kommt Lerna ins Spiel – ein Werkzeug, das leistungsstarke Funktionen hinzufügt:
- Ausführung von Befehlen in allen oder ausgewählten Paketen.
- Automatische Versionsverwaltung (fixed oder independent mode).
- Pu