Das JavaScript-Ökosystem ist damit beschäftigt, erfundene Probleme zu lösen

Veröffentlicht: 14. Aug. 2026

Ich habe das JavaScript-Ökosystem satt.

Mir ist egal, wie viele Tools ein Projekt verwendet. Nehmt eins. Nehmt zwanzig. Nehmt hundert, wenn sie die Arbeit erleichtern.

Mich interessiert, ob sie zusammenarbeiten.

Die meisten tun das nicht. Manche funktionieren kaum für sich allein.

Jedes Tool kümmert sich um einen winzigen Teil der Anwendung und tut so, als gäbe es den Rest nicht. Der Bundler hat seine eigene Vorstellung vom Projekt. Der Typechecker hat eine andere. Der Testrunner noch eine andere. Der Linter parst dieselben Dateien noch einmal. Der Dev-Server erfindet eine zweite Runtime. Das Deployment-Tool liest die Hälfte seiner eigenen Konfiguration und überlässt mir die andere Hälfte.

Dann installiere ich Plugins, damit die Tools voneinander wissen.

Ein Plugin für ein Plugin, damit ein Tool die Konfiguration eines anderen Tools lesen kann, ist keine Architektur. Es ist eine Entschuldigung.

Ich habe es so verdammt satt, die Lücken zwischen Tools zu flicken.

Wrangler ist ein gutes Beispiel

Ich habe vor Kurzem an einem Cloudflare-Projekt mit Wrangler gearbeitet.

Wrangler hat eine Konfigurationsdatei. Prima. Genau das will ich.

Cloudflare empfiehlt sogar, diese Datei als maßgebliche Quelle für die Worker-Konfiguration zu behandeln.1 Sehe ich genauso.

Warum kann ich damit dann nicht die ganze Anwendung konfigurieren?

Meine Anwendung verwendet mehrere Queues. Die Worker-Konfiguration enthielt das Queue-Binding. Trotzdem musste ich die Queue manuell mit der Wrangler-CLI erstellen. Wrangler kann die Queue also erstellen, hat nur keine Lust, dafür die Konfiguration zu verwenden.

Erst Infrastruktur über einen weiteren Befehl oder das Dashboard erstellen. Dann ihren Namen oder ihre ID in die Konfiguration kopieren. Dann hoffen, dass die externe Ressource und die lokale Datei weiterhin übereinstimmen.

Was ist hier die maßgebliche Quelle?

Die Konfiguration weiß, dass die Anwendung eine Queue braucht. Sie kennt den Namen des Bindings. Sie kennt den Namen der Queue. Das sind genug Informationen, um sie zu erstellen. Warum mache ich den letzten Schritt manuell?

Cloudflare hat inzwischen die automatische Bereitstellung mehrerer Ressourcen, darunter Queues, als Beta-Funktion hinzugefügt. Die offizielle Queue-Anleitung beschreibt weiterhin, dass man zuerst eine Queue erstellt und danach ihr Binding hinzufügt.2 Genau diese Lücke meine ich. Die Deklaration und der Lebenszyklus der Ressource wurden beim Entwurf als getrennte Probleme behandelt. Die Integration kam später als weitere Funktion dazu. Es sollte keine schicke neue Funktion sein, dass die Konfiguration meines Cloudflare-Projekts mein Cloudflare-Projekt konfigurieren kann.

Beim Routing war es in meinem Projekt noch schlimmer.

Die Routen standen bereits in der Wrangler-Konfiguration. Das Deployment verwendete sie. Die lokale Entwicklung bildete das Routing-Verhalten, von dem meine Anwendung abhing, nicht nach. wrangler dev startete einen Server. Es machte aus seinen eigenen Deployment-Routen keine lokalen Anwendungsrouten. wrangler dev funktioniert nur gut, wenn man nur einen Worker hat. Tja, mein Projekt hatte 5 Worker auf derselben Domain. Das kann Wrangler nicht.

