TypeScript теперь можно превратить в бинарник без Node.js

mr. Cooper • 1 час назад • Веб-разработка
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-ng

JavaScript-часть выполняется встроенным движком, поэтому 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, но на машине, где программа запускается, его можно не устанавливать.

Комментарии

Пока нет комментариев. Будьте первым, кто напишет.

Чтобы оставить комментарий, войдите в аккаунт.

Похожие статьи