← На главную

scriptc делает из TypeScript бинарник без Node, используя quickjs-ng для npm

26.07.2026 22:46 · hackernews

scriptc компилирует обычный TypeScript в небольшие и быстрые нативные исполняемые файлы — никакого Node, V8 или JavaScript-движка в бинарнике. Функция вычисления чисел Фибоначчи, вызванная через scriptc run fib.ts, мгновенно выдаёт результат, а scriptc build fib.ts создаёт автономный бинарник размером около 178 КБ, стартующий примерно за 2 мс. Код остаётся тем же, что и для Node: без аннотаций, без особого диалекта, проверку типов выполняет настоящий компилятор TypeScript, а поведение скомпилированной программы побайтово совпадает с Node. Для сборки требуется clang (есть в Xcode Command Line Tools), основная платформа — macOS arm64; бинарники для Linux и Windows получаются кросс-компиляцией и проходят собственную дифференциальную проверку.

Главная идея — статичность, которую можно увидеть. Команда scriptc coverage app.ts показывает, какой процент инструкций компилируется в нативный код, и перечисляет блокировщики с конкретными кодами вроде SC1090 или SC2020. Работает система трёх явных уровней: по умолчанию всё компилируется статически; ключ --dynamic встраивает легковесный JavaScript-движок quickjs-ng (~620 КБ) для npm-зависимостей, готового JS и any-кода, причём каждое значение при переходе обратно в статический код проверяется и ложный тип вызывает перехватываемую TypeError; всё остальное отвергается с точным диагнозом и подсказкой по исправлению. Никогда ничего не компилируется молча с неверной семантикой.

Статическая поверхность охватывает классы с динамической диспетчеризацией и девиртуализацией, замыкания с JS-семантикой захвата, мономорфизированные обобщения, размеченные объединения через сужение типов TypeScript, async/await на стековых файберах с точным планированием исключений и finally, деструктуризацию, spread, параметры по умолчанию, итераторы, шаблонные строки, регулярные выражения (тот же интерпретатор байт-кода из QuickJS, подключаемый только при использовании). Стандартная библиотека даёт строки UTF-16, массивы, Map/Set с JS-порядком и идентичностью, JSON с валидацией, Math, типизированные массивы, Buffer, иерархии Error. API Node охватывает fs, path, process, child_process, os, crypto, url, zlib, таймеры и событийный цикл без внешних зависимостей, а также серверный стек: net, http, https, tls (встроенный mbedTLS), dgram, dns, fs.watch, readline. fetch и веб-подмножество WHATWG работают поверх того же нативного стека без libcurl. npm-зависимости при --dynamic разрешаются алгоритмом Node, типизируются по их .d.ts, а JS вшивается в бинарник на этапе сборки — node_modules в рантайме не читается.

Каждое изменение проверяется двумя механизмами: дифференциальное тестирование более 800 программ под Node и в нативном бинарнике с побайтовым сравнением stdout, stderr и кодов выхода (форматирование чисел JS-точное, выверено фаззингом на миллионе чисел), и прогон всего корпуса под AddressSanitizer с аудитом счётчиков ссылок — утечки и use-after-free ломают сборку. Задокументированные расхождения с Node немногочисленны и никогда не происходят молча.

На Apple M-series старт занимает около 2.4 мс против 47 мс у Node (наравне с Zig, быстрее Go/Rust), бинарник весит 170–200 КБ статически и около 3 МБ с --dynamic и встроенными зависимостями (для сравнения, Go ~2 МБ, Node SEA 60–100 МБ), потребление памяти — 1–4 МБ RSS против 67–116 МБ у Node. Производительность сопоставима с системными языками на задачах с JS-семантикой f64. Имеются comptime(() => ...) для выполнения TypeScript на этапе компиляции, нативный FFI для прямых вызовов C ABI и проверяемые приведения типа JSON.parse(...) as Config с человекочитаемой ошибкой пути. Компилятор состоит из фронтенда на API tsc, типизированного IR, LLVM-бэкенда по умолчанию и эталонного C-бэкенда, выдающего читаемый код. Рантайм на C включает сборщик циклов, файберы, kqueue, серверный стек и JS-точное форматирование чисел; компоновка платная только за используемые возможности.

Читать оригинал →