Also schrieb ich meinen eigenen Dev-Server für ein Cloudflare-Projekt.

Ich musste Wranglers Einstellungen selbst lesen und das fehlende Routing über einen Proxy um Wrangler herum implementieren. Das Tool hatte die Konfiguration. Das Tool hatte einen Dev-Modus. Das Tool hatte einen Deployment-Modus. Trotzdem ergaben diese Teile keinen zusammenhängenden Workflow.

Das meine ich, wenn ich sage, dass ein Tool nicht funktioniert. Mir ist egal, ob ein Befehl startet und erfolgreich endet. Der Workflow muss funktionieren.

Eine Konfigurationsdatei sollte die Anwendung konfigurieren. Ein Dev-Modus sollte diese Konfiguration lokal ausführen. Ein Deployment-Modus sollte dasselbe Modell verwenden. Das Deployment sollte idempotent sein.

Das kommt mir schmerzhaft offensichtlich vor.

Das JavaScript-Ökosystem besteht aus Konfigurationsinseln

Wrangler ist ein Beispiel. Dasselbe Muster findet sich überall.

Der Bundler versteht einen Import dank eines Plugins. Der Linter meldet, dass sich derselbe Import nicht auflösen lässt.

Der Dev-Server liefert ein Modul aus. Der Production-Build schreibt es anders um.

Ein Framework generiert die Dateien für das Deployment. Das Plattform-Tool generiert eine weitere Konfiguration für diese generierten Dateien. Die ursprüngliche Konfiguration verweist jetzt auf eine generierte Konfiguration, die auf generierte Dateien verweist.

Prima.

Jedes Tool hat seinen eigenen Parser, Modulresolver, Abhängigkeitsgraphen, Cache, sein eigenes Konfigurationsformat, seine eigene Plugin-API und seine eigenen Fehlermeldungen. Jedes Tool baut sein eigenes unvollständiges Modell derselben Anwendung.

Das Projekt hat kein gemeinsames Modell. Es hat ein Komitee.

Und ich bin die Integrationsschicht.

Die frühen Tools waren in Ordnung

JavaScript hatte am Anfang nicht die Probleme von heute.

Frühes JavaScript bestand aus kleinen Skripten im Browser. Es gab kein standardisiertes Modulsystem. Browser implementierten Funktionen unterschiedlich schnell. Netzwerkverbindungen waren langsam. Weniger Bytes auszuliefern war sehr wichtig.

Die Tools für diese Probleme ergaben Sinn.

Ein Minifier verkleinerte den Download. Ein Tool zum Zusammenfügen kombinierte Skripte. Ein Transpiler übersetzte neuere Syntax für ältere Browser. Ein Modulbundler machte CommonJS oder andere Modulsysteme im Browser nutzbar. Ein Linter fand Fehler in einer Sprache, die sehr viel durchgehen ließ.

Das waren gute Tools für echte Probleme.

Dann wurden JavaScript-Anwendungen größer. Die Tools wuchsen mit. Sie wuchsen in die Breite.

Babel bekam Plugins und Presets. Bundler bekamen Loader und Plugins. Testrunner bekamen Transformationen. Linter bekamen eigene Parser. Frameworks bekamen Adapter. Dev-Server bekamen ihre eigenen Modulgraphen. Deployment-Tools bekamen Framework-Integrationen.

Jedes Tool lernte gerade genug über benachbarte Tools, um die Naht zu verstecken. Die Naht blieb.

Das Ökosystem machte das Problem immer weniger sichtbar. Das grundlegende Problem hat es nie behoben.

Jetzt braucht jedes Paar von Tools einen Vertrag.

Ein TypeScript-Plugin für den Bundler. Einen TypeScript-Parser für den Linter. Eine Transformation für den Testrunner. Ein Resolver-Plugin für Aliase. Einen Framework-Adapter für die Deployment-Plattform. Ein Plattform-Plugin für den Dev-Server.

