Skip to content

0016 — Initialisierung als geprüfter Plan ​

Status: angenommen · Milestone: M9

Kontext ​

Aus dem Template wird ein Kundenprojekt. Dabei ändert sich mehr als eine Datei: die Projektkonfiguration, die Paketdatei, die Startseite, das Asset-Manifest, die Workflows, die Testkette. Ein Skript, das das der Reihe nach erledigt, hat zwei Eigenschaften, die beide schlecht sind — es lässt sich vorher nicht ansehen, und wenn es in der Mitte scheitert, steht man mit einem Projekt da, das weder Template noch Kundenprojekt ist.

Dazu kommt eine Besonderheit dieses Repositories: Die Projektkonfiguration ist validiert. Ein Init, der eine project.config.ts schreibt, an der der nächste Build scheitert, hätte genau die Prüfung umgangen, für die es sie gibt.

Entscheidung ​

pnpm starter:init läuft in vier getrennten Schritten:

  1. Antworten sammeln — im Dialog oder aus --answers datei.json. Geprüft gegen ein Zod-Schema (src/lib/starter/answers.ts).
  2. Konfiguration bauen — buildConfigObject() bildet Antworten auf eine ProjectConfigInput ab, rein und ohne Seiteneffekt.
  3. Gegen das echte Schema prüfen — projectConfigSchema.safeParse(). Dasselbe Schema, das auch beim Bauen greift, nicht eine Kopie davon. Schlägt es fehl, bricht der Init ab, bevor eine Datei angefasst wurde.
  4. Plan erstellen und ausführen — planInit() liefert eine Liste von Aktionen (write, transform, copy, remove). --dry-run gibt sie aus, applyInit() führt sie aus. Das ist die einzige Stelle mit Schreibzugriff.

Der Quelltext der project.config.ts entsteht aus demselben Objekt, das die Prüfung bestanden hat (renderConfigSource()). Ein zweiter Weg — Textersetzung in einer Vorlage etwa — könnte gegenüber der Prüfung abdriften, und dann wäre geprüft worden, was gar nicht geschrieben wird.

Der Init entfernt so wenig wie möglich: Demo-Startseite und Demo-Bilder, die Demo-Prüfungen, den nicht gewählten Deployment-Workflow und — nur in Projekten ohne Standalone-PHP-Backend — den Handler samt seinen Tests. docs/, CHANGELOG.md und die Release-Please-Dateien bleiben liegen.

Git bleibt unangetastet: keine Commits, keine Remotes, keine Tags. Was der Init getan hat, steht danach als ein git diff da.

Begründung ​

Warum ein Plan und kein Ablauf. Ein Plan lässt sich ausgeben, im Test befragen und gegen das Arbeitsverzeichnis halten. tests/integration/starter-init.test.ts prüft, dass jeder Pfad darin tatsächlich existiert — wer eine Demo-Seite umbenennt, erfährt es dort und nicht in dem Kundenprojekt, in dem der Init dann stillschweigend nichts entfernt. Dieselbe Überlegung wie bei der .htaccess und beim Deployment-Plan (ADR 0012, ADR 0014): Was sich ansehen lässt, lässt sich prüfen.

Warum so wenig gelöscht wird. Ein Kundenprojekt lebt Jahre und soll Verbesserungen aus dem Starter übernehmen können. Jede gelöschte Datei ist beim Zusammenführen einer neueren Starter-Version ein Konflikt oder eine stillschweigend zurückkehrende Datei. docs.yml und release-please.yml prüfen ohnehin github.repository und laufen in einem abgeleiteten Repository nie — sie liegen zu lassen kostet nichts und erspart Konflikte. meta.starterVersion hält fest, von welchem Stand ein Projekt kommt.

Warum der Styleguide bleibt. Er ist nicht Demo-Material, sondern das Gate: die einzige Seite, auf der jedes Primitive in allen Varianten vorkommt, einschließlich Fehler- und Deaktiviert-Zustand. Ohne ihn prüfte axe nur noch, was zufällig auf einer realen Seite steht.

Warum Node und keine Prompt-Bibliothek. Der Dialog braucht Textfragen, Ja/Nein und eine Auswahl aus wenigen Möglichkeiten. node:readline/promises und node:util.parseArgs können das. Eine Abhängigkeit, die in jedem Kundenprojekt mitläuft, wäre dafür zu viel — und das Skript läuft ohne Übersetzungsschritt, weil Node 24 Typen selbst entfernt.

Verworfene Alternativen ​

Ein Generator wie create-* mit eigenem Paket. Hätte den Starter in zwei Repositories geteilt, die miteinander versioniert werden müssen. Der Init gehört zu dem Stand, aus dem ein Projekt entsteht — er lebt deshalb darin.

Platzhalter im Text ersetzen (__PROJEKTNAME__). Schneller zu schreiben, aber die geschriebene Datei wäre nie gegen das Schema geprüft worden; ein Tippfehler in einer Antwort fiele erst beim ersten Build auf. Außerdem hätte jede Änderung am Konfigurationsschema stillschweigend die Vorlage veralten lassen.

Den Init sich selbst entfernen lassen. Verlockend, weil ein zweiter Lauf gefährlich ist. Dagegen spricht dasselbe Argument wie oben: Gelöschte Dateien erschweren Upgrades. Der zweite Lauf wird stattdessen verhindert — meta.starterVersion ist gesetzt, und der Init bricht ab, solange niemand --force angibt.

Git-Historie neu anlegen. Ein Init, der .git anfasst, kann Arbeit vernichten, die er nicht wiederherstellen kann. Wer eine frische Historie will, legt sie selbst an.

Konsequenzen ​

  • Eine neue Frage im Dialog ist erst fertig, wenn sie in buildConfigObject() ankommt und ein Unit-Test die Ableitung festhält.
  • Wer eine Datei umbenennt, die der Init anfasst, muss planInit() mitziehen — der Integrationstest sagt es sonst.
  • Eine Änderung am Init ist erst fertig, wenn sie in einem Klon durchgespielt wurde. Kein Test im Starter kann sehen, ob ein initialisiertes Projekt noch baut: Der Starter behält ja alles, was das Kundenprojekt verliert. Genau dort saßen beim ersten Durchstich vier Prüfungen, die stillschweigend den Starter festschrieben — darunter ein Styleguide, der an den Bildern der Demo-Startseite hing und jedes frisch initialisierte Projekt sofort scheitern ließ. Das Verfahren steht unter Tests → Der Initialisierungsprozess.
  • Die erzeugte project.config.ts trägt keine Feldkommentare mehr. Was welches Feld bedeutet, steht in der Dokumentation; die Datei nennt dafür ihre Herkunft und die Version des Starters.
  • Die Testkette musste an drei Stellen vom Starter gelöst werden: Der PHP-Durchstich läuft nur mit vorhandenem Handler, die axe-Prüfung leitet ihre Seitenliste aus der gebauten Ausgabe ab, und der Styleguide hat eine eigene Spec bekommen, die ein Kundenprojekt behält (ADR 0004).