Scriptc: Vercel 的 TypeScript 转原生编译器
Scriptc: Vercel 的 TypeScript 转原生编译器
概述
Scriptc 将普通的 TypeScript 编译为自包含的原生二进制文件,其中不包含 Node、V8 或 JavaScript 引擎。
安装
使用 npm 全局安装编译器:
npm install -g scriptc
它需要 clang(在 macOS 上由 Xcode Command Line Tools 提供)。macOS arm64 是主要平台;Linux 和 Windows 二进制文件通过交叉编译构建。
静态编译 vs 动态编译
默认情况下,scriptc 将代码静态编译为原生代码。scriptc coverage 命令可以显示哪些内容可以静态编译,哪些仍需动态执行:
$ scriptc coverage app.ts
statements analyzed 4481
compile statically 4451 (99%)
blockers:
×2 functions with optional parameters as values SC1090
×1 Promise.reject SC2020
有三个明确的层级:
- 静态编译 (Compiled statically) – 原生代码,无需引擎;这是默认模式。
- 动态运行 (
--dynamic) – 一个嵌入式的 QuickJS-NG 引擎(约 620 KB)用于执行无法静态编译的代码(例如 npm 依赖项中附带的 JS、any类型代码)。跨回静态代码的值会在运行时进行验证,如果类型不符会抛出可捕获的TypeError。 - 拒绝 (Rejected) – 失败并返回特定的错误代码、代码帧,通常还会提供重写提示;不会发生静默编译错误。
哪些内容可以静态编译
静态编译的范围包括:
- 语言特性:具有单继承和真实动态分发的类(在证明安全时进行去虚化)、具有 JS 捕获语义的闭包、泛型(单态化)、作为标记值的判别联合类型、在 stackful fibers 上的
async/await、带有finally的异常、解构、展开、可选/默认/剩余参数、getters/setters、字符串/数组/Maps/Sets 的迭代器、模板字符串、正则表达式(来自 QuickJS 的精确 ECMAScript 字节码解释器,仅在使用了正则时链接)。 - 标准库:精确的 UTF-16 字符串、具有 JS 精确排序和标识度的数组/Maps/Sets、具有运行时验证转换的
JSON、Math、类型化数组和Buffer、带有类型化catch的Error层级结构。 - Node API 表面:
fs(同步和 promises)、path(字节级精确)、process、带有管道流的child_process、os、crypto、url/URL、zlib、无依赖事件循环上的定时器和信号处理器,以及完整的服务器栈(net、http、https、带有内置 mbedTLS 的tls)、dgram、dns、fs.watch、readline。 - fetch 和 WHATWG Web 子集:在相同的原生 net/TLS 栈上运行的 streams、
Headers、AbortSignal,支持重定向、gzip、AbortSignal.timeout、Node 风格的错误原因;无需 libcurl 或系统 HTTP 依赖。 - npm 依赖项(使用
--dynamic):通过 Node 的算法解析,针对附带的.d.ts进行类型检查,其 JS 代码在构建时嵌入到二进制文件中;二进制文件在运行时永远不会读取node_modules。
类型检查使用 TypeScript 真实的 es2025 库(如果存在则包含 @types/node),项目的 tsconfig.json 控制检查器的严格程度。任何到达但无法降级(lowering)的内容都会产生精确的诊断信息。
正确性
每次更改都会运行两种验证机制:
- 差异测试 (Differential testing):在 Node 和原生二进制文件下运行 800 多个程序的语料库;stdout、stderr 和退出码必须字节级一致。数字格式是 JS 精确的(最短往返,已针对一百万个双精度浮点数在 Node 上进行了模糊测试验证)。服务器使用实时客户端驱动程序对两种实现进行测试。
- 内存安全通道 (Memory-safety lane):在 AddressSanitizer 下使用引用计数审计重新运行整个语料库;内存泄漏和 use-after-free 会导致构建失败。
与 Node 的刻意差异(几十个,主要围绕定时器内部机制和错误对象属性)已被记录并编号;不会发生静默差异。
性能
在 Apple M 系列上针对 Node、Go、Rust 和 Zig 进行的测量结果(验证输出字节级一致):
- 启动速度:~2.4 ms(Node ~47 ms;与 Zig 持平,领先于 Go/Rust)。
- 二进制大小:静态构建为 170–200 KB;使用
--dynamic加上嵌入式依赖约为 ~3 MB(Go ~2 MB;Node SEA 60–100 MB)。 - 内存 (RSS):典型值为 1–4 MB(Node 67–116 MB)。
- 运行时:忠实于 JS 的 f64 语义;在大多数工作负载下具有竞争力;整数推导和所有权分析已在路线图中。
逃生舱 (Escape Hatches)
comptime(() =>...):在编译器内部的隔离 VM 中于构建时运行 TypeScript,并将结果作为字面量固化。- 原生 FFI (
--ffi):将仅含签名的 TypeScript 声明绑定到直接的 C ABI 调用,并链接在清单中声明的归档、对象和系统库;边界是显式且长度受限的。 --dynamic:为 npm 依赖和any代码嵌入 QuickJS-NG 引擎;scriptc coverage --dynamic报告哪些语句在何处运行以及还存在哪些阻碍。静态编译仍是默认选项;二进制文件永远不会静默增加引擎。- 检查式转换 (Checked casts):
JSON.parse(...) as Config会插入运行时验证,抛出一个可捕获的错误并指明违规路径(例如expected number at $.port, got string)。TypeScript 的as是一个承诺;scriptc 验证了它。
架构
编译器流水线为:
TypeScript --(tsc: parse + typecheck)--> Lowering --> Typed IR --> C --> clang --> Native executable
packages/compiler:前端(tsc API → IR)、带有验证器/序列化器的 IR、LLVM 和 C 后端。IR 是两端之间唯一的接口;LLVM 是默认的代码生成器,并带有透明的 C 回退方案。packages/runtime:C 运行时,提供带有循环收集器的引用计数值、stackful fibers 和事件循环 (kqueue)、服务器栈、JS 精确的数字格式化。功能单元是链接门控的;二进制文件仅为所用功能付费。packages/cli:实现scriptc build | run | coverage。
开发工作流
pnpm install && pnpm build
pnpm test # 差异化语料库 + 诊断快照
SCRIPTC_SAN=1 pnpm test # ASan + RC 审计下的相同语料库
pnpm scriptc build x.ts --emit-ir # 保留.scriptc/x.c 和 x.ir.json
每个功能上线时都会经过差异测试;两条通道都必须为绿色才能达到合并标准。
社区反应(精选评论)
- 对实际采用的怀疑:“我想不出有任何严肃的公司或项目会使用这个东西。” – @JoeDohn
- 与先前尝试的比较:“Porforr 一直在朝着同一个目标努力……我对 Vercel 如此快速地取得如此大的进展感到非常怀疑。” – @acmnrs
- 对 npm 生态系统的担忧:“大多数包只提供带有类型声明的未类型化 JavaScript……如果你使用任何 npm 包,你仍然需要一个 JavaScript 引擎。” – @sheept
- 对原生 JS 的历史视角:“GCJ 在 90 年代就存在了……GraalVM Native 终于更全面地解决了这个问题……但要让即使是简单的现有应用也能在原生环境下完美运行,仍然是一件苦差事。” – @weinzierl
- 对生产环境验证的渴望:“我非常渴望看到这些项目在生产环境中使用……充其量这可能证明它不仅仅是一个拙劣的宣传噱头。” – @notsylver
- 对 FFI 和嵌入的兴趣:“这看起来非常有前景……我很好奇它是否会编译成一个静态库,可以链接到现有的 C++ 应用并在 Android/iOS 目标上运行。” – @elendilm
- 关于跨平台支持的问题:“它会编译成 C 吗?Wasm?……Linux 和 Windows 二进制文件通过交叉编译构建……如果是这样,那也太逊了……” – @MrDrMcCoy
- 对大小的惊讶:“178kb?! 你往里塞了什么,一个 JVM 吗?” – @aabhay
- 热情:“终于有人做了这个” – @relug
- 一般兴趣:“我喜欢这个想法。” – @casper14
- 对 JavaScript 子集的困惑:“如果 JavaScript 是 TypeScript 的有效子集,你怎么能编译它?困惑。” – @xiaodai
- 用例推测:“这很有趣,所以我现在可以将 Electron 变成原生应用了吗?这有什么用途,让我的 Node 项目难以逆向工程吗?” – @zuzululu
- 对维护和炒作的担忧:“这足以让他们留在 HN 的首页……这是一种增长策略……它会被维护吗?” – @piterrro
- 对生产就绪性的怀疑:“我以前听过类似的故事……你能举出一个真正在生产环境中正常工作的例子吗?一个都没有。” – @ianberdin
- 关于创建单个可执行文件的查询:“这意味着我可以把我的基于 JS 的后端变成单个可执行文件并轻松发布,就像 Go 一样吗?” – @anta40
- 积极的评价:“不华丽,但它有效。” – @hnsmomdpvp
- 目标不明确:“它会编译成 C 吗?Wasm?我在 README 的废话里没找到。” – @khalic
这些评论反映了对技术成就的兴奋、对现实世界可用性的担忧、对 npm 生态系统的依赖,以及对长期维护和跨平台支持的疑问。