Der Anwendungscode sitzt unter all dem und wartet darauf, dass die Erwachsenen aufhören zu streiten.

Die fehlende Integration hat schon einen Namen

Deshalb muss ich über Compiler reden.

Das JavaScript-Ökosystem entdeckt Compilerarbeit immer wieder neu, verteilt auf separate Pakete. Für die fehlende Integration gibt es bereits eine bewährte Form. Wir arbeiten seit mindestens einem halben Jahrhundert an diesen Problemen.

Ein Compiler übersetzt ein Programm von einer Darstellung in eine andere. Maschinencode ist ein häufiges Ziel. Ein Compiler kann auch Assemblercode, Bytecode, WebAssembly, JavaScript, SQL oder eine andere Zwischendarstellung ausgeben.

Die kurze Definition lautet Übersetzung. Für diese Diskussion ist das gemeinsame Programmmodell entscheidend.

Eine klassische Compiler-Pipeline sieht ungefähr so aus:

scan

parse

resolve and check

lower

optimize

emit

link

Source text

Tokens

AST

Checked program

IR

Optimized IR

Target code

Final program

Der Scanner, auch Lexer genannt, wandelt Zeichen in Tokens um.

Der Parser macht aus diesen Tokens einen abstrakten Syntaxbaum, meist AST genannt. Der Baum beschreibt die Struktur des Programms.

Die semantische Analyse löst Namen auf und prüft die Bedeutung. Existiert diese Variable? Auf welche Funktion bezieht sich dieser Aufruf? Ist diese Operation für den gegebenen Typ gültig?

Lowering wandelt komplexe Sprachmerkmale in einfachere Darstellungen um. Compiler verwenden oft eine Zwischendarstellung, meist IR genannt, damit spätere Phasen mit einem stabilen Modell statt mit roher Quelltextsyntax arbeiten können.

Compiler-Passes zur Optimierung entfernen toten Code, falten Konstanten, setzen Funktionskörper an Aufrufstellen ein, propagieren Werte und vereinfachen den Kontrollfluss.

Die Codegenerierung gibt die Zieldarstellung aus.

Ein Linker kombiniert getrennt kompilierte Einheiten und löst Referenzen zwischen ihnen auf.34

Das Build-System weiß, welche Eingaben welche Ausgaben erzeugen. Das ermöglicht inkrementelle Builds, parallele Ausführung, Caching und reproduzierbare Ergebnisse.5

Debuginformationen ordnen generierte Anweisungen dem Quelltext zu, den der Entwickler geschrieben hat.

Das können getrennte Tools sein. Trotzdem einigen sie sich auf Formate, Symbole, Abhängigkeitsinformationen, Debugdaten und die Zuständigkeit für jede Phase. Ein einziges Binary ist optional. Ein gemeinsames Modell ist unverzichtbar.

Die Anzahl der Tools ist irrelevant. Die Integration ist das Feature.

JavaScript hat den Compiler umbenannt

Hier ist die Übersetzungshilfe:

Das Web bringt echte Randbedingungen mit. Browser laden URLs. Module können Seiteneffekte haben. Zu den Assets gehören CSS, Bilder, Schriftarten, Worker und WebAssembly. Die Ausgabe kann in Chunks aufgeteilt und später geladen werden.

Diese Details sind wichtig. Die zugrunde liegenden Probleme sind trotzdem alt.

Transpilieren ist Kompilieren

Ein Transpiler übersetzt eine Quellsprache in eine andere Quellsprache.

Babel bezeichnet sich selbst als JavaScript-Compiler. Es parst JavaScript, transformiert das Programm und gibt JavaScript aus.6

Das ist ein Compiler. Dass die Ausgabe lesbarer Quelltext ist, schafft keinen neuen Zweig der Informatik.

Bundling ist Linken mit Web-Ballast

