运行时
Nub 在原生 Node 上运行你的代码时添加了什么——无需构建步骤的 TypeScript 和 JSX、编辑器风格的解析、自动 .env 加载、数据文件导入和现代全局变量——加上关闭一切的原生 Node 逃生舱。
直接运行一个 TypeScript 文件——不需要 tsconfig、不需要构建步骤、不需要 ts-node:
nub index.ts
nub server.tsx
nub script.js在原生 Node 之上,nub <file> 添加了一个现代运行时:
- 🦆 完整 TypeScript — 不可擦除语法(
enum、namespace、参数属性),emitDecoratorMetadata装饰器 - ⚛️ JSX / TSX — 默认使用自动运行时
- 🧭 编辑器风格解析 — 无扩展名导入、
.js → .ts重写、tsconfig.json#paths - 🆕 现代语法如
using— 在旧 Node 上由转译器降级 - 🔐 自动
.env*加载 — Next.js / Vite 对等,支持${VAR}展开 - 🗂️ 数据文件导入 —
.yaml、.toml、.jsonc、.json5、.txt作为解析后的默认导出 - 🌐 按 Node 版本回填现代全局变量 —
Temporal、URLPattern、WebSocket、EventSource、node:sqlite等 - 🧵 堆栈跟踪中的 source map,自动
- ⚡ 比
tsx快约 2.9 倍启动
并且它是 node 的真正直接替代品:相同的 argv 形状、相同的标志集、相同的行为——每个标志原封不动到达 Node。没有供应商特定 API 接口,没有锁定。传递 --node(或设置 NODE_COMPAT=1)关闭所有增强并在原生 Node 上运行。
支持的 Node 版本
Nub 在原生 Node 上运行你的代码——它解析你项目锁定的 Node 版本,如果缺失则获取一个,但从不提供自己的运行时。支持的最低版本是 Node 18.19;低于此版本 Nub 拒绝运行。在最低版本及以上,Nub 根据你的 Node 是否有同步 module.registerHooks() 选择两种交付层级之一——快速或兼容。两者功能等价;差异仅在启动开销。参见下方的逐行表格。
支持的版本和层级
硬性最低版本是 Node 18.19——低于它没有加载器钩子 API 能承载 Nub 的增强,所以 Nub 拒绝运行。在最低版本之上,增强通过两种层级之一交付:
- 快速层 — 同步
module.registerHooks(),通过--requireCJS 预加载在线程中运行。没有加载器 worker,更低的启动开销。 - 兼容层 — 异步
module.register(),通过--importESM 预加载在加载器 worker 线程中运行。相同的增强,稍多的启动开销。
Nub 根据你的 Node 版本选择层级。快速层需要同步 module.registerHooks(),Node 在 v23.5.0 中添加并回移植到 v22.15.0——但从未回移植到 20.x 线。所以层级边界按 Node 主版本不同地落地:
| Node 线 | Nub 支持的最低版本 | 快速层最低版本 | 层级 | 推荐 |
|---|---|---|---|---|
| 18.x | 18.19 | — | 兼容(无 registerHooks) | |
| 20.x | 所有 20.x | — | 兼容(registerHooks 从未回移植到 20) | |
| 22.x | 所有 22.x | 22.15 | 22.15+ 快速,22.0–22.14 兼容 | ✓ 22.15+ |
| 23.x | 所有 23.x | 23.5 | 23.5+ 快速,23.0–23.4 兼容 | ✓ 23.5+ |
| 24.x | 所有 24.x | 24.0 | 快速(整条线) | ✓ |
| 25.x | 所有 25.x | 25.0 | 快速(整条线) | ✓ |
| 26.x | 所有 26.x | 26.0 | 快速(整条线) | ✓ 默认 |
快速层列中的破折号表示该线没有快速层构建——每个补丁在兼容层运行。两个层级都完全支持;你的代码在它们之间没有任何变化。✓ 标记快速层版本,推荐用于最低启动开销——最新(Node 26)是默认值。
为什么推荐快速层 Node
兼容层通过异步 module.register() 加载器 worker 路由模块加载,这比快速层慢约 1.4 倍冷启动——固定的 ~80 ms 启动开销加上 ~90 µs 每模块。运行时开销为零:执行速度相同,仅模块加载有惩罚。快速层(22.15+,同步 module.registerHooks())没有这些。
增强
简而言之:
- TypeScript — 整个 TypeScript 接口直接运行,包括不可擦除语法(
enum、namespace、参数属性)。类型被擦除而非检查。 - JSX —
.jsx和.tsx文件默认使用现代自动运行时运行,通过tsconfig.json或每文件@jsxImportSourcepragma 配置。 - 装饰器 — 旧版装饰器在
experimentalDecorators下无需构建步骤运行,包括用于 DI / ORM 生态系统的emitDecoratorMetadata。 - 解析 — 无扩展名导入、
.js → .ts重写和tsconfig.json路径别名在运行时解析,与tsc和你的编辑器解析的方式相同。 - 环境变量 —
.env*文件在 Node 启动前加载到环境中,支持${VAR}展开。无需dotenv,无需--env-file。 - 加载器 — 直接导入
.yaml、.toml、.jsonc、.json5和.txt文件作为解析值,无需 npm 解析器。 - 现代 API — 现代全局变量和内置(Temporal、URLPattern、WebSocket、EventSource、node:sqlite 等)开箱即用,按 Node 版本带填充或取消标志。
- Worker — 浏览器形态
Worker全局变量运行你的.ts入口点,包装node:worker_threads。 - Web Storage —
localStorage/sessionStorage行为和支持持久存储的选项。
现代 API
现代全局变量——TC39、Web 平台和较新的 Node 内置——在 Nub 下开箱即用。无论你运行哪个 Node 版本,API 都在——Node 提供的地方是原生的,Node 不提供的地方 Nub 自动填补差距。你面向 API 编写;版本分割是 Nub 的问题,不是你的。
const now = Temporal.Now.plainDateTimeISO();
const route = new URLPattern({ pathname: "/u/:id" });
const ws = new WebSocket("wss://api.example.com");
const stream = new EventSource("https://api/stream");
const { DatabaseSync } = require("node:sqlite");两种机制完成工作,按 API 选择:Nub 要么预加载 JS polyfill(功能检测,所以原生全局变量出现时它立即退到一边),要么注入旧 Node 隐藏 API 的 --experimental-* 标志。一旦 Node 稳定某个 API,它就是原生的,Nub 什么也不做。
最低版本列是该 API 在 Nub 下可用的最低 Node。
| API | 最低版本 | 方式 |
|---|---|---|
Temporal | — | Node 26 以下填充,以上原生 |
URLPattern | — | Node 24 以下填充,以上原生 |
RegExp.escape | — | Node 24 以下填充,以上原生 |
Error.isError | — | Node 24 以下填充,以上原生 |
Promise.try | — | Node 24 以下填充,以上原生 |
Float16Array | — | Node 24 以下填充,以上原生 |
navigator | — | Node 21 以下回填,以上原生 |
navigator.locks | — | Node 24.5 以下填充,以上原生 |
reportError | — | 填充 |
vm.Module | — | 取消标志 |
Wasm module imports | — | Node 24.5 以下(22.x 线 22.19)取消标志,以上原生 |
WebSocket | Node 20.10 | Node 22 以下取消标志,以上原生 |
EventSource | Node 20.18 | 原生线以下取消标志,以上原生 |
node:sqlite | Node 22.5 | Node 22.13 以下取消标志,以上原生 |
addon imports | Node 22.20 | 取消标志,从不原生 |
破折号表示该 API 在整个支持范围(18.19 及以上)内可用。有最低版本的行有真实截断,因为底层 Node 机制在它之下不存在:
WebSocket全局变量需要 Node 20.10——Nub 在原生线(Node 22)以下注入的标志在更旧的 20.x 补丁上不存在。EventSource全局变量需要 Node 20.18,即 Node 在 20 LTS 线上添加该标志的补丁。node:sqlite模块需要 Node 22.5,即 Node 首次提供它的版本。- 从 ES 模块导入原生
.node插件需要 Node 22.20(或 23 线上的 23.6),即 Node 添加 Nub 注入标志的版本。它从不原生——Node 保持导入在标志之后——所以 Nub 在每个有它的版本上注入。
Node 版本解析
与 Corepack 的垫片一样,nub 自身按需自动安装 Node 版本。当你用 nub 运行文件时,它推断你项目期望的 Node 版本并提供它——如果不在你的机器上则下载和缓存匹配的原生构建。推断从工作目录向上遍历到最近的锁定,取第一个产生版本的来源。最高优先级在前:
NODE_EXECUTABLE— 指向 Node 二进制文件的显式路径(硬覆盖)package.json#/devEngines/runtime.node-version.nvmrcpackage.json#/engines/node(范围)PATH上的node,当没有锁定时
此解析的 Node 版本被自动安装并缓存以供将来运行。
$ echo {{NODE_MAJOR}} > .node-version
$ nub hello.ts
Using Node.js {{NODE_VERSION}} (resolved from .node-version)
Installed in 9.8s
Hello world!常见情况不会让你思考的一些精确细节:
- 发现向上遍历目录树到最近的锁定,并跳过位于已安装依赖(
node_modules下)中的锁定文件——依赖自身的 CI 锁定从不驱动你的项目。 package.json字段(devEngines.runtime、engines.node)从工作区根清单读取(当上方存在一个时)——monorepo 在根处一次性锁定其 Node。- 一旦版本被锁定,Nub 按顺序找到它的二进制文件:
PATH上的node(所以fnm/Volta/mise自动切换就能工作)→ Nub 自身的下载存储(~/.cache/nub/node/<version>/)→nvm安装的版本 → 从 nodejs.org 下载匹配的原生构建,SHA-256 验证并缓存。
关于锁定、预安装和 nub node 子命令,参见管理 Node 版本。
node 的直接替代品
node <args> 接受的任何东西,nub <args> 也接受。整个标志空间的传递是默认的——每个标志原封不动到达 Node:
# 诊断
--prof --cpu-prof --report-*
# 模块解析
--conditions --preserve-symlinks
# 检查器
--inspect --inspect-brk
# 警告
--no-warnings --trace-deprecation从 stdin 读取程序的工作方式与 node - 相同。
nub --max-old-space-size=4096 build.ts
nub --import ./instrument.js server.ts
# 文件之后的一切是你脚本的 argv
nub script.ts --port 3000 --verbose
echo 'console.log(1 + 1)' | nub -兼容模式
Nub 默认增强。兼容模式关闭所有增强——没有加载钩子、没有预加载、没有取消标志、没有 .env 加载——并在原生 Node 上运行你的代码,与 node <file> 完全相同。它仍然运行项目的锁定 Node(版本提供保持开启),所以它是增量性逃生舱和差异调试工具:它使"原生 Node 用户会得到相同结果吗?"测试变为现实。(这与兼容层级不同——没有同步 registerHooks 的 Node 线的启动路径,在上方表格中——那是关于增强如何交付,而非是否运行。)
有两种拼写,每次调用的标志和全树的环境变量,它们组合使用——任一强制兼容模式。
--node
传递 --node 以零增强运行单次调用。你的代码在项目的锁定 Node 上运行(来自 .node-version / .nvmrc,如果缺失则获取),原样的——这使它成为差异调试的工具:这在原生 Node 上能复现吗,在这个项目锁定的确切版本上?裸 node script.js 无法回答这个问题,因为它运行你 shell 的 Node,而非项目的。
nub --node script.js # 项目的锁定 Node,原样
node script.js # 你 shell 的 Node,不增强,不提供NODE_COMPAT
设置一个 truthy 的 NODE_COMPAT(1、true 或 yes,不区分大小写)是 --node 的项目/全树形式。它具有相同效果——零增强,Node 版本提供仍然开启——但它是持久且继承的,被树中的每个后代 node / nub 继承,所以你不需要每次调用重复 --node:
export NODE_COMPAT=1 # 此 shell 中的每个 nub/node 现在运行原样
nub script.ts # 原生 Node,仍在项目的锁定版本上工作原理
每一项增强都基于 Node 自身的扩展接口——Nub 不修补任何东西,也不提供自己的运行时。它注册一个 module.registerHooks() 加载钩子(兼容层上是异步 module.register() 加载器 worker)用于转译和解析,并注入一个小的 --import 预加载用于全局变量和 .env 加载。在快速层上,转译通过内嵌的 oxc N-API 插件运行。任何增强的测试标准是——带有匹配 module.register() / --import / npm 插件的原生 Node 上的用户是否会得到相同结果——这正是 --node 和 NODE_COMPAT 关闭的。