- 编译器
- 语言运行时
- 开发工具
- CLI
【免费下载链接】scriptc
TypeScript-to-Native Compiler
导读
scriptc(TypeScript-to-Native Compiler)为真实世界的 Node/TypeScript 项目提供了一条关键的「采纳路径」(adoption path):当目标项目的node_modules中存在@types/node时,编译器会让项目自己的 Node 类型声明接管全局类型表面,而不是继续使用内置的降级(fallback)声明。本文以仓库中的 node-types fixture 为骨架,完整讲解这条路径的三个核心环节——依赖版本锁定、类型来源切换与 provenance 识别、以及「声明但未降级」表面的 SC2020 栅栏(fence)机制,并逐项结合 编译源码、诊断定义 与 harness 测试 验证其真实行为。读完本文,你将掌握:如何在有@types/node与没有@types/node的项目之间理解 scriptc 的类型表面差异,如何识别哪些 Node API 会被静态降级、哪些会被拒绝编译,以及如何用该机制定位真实项目中的兼容性问题。
一、fixture 的定位:为真实世界项目准备的测试样本
在 tests/fixtures/node-types/ 目录下,存在一个刻意与其余 fixture 不同的项目:它的node_modules中真实安装了@types/node,从而模拟一个真实的 Node/TypeScript 项目在 scriptc 下编译时的完整形态。这正是 README 中所说的「the adoption path for real-world Node/TypeScript projects」——大多数现有 fixture 与整个语料库(corpus)都不带@types/node,依赖的是 scriptc 内置的降级声明;而本 fixture 专门验证另一条路线:类型表面完全由项目自己的@types/node提供。
fixture 的结构如下:
argv-env.ts— 验证受支持的process表面(argv/env)在@types/node类型化下的静态降级;fenced.ts— 验证@types/node声明但 scriptc 不降级的表面会以 SC2020 家族栅栏命名@types/node,而不是编译出一个损坏的二进制,也绝不会出现裸的Cannot find name错误;source-import/— 验证被导入的 TypeScript 源码可以使用 TypeScript 默认 DOM lib 提供的RequestInfo全局,而 scriptc 同时采纳项目的@types/node声明;- 其余文件(
child-execfile.ts、child-stdin.ts、fetch-static.ts、path-os.ts、url-getters.ts、text-codecs.mts等)覆盖 spawn 子进程、流捕获、fetch、path/os、URL、编码器等相关子面。
二、版本锁定:vendored node_modules 是「已提交的测试数据」
README 开篇强调了一个关键工程决策:vendored(内置提交的)node_modules是 COMMITTED TEST DATA。也就是说,这些依赖不是通过安装步骤动态拉取的,而是直接随仓库提交,并精确固定版本:
@types/node24.13.3undici-types7.18.2(@types/node的依赖——它声明了 web 平台全局:fetch/Response/AbortSignal/ReadableStream/…)
对应的依赖声明位于 tests/fixtures/node-types/package.json:
{ "name": "node-types-fixture", "private": true, "devDependencies": { "@types/node": "24.13.3" } }这种做法的直接收益是:类型表面永不随 registry 漂移。因为@types/node是一个持续演进的高频发布包,如果允许浮动安装,那么同一份测试源码在不同时间点会看到不同的类型声明,导致「固定的诊断输出」(pinned diagnostics)不稳定。把 node_modules 提交进仓库后,类型表面、以及由此推导出的栅栏诊断快照(见 node-types-fenced.txt)都与版本绑定,harness 测试可以在任何时间、任何环境复现完全一致的结果。
三、类型来源切换:fallback 声明让位,provenance 接管识别
3.1 三种 shipped 声明文件的职责划分
scriptc 在编译任何程序时都会注入多份声明文件,其路径解析全部集中在 packages/compiler/src/frontend/dts-paths.ts:
| 函数 | 注入的声明文件 | 参与时机 |
|---|---|---|
ambientDtsPath()(L16-L18) | 核心 ambient:comptime/__island_eval/setTimeout | 每一个scriptc 构建的程序,包括预检(preflight)程序 |
overridesDtsPath()(L25-L27) | divergence/precision 覆盖:JSON.parse(): unknown、pop(): T、Promise executor 形状等 | 仅降级(lowering)程序;预检的 project-world second chance 构建不注入,因此项目在自有 tsc 下能通过类型检查时,不会被覆盖层人为制造的错误挡住预检 |
fallbackDtsPath()(L33-L35) | 降级声明:console、process、node:fs | 仅当目标项目没有@types/node时;一旦项目自带@types/node,本文件整体让位 |
3.2 fallback 让位的条件
fallbackDtsPath() 的注释 明确了这一逻辑:
Path of the shipped FALLBACK declarations (console, process, node:fs) — part of the program only when the target project has no
@types/node. With@types/node, the project's real Node types stand in and this file stands down (its declaration forms would collide).
换句话说,如果同时注入两份声明,console/process/node:fs的声明形式会发生冲突(collide),因此必须在「shipped fallback」与「项目自己的 @types/node」之间二选一。这正是 node-types fixture 与其余 fixture 的本质差异所在:其它 fixture 与整个语料库没有@types/node,保持 shipped fallback 声明,行为与之前完全一致;本 fixture 则让真实类型声明接管。
3.3 provenance:降级表如何「认出」@types/node 声明的成员
类型来源切换之后,一个核心问题随之而来:argv-env.ts中通过@types/node类型化的process.argv、process.env,与之前通过 fallback 声明类型化的同名成员,为何能降级到同一个静态 libCall?
答案在 isNodeTypesPath():
export function isNodeTypesPath(file: string): boolean { const pkg = npmPackageNameOf(file); return pkg === "@types/node" || pkg === "undici-types"; }scriptc 的降级(lowering)表按「成员名 + @types/node provenance」双重识别:凡是来自@types/node包或undici-types包(后者正是@types/node声明的 web 平台全局的来源)的类型,都被认定为合法的 Node 类型表面。因此,fallback 声明让位后,相同的成员名仍然降级到相同的 libCalls——只是它们的类型来源从内置声明换成了项目真实的@types/node。与此同时,provenance 也是 SC2020 家族栅栏的另一半依据:凡是这两个包声明但不在降级表内的表面,都会被诊断为「typed by @types/node but has no scriptc lowering yet」。
四、受支持的表面:argv/env 在 @types/node 下的静态降级
4.1 样本程序 argv-env.ts
该文件头注释明确了测试意图:受支持的 process 表面,由 @types/node 类型化(fallback 声明在本 fixture 中让位),argv 与 env 读取降级到与以往相同的静态 libCall;测试以参数和环境变量运行二进制并固定输出。
const args = process.argv; console.log(args.length - 2); for (let i = 2; i < args.length; i = i + 1) { console.log(args[i]); } const greeting = process.env.SCRIPTC_FIXTURE_GREETING; if (greeting !== undefined) { console.log(greeting); } else { console.log("no greeting"); } process.stdout.write("written without newline"); console.log(" <- flushed in order");注意其中的细节设计:argv[0]/argv[1]是 Node 形状的 exec/script 路径(机器相关),所以程序只打印用户参数——从argv[2]开始;process.env的读取使用undefined判空而非真值判断,以覆盖「环境变量未设置」的分支;最后用process.stdout.write做原始字节写入,验证在@types/node的WriteStream类型下字节写出同样正常降级,并与后续console.log的输出顺序一致。
4.2 harness 验证:编译并运行二进制
对应测试位于 tests/harness/project-config.test.ts:
test("node-types: the supported process surface lowers statically under @types/node", async () => { const outDir = outDirFor("node-types"); const result = await compile(join(nodeTypesDir, "argv-env.ts"), { /* ... */ }); expect(result.ok, ...).toBe(true); const { stdout } = await execFileAsync(result.binaryPath, ["alpha", "beta"], { env: { ...process.env, SCRIPTC_FIXTURE_GREETING: "hi from env" }, }); expect(stdout).toBe("2\nalpha\nbeta\nhi from env\nwritten without newline <- flushed in order\n"); });测试断言分两层:第一,编译必须成功(result.ok === true)——证明在@types/node类型化下,process.argv/process.env/process.stdout.write全部静态降级、没有产生任何栅栏;第二,编译出的原生二进制必须行为正确——带["alpha", "beta"]运行,argv 计数输出2、逐项输出alpha、beta,再从环境变量输出hi from env,最后验证无换行的字节写出written without newline与后续log的<- flushed in order顺序衔接。README 中描述的「the binary runs」正是由这组双断言钉死的。
同类受支持表面的测试还有:URL 的port/hashgetter 静态降级(L91 起)、被捕获的NodeJS.WritableStream值经procStream标量写出、refined spawn 返回值暴露可写子进程 stdin、回调式execFile使用类型化的 error-first 重载、path/os在 @types/node 形状下静态降级、fetch的AbortSignal与可读 body 静态降级、全局与node:util编解码器实例共享存储的原生表示、导入的console方法共享原生输出格式化等(见 tests/harness/project-config.test.ts 的 node-types 系列)。
五、声明但未降级的表面:SC2020 家族栅栏
5.1 设计原则:宁要明确的编译失败,不要损坏的二进制
README 对 fenced.ts 的定位非常明确:表面是@types/node声明的,但 scriptc 不降级。对这类表面,@types/node负责让所有使用处通过类型检查,而 scriptc 必须让编译以 SC2020 家族栅栏失败、并在诊断中点名@types/node——绝不落入裸的Cannot find name类型错误,也绝不 typecheck 出一个运行时会崩溃的损坏二进制。
这一原则的源码依据在 packages/compiler/src/diagnostics/diagnostic.ts 的栅栏代码注册表:
SC2020: { name: "standard-library or @types/node surface with no lowering", status: "unsupported" },SC2020 属于FENCE_CODES中的 construct-fence 代码(由工厂函数铸出,永不作为UNSUPPORTED的键),状态为unsupported——即两个 tier(静态与--dynamic)都没有降级实现。诊断消息的通用形态是「<feature>is typed by @types/node but has no scriptc lowering yet」,并附带 hint 说明支持面或替代途径(例如use --dynamic for the wider Web API)。
5.2 逐项走读 fenced.ts 的栅栏行为
fenced.ts是这份「声明但未降级表面」的完整清单,值得逐段分析:
process / Buffer:
console.log(process.memoryUsage()); // V8-heap 报告不降级 console.log(Buffer.poolSize);注释说明:uptime/cpuUsage/resourceUsage现在已能降级,但 V8-heap 内存报告memoryUsage()不在其中;Buffer.poolSize同样栅栏。快照 node-types-fenced.txt 给出了真实诊断输出:
fenced.ts:5:13 - error SC2020: 'process.memoryUsage' is typed by @types/node but has no scriptc lowering yet hint: the type checker sees everything @types/node declares, but only the supported surface compilessetInterval 的 Timeout 返回值:
const timer = setInterval(() => { console.log("tick"); }, 1000); timer.unref(); timer.refresh(); timer.close();setInterval本身会降级,且其Timeout返回值在@types/node下映射为数值句柄(numeric handle):持有该返回值并调用unref()/ref()/hasRef()/refresh()都能编译;但超出该子面的Timeout表面(如close、[Symbol.toPrimitive])保持栅栏——快照中正是'Timeout.close' is typed by @types/node but has no scriptc lowering yet,hint 提示unref(), ref(), hasRef(), and refresh() are the supported Timeout methods(node-types-fenced.txt)。这也印证了 lower-builtins.ts 中注释提到的「fallback 的Timeout或 @types/node 的NodeJS.Timeout」由 provenance 参与识别的设计。
web 平台全局(undici):
const fetchInit: RequestInit = { method: "POST", headers: {...}, body: "{}" }; fetch("https://example.invalid/", fetchInit); async function inspectResponse(url: string): Promise<void> { const response = await fetch(url); console.log(response.type); response.clone(); } ReadableStream.from(new Set([1, 2]));fetch、AbortController、类型化的RequestInit均降级;但更宽的Response成员(type、clone)保持各自的 profile 栅栏——快照中'Response.type in a static build'的 hint 明确给出原生静态Response支持面:status/ok/statusText/url/redirected/headers/body/bodyUsed加上json()/text()/bytes(),并提示use --dynamic for the wider Web API(node-types-fenced.txt)。后续body.tee()、body["tee"]括号访问等可读流成员同样栅栏。
受支持内建模块的溢出成员(module-qualified fence):
import { watchFile } from "fs"; import { cpus } from "node:os"; import { win32 } from "path"; watchFile("x", () => {}); console.log(cpus().length); console.log(win32.sep);这些模块(fs/os/path)整体受支持,但超出降级表的成员(watchFile、cpus、win32)在@types/node下通过类型检查,却以**模块限定名(module-qualified)**栅栏——调用与值读取一律如此。
URL getter 子面:
const u = new URL("https://example.com/x?a=1"); console.log(u.toJSON()); u.searchParams.get("a");URL 的port/hash/password等 getter 在@types/node下降级(有独立测试 L91 起 验证),但未实现成员仍按成员名栅栏并附受支持清单;searchParams及其方法表面在@types/node的声明下降级(像 URL 本身一样做 provenance 映射)。
zlib 编解码器:
import { brotliCompressSync, deflateSync } from "zlib"; deflateSync("data", { level: 9, strategy: 1 }); brotliCompressSync(Buffer.from("data"));一次性 zlib/raw/gzip 编解码器接受字面量压缩等级(level: 9, strategy: 1);其他选项保持栅栏;而 Brotli 保持成员限定栅栏、并在 hint 中指名已降级的家族。
http2 兼容切片(divergence 56):
import * as http2 from "node:http2"; http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", SNICallback: undefined }); http2.createSecureServer({ ...(1 ? { SNICallback: undefined } : {}) }); http2.createSecureServer({ ..., streamResetBurst: 1 + 0 }); http2.connect("https://localhost");SNICallback选项按名称栅栏并带 serve-one-pair 提示;其 conditional-spread portless 拼写在 spread 处栅栏(因为只有内联对象字面量才会展平,计算式 spread 不会);非字面量的 h2 session-tuning 值(如1 + 0计算的streamResetBurst)栅栏——字面量会被接受并忽略(因为没有 h2 session 可调);connect以 client gap 命名并在 hint 中给出 fallback。
crypto 超出降级切片的部分:
import { createCipheriv, generateKeyPair, pbkdf2Sync, setFips } from "node:crypto"; generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); pbkdf2Sync("pw", "salt", 100000, 64, "sha512"); setFips(false);在已降级的 crypto 切片(随机数、哈希链、内省 statics)之外:非对称密钥操作点名缺失的公钥栈(public-key stack)、对称密码点名缺失的密码栈(cipher stack)、KDF 点名其家族、setFips点名 FIPS 真值——顺带一提,getFips()是能降级到0的。fetch的integrity选项同样栅栏。
5.3 快照测试:把栅栏钉成固定输出
project-config.test.ts 的 L235-L247 对fenced.ts编译失败路径做了快照断言:编译必须失败(result.ok === false),将诊断渲染后与snapshots/node-types-fenced.txt 逐字节比对。这保证了:任何对@types/node类型表面或降级表的改动,若导致栅栏代码、消息文案或 hint 变化,都会被测试立即捕获——结合第二节的版本锁定,整条链路的可复现性由此闭环。
六、跨模块类型采纳:source-import/ 与 RequestInfo 全局
6.1 场景与样本
fixture 中的source-import/目录验证一个更微妙的组合场景:被导入的 TypeScript 源码使用了 TypeScript 默认 DOM lib 提供的RequestInfo全局,而 scriptc 同时采纳项目的@types/node声明。
source-import/dependency.ts 定义了一个引用该全局的类型:
export type RequestHandler = (input: URL | RequestInfo) => void; export const message = "source fetch types resolve";source-import/main.ts 则是标准的跨文件导入:
import { message } from "./dependency.ts"; console.log(message);目录内另有package.json声明{"type":"module"},将导入语义固定为 ESM。这里的关键点在于:RequestInfo并非来自@types/node,而是来自 TypeScript 自身的默认 DOM lib;在采纳@types/node的项目中,这两套类型来源会同时存在于编译程序中。fixture 要证明的是:这样的类型解析对 scriptc 的降级流程是良性的——依赖模块可以正常类型化、正常编译,跨文件类型引用不会因为「@types/node 接管」而被误伤。
6.2 harness 验证
对应的测试为 project-config.test.ts 的node-types: imported TypeScript sources can use the RequestInfo global。它把source-import/main.ts作为入口编译,验证导入链完整通过。这个用例回答了一个真实项目里最常见的问题:当我把一个用了 DOM/Web 全局(RequestInfo、fetch、Response等)的模块 import 进一个安装@types/node的项目时,会不会因为类型来源混杂而编译失败?答案是否定的——scriptc 对「@types/node 声明的表面」与「TypeScript 默认 lib 提供的表面」分别处理,各自按自己的降级规则执行。
七、没有 @types/node 的项目:行为完全不变
README 的最后一段给出了这条采纳路径的边界条件:
Projects WITHOUT
@types/node(every other fixture and the whole corpus) keep the shipped fallback declarations and behave exactly as before.
结合 dts-paths.ts 的实现,这一句包含两层含义:
- 注入层面:无
@types/node时,fallbackDtsPath()指向的scriptc-node-fallback.d.ts(console、process、node:fs)照常注入程序;有@types/node时该文件让位,避免声明冲突; - 行为层面:同一份用户源码,在两个世界中只要落在降级表内,就降级到同一组 libCall——因此「with @types/node」不会改变任何已支持表面的编译结果,只会新增:a) 更精确的 Node 类型(原本 fallback 声明不覆盖或覆盖较粗的类型);b) 对「声明但未降级」表面从「可能报裸类型错误」升级为「精确命名的 SC2020 栅栏」。
从源码结构看,这正是设计上的刻意对称:fixture 只在「类型来源」上做切换,不在「降级行为」上分叉。对维护者而言,这意味着新增一个 node-types 之外的 fixture 或语料库用例时,无需担心@types/node的在场与否会悄悄改变既有编译语义。
八、如何在真实项目中复用这套机制
把上述机制落到自己的 Node/TypeScript 项目上,实践要点如下:
- 在
devDependencies中固定@types/node版本(如"@types/node": "24.13.3"),使类型表面可复现;若项目还需要 web 平台全局,undici-types会随@types/node一并进入,无需单独声明。 - 理解「类型检查通过 ≠ 可以编译」:
@types/node会让所有声明处的代码通过 tsc 的类型检查,但 scriptc 只降级受支持的子面。遇到SC2020诊断时,先读 hint——它要么给出该成员的实际受支持子面(如 Timeout 的unref()/ref()/hasRef()/refresh()),要么给出替代途径(如--dynamic下的更宽 Web API)。 - 利用栅栏而非规避类型:对「声明但未降级」的表面,正确做法是让编译在 CI 中明确失败并暴露诊断(像 fenced.ts 的快照测试那样钉住行为),而不是用
any绕过类型系统——绕过只会把「编译期可知的问题」推迟成运行时的损坏二进制。 - 按需迁移到受支持子面:如果项目依赖
process.memoryUsage()这类未降级成员,可以围绕其 hint 中列出的受支持成员(如uptime()/cpuUsage()/resourceUsage())重构代码,使程序进入可静态编译的集合。 - 在仓库内做同样的版本锁定与快照测试:仿照本 fixture 将
node_modules作为已提交测试数据,配合toMatchFileSnapshot式的诊断快照,可以让「@types/node 升级导致类型表面漂移」这一类回归在第一时间被发现。
结语
node-types fixture 用一份极小但信息密度极高的测试目录,完整演示了 scriptc 对真实 Node/TypeScript 项目的接入策略:版本锁定的@types/node与undici-types提供可信类型表面,dts-paths.ts 中的fallbackDtsPath()/isNodeTypesPath()完成声明切换与 provenance 识别,diagnostic.ts 中的 SC2020 家族栅栏保证「声明但未降级」的表面以精确诊断失败而非损坏二进制收场,而 project-config.test.ts 与 node-types-fenced.txt 把这一切钉成可复现的断言。理解这条路径,是让真实世界项目顺利进入 scriptc 静态编译世界的第一步。
- 编译器
- 语言运行时
- 开发工具
- CLI
【免费下载链接】scriptc
TypeScript-to-Native Compiler
相关推荐
Nerd:让JavaScript编译为原生二进制
Nerd:让JavaScript编译为原生二进制 项目介绍 Nerd 是一个 JavaScript 原生编译器,旨在使 JavaScript 变得更加通用。它可
node-fetch TypeScript类型定义完全指南:@types/node-fetch使用详解
node fetch TypeScript类型定义完全指南:@types/node fetch使用详解 概述 TypeScript类型定义(Type Defin
后端scriptc 变更全览:从 TypeScript 到原生可执行文件的编译器演进路线
scriptc 变更全览:从 TypeScript 到原生可执行文件的编译器演进路线 scriptc 是一个把 TypeScript 和 JavaScript
编译器语言运行时开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考