Ein Linker verbindet Referenzen zwischen kompilierten Einheiten.

Ein Bundler folgt Imports, löst Module auf, kombiniert Code, schreibt Modulformate um und gibt eine oder mehrere Dateien aus. Außerdem kümmert er sich um CSS, Bilder und was auch immer logo.svg?component heute bedeuten soll.

Den Web-Ballast gibt es wirklich. Die Kernaufgabe bleibt das Linken.

Tree shaking ist Dead Code Elimination mit einer Pflanzenmetapher

Compiler entfernen seit Jahrzehnten ungenutzten Code.

JavaScript machte die Analyse mühsam, weil Module und Eigenschaftszugriffe Seiteneffekte haben können. Kann dieser Import eine globale Variable verändern? Kann dieser Getter Code ausführen? Kann ein dynamischer Zugriff einen Export erreichen, der ungenutzt aussieht?

Das sind Fragen der Datenfluss- und Seiteneffektanalyse. Rollup selbst beschreibt Tree shaking als eine Form von Dead Code Elimination.7

Wir haben dem Compiler-Pass einen niedlichen Namen gegeben und es so klingen lassen, als hätte das Web ihn erfunden.

Mir ist egal, wer es erfunden hat. Aber bestehende Ideen umzubenennen macht es schwer, aus früheren Implementierungen zu lernen.

Source maps sind Debuginformationen in JSON

Generierter Code sieht selten so aus wie der Quelltext, den ein Entwickler geschrieben hat. Native Toolchains speichern dafür Debuginformationen. JavaScript verwendet Source maps. ECMA-426 standardisiert das Format, mit dem generiertes JavaScript, WebAssembly und CSS auf die ursprünglichen Quellen zurückgeführt werden.8

Dasselbe Problem. Diesmal JSON.

Dann hängen wir fünf isolierte Transformationen hintereinander und beten, dass jedes Tool die Zuordnungen korrekt zusammensetzt.

Noch einmal, mir ist egal, wer es erfunden hat. Und ich sage nicht, dass wir Source maps durch DWARF ersetzen sollten. Ich sage nur, dass das Web aufhören sollte, gelöste Probleme neu zu erfinden.

Plugins sind Compiler-Passes ohne gemeinsamen Compiler

Compiler-Passes funktionieren, weil sie mit vereinbarten Darstellungen arbeiten.

JavaScript-Plugins erhalten oft einen toolspezifischen AST, einen toolspezifischen Modulgraphen, toolspezifische Hooks und toolspezifische Metadaten. Mit einem Plugin für ein Tool kann ein anderes Tool meistens gar nichts anfangen.

Also wird dieselbe Transformation mehrfach implementiert. Oder ein Adapter wandelt das Ergebnis eines Tools in die Eingabe eines anderen um und verliert unterwegs Informationen.

Das ist Compilerarchitektur ohne Architektur.

Umbenennen erschwert uns die Lösung des Problems

Namen sind wichtig, weil sie ein Problem mit bestehendem Wissen verbinden.

Sucht nach “tree shaking” und ihr findet Bundler-Dokumentation. Sucht nach “dead code elimination” und ihr findet jahrzehntelange Compilerforschung zu Liveness, Erreichbarkeit, Reinheit, Seiteneffekten und der Analyse ganzer Programme.

Sucht nach “hot module replacement” und ihr findet Framework-APIs. Sucht nach “incremental compilation” und ihr findet Arbeiten zu Abhängigkeitsgraphen, Invalidierung, Caching und partieller Neuberechnung.

Sucht nach “bundling” und ihr findet Konfigurationsbeispiele. Sucht nach “linking” und ihr findet Symbolauflösung, getrennte Kompilierung, Relokation, Schnittstellen und Linkzeitoptimierung.

