12k
All articles

Compilar TypeScript a un binario nativo con scriptc

scriptc compila TypeScript en binarios nativos, con compilación estática, motor dinámico de 620 KB, cobertura y diagnósticos claros.

OpenReplay Team
OpenReplay Team
Compilar TypeScript a un binario nativo con scriptc

scriptc compila TypeScript común y corriente en un ejecutable nativo autocontenido. Una compilación estática no incluye ningún motor de JavaScript, salvo un intérprete de expresiones regulares que solo se enlaza si tu código utiliza regex. El compilador real de TypeScript verifica los tipos del programa, scriptc lo baja a una representación intermedia tipada, y del otro extremo sale código nativo.

Si distribuyes una CLI escrita en TypeScript, ya conoces el trato. La herramienta son 40 KB de lógica. El mecanismo de entrega es un runtime de 100 MB, un paso de instalación y un coste de arranque que el usuario percibe.

Lo interesante no es el binario. Otras herramientas producen uno empaquetando un runtime en su interior. scriptc deja el motor fuera siempre que puede, y te dice qué partes de tu programa puede manejar y cuáles no. Este artículo cubre los tres desenlaces posibles para cualquier construcción y el único comando que te indica dónde encaja tu propio código.

Puntos clave

  • scriptc compila el TypeScript que ya escribes. No hay ningún dialecto que aprender, nada que anotar ni una biblioteca estándar de reemplazo, y la verificación de tipos se ejecuta con el compilador real de TypeScript.
  • La compilación estática es el modo por defecto y el único disponible a menos que pases --dynamic, que incrusta quickjs-ng en el binario con un coste aproximado de 620 KB.
  • Todo aquello que no encaje en ninguno de los dos niveles detiene la compilación. Obtienes un código SC, las líneas problemáticas y normalmente una reescritura sugerida, en lugar de un binario sutilmente incorrecto.
  • Ejecutar scriptc coverage te da un veredicto por sentencia: qué partes entran en el nivel estático, qué partes arrastrarían el motor y un diagnóstico codificado que nombra cada bloqueo.
  • La mayoría de los paquetes npm distribuyen JavaScript plano junto con archivos de declaración separados, lo que deja al nivel estático sin código fuente tipado, de modo que los árboles de dependencias reales vuelven a meter el motor incrustado en el binario.

¿Qué es scriptc y cómo funciona su pipeline?

scriptc toma un punto de entrada .ts, verifica sus tipos con el compilador de TypeScript, baja el programa verificado a una IR tipada y emite código nativo a partir de ahí. El README de scriptc establece LLVM como generador de código por defecto y mantiene C como backend de referencia legible permanente, seleccionable con --backend c, de modo que “TypeScript a C a clang” describe solo una de las dos rutas. El código fuente que le entregas es el mismo que ya ejecutas en Node.

La instalación es un npm install global, y las compilaciones de ejecutables requieren un driver de enlazado en el host:

npm install -g scriptc

El Quickstart sitúa el compilador en Node 24 o superior. Las compilaciones de ejecutables también necesitan un enlazador de plataforma y un SDK o sysroot compatible, y Platform Support es específica respecto a lo demás: en hosts macOS, Linux y Windows soportados, el nivel LLVM enlaza un paquete de runtime precompilado, por lo que solo se requiere un compilador de C para compilaciones explícitas en C, fallbacks de LLVM y --sanitize. La salida de código fuente seleccionada con --emit=ir|c|llvm no necesita nada más que Node.

Un programa mínimo y los dos comandos que importan:

// 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 compila y ejecuta en un solo paso, que es lo que quieres dentro de un ciclo de watch. scriptc build -o produce el artefacto que realmente distribuyes.

Nivel 1: compilación estática, el modo por defecto

La compilación estática es el modo por defecto en scriptc y el único que obtienes salvo que optes explícitamente por lo contrario. En la página principal de scriptc, el nivel 1 se presenta como TypeScript cotidiano: clases y closures, async/await, la biblioteca estándar y las partes de Node que la mayoría de los programas utilizan, como fs, path, process y http. Todo ello se convierte en código nativo, y el binario no contiene ningún motor.

La superficie detallada va más allá de lo que sugiere una lista de titulares. La página de introducción la organiza en tres grupos. Del lado del lenguaje obtienes clases con herencia simple y despacho dinámico, closures que capturan tal como lo hace JavaScript, declaraciones de funciones genéricas resueltas mediante monomorfización, uniones discriminadas gestionadas a través del propio narrowing de TypeScript, async/await planificado exactamente como lo planifica JavaScript, excepciones con finally, destructuring, spread, accessors, iteradores y template literals. El grupo de la biblioteca estándar cubre strings, arrays, Map y Set, JSON, Math, typed arrays y la jerarquía de Error. El grupo de Node alcanza fs en sus variantes sync y promise, además de path, process, child_process, os, crypto, url/URL, zlib y timers, e incluye toda la pila de servidor: net, http, https, tls, dgram, dns y readline.

