直接运行一个 TypeScript 文件——不需要 tsconfig、不需要构建步骤、不需要 ts-node

nub index.ts
nub server.tsx
nub script.js

在原生 Node 之上,nub <file> 添加了一个现代运行时:

  • 🦆 完整 TypeScript — 不可擦除语法(enumnamespace、参数属性),emitDecoratorMetadata 装饰器
  • ⚛️ JSX / TSX — 默认使用自动运行时
  • 🧭 编辑器风格解析 — 无扩展名导入、.js → .ts 重写、tsconfig.json#paths
  • 🆕 现代语法如 using — 在旧 Node 上由转译器降级
  • 🔐 自动 .env* 加载 — Next.js / Vite 对等,支持 ${VAR} 展开
  • 🗂️ 数据文件导入 — .yaml.toml.jsonc.json5.txt 作为解析后的默认导出
  • 🌐 按 Node 版本回填现代全局变量 — TemporalURLPatternWebSocketEventSourcenode: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(),通过 --require CJS 预加载在线程中运行。没有加载器 worker,更低的启动开销。
  • 兼容层 — 异步 module.register(),通过 --import ESM 预加载在加载器 worker 线程中运行。相同的增强,稍多的启动开销。

Nub 根据你的 Node 版本选择层级。快速层需要同步 module.registerHooks(),Node 在 v23.5.0 中添加并回移植到 v22.15.0——但从未回移植到 20.x 线。所以层级边界按 Node 主版本不同地落地:

Node 线Nub 支持的最低版本快速层最低版本层级推荐
18.x18.19兼容(无 registerHooks
20.x所有 20.x兼容(registerHooks 从未回移植到 20)
22.x所有 22.x22.1522.15+ 快速,22.0–22.14 兼容✓ 22.15+
23.x所有 23.x23.523.5+ 快速,23.0–23.4 兼容✓ 23.5+
24.x所有 24.x24.0快速(整条线)
25.x所有 25.x25.0快速(整条线)
26.x所有 26.x26.0快速(整条线)默认

快速层列中的破折号表示该线没有快速层构建——每个补丁在兼容层运行。两个层级都完全支持;你的代码在它们之间没有任何变化。✓ 标记快速层版本,推荐用于最低启动开销——最新(Node 26)是默认值。

为什么推荐快速层 Node

兼容层通过异步 module.register() 加载器 worker 路由模块加载,这比快速层慢约 1.4 倍冷启动——固定的 ~80 ms 启动开销加上 ~90 µs 每模块运行时开销为零:执行速度相同,仅模块加载有惩罚。快速层(22.15+,同步 module.registerHooks())没有这些。

增强

简而言之:

  • TypeScript — 整个 TypeScript 接口直接运行,包括不可擦除语法(enumnamespace、参数属性)。类型被擦除而非检查。
  • JSX.jsx.tsx 文件默认使用现代自动运行时运行,通过 tsconfig.json 或每文件 @jsxImportSource pragma 配置。
  • 装饰器 — 旧版装饰器在 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 StoragelocalStorage / 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最低版本方式
TemporalNode 26 以下填充,以上原生
URLPatternNode 24 以下填充,以上原生
RegExp.escapeNode 24 以下填充,以上原生
Error.isErrorNode 24 以下填充,以上原生
Promise.tryNode 24 以下填充,以上原生
Float16ArrayNode 24 以下填充,以上原生
navigatorNode 21 以下回填,以上原生
navigator.locksNode 24.5 以下填充,以上原生
reportError填充
vm.Module取消标志
Wasm module importsNode 24.5 以下(22.x 线 22.19)取消标志,以上原生
WebSocketNode 20.10Node 22 以下取消标志,以上原生
EventSourceNode 20.18原生线以下取消标志,以上原生
node:sqliteNode 22.5Node 22.13 以下取消标志,以上原生
addon importsNode 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
  • .nvmrc
  • package.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.runtimeengines.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_COMPAT1trueyes,不区分大小写)是 --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 上的用户是否会得到相同结果——这正是 --nodeNODE_COMPAT 关闭的。