Ein neuer Name setzt die Diskussion auf null zurück. Er macht alte Lösungen schwerer auffindbar. Er lässt das Ökosystem einzigartig erscheinen, obwohl es ein altes Problem wiederholt. Ich sage nicht, dass die bestehenden Lösungen perfekt sind. Aber nur wenn wir sie kennen, können wir aus ihnen lernen.

Dann macht die Isolation die neue Lösung schlechter.

Ein Compiler kann einmal parsen und das Ergebnis teilen. JavaScript-Tools parsen dieselbe Datei wiederholt. C und C++ hatten vor Jahrzehnten genau dieselben Probleme. Sie haben sie gelöst.

Eine Compiler-Toolchain kann einen gemeinsamen Abhängigkeitsgraphen verwenden. JavaScript-Tools ermitteln jeweils ihre eigenen Graphen und kommen zu unterschiedlichen Ergebnissen.

Ein Build-System kann eine Aktion anhand deklarierter Ein- und Ausgaben cachen. JavaScript-Tools haben unabhängige Caches und invalidieren sie unterschiedlich.

Eine Toolchain kann ein einziges Modell zur Modulauflösung definieren. JavaScript-Projekte konfigurieren TypeScript, den Bundler, den Testrunner, den Linter, Node.js und die Deployment-Plattform getrennt.

Ein Compiler kann Compiler-Meldungen und Debuginformationen über Phasen hinweg zusammenführen. JavaScript gibt mir einen Fehler im Editor, einen anderen im Testrunner und einen dritten nach dem Deployment.

Die grundlegende Lösung ist peinlich offensichtlich.

Schafft ein gemeinsames Projektmodell. Verwendet einen Modulgraphen. Teilt Syntaxbäume oder eine stabile IR, wo es sinnvoll ist. Definiert die Ziel-Runtime einmal. Lasst Entwicklung, Tests, Build und Deployment dieselben Einstellungen verwenden. Gebt jeder Phase eine klare Zuständigkeit.

Wir wissen bereits, wie das geht.

Das Ökosystem entscheidet sich immer wieder für noch ein Plugin.

TypeScript hat ein Programm, das verschwindet

TypeScript macht diese ganze Geschichte interessanter.

Es fügt vielen Projekten ein weiteres Tool und eine weitere Compilerkonfiguration hinzu. Sein Sprachdesign gibt Library-Autoren aber auch eine seltene Möglichkeit, Programme zu verbinden, ohne dass sie dieselben Daten zur Laufzeit mitführen müssen.

Eine TypeScript-Datei enthält zwei miteinander verbundene Programme.

Ein Programm wird zu JavaScript. Es enthält Werte, Objekte, Strings, Arrays, Funktionen, Klassen und Seiteneffekte.

Das andere Programm existiert nur, während TypeScript den Quelltext prüft. Es enthält Typen, generische Parameter, Conditional Types, Mapped Types, Literaltypen, indizierte Zugriffe, keyof und typeof auf Typebene.

TypeScript modelliert Werte und Typen als getrennte Namensräume. Manche Deklarationen erzeugen einen Wert. Manche erzeugen einen Typ. Klassen erzeugen beides.9

Dann löscht der Compiler das Typprogramm.

Interfaces verschwinden. Typannotationen verschwinden. Generische Argumente verschwinden. Reine Typimports verschwinden. Sie können keine Laufzeitprüfungen durchführen, weil sie nicht existieren, wenn das Programm läuft.10

Dass statische Typen zur Laufzeit verschwinden, ist nichts Einzigartiges. Interessant ist, wie weit TypeScript diese Trennung treibt und dabei eine Sprache für JavaScript-Libraries bleibt.

Eine Library kann Type-Inference für generische Typen und Berechnungen auf Typebene verwenden, um Beziehungen zwischen Aufrufen zu modellieren. Der Editor kann dieses Typprogramm laufend ausführen und Autovervollständigung anbieten. Der Compiler kann eine ungültige Abfolge zurückweisen. Dann kann dieser ganze Apparat aus dem ausgegebenen JavaScript verschwinden.

