12k
All articles

TypeScript mit scriptc zu einem nativen Binary kompilieren

scriptc kompiliert TypeScript zu nativen Binaries, mit statischen Builds, 620-KB-Dynamic-Engine, Coverage-Check und klaren Diagnosen.

OpenReplay Team
OpenReplay Team
TypeScript mit scriptc zu einem nativen Binary kompilieren

scriptc kompiliert gewöhnliches TypeScript zu einer eigenständigen, nativen ausführbaren Datei. Ein statischer Build enthält keine JavaScript-Engine – abgesehen von einem Regex-Interpreter, der nur dann eingebunden wird, wenn Ihr Code reguläre Ausdrücke verwendet. Der echte TypeScript-Compiler prüft die Typen des Programms, scriptc überführt es in eine typisierte Zwischendarstellung (IR), und am Ende kommt nativer Code heraus.

Wer ein CLI in TypeScript ausliefert, kennt den Kompromiss: Das Tool besteht aus 40 KB Logik. Der Auslieferungsmechanismus ist eine 100 MB große Runtime, ein Installationsschritt und eine Startzeit, die dem Nutzer auffällt.

Das Interessante ist nicht das Binary. Andere Tools erzeugen ebenfalls eines, indem sie eine Runtime darin verpacken. scriptc lässt die Engine weg, wo es möglich ist – und sagt Ihnen, welche Teile Ihres Programms es verarbeiten kann und welche nicht. Dieser Artikel behandelt die drei möglichen Ergebnisse, in denen jedes Konstrukt landen kann, und den einen Befehl, der Ihnen verrät, wo Ihr eigener Code einzuordnen ist.

Die wichtigsten Erkenntnisse

  • scriptc kompiliert das TypeScript, das Sie bereits schreiben. Es gibt keinen Dialekt zu lernen, nichts zu annotieren und keine ersetzende Standardbibliothek; die Typprüfung läuft über den echten TypeScript-Compiler.
  • Statische Kompilierung ist der Standard und der einzige Modus, sofern Sie nicht --dynamic übergeben – damit wird quickjs-ng mit rund 620 KB in das Binary eingebettet.
  • Alles, was in keine der beiden Stufen passt, bricht den Build ab. Sie erhalten einen SC-Code, die betreffenden Zeilen und meist einen Vorschlag zum Umschreiben – anstelle eines Binarys, das subtil falsch ist.
  • scriptc coverage liefert ein Urteil pro Statement: welche Teile es in die statische Stufe schaffen, welche Teile die Engine hineinziehen würden, und eine kodierte Diagnose, die jeden Blocker benennt.
  • Die meisten npm-Pakete liefern reines JavaScript plus separate Deklarationsdateien aus. Damit fehlt der statischen Stufe typisierter Quellcode, weshalb reale Abhängigkeitsbäume die eingebettete Engine wieder ins Binary holen.

Was ist scriptc, und wie läuft die Pipeline?

scriptc nimmt einen .ts-Einstiegspunkt, prüft ihn mit dem TypeScript-Compiler auf Typen, überführt das geprüfte Programm in eine typisierte IR und erzeugt daraus nativen Code. Die scriptc-README macht LLVM zum Standard-Codegenerator und behält C als dauerhaft lesbares Referenz-Backend, das Sie mit --backend c auswählen – „TypeScript zu C zu clang” beschreibt also nur einen der beiden Pfade. Der Quellcode, den Sie hineingeben, ist derselbe, den Sie bereits auf Node ausführen.

Die Installation erfolgt als globale npm-Installation, und ausführbare Builds benötigen einen Linker-Treiber auf dem Host:

npm install -g scriptc

Der Quickstart setzt den Compiler auf Node 24 oder neuer voraus. Ausführbare Builds benötigen zudem einen Plattform-Linker und ein passendes SDK oder Sysroot. Platform Support wird beim Rest konkret: Auf unterstützten macOS-, Linux- und Windows-Hosts bindet die LLVM-Stufe ein vorkompiliertes Runtime-Pack ein, sodass ein C-Compiler nur für explizite C-Builds, LLVM-Fallbacks und --sanitize erforderlich ist. Die Quellcode-Ausgabe über --emit=ir|c|llvm benötigt nichts außer Node.

Ein minimales Programm und die beiden entscheidenden Befehle:

