TypeScript теперь можно превратить в бинарник без Node.js
TypeScript-код теперь можно собрать в самостоятельный исполняемый файл, который запускается без установленного Node.js. Экспериментальный scriptc от Vercel Labs компилирует TypeScript в нативный код и позволяет получить executable вместо привычной связки JavaScript + Node.js.
Программу с console.log() можно собрать одной командой:
//bash
scriptc build app.ts -o appПосле этого:
//bash
./appНа машине, где запускается app, Node.js уже не нужен.
Сам scriptc при этом работает поверх Node.js - для сборки требуется Node.js 24 или новее. Получается довольно чёткое разделение: Node.js нужен на этапе разработки, а готовая программа может работать без него.
От TypeScript к native executable
Возьмём простой код:
//ts
function add(a: number, b: number): number {
return a + b;
}
console.log(add(20, 22));Обычный TypeScript Compiler уберёт типы и выдаст JavaScript. Дальше его выполняет V8 внутри Node.js:
//text
app.ts
↓
TypeScript Compiler
↓
app.js
↓
Node.js / V8У scriptc конечный результат другой:
//text
app.ts
↓
анализ
↓
нативный код
↓
appНа этапе сборки Node.js всё равно используется. После сборки получается отдельный executable, который можно перенести на другую машину.
Для небольшой CLI-утилиты разница заметная. Вместо проекта с package.json, зависимостями и требованием установить Node.js можно передать один файл.
При этом scriptc не компилирует любой JavaScript целиком. Он анализирует программу и статически собирает ту часть, для которой это возможно.
Динамический код и coverage
Например:
//ts
const value: any = getValue();
value.doSomething();Что находится в value, становится известно только во время выполнения. Для статической компиляции это уже другая задача.
У scriptc есть команда:
//bash
scriptc coverage app.tsОна показывает покрытие статической компиляции и помогает найти места, где остаётся динамический код.
На небольшом проекте это можно проверить практически сразу: написал исходник, запустил coverage и увидел, какие участки компилятор способен обработать статически.
Для кода, который нельзя полностью перевести в native code, предусмотрен динамический режим:
//bash
scriptc build app.ts --dynamic -o appВ таком варианте часть программы может выполняться через встроенный QuickJS-ng.
Это пригодится, например, при работе с npm-пакетами:
//ts
import pc from "picocolors";
console.log(pc.green("Build completed"));После установки зависимости:
//bash
npm install picocolorsприложение можно собрать с --dynamic.
Внутри получится смешанная модель:
//text
TypeScript
↓
scriptc
├── native code
└── QuickJS-ngJavaScript-часть выполняется встроенным движком, поэтому Node.js на машине пользователя всё равно не требуется.
Что меняется по сравнению с обычным JavaScript
У нативной модели есть несколько особенностей.
Возьмём массив:
//ts
const values = [10, 20, 30];
console.log(values[10]);В обычном JavaScript результатом будет:
//text
undefinedВ модели scriptc массивы плотные, и обращение за допустимые границы может привести к runtime trap.
Поэтому обычный JavaScript-проект нельзя переносить сюда без проверки. Синтаксис остаётся знакомым, но некоторые динамические конструкции ведут себя иначе.
При этом проект умеет работать и с Node API. В документации есть пример HTTP-сервера:
//ts
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({
path: req.url
}));
});
server.listen(8080, () => {
console.log("Server started");
});Собирается он так:
//bash
scriptc build server.ts -o serverЗапуск:
//bash
./serverПосле этого сервер слушает порт 8080.
Для небольшого внутреннего сервиса исходники можно оставить на TypeScript, а на сервер отправлять уже готовый executable.
Где это выглядит наиболее практично
Самый понятный сценарий - небольшие CLI-утилиты.
Допустим, есть json-cleaner, который читает JSON, исправляет несколько полей и сохраняет результат.
На TypeScript такую программу удобно писать: есть типы, привычный синтаксис и npm-экосистема. Пользователю готовой утилиты всё это уже не обязательно.
После сборки:
//bash
scriptc build json-cleaner.ts -o json-cleanerостаётся файл:
//text
json-cleanerЕго можно передать на другую машину и запустить непосредственно там.
Для анализаторов логов, генераторов файлов, внутренних CLI и небольших инструментов обработки данных это вполне конкретный вариант распространения.
Для большого веб-приложения с десятками npm-зависимостей ситуация совсем другая. Там уже приходится учитывать, какая часть проекта статически компилируется, какие API используются и сколько кода уйдёт в динамический runtime.
А что с WebAssembly
scriptc умеет собирать TypeScript и в WebAssembly через WASI.
В этом случае результатом становится WASM-модуль, который запускается в соответствующем WASI runtime. Для такой сборки требуется Zig.
Это отдельный сценарий от native executable: возможности файловой системы, сети и других ресурсов зависят уже от конкретного WASI runtime.
Не просто упаковка JavaScript
Бандлер собирает JavaScript и его зависимости в форму, которую затем должен выполнить JavaScript runtime.
scriptc идёт дальше: пытается определить, какой код можно скомпилировать в native code, а остальной оставить для динамического выполнения.
Поэтому coverage здесь имеет практический смысл. Один проект может почти целиком уйти в статическую компиляцию, в другом существенная часть кода останется динамической.
Пока scriptc остаётся экспериментальным проектом. Для небольших программ его подход выглядит понятнее всего: исходник остаётся TypeScript, а результатом становится отдельный executable.
Node.js нужен для сборки этого executable, но на машине, где программа запускается, его можно не устанавливать.
Комментарии
Чтобы оставить комментарий, войдите в аккаунт.