Die Runtime braucht nicht immer eine Kopie der Informationen, die der Compiler verwendet hat. Manchmal braucht nur der Compiler sie, um nachzuweisen, dass das Runtime-Programm gültig ist.

Dieser Unterschied ist wichtig.

Viele TypeScript-Libraries halten neben dem Typzustand auch einen Schemawert zur Laufzeit vor. tsql funktioniert ausdrücklich anders.

Diese Trennung hat einen klaren Nachteil. TypeScripts Typen können lügen.

const strArray = ["a", "b", "c"];

function addNumber(arr: Array<string | number>) {
	arr.push(1);
}

addNumber(strArray);
//        ^? string[]
// TypeScript says that `strArray` stays a string array but at runtime we add a number to it.
// It became an array of strings and numbers without any warning, error, or type change.

for (const str of strArray) {
	console.log(str.toUpperCase());
	// throws at runtime because a number does not have `.toUpperCase()`
}
[LOG]: "A"
[LOG]: "B"
[LOG]: "C"
[ERR]: "Executed JavaScript Failed:"
[ERR]: str.toUpperCase is not a function

Das Typprogramm ist nicht das Runtime-Programm. Dadurch kann eine Lücke zwischen ihnen entstehen.

Oder es kann der Runtime Arbeit vollständig abnehmen.

tsql hat drei Programme

Beim Bau von tsql habe ich das übliche TypeScript-SQL-Modell umgedreht.

Die App-Runtime kennt das Datenbankschema nicht.

Sie lädt kein Schemaobjekt zur Laufzeit. Sie schleppt den Migrationsverlauf nicht in die Request-Verarbeitung mit. Sie prüft eine SQL-Query nicht noch einmal zur Laufzeit anhand von Schemametadaten.

Der Compiler kennt das Schema.

Genau darum geht es. Codegenerierung vollständig loswerden.

Tsql hat drei Programme mit unterschiedlichen Aufgaben:

erased by TypeScript

erased by TypeScript

Type-level program

migration 001: Schema0 → Schema1

migration 002: Schema1 → Schema2

queries: Schema2 → proven queries

Application runtime

format and execute SQL

without a schema

Migration program

apply migrations in CI

step by step

Die Tools sind getrennt. Sie sind nicht isoliert.

Das Programm auf Typebene ist der Vertrag zwischen den anderen beiden.

Programm eins: die Typebene

Dieses Programm läuft im Editor und im TypeScript-Compiler.

Es bietet Autovervollständigung. Es prüft die Query-Struktur, Tabellennamen, Spaltennamen, Eingabewerte und Ergebnistypen. Es weist nach, dass eine Query für das durch die Migrationen erzeugte Schema gültig ist.

Die Migrationskette ist außerdem ein Zustandsautomat auf Typebene.

Vom Prinzip her sieht das so aus:

migration 001

migration 002

migration 003

Schema0 = {}

Schema1

Schema2

Schema3

Jede Migration erhält das Schema, das unmittelbar vor ihr existierte.

Migration 003 sieht nicht das endgültige Schema. Sie sieht genau das, was die Migrationen 001 und 002 erzeugt haben.

Erstellt eine Tabelle, und sie existiert im Typ der nächsten Migration. Benennt eine Spalte um, und der alte Name verschwindet aus dem nächsten Schritt. Löscht eine Tabelle, und spätere Migrationen können sie nicht mehr verwenden. Versucht etwas zu ändern, das noch nicht existierte, und TypeScript weist die Migration zurück.

Für jeden Zeitpunkt im Migrationsverlauf gibt es ein vollständig typisiertes Schema.

Der endgültige Schematyp wird dann verwendet, um die SQL-Queries der Anwendung zu prüfen. Der Editor kann die vorhandenen Tabellen und Spalten vervollständigen. Der ausgewählte Ergebnistyp lässt sich aus der Query berechnen. Ein falsch geschriebener Spaltenname oder ein inkompatibler Wert führt zu einem Fehler, bevor die Anwendung startet.