Eso significa que un servicio HTTP compila, no solo una función pura:

// 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

Cualquier enumeración de la superficie estática queda obsoleta a medida que el compilador avanza. El changelog la registra versión a versión, y cada release incluye además un surface-manifest.json legible por máquina que lista la superficie del lenguaje y de la biblioteca estándar que el nivel estático maneja en esa versión, con ids estables por entrada para que las herramientas puedan hacer diff entre dos releases. Ese archivo sobrevive a cualquier lista en prosa, incluida esta.

Nivel 2: el nivel dinámico y su motor de 620 KB

Pasar --dynamic incrusta un motor de JavaScript en el binario, y nada más lo hace. La guía de dependencias npm llama al resultado una isla dinámica: un motor incrustado de aproximadamente 620 KB que ejecuta todo lo que no puede ser estático, lo que en la práctica significa el JavaScript distribuido por los paquetes npm y cualquier cosa que el verificador tipe como any. Los valores se validan al cruzar de vuelta hacia el código estático. El motor es quickjs-ng.

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

El punto clave del diseño es que sea opt-in. Un binario de scriptc nunca incorpora un motor en silencio; los 620 KB son siempre algo que pediste. De ahí se derivan dos consecuencias. El JavaScript del paquete entra en el ejecutable en tiempo de compilación, de modo que el binario final es autocontenido y no tiene motivo para consultar node_modules al ejecutarse. Y el límite se verifica en lugar de confiarse: un archivo de declaración que prometía string y entrega un objeto lanza un TypeError capturable en vez de corromper memoria en código nativo que asumió otra cosa.

Nivel 3: rechazado en tiempo de compilación

El código que scriptc no puede compilar estáticamente ni enrutar a través del nivel dinámico hace fallar la compilación. La promesa que la página principal hace para este nivel es que el fallo sea legible: un código de error específico, las líneas problemáticas y, en la mayoría de los casos, una pista sobre cómo reescribirlas. Nada se convierte silenciosamente en algo casi equivalente. Los códigos de diagnóstico llevan el prefijo SC, y SC3002 es el que te encuentras en el target WASI: sockets y fetch, procesos hijo, APIs de señales y fs.watch detienen la compilación antes del paso de enlazado, porque Preview 1 no le ofrece al guest ninguna forma de hacer nada de eso.

Esta división en tres vías es la razón por la que vale la pena tomarse en serio el resto del diseño. Un compilador que degradara silenciosamente una construcción a algo casi equivalente volvería condicional cualquier afirmación sobre rendimiento y semántica. Negarse a emitir, con un número de línea y una reescritura sugerida, es lo que hace verificable la promesa del nivel estático.

¿Cómo te dice scriptc coverage si tu código cumple los requisitos?

scriptc coverage es la forma de responder a “¿compilaría mi código?” sin migrar nada. Recorre el programa sentencia por sentencia e informa cuáles entran en el nivel estático, cuáles necesitarían el motor y qué bloquea al resto, con un código de diagnóstico asociado a cada punto bloqueante. Ejecútalo sobre tu punto de entrada real, no sobre un archivo de juguete.

El Quickstart pasa un hello.ts de dos sentencias por el comando: 2 sentencias analizadas, 2 compilando estáticamente, 100 %, y una línea de veredicto que indica que el programa no tiene remanente dinámico. El ejemplo del README es en cambio un proyecto real, y reporta 4451 de 4481 sentencias estáticas, es decir, un 99 %. Un proyecto realista imprime un porcentaje menor y una lista de sitios nombrados. Léelo en tres pasadas: el porcentaje del titular te dice si el proyecto es siquiera candidato; los diagnósticos por sitio te dicen qué está bloqueando; y la identidad de cada bloqueo te dice qué solución aplica.

Los bloqueos se dividen limpiamente en dos tipos. Un import de npm sin tipos no es algo que reescribas, es algo que aceptas, y significa compilar con --dynamic. Un tipo laxo en tu propio código normalmente sí se puede arreglar:

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

Declara la forma y la misma función compila estáticamente:

interface Config { port: number }

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

Cuando el análisis se detiene antes de tiempo, por un error de tipos o una barrera de import, el changelog registra que coverage ahora imprime los mismos diagnósticos que imprimiría una compilación fallida, con code frames incluidos, en lugar de una escueta línea de resumen. Añadir --dynamic al comando va más allá y te indica qué sitios acabaría ejecutando el motor incrustado.

¿Qué cifras publica el proyecto?

La página principal sitúa un binario hello-world en aproximadamente 320 KB, con un arranque de unos 4 ms y libSystem como su única biblioteca enlazada, frente a un runtime de Node de unos 120 MB que tarda unos 35 ms en imprimir la misma línea. La tabla de benchmarks del README es más optimista sobre la misma carga de trabajo: de 170 a 200 KB y unos 2,4 ms de arranque, frente a los ~47 ms de Node. Las dos fuentes del proyecto no coinciden, así que conviene saber de cuál procede cada cifra. En cualquier caso, son las cifras propias del proyecto para hello-world en su host macOS de primera clase, no una afirmación general sobre tu aplicación.

