Compile TypeScript para um Binário Nativo com o scriptc
scriptc compila TypeScript para binários nativos, com builds estáticos, engine dinâmica de 620 KB, coverage e diagnósticos claros.
O scriptc compila TypeScript comum em um executável nativo autocontido. Um build estático não embarca nenhum motor JavaScript, exceto por um interpretador de regex que só é linkado se o seu código usar expressões regulares. O compilador TypeScript de verdade faz a verificação de tipos do programa, o scriptc o rebaixa para uma representação intermediária tipada, e código nativo sai do outro lado.
Se você entrega uma CLI escrita em TypeScript, conhece bem esse dilema. A ferramenta tem 40KB de lógica. O mecanismo de entrega é um runtime de 100MB, uma etapa de instalação e um custo de inicialização que o usuário percebe.
A parte interessante não é o binário. Outras ferramentas produzem um empacotando um runtime dentro dele. O scriptc deixa o motor de fora sempre que pode, e informa quais partes do seu programa ele consegue ou não tratar. Este artigo cobre os três desfechos possíveis para qualquer construção, e o único comando que diz onde o seu próprio código se encaixa.
Principais Conclusões
- O scriptc compila o TypeScript que você já escreve. Não há dialeto a aprender, nada para anotar e nenhuma biblioteca padrão substituta, e a verificação de tipos roda através do compilador TypeScript de verdade.
- A compilação estática é o padrão e o único modo, a menos que você passe
--dynamic, que embute o quickjs-ng com cerca de 620KB no binário. - Qualquer coisa que não se encaixe em nenhum dos dois níveis interrompe o build. Você recebe um código
SC, as linhas problemáticas e, normalmente, uma reescrita sugerida, em vez de um binário sutilmente incorreto. - Executar
scriptc coveragefornece um veredito por instrução: quais partes alcançam o nível estático, quais partes puxariam o motor e um diagnóstico codificado nomeando cada bloqueador. - A maioria dos pacotes npm entrega JavaScript puro mais arquivos de declaração separados, o que não dá ao nível estático nenhum código-fonte tipado, então árvores de dependências reais trazem o motor embutido de volta para dentro do binário.
O Que É o scriptc e Como o Pipeline Funciona?
O scriptc pega um ponto de entrada .ts, faz a verificação de tipos com o compilador TypeScript, rebaixa o programa verificado para uma IR tipada e emite código nativo a partir daí. O README do scriptc define o LLVM como gerador de código padrão e mantém o C como um backend de referência legível permanente, selecionável com --backend c, de modo que “TypeScript para C para clang” descreve apenas um dos dois caminhos. O código-fonte que você fornece é o mesmo que você já executa no Node.
A instalação é um npm install global, e builds de executáveis exigem um driver de linker no host:
npm install -g scriptc
O Quickstart coloca o compilador no Node 24 ou mais recente. Builds de executáveis também precisam de um linker da plataforma e de um SDK ou sysroot compatível, e a página Platform Support é específica sobre o resto: em hosts macOS, Linux e Windows suportados, o nível LLVM linka um pacote de runtime pré-compilado, então um compilador C só é necessário para builds C explícitos, fallbacks do LLVM e --sanitize. A saída de código-fonte selecionada com --emit=ir|c|llvm não precisa de nada além do Node.
Um programa mínimo e os dois comandos que importam:
// 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 e executa em uma única etapa, que é o que você quer em um loop de watch. scriptc build -o produz o artefato que você de fato entrega.
Nível 1: Compilado Estaticamente, o Padrão
A compilação estática é o padrão no scriptc e o único modo que você obtém, a menos que explicitamente opte por sair dele. Na página inicial do scriptc, o nível 1 é apresentado como TypeScript do dia a dia: classes e closures, async/await, a biblioteca padrão e as partes do Node que a maioria dos programas utiliza, como fs, path, process e http. Tudo isso vira código nativo, e o binário não contém motor algum.
A superfície detalhada vai além do que uma lista de destaques sugere. A página de introdução a organiza em três grupos. No lado da linguagem, você tem classes com herança simples e despacho dinâmico, closures que capturam do jeito que o JavaScript captura, declarações de funções genéricas resolvidas por monomorfização, uniões discriminadas tratadas através do próprio narrowing do TypeScript, async/await agendado exatamente como o JavaScript agenda, exceções com finally, desestruturação, spread, accessors, iteradores e template literals. O grupo da biblioteca padrão cobre strings, arrays, Map e Set, JSON, Math, typed arrays e a hierarquia de Error. O grupo do Node alcança o fs em suas formas sync e promise, além de path, process, child_process, os, crypto, url/URL, zlib e timers, e inclui toda a pilha de servidor: net, http, https, tls, dgram, dns e readline.
Isso significa que um serviço HTTP compila, não apenas uma função 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
Qualquer enumeração da superfície estática se desatualiza conforme o compilador evolui. O changelog acompanha isso release por release, e cada release também entrega um surface-manifest.json legível por máquina listando a superfície de linguagem e de biblioteca padrão que o nível estático trata naquela versão, com ids estáveis por entrada para que ferramentas possam comparar duas releases. Esse arquivo sobrevive a qualquer lista em prosa, incluindo esta.
Nível 2: O Nível Dinâmico e Seu Motor de 620KB
Passar --dynamic embute um motor JavaScript no binário, e nada mais faz isso. O guia de dependências npm chama o resultado de ilha dinâmica: um motor embutido de aproximadamente 620KB que executa o que não puder ser estático, o que na prática significa o JavaScript entregue por pacotes npm e qualquer coisa que o verificador tipe como any. Os valores são validados ao cruzarem de volta para o código estático. O motor é o quickjs-ng.
npm install picocolors
scriptc build cli.ts --dynamic -o cli
O ponto de projeto é o opt-in. Um binário scriptc nunca ganha um motor silenciosamente; os 620KB são sempre algo que você pediu. Duas consequências decorrem disso. O JavaScript do pacote vai para dentro do executável em tempo de build, então o binário finalizado é autocontido e não tem motivo para olhar o node_modules em tempo de execução. E a fronteira é verificada, não confiada: um arquivo de declaração que prometeu string e entrega um objeto lança um TypeError capturável em vez de corromper memória em código nativo que supunha o contrário.
Nível 3: Rejeitado em Tempo de Compilação
Código que o scriptc não compila estaticamente e não consegue rotear pelo nível dinâmico faz o build falhar. A promessa que a página inicial faz para este nível é que a falha seja legível: um código de erro específico, as linhas problemáticas e, na maioria dos casos, uma dica sobre como reescrevê-las. Nada é silenciosamente convertido em algo quase equivalente. Os códigos de diagnóstico carregam o prefixo SC, e o SC3002 é aquele que você encontra no alvo WASI: sockets e fetch, processos filhos, APIs de sinais e fs.watch todos interrompem o build antes da etapa de link, porque o Preview 1 não dá ao guest nenhuma forma de fazer qualquer um deles.
Essa divisão em três é a razão pela qual o restante do design merece ser levado a sério. Um compilador que silenciosamente degradasse uma construção em algo quase equivalente tornaria condicional toda afirmação sobre desempenho e semântica. Recusar-se a emitir, com um número de linha e uma reescrita sugerida, é o que torna verificável a promessa do nível estático.
Como o scriptc coverage Diz Se o Seu Código Se Qualifica?
scriptc coverage é como você responde “meu código compilaria?” sem migrar nada. Ele percorre o programa instrução por instrução e reporta quais alcançam o nível estático, quais precisariam do motor e o que está bloqueando o restante, com um código de diagnóstico anexado a cada ponto de bloqueio. Execute-o no seu ponto de entrada real, não em um arquivo de brinquedo.
O Quickstart percorre um hello.ts de duas instruções através do comando: 2 instruções analisadas, 2 compilando estaticamente, 100%, e uma linha de veredito dizendo que o programa não tem resto dinâmico. O exemplo do README, por sua vez, é um projeto real, e reporta 4451 de 4481 instruções estáticas, ou 99%. Um projeto realista imprime um percentual mais baixo e uma lista de pontos nomeados. Leia isso em três passadas: o percentual de destaque diz se o projeto é sequer um candidato; os diagnósticos por ponto dizem o que está bloqueando; e a identidade de cada bloqueador diz qual correção se aplica.
Os bloqueadores se dividem claramente em dois tipos. Um import npm não tipado não é algo que você reescreve, é algo que você aceita, e significa buildar com --dynamic. Um tipo frouxo no seu próprio código geralmente é corrigível:
// forces the dynamic tier: the payload is any
function port(config: any): number {
return config.port + 1;
}
Declare o formato e a mesma função compila estaticamente:
interface Config { port: number }
function port(config: Config): number {
return config.port + 1;
}
Onde a análise para cedo, por um erro de tipo ou uma barreira de import, o changelog registra que o coverage agora imprime os mesmos diagnósticos que um build falho imprimiria, com code frames e tudo, em vez de uma simples linha de resumo. Adicionar --dynamic ao comando vai além e diz quais pontos o motor embutido acabaria executando.
Que Números o Projeto Publica?
A página inicial coloca um binário hello-world em aproximadamente 320KB, com uma inicialização de cerca de 4ms e o libSystem como sua única biblioteca linkada, contra um runtime Node de cerca de 120MB que leva aproximadamente 35ms para imprimir a mesma linha. A tabela de benchmark do README é mais otimista sobre a mesma carga de trabalho: 170 a 200KB e cerca de 2,4ms de inicialização, contra os ~47ms do Node. As duas fontes do projeto não concordam, então vale saber de qual delas veio um determinado número. De todo modo, esses são os números do próprio projeto para o hello-world em seu host macOS de primeira classe, não uma afirmação geral sobre a sua aplicação.
Trate-os como um piso, não como uma previsão. Um binário construído com --dynamic carrega o motor e o JavaScript embutido do pacote, então a classe de tamanho muda. O número que se transfere de forma limpa para a sua própria estimativa é o custo de 620KB do motor, porque é um acréscimo fixo e documentado que você ou assume ou evita.
Quanto Custa de Fato a Adoção?
O scriptc vive sob o namespace vercel-labs e ainda está na 0.1.x. A discussão da comunidade desde seu lançamento no final de julho de 2026 tem se concentrado exatamente nesse status: se um projeto Labs acumula os anos de manutenção que um compilador no seu pipeline de build exige. O repositório publica releases npm com tags e uma licença Apache-2.0, mas nenhuma declaração de suporte ou SLA as acompanha.
O limite prático mais agudo é o ecossistema. A maioria dos pacotes npm entrega JavaScript compilado ao lado de declarações .d.ts separadas, o que não dá ao nível estático nenhum código-fonte tipado para compilar, então esse código roda no motor embutido sob --dynamic e o motor é entregue junto com o seu binário. Pacotes sem declarações nenhuma não degradam silenciosamente: eles falham na barreira de typecheck com o erro padrão de declaração ausente do TypeScript, exatamente como fariam em qualquer projeto TypeScript estrito. Outras arestas estão documentadas individualmente, até detalhes como o scriptc run não repassar argumentos extras de CLI ao programa, e a página de limitações é a lista que vale a pena ler antes de planejar uma migração.
O formato honesto do encaixe: uma CLI fortemente tipada ou um serviço pequeno com poucas ou nenhuma dependência de runtime é um forte candidato, e um projeto com uma árvore de dependências profunda está comprando um motor de 620KB mais JavaScript embutido para a maior parte do seu código. Instale a CLI, rode scriptc coverage no seu ponto de entrada, e deixe o percentual e a lista de bloqueadores decidirem, em vez do texto de destaque.
Perguntas Frequentes
Uma máquina que executa um binário scriptc precisa ter Node.js ou clang instalados?
Não. Tudo o que o scriptc precisa é um requisito de tempo de build. O compilador roda no Node.js 24, e builds de executáveis precisam de um driver de linker da plataforma mais um SDK ou sysroot compatível. Em hosts macOS, Linux e Windows suportados, o nível LLVM linka um pacote de runtime pré-compilado em vez de compilar C, então um compilador C como o clang só é necessário para builds C explícitos, fallbacks do LLVM e builds com sanitizer. Os executáveis em si não requerem Node: um build estático entrega um pequeno runtime nativo, sem Node e sem motor JavaScript além do interpretador de regex linkado quando o seu código usa expressões regulares. A emissão de código-fonte com os alvos de emit ir, c e llvm precisa apenas do Node.
O scriptc pode gerar binários Linux ou Windows a partir de um Mac?
Sim. O scriptc tem como alvos macOS, Linux, Windows e WebAssembly via WASI Preview 1, com macOS arm64 como host de primeira classe. Compilar cruzado através do zig é uma rota para binários Linux e Windows, e ambos os alvos também têm helpers nativos e pacotes de runtime próprios, cobrindo Linux x64 e arm64 e Windows x64. O caminho WASI é conduzido pelas variáveis de ambiente SCRIPTC_CC e SCRIPTC_TARGET definidas como zigcc e wasm32-wasi, e APIs ausentes no Preview 1, como sockets, processos filhos e monitoramento de sistema de arquivos, falham antes do link com SC3002.
O que acontece quando um pacote npm rodando no motor embutido muta um objeto que você passou para ele?
O lado estático nunca vê a mutação. Em um build dinâmico, os valores são copiados através da fronteira em vez de compartilhados, então qualquer coisa que o pacote executado pelo motor altere deixa o original estático intocado, e qualquer coisa que o código estático altere deixa a cópia do motor intocada. O scriptc lista isso como um de seus desvios deliberados do JavaScript, onde ambos os lados estariam segurando o mesmo objeto.
O código de dependências npm pode ser compilado estaticamente em vez de rodar no motor?
Sim, com a flag experimental --npm-static. Você nomeia os pacotes, ou passa auto, e o compilador tenta retirá-los do motor embutido e compilar o JavaScript que eles entregam como módulos estáticos do programa, tipados por seus próprios arquivos de declaração. A cobertura é alta, mas parcial: pontos que o compilador estático não consegue assumir são adiados e nomeados no relatório, e um pacote que o preflight recusar volta para o motor com uma nota, em vez de quebrar o build. Rode o coverage para ver quais dos seus pacotes passam.
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