Das ähnelt Typestate. Der Typ hält den aktuellen Zustand fest und steuert, welche Operationen als Nächstes gültig sind.11

Entscheidend ist, dass dieses Schema ein Typ ist.

Es ist ein Nachweis, den der Compiler verwendet. Es sind keine Metadaten, die die App-Runtime braucht.

Nachdem TypeScript das Programm geprüft hat, verschwindet der Schematyp.

Programm zwei: die App-Runtime

Die Runtime erhält die Queries, die die Typprüfung bestanden haben.

Sie formatiert sie als SQL. Sie erstellt die gebundenen Parameter. Sie sendet beides an die Datenbank.

Das tut sie blind.

Es gibt kein Nachschlagen im Schema zur Laufzeit. Es gibt keine Query-Validierung anhand von Migrationsmetadaten. Es gibt keine Datenbankintrospektion zur Laufzeit. Es läuft keine zweite Version des Typecheckers in Produktion.

Die Typebene hat diese Arbeit bereits erledigt. Die Runtime vertraut ihr.

Natürlich kann TypeScript nicht beweisen, dass der Datenbankserver online ist, dass ein Unique-Constraint keinen Wert zurückweist oder dass niemand die Produktionsdatenbank von Hand geändert hat. Der Nachweis ist enger gefasst, aber nützlich. Wenn die Migrationskette angewendet wurde, ist die Query strukturell und hinsichtlich ihrer Typen für das dadurch erzeugte Schema korrekt.

Das reicht, um viel Runtime-Logik zu entfernen.

Die App-Runtime braucht nur die Query selbst und die zu sendenden Werte. Sie braucht nicht das Schema, mit dem die Korrektheit der Query nachgewiesen wurde.

Migrationen existieren in diesem Programm nicht.

Programm drei: das Migrationsprogramm

Migrationen laufen größtenteils unabhängig von der App-Runtime.

Normalerweise läuft dieses Programm in CI oder während des Deployments. Es nimmt die vorhandenen Migrationen und wendet sie Schritt für Schritt auf die Datenbank an.

Die Migrationen werden hier nicht zum ersten Mal geprüft. Das Programm auf Typebene hat jede Migration bereits anhand genau des Schemazustands geprüft, der vor ihr existierte.

Das Migrationsprogramm führt nur die nachweislich gültigen Schritte aus.

Je nach Hosting-Plattform könnt ihr die Migrationen beim Start des Backends oder in CI ausführen. Bei Edge Computing wie Cloudflare Workers gibt es keinen Backend-Start, also läuft das Migrationsprogramm in CI. Die App-Runtime muss nicht für die Ausführung der Migrationen zuständig sein, kann es aber.

Diese Trennung ist beabsichtigt:

TypeScript compiler

proves every migration transition

proves every query against the final schema

CI migration program

makes the database match those proven transitions

Application runtime

formats and executes the proven queries

Drei Programme. Drei Zuständigkeiten. Ein Vertrag.

Tsql ist sogar egal, mit welcher Datenbank es spricht. Es will nur etwas, dem es SQL-Strings schicken kann und von dem es Ergebnisse zurückbekommt. Das Projekt liefert eigene Adapter für SQLite, MySQL und PostgreSQL mit. Andere SQL-Datenbanken lassen sich mit einem eigenen Adapter unterstützen. Das sollte nicht mehr als ein paar Minuten dauern.

So sollte Integration aussehen.

Integration bedeutet nicht, alles in einen Prozess zu stecken. Sie bedeutet nicht, jede Phase ein riesiges Konfigurationsobjekt laden zu lassen. Sie bedeutet, dass die Phasen sich darüber einig sind, was sie austauschen, und dass jede Phase für ihren Teil zuständig ist.

