📌 项目地址:vercel-labs/scriptc | ⭐ 5,311 颗星 | 🔧 TypeScript | 📜 未标注
scriptc 做的事一句话说清楚:把 TypeScript 和 JavaScript 编译成原生可执行文件。注意是编译,不是打包。产物里没有 Node,也没有任何 JavaScript 引擎,只带一个很小的原生 runtime。
它走的是第三条路
用 JS 语法交付原生程序,现有的方案分两类。Deno、Bun 这类运行时,产物体积大,因为整个引擎都打进去了。pkg、Node SEA 这类打包工具,本质还是脚本加 Node 捆绑销售。
scriptc 是静态编译。它用 TypeScript 编译器做解析和类型检查,然后走自己的管线,逐层产出:类型化 IR、可读的 C 代码、LLVM IR 文本、汇编、目标文件、可执行文件,还有 WebAssembly 模块。
有个设计我比较欣赏:编不过去的代码不会悄悄降级到动态执行,而是直接报诊断错误。如果你确实要跑 npm 包或者 any 类型的代码,用 --dynamic 显式嵌入 quickjs-ng 引擎。默认强制静态,要动态得自己开口。
十分钟上手
要求 Node.js 24 或更新版本:
$ npm install -g scriptc
写个 hello.ts:
const who = process.argv.length > 2 ? process.argv[2] : "world";
console.log(`hello, ${who}`);
一步编译并运行:
$ scriptc run hello.ts
hello, world
或者产出独立可执行文件:
$ scriptc build hello.ts -o hello
$ ./hello ctate
hello, ctate
真正值钱的部分:–emit
--emit 参数让你停在编译管线的任意一层,拿走中间产物:
$ scriptc build hello.ts --emit=ir >/dev/null
$ ls .scriptc/
hello.ir.json
$ scriptc build hello.ts --emit=c >/dev/null
$ ls .scriptc/
hello.c
hello.ir.json
一共五档:
--emit=ir:类型化中间表示(hello.ir.json)--emit=c:可读的 C 源码--emit=llvm:LLVM IR 文本(.ll)--emit=asm:汇编(.s)--emit=obj:目标文件(.o)
产物在 .scriptc/ 目录下累积,重编某一档只更新对应文件。想学编译器的人可以拿同一份 TS 代码,对照着看 IR、C、LLVM IR、汇编四层输出,这比读十篇编译原理博客都直观。
各档对工具链的要求也不同。--emit=ir|c|llvm 只要 Node 就能跑。--emit=asm|obj 用 scriptc 自带的平台辅助工具,编译器、归档器、链接器、SDK 一个都不用装。只有生成最终可执行文件时才需要一个平台链接器驱动加 SDK/sysroot,具体用哪个驱动,设 SCRIPTC_LINKER 环境变量指定。
macOS 15+ arm64 上还有个更激进的安排:普通 LLVM 层级的可执行文件构建,用 scriptc 自带的辅助工具加预编译 runtime pack 完成,clang 在这里只当链接器驱动,程序和 runtime 的 C 代码完全不经过 clang 编译。只有显式 C 构建、LLVM 回退、--sanitize,以及旧的 SCRIPTC_CC=clang|zigcc 兼容路径,才额外需要 C 编译器。
两个容易踩的坑
--emit=obj 产出的是可重定位的程序对象,不是独立库。它带着未定义的 scr_* 运行时引用和一个必需的 scr_runtime_abi_v2 标记。要自包含的归档产物,走 scriptc build --lib --profile ... 这条路。
辅助工具目前只在 macOS 15+ arm64 上运行,产物的部署目标是 arm64-apple-macosx14.0.0。另外,sanitizer 模式下不允许输出汇编和目标文件,会被直接拒绝。
该不该现在用
官方定位是实验性项目,别放进生产关键路径。目标平台倒是覆盖得全:macOS、Linux、Windows,外加 WebAssembly(走 WASI Preview 1)。
我觉得两类人值得现在就试。一类是想给命令行小工具做零依赖单文件分发的开发者,产物不带引擎,体积可控。另一类是想亲眼看看“TS 到机器码”每一步长什么样的编译学习者,--emit 五档输出就是现成的教材。
反过来,如果你的代码重度依赖 eval 和反射这类动态特性,现阶段会很痛苦:静态模式下这些代码直接编不过去,要么改代码,要么接受 --dynamic 的体积代价。