Compiler du TypeScript en binaire natif avec scriptc
scriptc compile TypeScript en binaire natif, avec builds statiques, moteur dynamique de 620 Ko, analyse de couverture et diagnostics clairs.
scriptc compile du TypeScript ordinaire en un exécutable natif autonome. Une compilation statique n’embarque aucun moteur JavaScript, à l’exception éventuelle d’un interpréteur d’expressions régulières qu’elle ne lie que si votre code en utilise. Le vrai compilateur TypeScript effectue la vérification de types du programme, scriptc l’abaisse vers une représentation intermédiaire typée, et du code natif sort à l’autre bout.
Si vous distribuez une CLI écrite en TypeScript, vous connaissez le marché. L’outil, c’est 40 Ko de logique. Le mécanisme de livraison, c’est un runtime de 100 Mo, une étape d’installation et un coût de démarrage que l’utilisateur remarque.
Ce qui est intéressant, ce n’est pas le binaire. D’autres outils en produisent un en y empaquetant un runtime. scriptc laisse le moteur de côté quand il le peut, et il vous indique quelles parties de votre programme il sait traiter et lesquelles il ne sait pas. Cet article couvre les trois issues possibles pour n’importe quelle construction, et la commande unique qui vous dit où se situe votre propre code.
Points clés
- scriptc compile le TypeScript que vous écrivez déjà. Il n’y a aucun dialecte à apprendre, rien à annoter ni de bibliothèque standard de remplacement, et la vérification de types passe par le vrai compilateur TypeScript.
- La compilation statique est le mode par défaut, et le seul, sauf si vous passez
--dynamic, qui embarque quickjs-ng dans le binaire pour environ 620 Ko. - Tout ce qui n’entre dans aucun des deux niveaux interrompt la compilation. Vous obtenez un code
SC, les lignes fautives et généralement une suggestion de réécriture, plutôt qu’un binaire subtilement erroné. - Exécuter
scriptc coveragevous donne un verdict instruction par instruction : quelles parties atteignent le niveau statique, quelles parties exigeraient le moteur, et un diagnostic codé nommant chaque bloqueur. - La plupart des paquets npm livrent du JavaScript brut accompagné de fichiers de déclaration séparés, ce qui ne fournit aucune source typée au niveau statique : les arbres de dépendances réels réintroduisent donc le moteur embarqué dans le binaire.
Qu’est-ce que scriptc, et comment fonctionne le pipeline ?
scriptc prend un point d’entrée .ts, en vérifie les types avec le compilateur TypeScript, abaisse le programme vérifié vers une IR typée, et émet du code natif à partir de là. Le README de scriptc fait de LLVM le générateur de code par défaut et conserve C comme backend de référence lisible permanent, que vous sélectionnez avec --backend c : « TypeScript vers C vers clang » ne décrit donc qu’un seul des deux chemins. La source que vous lui donnez est celle que vous exécutez déjà sur Node.
L’installation se fait via un npm install global, et les compilations d’exécutables nécessitent un pilote d’éditeur de liens sur la machine hôte :
npm install -g scriptc
Le Quickstart situe le compilateur sur Node 24 ou plus récent. Les compilations d’exécutables exigent aussi un éditeur de liens de plateforme et un SDK ou sysroot correspondant, et la page Platform Support précise le reste : sur les hôtes macOS, Linux et Windows pris en charge, le niveau LLVM lie un pack de runtime précompilé, si bien qu’un compilateur C n’est requis que pour les compilations C explicites, les replis LLVM et --sanitize. La sortie de sources sélectionnée avec --emit=ir|c|llvm ne nécessite rien d’autre que Node.
Un programme minimal et les deux commandes qui comptent :
// 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 compile et exécute en une seule étape, ce qui est exactement ce que vous voulez dans une boucle de surveillance. scriptc build -o produit l’artefact que vous distribuez réellement.
Niveau 1 : compilation statique, le mode par défaut
La compilation statique est le mode par défaut de scriptc et le seul dont vous disposez sauf renoncement explicite. Sur la page d’accueil de scriptc, le niveau 1 est présenté comme du TypeScript de tous les jours : classes et closures, async/await, la bibliothèque standard, et les parties de Node auxquelles la plupart des programmes font appel, telles que fs, path, process et http. Tout cela devient du code natif, et le binaire ne contient aucun moteur.
La surface détaillée va plus loin que ne le laisse supposer une liste de titres. La page d’introduction la répartit en trois groupes. Côté langage, vous disposez des classes à héritage simple avec répartition dynamique, des closures qui capturent comme le fait JavaScript, des déclarations de fonctions génériques résolues par monomorphisation, des unions discriminées traitées via le mécanisme de réduction de type propre à TypeScript, d’async/await ordonnancé exactement comme JavaScript l’ordonnance, des exceptions avec finally, de la déstructuration, du spread, des accesseurs, des itérateurs et des littéraux de gabarit. Le groupe bibliothèque standard couvre les chaînes, les tableaux, Map et Set, JSON, Math, les tableaux typés et la hiérarchie Error. Le groupe Node atteint fs dans ses formes synchrone et à promesses, ainsi que path, process, child_process, os, crypto, url/URL, zlib et timers, et il inclut toute la pile serveur : net, http, https, tls, dgram, dns et readline.
Cela signifie qu’un service HTTP se compile, pas seulement une fonction pure :
// 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
Toute énumération de la surface statique se périme à mesure que le compilateur évolue. Le changelog la suit version par version, et chaque version livre également un fichier surface-manifest.json exploitable par machine, listant la surface du langage et de la bibliothèque standard que le niveau statique prend en charge à cette version, avec des identifiants stables par entrée permettant à l’outillage de comparer deux versions. Ce fichier survivra à n’importe quelle liste rédigée, y compris celle-ci.
Niveau 2 : le niveau dynamique et son moteur de 620 Ko
Passer --dynamic embarque un moteur JavaScript dans le binaire, et rien d’autre ne le fait. Le guide des dépendances npm qualifie le résultat d’îlot dynamique : un moteur embarqué d’environ 620 Ko qui exécute tout ce qui ne peut pas être statique, ce qui signifie en pratique le JavaScript livré par les paquets npm et tout ce que le vérificateur type en any. Les valeurs sont validées lorsqu’elles repassent vers le code statique. Le moteur est quickjs-ng.
npm install picocolors
scriptc build cli.ts --dynamic -o cli
Le principe de conception, c’est l’activation explicite. Un binaire scriptc ne grossit jamais silencieusement d’un moteur ; les 620 Ko sont toujours quelque chose que vous avez demandé. Deux conséquences en découlent. Le JavaScript du paquet est intégré à l’exécutable au moment de la compilation, si bien que le binaire final est autonome et n’a aucune raison de consulter node_modules à l’exécution. Et la frontière est vérifiée plutôt que présumée fiable : un fichier de déclaration qui promettait une string et livre un objet lève une TypeError interceptable au lieu de corrompre la mémoire dans du code natif qui supposait le contraire.
Niveau 3 : rejeté à la compilation
Le code que scriptc ne peut pas compiler statiquement ni router vers le niveau dynamique fait échouer la compilation. La promesse que fait la page d’accueil pour ce niveau, c’est que l’échec soit lisible : un code d’erreur précis, les lignes fautives, et dans la plupart des cas une indication sur la manière de les réécrire. Rien n’est discrètement converti en quelque chose de presque équivalent. Les codes de diagnostic portent un préfixe SC, et SC3002 est celui que vous rencontrez sur la cible WASI : les sockets et fetch, les processus enfants, les API de signaux et fs.watch interrompent tous la compilation avant l’étape d’édition de liens, parce que Preview 1 ne donne à un invité aucun moyen de les réaliser.
Cette répartition en trois voies est la raison pour laquelle le reste de la conception mérite d’être pris au sérieux. Un compilateur qui dégraderait discrètement une construction en quelque chose de presque équivalent rendrait conditionnelle chaque affirmation sur les performances et la sémantique. Refuser d’émettre, avec un numéro de ligne et une suggestion de réécriture, c’est ce qui rend vérifiable la promesse du niveau statique.
Comment scriptc coverage vous dit-il si votre code est éligible ?
scriptc coverage est le moyen de répondre à la question « est-ce que mon code se compilerait ? » sans rien migrer. La commande parcourt le programme instruction par instruction et indique lesquelles atteignent le niveau statique, lesquelles nécessiteraient le moteur, et ce qui bloque le reste, avec un code de diagnostic attaché à chaque site bloquant. Exécutez-la sur votre véritable point d’entrée, pas sur un fichier jouet.
Le Quickstart fait passer un hello.ts de deux instructions par la commande : 2 instructions analysées, 2 compilées statiquement, 100 %, et une ligne de verdict indiquant que le programme n’a aucun reliquat dynamique. L’exemple du README porte, lui, sur un projet réel, et rapporte 4451 instructions statiques sur 4481, soit 99 %. Un projet réaliste affiche un pourcentage plus faible et une liste de sites nommés. Lisez-le en trois passes : le pourcentage principal vous dit si le projet est un candidat tout court ; les diagnostics par site vous disent ce qui bloque ; et l’identité de chaque bloqueur vous dit quel correctif s’applique.
Les bloqueurs se répartissent nettement en deux catégories. Un import npm non typé n’est pas quelque chose que l’on réécrit, c’est quelque chose que l’on accepte, et cela implique de compiler avec --dynamic. Un typage lâche dans votre propre code est généralement corrigeable :
// forces the dynamic tier: the payload is any
function port(config: any): number {
return config.port + 1;
}
Déclarez la forme et la même fonction se compile statiquement :
interface Config { port: number }
function port(config: Config): number {
return config.port + 1;
}
Là où l’analyse s’arrête prématurément, sur une erreur de type ou une barrière d’import, le changelog indique que coverage affiche désormais les mêmes diagnostics qu’une compilation échouée, extraits de code compris, plutôt qu’une simple ligne de résumé. Ajouter --dynamic à la commande va plus loin et vous indique quels sites le moteur embarqué finirait par exécuter.
Quels chiffres le projet publie-t-il ?
La page d’accueil situe un binaire hello-world à environ 320 Ko, avec un démarrage d’environ 4 ms et libSystem comme unique bibliothèque liée, face à un runtime Node d’environ 120 Mo qui met à peu près 35 ms à afficher la même ligne. Le tableau de benchmarks du README est plus optimiste sur la même charge de travail : 170 à 200 Ko et environ 2,4 ms de démarrage, contre ~47 ms pour Node. Les deux sources du projet ne concordent pas, il vaut donc la peine de savoir d’où provient un chiffre donné. Quoi qu’il en soit, ce sont les mesures propres au projet pour hello-world sur son hôte macOS de première classe, et non une affirmation générale sur votre application.
Considérez-les comme un plancher plutôt que comme une prévision. Un binaire compilé avec --dynamic embarque le moteur et le JavaScript des paquets intégrés, ce qui change de classe de taille. Le chiffre qui se transpose proprement à votre propre estimation est le coût de 620 Ko du moteur, parce qu’il s’agit d’un ajout fixe et documenté que vous acceptez ou évitez.
Que coûte réellement l’adoption ?
scriptc réside dans l’espace de noms vercel-labs et en est encore à la version 0.1.x. Les discussions de la communauté depuis sa sortie fin juillet 2026 se sont précisément concentrées sur ce statut : un projet Labs accumulera-t-il les années de maintenance qu’exige un compilateur dans votre pipeline de build ? Le dépôt publie des versions npm étiquetées et une licence Apache-2.0, mais aucune déclaration de support ou de SLA ne les accompagne.
La limite pratique la plus tranchante, c’est l’écosystème. La plupart des paquets npm livrent du JavaScript compilé accompagné de déclarations .d.ts séparées, ce qui ne donne au niveau statique aucune source typée à compiler : ce code s’exécute donc dans le moteur embarqué sous --dynamic, et le moteur part avec votre binaire. Les paquets totalement dépourvus de déclarations ne se dégradent pas silencieusement : ils échouent au contrôle de vérification de types avec l’erreur standard de déclaration manquante de TypeScript, exactement comme dans n’importe quel projet TypeScript en mode strict. D’autres aspérités sont documentées individuellement, jusqu’à des détails comme le fait que scriptc run ne transmet pas les arguments CLI supplémentaires au programme, et la page des limitations est la liste à lire avant de planifier une migration.
La forme honnête de l’adéquation : une CLI ou un petit service rigoureusement typé, avec peu ou pas de dépendances à l’exécution, est un excellent candidat ; un projet doté d’un arbre de dépendances profond achète un moteur de 620 Ko plus du JavaScript embarqué pour la majeure partie de son code. Installez la CLI, exécutez scriptc coverage sur votre point d’entrée, et laissez le pourcentage et la liste des bloqueurs décider à la place du slogan.
FAQ
Une machine exécutant un binaire scriptc a-t-elle besoin de Node.js ou de clang installés ?
Non. Tout ce dont scriptc a besoin relève de la compilation. Le compilateur s'exécute sur Node.js 24, et les compilations d'exécutables nécessitent un pilote d'éditeur de liens de plateforme ainsi qu'un SDK ou sysroot correspondant. Sur les hôtes macOS, Linux et Windows pris en charge, le niveau LLVM lie un pack de runtime précompilé au lieu de compiler du C, si bien qu'un compilateur C tel que clang n'est requis que pour les compilations C explicites, les replis LLVM et les compilations avec sanitizer. Les exécutables eux-mêmes ne nécessitent pas Node : une compilation statique embarque un petit runtime natif, sans Node ni moteur JavaScript, hormis l'interpréteur d'expressions régulières lié lorsque votre code en utilise. L'émission de sources avec les cibles d'émission ir, c et llvm ne nécessite que Node.
scriptc peut-il produire des binaires Linux ou Windows depuis un Mac ?
Oui. scriptc cible macOS, Linux, Windows et WebAssembly via WASI Preview 1, macOS arm64 étant l'hôte de première classe. La compilation croisée via zig est une voie vers les binaires Linux et Windows, et ces deux cibles disposent également de leurs propres utilitaires natifs et packs de runtime, couvrant Linux x64 et arm64 ainsi que Windows x64. Le chemin WASI est piloté par les variables d'environnement SCRIPTC_CC et SCRIPTC_TARGET définies respectivement à zigcc et wasm32-wasi, et les API absentes de Preview 1, telles que les sockets, les processus enfants et la surveillance du système de fichiers, échouent avant l'édition de liens avec SC3002.
Que se passe-t-il lorsqu'un paquet npm exécuté dans le moteur embarqué modifie un objet que vous lui avez transmis ?
Le côté statique ne voit jamais la mutation. Dans une compilation dynamique, les valeurs sont copiées à travers la frontière plutôt que partagées : tout ce que modifie le paquet exécuté par le moteur laisse l'original statique intact, et tout ce que modifie le code statique laisse la copie du moteur intacte. scriptc présente cela comme l'un de ses écarts délibérés par rapport à JavaScript, où les deux côtés détiendraient le même objet.
Le code des dépendances npm peut-il être compilé statiquement au lieu de s'exécuter dans le moteur ?
Oui, avec l'option expérimentale --npm-static. Vous nommez les paquets, ou vous passez auto, et le compilateur tente de les extraire du moteur embarqué et de compiler le JavaScript qu'ils livrent sous forme de modules de programme statiques, typés par leurs propres fichiers de déclaration. La couverture est élevée mais partielle : les sites que le compilateur statique ne peut pas prendre en charge sont différés et nommés dans le rapport, et un paquet que le contrôle préalable refuse retourne au moteur avec une note plutôt que de faire échouer la compilation. Exécutez coverage pour voir lesquels de vos paquets passent le test.
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