Die Typebene verbindet Migrationen und Queries, ohne ihre Runtime-Programme zu koppeln. Das Migrationsprogramm ändert die Datenbank. Die App-Runtime verwendet die Datenbank. Keines von beiden muss den Nachweis des Compilers neu implementieren.

Das ist etwas ganz anderes als der übliche Haufen Tools im JavaScript-Ökosystem. Dort baut jedes Tool ein unvollständiges Projektmodell neu auf und versucht hinterher, das benachbarte Tool zu überprüfen.

In tsql beseitigt die Trennung doppelte Arbeit. Isolation würde sie zurückbringen.

Wie man TypeScripts Trennung ausnutzen kann

Die nützliche Erkenntnis aus tsql ist, dass man Besonderheiten wie TypeScripts geteiltes Programmmodell immer als Vorteil nutzen kann. Ich höre Entwickler meistens darüber klagen, wie seltsam TypeScript ist und dass es nicht typsicher ist und so weiter. Stimmt. Aber habt ihr euch mal die Vorteile angesehen?

Wir kommen vorerst nicht von TypeScript weg. Darüber zu debattieren bringt nichts. Konzentriert euch auf die Vorteile, die es mitbringt.

TypeScript ist anders. Deshalb ist es eine gute Idee, besondere Tools zu bauen, die in anderen Sprachen wie C#, Java oder Go buchstäblich unmöglich wären.

Ich habe versucht, tsql nach C# zu portieren, aber es war unmöglich.

Ich will wieder Toolchains

Ich will ein Modell zur Modulauflösung. Einen Abhängigkeitsgraphen. Eine Definition der Ziel-Runtime. Eine zuständige Stelle für jede Compiler-Meldung.

Die Tools können getrennt bleiben. Sie brauchen gemeinsame Verträge und einen gemeinsamen Zustand.

Wir haben die Begriffe bereits. Wir haben die Forschung bereits. Wir haben bereits bewährte Architekturen.

Hört auf, alte Compilerarbeit umzubenennen. Hört auf, jede Phase isoliert neu zu bauen. Hört auf, noch einen Adapter auszuliefern und die Naht ein Ökosystem zu nennen.

Behaltet die Tools. Bringt sie zum Laufen.

Ich will Software schreiben, die echte Probleme löst, keine erfundenen.

Fußnoten

  1. Cloudflare, “Configuration - Wrangler”. Die Dokumentation empfiehlt, die Wrangler-Konfigurationsdatei als maßgebliche Quelle für die Worker-Konfiguration zu behandeln. Abgerufen am 14. Aug. 2026.

  2. Cloudflare dokumentiert die automatische Bereitstellung derzeit als Beta-Funktion für Ressourcen, darunter Queues. Die Einstiegsanleitung für Queues beschreibt weiterhin wrangler queues create vor dem Hinzufügen des Bindings. Abgerufen am 14. Aug. 2026.

  3. LLVM Project, “Clang Compiler User’s Manual: Terminology”. Abgerufen am 14. Aug. 2026.

  4. GNU Binutils, “The GNU linker: Overview”. Abgerufen am 14. Aug. 2026.

  5. Bazel, “Hermeticity”. Abgerufen am 14. Aug. 2026.

  6. Babel, “What is Babel?”. Abgerufen am 14. Aug. 2026.

  7. Rollup, “What is tree-shaking?”. Abgerufen am 14. Aug. 2026.

  8. Ecma International, “ECMA-426 Source Map Format”. Abgerufen am 14. Aug. 2026.

  9. TypeScript, “Declaration Merging: Basic Concepts”. Abgerufen am 14. Aug. 2026.

  10. TypeScript, “The Basics: Erased Types”. Abgerufen am 14. Aug. 2026.

  11. Robert E. Strom und Shaula Yemini, “Typestate: A Programming Language Concept for Enhancing Software Reliability”, IEEE Transactions on Software Engineering, 1986.