Nub 简介:一体化 Node.js 工具链
Nub 是一款 Rust 编写的 Node.js 工具包,可在原生 Node 上运行 TypeScript、脚本、安装依赖和管理 Node 版本,同时保留 lockfile 与安全检查。
Nub 是一个用 Rust 编写的 Node.js 命令行工具链。它负责转译 TypeScript、调度 package.json scripts、安装依赖并配置 Node 版本,然后把执行交给项目本就锁定的原生 node 二进制文件。它是对 Node 的增强而非替代,这正是它与 Bun 或 Deno 的根本区别。
大多数权衡过 Bun、Deno 与 Node 的团队,都没能迈过第一道坎:你不会仅仅因为开发体验更好,就把生产服务底下的运行时换掉。Nub 走的是另一条路,它自称是一个让你的 Node、lockfile 和包管理器保持原样的 Rust 工具链。下面说说它能带来什么,以及尝试它的代价。
核心要点
- Nub 是一个 Rust CLI,在原生
node二进制之上叠加了 TypeScript 执行、脚本调度、包安装和 Node 版本管理,因此不存在需要重新做兼容性验证的新运行时。 - Node 自带的 TypeScript 支持仅仅删除类型注解,凡是需要生成代码的语法一律拒绝,例如 enum、参数属性(parameter properties)或包含运行时代码的 namespace;而 Nub 的 loader 会真正编译这些形式。
- Nub 的安装器采用 pnpm 风格,可原地读写现有的 npm、pnpm 和 bun lockfile,yarn lockfile 则为只读。
- 安装期的各项防护无需任何配置:依赖的构建脚本在你批准之前始终被阻止,每次新解析都会对照 OSV 进行检查,24 小时的发布时效门槛则将刚发布的版本挡在门外。
- 没有 Nub 专有 API,也没有 Nub 自己的 lockfile,
nub.jsonc是可选的,因此移除 Nub 后项目即可回归纯 Node。
Nub 是什么,又不是什么?
Nub 不是第四种运行时。它是一个位于 Node 之前的单一二进制文件,承担目前需要 tsx、nvm、npx 和一个包管理器才能完成的工作,然后 exec 真正的 Node。它的主页把机制讲得很直白:oxc 在一个原生 addon 内部于内存中编译你的文件,随后由原生 node 二进制执行编译产物。底层没有另一套运行时,文件运行器接受的 flag 与 node 完全一致。
你的部署目标不会有任何变化。V8 版本、原生模块编译所针对的 C++ ABI、你的探针挂载的 process 接口,全都还是你原本就在发布的那个 Node。增强路径要求 Node 18.19 或更高版本(Node 18 LTS),支持 macOS、Linux 和 Windows,且每个平台都支持 x64 与 arm64。
该项目仍处于早期阶段。npm 包 @nubjs/nub 以 MIT 协议发布,截至最新版本仍处于 1.0 之前的 0.9.x 线上,新版本发布频繁。
Nub 如何做到无构建步骤运行 TypeScript?
Node 自带的 TypeScript 支持是剥离类型,而不是编译类型。类型注解被替换为空白字符,任何需要为其生成 JavaScript 的语法都会被拒绝。Node 的文档列出了这些情形:enum、包含运行时代码的 namespace、参数属性以及 import = 别名都会抛出 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX;装饰器无法解析;并且由于 Node 从不读取 tsconfig.json,paths 别名不会生效。更完整的转换模式曾经隐藏在 --experimental-transform-types 之后,但 Node 在第 26 版中移除了该 flag,因此类型擦除成为目前唯一的内置路径。
而这些被排除的语法,恰恰是 NestJS 或 TypeORM 代码库赖以构成的部分。看一个同时使用 enum、参数属性和无扩展名相对导入的文件:
// invoice.ts
import { Model } from "./base"
enum Status { Draft, Sent, Paid }
export class Invoice extends Model {
constructor(public status: Status = Status.Draft) {
super()
}
}
在普通的 node invoice.ts 下,enum 和参数属性都不可擦除,而且导入缺少扩展名。在 nub invoice.ts 下,同一个文件原封不动即可运行。Nub 将每个文件交给它的原生 addon 编译,所以 enum、参数属性和裸导入都能正常工作。它还会遍历你的 tsconfig.json 以及该文件 extends 的任何配置,然后通过 module.registerHooks() 的 resolve hook 把 paths 别名传给 Node 自身的解析器。
装饰器只支持一种形态。发布博文介绍的是 legacy experimentalDecorators,即 NestJS、TypeORM 和 Angular 所基于的那种形式,同时支持 emitDecoratorMetadata。TypeScript 5 默认使用的 Stage 3 装饰器则被拒绝,因为该转换在 oxc 中仍是一项未完成的工作。运行器会生成内联 source map,因此堆栈跟踪指向你的源码而非生成产物。最后这一点并非锦上添花:丢失 source map 的转译 TypeScript 会产出指向没人写过的代码的堆栈,这是排障时间被反复浪费的一个常见根源。
Nub 替代了哪些命令?
Nub 的单一二进制涵盖了目前分散在一整排工具中的工作。文档中的替代映射关系很直接:
| Nub 命令 | 替代对象 |
|---|---|
nub <file> | node、tsx、ts-node、dotenv-cli |
nub run <script> | npm run、pnpm run、yarn run |
nubx | npx、pnpm dlx、pnpm exec、yarn dlx |
nub install | npm、pnpm、yarn |
nub watch | nodemon、node --watch、tsx watch |
nub node | nvm、fnm、n、volta |
nub pm | corepack |
这张表并非全部。README 还介绍了 nubr,这是一个单一命令,可以运行一个文件、一个 package.json script 或 node_modules/.bin 中的某个 bin,并按该顺序依次尝试。对于无法安装二进制文件的项目,它还以 @nubjs/runner 的形式单独发布。
关键特性在于这些能力彼此独立。采用文件运行器并不强制你采用安装器;把 dev 脚本从 tsx watch src/server.ts 换成 nub watch src/server.ts,package.json 依然是一份普通的、兼容 npm 的 manifest。项目方公布了自己的基准测试来支撑速度方面的主张:脚本调度比 pnpm run 快 24 倍,bin 执行比 npx 快 19 倍,安装比 pnpm install 快 18 倍。README 中的成对计时数据显示,脚本调度为 14.7 ms,而 npm 为 329.9 ms;热态的 frozen install 为 171 ms,而 pnpm 为 3193 ms,两者均在 macOS 上测得。另一项安装基准测试使用 hyperfine 在 ubuntu-latest runner 上针对一个 1,168 个包的依赖树运行,结果为 Nub 346 ms、pnpm 3453 ms。
包管理器:pnpm 风格且保留 lockfile
Nub 的安装器不引入新的 lockfile 格式。它会从 package.json#packageManager 或找到的任一 lockfile 判断项目已在使用哪个包管理器,然后以 compat-mode 运行,并遵循该工具的配置文件和环境变量。CLI 本身是 pnpm 风格的,因此 nub install、nub add -E -D react、nub remove、nub update 和 nub ci 的行为符合肌肉记忆。
具体到 lockfile:npm、pnpm 和 bun 的 lockfile 可原地读写,yarn lockfile 为只读。不会发生任何转换,diff 中也不会出现第二份 lockfile。对于使用 pnpm 的团队而言,这正是决定它是否具备评估价值的关键问题。
无需 nvm 的 Node 版本解析
nub node 会解析项目所期望的 Node 版本,并按需完成配置。版本来源于 .node-version、.nvmrc 或 package.json#engines,缺失的版本会被自动下载并缓存,同时也提供显式命令:nub node install 26、nub node ls、nub node pin 26 和 nub node uninstall 22。它不依赖 shell hook,也不会重写你的 PATH——而后者正是 nvm 在 CI 和非交互式 shell 中容易出问题的地方。
供应链默认防护,以及不存在锁定
Nub 的安装期防护默认开启,无需配置。文档记录了其中四项。依赖的构建脚本在你批准该包之前不会运行。每次新解析都会对照 OSV 检查是否为已知的恶意版本。若某个版本丢失了此前发布所具备的发布信任证据,将被直接拒绝。minimumReleaseAge 默认为 24 小时,与 pnpm 采用的窗口一致,因此几分钟前刚发布的版本无法进入你的依赖树。发布博文还补充说明,解析结果指向 git+、file: 或裸 tarball URL 的传递依赖会被拒绝,而不是悄无声息地拉取。如果你已经梳理过针对 npm 供应链攻击的防御姿态,那么这就是把那份清单变成默认行为,而不是一份需要你自己维护的 .npmrc。
可逆性主张是另一半。Nub 不添加任何需要 import 的 API,不写入自己的 lockfile,并把 nub.jsonc 视为可选配置而非必需项。卸载这个二进制文件后,项目就用原有的工具链在纯 Node 上运行,因为源码从一开始就没有引用过 Nub。
谁该试试 Nub,谁不该?
如果你正通过 tsx 或 ts-node 运行 TypeScript,为了版本锁定还留着 nvm,而又不愿为摆脱这些花上一个季度去验证一套新运行时,那就值得一试。先在一个服务上从文件运行器开始,暂时不动安装器,看看 enum 与装饰器这类构建步骤摩擦是否就此消失。如果你需要为受监管的发布流程准备一套锁定的、乏味可靠的工具链,那么暂时先跳过它,因为一个发布间隔以天计的 1.0 之前的项目还达不到这个要求。而搞清楚这一点的成本,不过是 npm install -g @nubjs/nub 加上对一个你已有的文件执行一条命令。
常见问题
如何让 Nub 完全不做任何增强地运行一个文件?
使用兼容模式:单次调用传入 --node,或将 NODE_COMPAT 设为 1、true 或 yes 以覆盖整个进程树。在该模式下 Nub 不施加任何处理,因此没有 load hook、没有 preload、不注入 flag,也不加载 .env。它仍会判断项目锁定的是哪个 Node 并在需要时安装,因此你的代码会在正确的版本上以原生方式运行。这使它非常适合用来区分 Nub 的 bug 和 Node 的 bug。
Nub 支持哪些平台和 Node 版本?
Nub 为 Linux、macOS 和 Windows 提供预构建的 Rust 二进制文件,覆盖 x64 和 arm64,并在安装时拉取与你的平台匹配的 N-API addon。增强模式需要 Node 18.19 或更高版本,因为 transpile-on-import 路径所依赖的 loader-hook API 从该版本才开始出现。在更低版本上,增强命令会中止并报错,错误信息会指出最低版本要求并引导你使用兼容模式。
为什么安装会以 ERR_NUB_ALLOW_BUILDS_RENAMED 失败?
Nub 0.9.0 将 package.json 中的顶层构建允许列表从 allowBuilds 更名为 allowScripts,以匹配 npm 12 读取的字段。仍在根级携带 allowBuilds 映射的项目会被该错误直接拒绝,而不是仅给出警告,因此修复方式就是重命名这个键。pnpm 的 allowBuilds 是另一项设置,不受影响,无论它位于 pnpm-workspace.yaml 还是 package.json#pnpm 之下。
我可以在不切换包管理器的情况下使用 nubx 吗?
可以。nubx 会在 node_modules/.bin 中查找本地已安装的 CLI,无论它是由谁放进去的,因此在由 npm、pnpm、yarn 或 bun 安装的项目上都能工作,无需任何迁移。它以相同的名称接受 pnpm exec 的各项 flag,而 nub dlx 则与 pnpm dlx 保持一致,连 shell 模式也不例外,所以你现有的命令行可以直接沿用。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k