Tómalas como un suelo, no como un pronóstico. Un binario compilado con --dynamic carga con el motor y el JavaScript incrustado del paquete, por lo que la categoría de tamaño cambia. La cifra que se traslada limpiamente a tu propia estimación es el coste de 620 KB del motor, porque es una adición fija y documentada que o bien asumes o bien evitas.

¿Cuál es el coste real de adoptarlo?

scriptc vive bajo el namespace vercel-labs y sigue en la versión 0.1.x. La discusión en la comunidad desde su lanzamiento a finales de julio de 2026 se ha centrado precisamente en ese estatus: si un proyecto de Labs acumula los años de mantenimiento que exige un compilador dentro de tu pipeline de build. El repositorio publica releases etiquetadas en npm y una licencia Apache-2.0, pero no las acompaña ninguna declaración de soporte ni SLA.

El límite práctico más agudo es el ecosistema. La mayoría de los paquetes npm distribuyen JavaScript compilado junto con declaraciones .d.ts separadas, lo que deja al nivel estático sin código fuente tipado que compilar, de modo que ese código se ejecuta en el motor incrustado bajo --dynamic y el motor viaja con tu binario. Los paquetes que no tienen declaración alguna no se degradan en silencio: fallan en la fase de typecheck con el error estándar de declaración faltante de TypeScript, exactamente como lo harían en cualquier proyecto TypeScript en modo estricto. Otras asperezas están documentadas de forma individual, hasta detalles como que scriptc run no reenvía argumentos CLI adicionales al programa, y la página de limitaciones es la lista que vale la pena leer antes de planificar una migración.

La forma honesta del encaje: una CLI estrictamente tipada o un servicio pequeño con pocas o ninguna dependencia en runtime es un candidato sólido, y un proyecto con un árbol de dependencias profundo está comprando un motor de 620 KB más JavaScript incrustado para la mayor parte de su código. Instala la CLI, ejecuta scriptc coverage sobre tu punto de entrada y deja que decidan el porcentaje y la lista de bloqueos, no el titular.

Preguntas frecuentes

¿Una máquina que ejecuta un binario de scriptc necesita tener instalado Node.js o clang?

No. Todo lo que scriptc necesita es un requisito de tiempo de compilación. El compilador se ejecuta sobre Node.js 24, y las compilaciones de ejecutables necesitan un driver de enlazado de plataforma más un SDK o sysroot compatible. En hosts macOS, Linux y Windows soportados, el nivel LLVM enlaza un paquete de runtime precompilado en lugar de compilar C, por lo que solo se requiere un compilador de C como clang para compilaciones explícitas en C, fallbacks de LLVM y compilaciones con sanitizer. Los ejecutables en sí no requieren Node: una compilación estática incluye un pequeño runtime nativo, sin Node y sin motor de JavaScript más allá del intérprete de expresiones regulares que se enlaza cuando tu código usa regex. La emisión de código fuente con los targets de emisión ir, c y llvm solo necesita Node.

¿Puede scriptc compilar binarios de Linux o Windows desde un Mac?

Sí. scriptc tiene como targets macOS, Linux, Windows y WebAssembly vía WASI Preview 1, con macOS arm64 como host de primera clase. La compilación cruzada mediante zig es una vía para obtener binarios de Linux y Windows, y ambos targets cuentan además con helpers nativos y paquetes de runtime propios, que cubren Linux x64 y arm64 y Windows x64. La ruta WASI se controla mediante las variables de entorno SCRIPTC_CC y SCRIPTC_TARGET, con los valores zigcc y wasm32-wasi, y las APIs ausentes en Preview 1, como sockets, procesos hijo y observación del sistema de archivos, fallan antes del enlazado con SC3002.

¿Qué ocurre cuando un paquete npm que se ejecuta en el motor incrustado muta un objeto que le pasaste?

El lado estático nunca ve la mutación. En una compilación dinámica, los valores se copian al cruzar el límite en lugar de compartirse, de modo que cualquier cambio que haga el paquete ejecutado por el motor deja intacto el original estático, y cualquier cambio del código estático deja intacta la copia del motor. scriptc enumera esto como una de sus desviaciones deliberadas respecto a JavaScript, donde ambos lados estarían sosteniendo el mismo objeto.

¿Puede compilarse estáticamente el código de las dependencias npm en lugar de ejecutarse en el motor?

Sí, con el flag experimental --npm-static. Nombras los paquetes, o pasas auto, y el compilador intenta sacarlos del motor incrustado y compilar el JavaScript que distribuyen como módulos estáticos del programa, tipados por sus propios archivos de declaración. La cobertura es alta pero parcial: los sitios que el compilador estático no puede asumir se difieren y se nombran en el informe, y un paquete que el preflight rechaza vuelve al motor con una nota en lugar de romper la compilación. Ejecuta coverage para ver cuáles de tus paquetes lo superan.

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.