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 的二進位檔是透過交叉編譯(cross-compilation)構建的。

靜態與動態編譯

預設情況下,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

有三個層級是明確的:

  1. 靜態編譯 (Compiled statically) – 原生程式碼,無引擎;這是預設模式。
  2. 動態執行 (--dynamic) – 使用嵌入的 QuickJS-NG 引擎(約 620 KB)來執行無法靜態編譯的程式碼(例如 npm 依賴項中附帶的 JS、any 類型的程式碼)。從靜態程式碼回傳的值會在執行時進行驗證,若類型不符會拋出可捕捉的 TypeError
  3. 拒絕 (Rejected) – 以特定的錯誤碼、程式碼框架(code frame)並通常附帶重寫提示來失敗;不會發生靜默編譯錯誤。

哪些內容可以靜態編譯

靜態支援的範圍包括:

  • 語言特性:具有單一繼承和真實動態派發(在證明安全時進行去虛擬化)的類別、具有 JS 擷取語義的閉包、泛型(單態化/monomorphized)、作為標記值的辨識聯合類型(discriminated unions)、在 stackful fibers 上的 async/await、帶有 finally 的異常處理、解構、展開運算子、選擇性/預設/剩餘參數、getters/setters、針對字串/陣列/Maps/Sets 的迭代器、模板字面量、正規表達式(來自 QuickJS 的精確 ECMAScript 位元組碼解釋器,僅在使用了正規表達式時才會連結)。
  • 標準函式庫:精確的 UTF-16 字串、具有 JS 精確排序與識別性的陣列/Maps/Sets、具有執行時驗證轉換的 JSONMath、類型化陣列與 Buffer、帶有類型化 catchError 層級結構。
  • Node API 介面fs(同步與 promises)、path(位元組精確)、process、帶有管道流的 child_processoscryptourl/URLzlib、無依賴事件迴圈上的定時器與訊號處理器,以及完整的伺服器堆疊(nethttphttps、帶有內置 mbedTLS 的 tls)、dgramdnsfs.watchreadline
  • fetch 與 WHATWG Web 子集:在相同的原生 net/TLS 堆疊上運行的 streams、HeadersAbortSignal,包含重定向、gzip、AbortSignal.timeout、Node 格式的錯誤原因;不依賴 libcurl 或系統 HTTP。
  • npm 依賴項 (使用 --dynamic):透過 Node 的演算法解析,針對附帶的 .d.ts 進行類型檢查,其 JS 會在建置時嵌入到二進位檔中;二進位檔在執行時永遠不會讀取 node_modules

類型檢查使用 TypeScript 真正的 es2025 lib(加上存在時的 @types/node),且專案的 tsconfig.json 控制檢查器的嚴格程度。任何到達但無法降階(lowering)的部分都會產生精確的診斷資訊。

正確性

每次變更都會執行兩種強制執行機制:

  • 差異測試 (Differential testing):包含 800 多個程式的語料庫會在 Node 和原生二進位檔下運行;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,並將結果作為字面量烘焙(bake)。
  • 原生 FFI (--ffi):將僅含簽名的 TypeScript 宣告綁定到直接的 C ABI 調用,並連結在 manifest 中聲明的庫、物件和系統庫;邊界是明確且長度受限的。
  • --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 精確的數字格式化。功能單元是連結門控的(link-gated);二進位檔僅為其使用的功能付費。
  • 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 生態系統的依賴,以及對長期維護和跨平台支援的疑問。

Sources