// slug.ts
function slug(title: string): string {
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
console.log(slug("Compile TypeScript to a Native Binary"));
scriptc run slug.ts
scriptc build slug.ts -o slug && ./slug

scriptc run kompiliert und führt in einem Schritt aus – genau das, was Sie in einer Watch-Schleife wollen. scriptc build -o erzeugt das Artefakt, das Sie tatsächlich ausliefern.

Stufe 1: Statisch kompiliert – der Standard

Statische Kompilierung ist in scriptc der Standard und der einzige Modus, solange Sie ihn nicht explizit abwählen. Auf der scriptc-Homepage wird Stufe 1 als alltägliches TypeScript präsentiert: Klassen und Closures, async/await, die Standardbibliothek sowie jene Teile von Node, die die meisten Programme nutzen – etwa fs, path, process und http. All das wird zu nativem Code, und das Binary enthält keine Engine.

Die konkrete Oberfläche geht weiter, als eine Schlagzeilenliste vermuten lässt. Die Einführungsseite sortiert sie in drei Gruppen. Auf Sprachseite erhalten Sie Klassen mit Einfachvererbung und dynamischem Dispatch, Closures, die so erfassen, wie JavaScript es tut, generische Funktionsdeklarationen, die per Monomorphisierung aufgelöst werden, Discriminated Unions, die über TypeScripts eigenes Narrowing behandelt werden, async/await mit genau der Scheduling-Semantik von JavaScript, Exceptions mit finally, Destrukturierung, Spread, Accessoren, Iteratoren und Template Literals. Die Gruppe der Standardbibliothek umfasst Strings, Arrays, Map und Set, JSON, Math, Typed Arrays und die Error-Hierarchie. Die Node-Gruppe reicht von fs in der synchronen wie der Promise-Variante über path, process, child_process, os, crypto, url/URL, zlib und Timer bis zum vollständigen Server-Stack: net, http, https, tls, dgram, dns und readline.

Das heißt: Nicht nur eine reine Funktion kompiliert, sondern ein ganzer HTTP-Service:

// server.ts
import http from "node:http";

http.createServer((req, res) => {
  res.writeHead(200, { "content-type": "application/json" });
  res.end(JSON.stringify({ path: req.url }));
}).listen(3000);
scriptc build server.ts -o server && ./server

Jede Auflistung der statischen Oberfläche veraltet, während sich der Compiler weiterentwickelt. Das Changelog verfolgt sie Release für Release, und jedes Release liefert zusätzlich eine maschinenlesbare surface-manifest.json mit der Sprach- und Standardbibliotheks-Oberfläche, die die statische Stufe in dieser Version abdeckt – mit stabilen IDs je Eintrag, sodass Tooling zwei Releases vergleichen kann. Diese Datei überlebt jede Prosaliste, auch diese hier.

Stufe 2: Die dynamische Stufe und ihre 620-KB-Engine

--dynamic bettet eine JavaScript-Engine in das Binary ein – und nichts sonst tut das. Der Leitfaden zu npm-Abhängigkeiten nennt das Ergebnis eine „dynamic island”: eine eingebettete Engine von rund 620 KB, die alles ausführt, was nicht statisch sein kann – in der Praxis das von npm-Paketen ausgelieferte JavaScript und alles, was der Checker als any typisiert. Werte werden validiert, sobald sie zurück in statischen Code übergehen. Die Engine ist quickjs-ng.

npm install picocolors
scriptc build cli.ts --dynamic -o cli

Der entscheidende Designpunkt ist das Opt-in. Ein scriptc-Binary wächst niemals stillschweigend um eine Engine; die 620 KB sind immer etwas, das Sie angefordert haben. Daraus folgen zwei Konsequenzen. Das JavaScript des Pakets landet zur Build-Zeit in der ausführbaren Datei, sodass das fertige Binary eigenständig ist und zur Laufzeit keinen Grund hat, in node_modules zu schauen. Und die Grenze wird geprüft, nicht vertraut: Eine Deklarationsdatei, die string versprochen hat und ein Objekt liefert, wirft einen abfangbaren TypeError, anstatt in nativem Code Speicher zu korrumpieren, der von etwas anderem ausgegangen ist.

Stufe 3: Zur Kompilierzeit abgelehnt

Code, den scriptc nicht statisch kompilieren und auch nicht über die dynamische Stufe leiten kann, lässt den Build scheitern. Das Versprechen der Homepage für diese Stufe: Der Fehlschlag ist lesbar – ein konkreter Fehlercode, die betreffenden Zeilen und in den meisten Fällen ein Hinweis, wie sie umzuschreiben sind. Nichts wird still in etwas „fast Gleichwertiges” verwandelt. Diagnosecodes tragen das Präfix SC, und SC3002 ist derjenige, dem Sie beim WASI-Target begegnen: Sockets und fetch, Kindprozesse, Signal-APIs und fs.watch brechen den Build alle vor dem Link-Schritt ab, weil Preview 1 einem Guest keine Möglichkeit dafür gibt.

Diese Dreiteilung ist der Grund, warum der restliche Entwurf ernst zu nehmen ist. Ein Compiler, der ein Konstrukt still zu etwas „fast Gleichwertigem” degradiert, würde jede Aussage über Performance und Semantik zu einer bedingten machen. Die Ausgabe zu verweigern – mit Zeilennummer und Umschreibvorschlag – macht das Versprechen der statischen Stufe überhaupt überprüfbar.

Wie sagt Ihnen scriptc coverage, ob Ihr Code qualifiziert ist?

scriptc coverage ist der Weg, die Frage „Würde mein Code kompilieren?” zu beantworten, ohne irgendetwas zu migrieren. Es geht das Programm Statement für Statement durch und meldet, welche die statische Stufe erreichen, welche die Engine benötigen würden und was den Rest blockiert – mit einem Diagnosecode an jeder blockierenden Stelle. Führen Sie es auf Ihrem echten Einstiegspunkt aus, nicht auf einer Spielzeugdatei.

Der Quickstart führt eine hello.ts mit zwei Statements durch den Befehl: 2 analysierte Statements, 2 statisch kompiliert, 100 %, und eine Urteilszeile, die besagt, dass das Programm keinen dynamischen Rest hat. Das Beispiel der README nimmt stattdessen ein echtes Projekt und meldet 4451 von 4481 Statements statisch, also 99 %. Ein realistisches Projekt liefert einen niedrigeren Prozentsatz und eine Liste benannter Stellen. Lesen Sie sie in drei Durchgängen: Der Prozentsatz in der Schlagzeile sagt Ihnen, ob das Projekt überhaupt ein Kandidat ist; die Diagnosen pro Stelle sagen Ihnen, was blockiert; und die Identität jedes Blockers sagt Ihnen, welche Korrektur greift.

Blocker teilen sich klar in zwei Arten. Ein untypisierter npm-Import ist nichts, was man umschreibt, sondern etwas, das man akzeptiert – und das bedeutet, mit --dynamic zu bauen. Ein lockerer Typ im eigenen Code ist hingegen meist behebbar:

// forces the dynamic tier: the payload is any
function port(config: any): number {
  return config.port + 1;
}

Deklarieren Sie die Struktur, und dieselbe Funktion kompiliert statisch:

interface Config { port: number }

function port(config: Config): number {
  return config.port + 1;
}

Wo die Analyse früh abbricht – bei einem Typfehler oder einer Import-Grenze –, vermerkt das Changelog, dass coverage nun dieselben Diagnosen ausgibt wie ein fehlgeschlagener Build, inklusive Code-Frames, statt einer nackten Zusammenfassungszeile. Fügt man --dynamic zum Befehl hinzu, geht es noch weiter und zeigt, welche Stellen die eingebettete Engine am Ende ausführen würde.

Welche Zahlen veröffentlicht das Projekt?

Die Homepage gibt für ein Hello-World-Binary rund 320 KB an, mit etwa 4 ms Startzeit und libSystem als einziger gelinkter Bibliothek – gegenüber einer Node-Runtime von etwa 120 MB, die für dieselbe Zeile rund 35 ms braucht. Die Benchmark-Tabelle der README ist bei derselben Arbeitslast optimistischer: 170 bis 200 KB und etwa 2,4 ms Startzeit gegenüber Nodes ~47 ms. Die beiden Projektquellen stimmen nicht überein, es lohnt sich also zu wissen, woher eine jeweilige Zahl stammt. In jedem Fall sind das die projekteigenen Werte für Hello-World auf dem First-Class-Host macOS – keine allgemeine Aussage über Ihre Anwendung.

Betrachten Sie sie als Untergrenze, nicht als Prognose. Ein mit --dynamic gebautes Binary trägt die Engine und das eingebettete Paket-JavaScript, wodurch sich die Größenklasse verändert. Die Zahl, die sich sauber auf Ihre eigene Schätzung übertragen lässt, sind die 620 KB Engine-Kosten, weil es ein fixer, dokumentierter Zuschlag ist, den Sie entweder in Kauf nehmen oder vermeiden.

Was kostet die Einführung tatsächlich?

scriptc lebt im Namensraum vercel-labs und steht noch bei 0.1.x. Die Community-Diskussion seit der Veröffentlichung Ende Juli 2026 dreht sich genau um diesen Status: ob ein Labs-Projekt die jahrelange Pflege aufbringt, die ein Compiler in Ihrer Build-Pipeline verlangt. Das Repository veröffentlicht getaggte npm-Releases und eine Apache-2.0-Lizenz, aber es gibt keine Support- oder SLA-Erklärung dazu.

Die härtere praktische Grenze ist das Ökosystem. Die meisten npm-Pakete liefern kompiliertes JavaScript neben separaten .d.ts-Deklarationen aus. Damit hat die statische Stufe keinen typisierten Quellcode zum Kompilieren, weshalb dieser Code unter --dynamic in der eingebetteten Engine läuft – und die Engine mit Ihrem Binary ausgeliefert wird. Pakete ohne jegliche Deklarationen degradieren nicht still: Sie scheitern am Typecheck-Gate mit TypeScripts Standardfehler für fehlende Deklarationen, genau wie in jedem strikten TypeScript-Projekt. Weitere Kanten sind einzeln dokumentiert, bis hinunter zu Details wie der Tatsache, dass scriptc run zusätzliche CLI-Argumente nicht an das Programm weitergibt; die Limitations-Seite ist die Liste, die Sie vor der Planung einer Migration lesen sollten.

Das ehrliche Bild der Passung: Ein streng typisiertes CLI oder ein kleiner Service mit wenigen oder keinen Laufzeitabhängigkeiten ist ein starker Kandidat; ein Projekt mit tiefem Abhängigkeitsbaum kauft sich für den größten Teil seines Codes eine 620-KB-Engine plus eingebettetes JavaScript ein. Installieren Sie das CLI, führen Sie scriptc coverage auf Ihrem Einstiegspunkt aus, und lassen Sie den Prozentsatz und die Blocker-Liste entscheiden – nicht die Schlagzeile.

FAQs

Benötigt ein Rechner, der ein scriptc-Binary ausführt, eine Node.js- oder clang-Installation?

Nein. Alles, was scriptc braucht, ist eine Build-Zeit-Anforderung. Der Compiler läuft auf Node.js 24, und ausführbare Builds benötigen einen Plattform-Linker-Treiber plus ein passendes SDK oder Sysroot. Auf unterstützten macOS-, Linux- und Windows-Hosts bindet die LLVM-Stufe ein vorkompiliertes Runtime-Pack ein, anstatt C zu kompilieren, sodass ein C-Compiler wie clang nur für explizite C-Builds, LLVM-Fallbacks und Sanitizer-Builds erforderlich ist. Die ausführbaren Dateien selbst benötigen kein Node: Ein statischer Build liefert eine kleine native Runtime, ohne Node und ohne JavaScript-Engine – abgesehen vom Regex-Interpreter, der eingebunden wird, wenn Ihr Code reguläre Ausdrücke verwendet. Die Quellcode-Ausgabe mit den Emit-Zielen ir, c und llvm benötigt lediglich Node.

Kann scriptc Linux- oder Windows-Binaries von einem Mac aus bauen?

Ja. scriptc unterstützt macOS, Linux, Windows und WebAssembly über WASI Preview 1, mit macOS arm64 als First-Class-Host. Cross-Compilierung über zig ist ein Weg zu Linux- und Windows-Binaries; beide Targets haben zudem eigene native Helper und Runtime-Packs, die Linux x64 und arm64 sowie Windows x64 abdecken. Der WASI-Pfad wird über die Umgebungsvariablen SCRIPTC_CC und SCRIPTC_TARGET gesteuert, gesetzt auf zigcc und wasm32-wasi, und APIs, die in Preview 1 fehlen – etwa Sockets, Kindprozesse und Dateisystem-Überwachung –, scheitern vor dem Linken mit SC3002.

Was passiert, wenn ein npm-Paket, das in der eingebetteten Engine läuft, ein übergebenes Objekt mutiert?

Die statische Seite sieht die Mutation nie. In einem dynamischen Build werden Werte über die Grenze hinweg kopiert und nicht geteilt; alles, was das in der Engine ausgeführte Paket ändert, lässt das statische Original unberührt, und alles, was statischer Code ändert, lässt die Kopie der Engine unberührt. scriptc führt dies als eine seiner bewussten Abweichungen von JavaScript auf, wo beide Seiten dasselbe Objekt halten würden.

Kann Code aus npm-Abhängigkeiten statisch kompiliert werden, anstatt in der Engine zu laufen?

Ja, mit dem experimentellen Flag --npm-static. Sie benennen die Pakete oder übergeben auto, und der Compiler versucht, sie aus der eingebetteten Engine herauszuziehen und ihr ausgeliefertes JavaScript als statische Programmmodule zu kompilieren, typisiert durch ihre eigenen Deklarationsdateien. Die Abdeckung ist hoch, aber unvollständig: Stellen, die der statische Compiler nicht übernehmen kann, werden zurückgestellt und im Bericht benannt; ein Paket, das der Preflight ablehnt, geht mit einem Hinweis zurück an die Engine, anstatt den Build zu brechen. Führen Sie coverage aus, um zu sehen, welche Ihrer Pakete es schaffen.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.