本指南逐步讲解将项目从 Bun 运行时迁移到 Nub 驱动的原生 Node。迁移的形态是:Nub 在原生 Node 上直接运行你的 TypeScript——你项目固定的版本,按需提供——你的包管理器和锁文件保持原位,你代码调用的 Bun.* API 被替换为它们的 Node 或 npm 生态系统等价物。大部分工作是最后一部分——Bun 的全包内置全局 API 是没有精确 Node 对应物的接口,所以每个都需要有意的替换。

以下所有内容基于 Nub 今天实际的行为。Nub 尚未做到的地方,本指南直说而非掩盖。

运行时和 TypeScript 执行

Bun 直接运行 .ts 文件。Nub 也是——指向入口文件,它在原生 Node 上转译 TypeScript(和 JSX、enum、namespace、参数属性、emitDecoratorMetadata),无需 tsconfig 构建步骤也无需注册单独加载器。

bun run src/index.ts          # 之前
nub src/index.ts              # 之后

裸文件没有 nub run <file> 形式——nub <file> 是文件运行器,nub run <script> 是 package.json 脚本运行器。保持两者清晰:nub run dev 运行 "dev" 脚本,nub src/server.ts 运行文件。

如果你需要对照未增强的 Node 比较——排除转译或运行时增强差异——nub --node <file> 在关闭 Nub 增强的纯 Node 上运行文件,同时仍使用你项目固定的 Node 版本。Truthy 的 NODE_COMPAT 环境变量做相同的事情,全树持久。

选择 Node 版本

Bun 捆绑自己的运行时,所以 Bun 项目从不选择 Node 版本。在 Nub 上你需要,因为 Nub 运行你的 Node。按 Node 生态系统其余部分的方式固定——.nvmrcpackageManager/devEngines 条目,或 nub node pin——Nub 提供并使用该版本。选择你的依赖实际支持的版本;一些包伸手到全局变量(Float16Array、web crypto 接口、structuredClone),它们只在给定 Node 主版本起存在,过旧的下限会在运行时而非安装时失败。不确定时,固定当前 LTS 或最新稳定版并设置现实的 engines.node 下限。

Nub 的运行时增强按 Node 版本分层:快速层(Node 22.15+)使用同步模块钩子,兼容层(18.19–22.14)使用异步加载器 worker 路径。两者都转译 TypeScript;机制不同。较新的 Node 是更平滑的路径。

包管理器和锁文件

Nub 的包管理器接口镜像 pnpm 的命令语法:

bun install                   # → nub install   (别名: nub i)
bun add <pkg>                 # → nub add <pkg>
bun remove <pkg>              # → nub remove <pkg>
bunx <pkg>                    # → nubx <pkg>     (别名: nub dlx <pkg>)

Nub 与你项目已有的工具锁文件兼容——它往返 npm、pnpm 和 bun 锁文件并读取 yarn 的——所以你不必转换锁文件来采用 Nub。CLI 语法是 pnpm 的,但锁文件轴是多 PM 的。如果你要 Nub 作为 Nub 身份项目拥有解析(只消费中性的、跨工具配置),丢弃 Bun 品牌的 bunfig.toml 并让 Nub 写入中性 .npmrc;如果你保持现任 PM,Nub 忠实镜像该 PM。

与 Bun 的一个真实差异:Nub 遵循 pnpm 的依赖模型,自动安装 peer 依赖。在 Bun 下因 Bun 静默安装其 peer 而工作的包会显现为缺失——将 peer 显式添加到你的 package.json。这是迁移后最常见的安装意外。

脚本:并行和顺序

Bun 暴露 bun run --parallelbun run --sequential 来扇出多个脚本。Nub 的 nub run 镜像 pnpm 的脚本语法,不是相同形态:

  • 单个脚本名称运行该脚本:nub run build
  • /regex/ 字面量运行每个名称匹配的脚本,按 package.json 顺序:nub run "/^build:/"。默认匹配集并发运行(--sequential 串行化它)。这是 pnpm 的正则脚本选择器,是 Nub 今天支持的原生多脚本形式。
  • 空格分隔的 nub run a b 不是多脚本形式——与 pnpm 中完全一样,那运行脚本 a 并带 b 作为参数。不要期望 nub run lint test 运行两个脚本。

没有带显式命名脚本列表的原生 run-p / run-snub run --parallel lint test typecheck)。如果你的 Bun 设置依赖 --parallel <a> <b> <c>,可移植的替代是 npm-run-all2,它提供 run-prun-s 并在 Nub 下干净运行:

// package.json
{
  "scripts": {
    "check": "run-p lint typecheck test",   // 并行
    "release": "run-s build publish"         // 顺序
  }
}
nub run check

如果你的脚本名称共享前缀,正则选择器是更轻的选项且无需依赖:命名为 check:lintcheck:typecheckcheck:test 并运行 nub run "/^check:/"

环境变量文件

Bun 自动加载 .env.env.local.env.<mode> 并接受显式 --env-file。Nub 为文件运行器、nub runnub watch 自动发现和加载你的 .env* 文件,并接受显式 --env-file

bun --env-file=.env.production src/index.ts
nub --env-file=.env.production src/index.ts

需了解的行为:

  • 传递任何 --env-file 标志抑制 Nub 的自动 .env* 发现——只有你命名的文件加载,匹配"显式即显式"约定。这与显式文件退出自动级联的方式一致。
  • Nub 在 env 文件值中展开 ${VAR} 交叉引用(自动加载路径和显式标志路径都展开)。纯 Node 的 --env-file 解析器不展开;Nub 弥合该差距。
  • Shell 环境仍优先于文件值(环境中已设置的变量不被覆盖)。

两个需规划的差距:

  • Nub 目前不将 --env-file-if-exists 实现为自身标志。如果你传递它,Nub 不将其视为 env 文件指令——它转发给 Node,Node 20.12+/22+ 为其运行的文件遵守它,但 Nub 自身的 .env-合并层不会像对待 --env-file 那样拾取这些变量。优先使用 --env-file 和你知道存在的文件,或依赖自动发现(已跳过缺失文件)。
  • Nub 的 --env-file 适用于其上的运行;它不是通用的 nub exec 风格 env 注入器用于任意子进程。对于将 env 文件加载到非 Nub 运行入口的工具(例如 Prisma CLI 调用),dotenv-cli 仍是可移植的答案。

关于 Bun 的 --env-file "级联"的说明

有时假设 Bun 的 --env-file=.env 展开为级联——也加载 .env.local.env.development 等。它不。在 Bun 中,显式文件路径和默认级联是互斥的:当一个或多个 --env-file 参数存在时,Bun 只加载那些命名的文件(按逗号分割,后者优先)并完全跳过默认 .env/.env.local/.env.<mode> 发现。级联是你完全不传递 --env-file 时的默认行为。这与 Node 的单文件 --env-file 语义一致并与 Nub 的行为一致,所以这里没有需要特殊复现的东西——--env-file 在所有三个运行时中意味着"只加载这些文件"。

Shell 命令:Bun.$

Bun 的 $ 模板标签运行 shell 命令,带可链式结果 API。没有 Node 内置对应物。常见替换:

// 之前 (Bun)
import { $ } from "bun";
const out = await $`ls -la ${dir}`.text();
// 之后 (Node) — tinyexec
import { x } from "tinyexec";
const { stdout } = await x("ls", ["-la", dir]);

tinyexec 是小型、轻依赖的进程运行器,是大多数 $ 调用点的最轻直接替代。如果你的代码大量依赖 $ 的链方法(.text().quiet().nothrow()),在 tinyexec(或 execa,更重但功能完整)上约 100–150 行的薄包装保留调用形态,使你不必重写每个调用点。

Bun.$ 做的两件事是普通进程运行器不做的:它解析和运行嵌入的 mini-shell(管道、重定向、模板字符串中的 glob),并自动转义插值。tinyexec 和 execa 运行带参数数组的单个程序——它们不解释管道。对于管道命令,在 JavaScript 中重构为显式步骤(例如用 node:fs 读取目录而非 ls | grep),这也比将用户控制的字符串交给 shell 更安全。

数据库访问:Bun.sql

Bun 提供 Bun.sql,基于 postgres.js 建模的 Postgres 客户端。它没有 Node 等价物。两个干净的替换,取决于你已有的:

  • postgres (postgres.js) — 几乎零摩擦的交换,因为 Bun.sql 基于它建模。标签模板 API 基本相同;主要变化是你自行构造客户端并在进程关闭时调用 sql.end()

    import postgres from "postgres";
    const sql = postgres(process.env.DATABASE_URL!);
    const rows = await sql`select * from users where id = ${id}`;
    await sql.end();
  • pg — 如果你已使用 Prisma(其驱动适配器拉入 pg),重用 pg 进行原始查询避免添加第二个 Postgres 客户端:

    import { Pool } from "pg";
    const pool = new Pool({ connectionString: process.env.DATABASE_URL });
    const { rows } = await pool.query("select * from users where id = $1", [id]);

如果你使用 Prisma,迁移主要是关于原始查询——你的 Prisma 模型不受运行时交换影响。选择一个原始查询客户端并一致使用。

完整的 Bun.*bun:* API 映射

这是综合参考:每个 Bun 特定 API,按类别分组,附推荐的 Node 或 npm 生态系统替代品。从中读取三件事。首先,Bun "API" 的大部分是标准 Web API(fetchWebSocketRequest/ResponseBlobURLcrypto.subtlestructuredClone、流)——那些在 Node 和 Nub 的运行时上存在,所以没有需要迁移的。其次,仅存在于使 TypeScript/JSX 运行的东西(Bun.Transpiler)在 Nub 下不必要,后者直接运行 TypeScript。第三,真正 Bun 独有的电池——Bun.* 服务器、数据库、FS、哈希和 shell 助手——每个映射到一个维护的 Node 原语或成熟的 npm 包,下方列出。每个调用点选择一个替换并保持一致。

命名单个包的地方,是截至撰写时的当前维护选项;标准库覆盖的地方,node:* 模块是首选。

进程、shell 和系统

Bun API推荐替换备注
Bun.$ (shell)tinyexecexeca参见上方 shell 部分。tinyexec 是更轻的直接替代;execa 功能完整。两者都不解释管道——将管道重构为显式步骤。
Bun.spawnBun.spawnSyncnode:child_processspawnspawnSync如果你的代码将 .stdout 读取为 ReadableStream,则包装以重现 web-stream 结果形态。
Bun.which(cmd)which或用 node:fs 手动针对 process.env.PATH 解析。
Bun.openInEditor()openlaunch-editor在用户编辑器中打开文件;无 Node 内置。
Bun.envprocess.env直接重命名。
Bun.argvprocess.argv直接重命名。
Bun.mainprocess.argv[1](解析后的)或 import.meta.main(Node 24+)入口模块路径。
Bun.versionBun.revisionprocess.versionsprocess.versionNub 运行 Node,所以报告 Node 版本。

文件系统和 globbing

Bun API推荐替换备注
Bun.file(path)node:fs / node:fs/promisesreadFilecreateReadStreamBun.file 返回惰性 Blob 类句柄;最近的 Node 形态是 fs.openAsBlob(path)(Node 20+)获得真实 Blob,否则 readFile
Bun.write(path, data)node:fs/promises writeFile如果你依赖 Bun.write 的自动 mkdir 父目录,则包装(先 mkdir(dirname, { recursive: true }))。
Bun.stdinBun.stdoutBun.stderrprocess.stdin / process.stdout / process.stderrNode 的流;如果你将它们读取为 web stream 则包装。
Bun.Globtinyglobbyfast-globnode:fs 还有内置 glob/globSync(Node 22+)用于更简单模式。tinyglobby 是轻量的当前选择。
Bun.FileSystemRouterfind-my-way 或框架的路由器基于文件系统的路由解析;大多数框架(Next、Nitro、Hono 带文件路由插件)提供自己的。
Bun.mmapmmap-io内存映射文件;小众,原生插件。除非你特别需要 mmap,否则优先流式读取。

HTTP、网络和实时通信

Bun API推荐替换备注
Bun.serveHono@hono/node-server)、FastifyExpressnode:http 是原始原语;框架是实际替换。Hono 在精神上最接近(Web 标准 Request/Response)并在 Node 上不变运行。
Bun.serve WebSocket 处理器ws标准 Node WebSocket 服务器。与你的 HTTP 框架的升级处理配对。
new WebSocket() (客户端)运行时内置WebSocket 客户端全局变量在现代 Node 和 Nub 的运行时上存在——无依赖。
Bun.listenBun.connect (TCP)node:netcreateServerconnect直接标准库映射。
Bun.udpSocketnode:dgram直接标准库映射。
Bun.dns.lookupBun.dns.prefetchnode:dns / node:dns/promisesNode 中无 prefetch 原语;查找直接映射。
Bun.CookieBun.CookieMapcookie(解析/序列化)或你的框架的 cookie 助手。
Bun.CSRF.generateBun.CSRF.verifycsrf-csrfcsrf可用时的框架级 CSRF 中间件。
fetchRequestResponseHeadersFormData运行时内置标准 Web API — 在 Node 和 Nub 上存在;无需迁移。

数据库和存储

Bun API推荐替换备注
Bun.sqlBun.SQL (Postgres)postgres (postgres.js) 或 pg参见上方数据库部分Bun.sql 基于 postgres.js 建模,所以它是近乎直接替代;如果你已通过 Prisma 拉入则用 pg
bun:sqlitenode:sqlite (Node 22.5+) 或 better-sqlite3node:sqlite 是标准库同步 SQLite(实验性但功能可用);better-sqlite3 是成熟的、经过实战检验的选择,API 最接近。
Bun.redisBun.RedisClientioredisredis两者都是维护的、功能完整的 Redis/Valkey 客户端。
Bun.s3Bun.S3Client@aws-sdk/client-s3官方 AWS SDK;与任何 S3 兼容存储一起工作。
Bun.secretskeytar 或通过小型原生插件的平台钥匙串OS 凭据存储访问;keytar 是成熟的跨平台选项。

哈希、加密和压缩

Bun API推荐替换备注
Bun.password.hashBun.password.verifynode:crypto scrypt@node-rs/argon2Bun.password 默认 argon2id;@node-rs/argon2 匹配该算法。node:cryptoscrypt 是零依赖标准库路径。如果你特别需要 bcrypt 则用 bcrypt
Bun.hashBun.hash.* (非加密)xxhash-wasmnode:cryptoBun.hash 是快速非加密哈希(wyhash 族);xxhash-wasm 是接近的、快速的等价物。稳定加密摘要用 node:crypto
Bun.CryptoHashernode:crypto createHash / createHmacSHA/MD5/HMAC 的直接映射;流式 update/digest 形态相同。
Bun.shanode:crypto createHash('sha256')便捷一次性 SHA。
Bun.gzipSyncBun.gunzipSyncnode:zlibgzipSyncgunzipSync直接标准库映射。
Bun.deflateSyncBun.inflateSyncnode:zlibdeflateSyncinflateSync直接标准库映射。
Bun.zstdCompressSyncBun.zstdDecompressSync(+ 异步)node:zlib zstd (Node 22.15+ / 23.8+)Node 最近添加了 zstd 支持;旧下限用 @mongodb-js/zstdfzstd。检查你固定的 Node 版本。
crypto.subtlecrypto.randomUUIDcrypto.getRandomValues运行时内置标准 Web Crypto — 在 Node 和 Nub 上存在。

构建、转译和模块

Bun API推荐替换备注
Bun.Transpiler在 Nub 下不需要Nub 直接运行 TypeScript 和 JSX,所以进程内转译器 API 不必要。如果你确实需要在运行时转译字符串,用 oxc-transformesbuildtransform
Bun.buildesbuildtsuprolldown用于应用/库打包。tsup 包装 esbuild 用于常见库场景;rolldown 是基于 Rust 的 Rollup 兼容打包器。
Bun.plugin (加载器插件)打包器的插件 API (esbuild/rollup/rolldown 插件)Bun 的运行时加载器插件模型没有 Node 等价物;将自定义加载器移入你的打包器或 module.register() 钩子。
Bun.resolveSyncimport.meta.resolve()node:module createRequire(...).resolveimport.meta.resolve 是标准 ESM 解析器;createRequire().resolve 用于 CJS 风格解析。
import.meta.dirimport.meta.fileimport.meta.dirnameimport.meta.filenameNode ESM 等价物(Node 20.11+/21.2+)。参见上方 import.meta 部分

测试

Bun API推荐替换备注
bun:testvitestnode:testVitest 在人体工程学上最接近(describe/it/expect、watch 模式、mocking)并在 Nub 下运行。node:test + node:assert 是零依赖标准库运行器。
bun:test mocking(mockspyOnVitest 的 vi.fn/vi.spyOn,或 node:testmockVitest 的 mocking 是与 Bun 最接近的匹配。
bun:test 快照Vitest 快照内置于 Vitest;node:test 无原生快照支持。

FFI 和原生

Bun API推荐替换备注
bun:ffidlopenCString、…)koffiNode 的维护的、积极开发的 FFI 库(未维护的 ffi-napi 的后继者)。
Node-API (N-API 插件)Node-API工作方式相同——N-API 插件是 Node 标准。用 node-gypnapi-rs 为 Rust 构建。
bun:jsc (引擎内部)无等价物JavaScriptCore 特定;无 V8/Node 对应物。移除或在功能重叠处替换为 node:v8 / node:vm(堆快照、序列化)。

HTML、文本和解析

Bun API推荐替换备注
HTMLRewriternode-html-parsercheerio对于流式转换,html-rewriter-wasm 移植了 Cloudflare 的重写器。Nub 不提供内置重写器。
Bun.escapeHTMLescape-html小型,单一用途。
Bun.stringWidthstring-width终端显示宽度(处理宽字符、ANSI)。
Bun.stripANSIstrip-ansi移除 ANSI 转义码。
Bun.wrapAnsiwrap-ansiANSI 感知的自动换行。
Bun.markdownmarkedmarkdown-itMarkdown 转 HTML。
Bun.colorculoricolor跨格式颜色解析/转换。
Bun.semversemver权威 semver 库;Bun 的 API 是子集。
Bun.TOML.parsesmol-toml@iarna/tomlTOML 解析。
Bun.YAML.parseyamlYAML 解析/序列化。
Bun.Imagesharp图像解码/编码/转换。

工具、计时和检查

Bun API推荐替换备注
Bun.inspectnode:util inspect直接映射;相同的深度/颜色选项。
Bun.deepEqualsBun.deepMatchnode:util isDeepStrictEqual,或 dequalisDeepStrictEqual 用于严格相等;dequal 用于快速松散相等检查。
Bun.sleepBun.sleepSyncnode:timers/promises setTimeout(ms)await setTimeout(ms) 用于异步;真正同步睡眠用 SharedArrayBuffer 上的 Atomics.wait(很少需要)。
Bun.nanosecondsprocess.hrtime.bigint()高分辨率单调时钟。
Bun.randomUUIDv7uuidv7crypto.randomUUID() 内置但发出 v4;uuid 的 v7 用于时间排序 ID。
Bun.peek无等价物同步读取已结算 promise 的值;重构为 await 或自行跟踪解析。
Bun.fileURLToPathBun.pathToFileURLnode:urlfileURLToPathpathToFileURL直接标准库映射。
Bun.readableStreamTo*TextJSONBlobArrayBufferBytesArrayFormData直接消费 web streamnew Response(stream).text()/.json()/.blob()/.arrayBuffer()/.formData() 覆盖大部分;node:stream Readable.fromWeb 桥接到 Node 流。
Bun.deflate/buffer sink(Bun.ArrayBufferSinkBun.allocUnsafeBun.concatArrayBuffersBufferBuffer.allocUnsafeBuffer.concat)和 node:streamNode 的 Buffer 覆盖分配/拼接助手;用 PassThrough 流进行 sink 式累积。
Bun.gcBun.generateHeapSnapshotnode:v8writeHeapSnapshot--expose-gc 用于 global.gc堆快照和手动 GC;GC 需要 --expose-gc 标志。
Bun.stdin/stdout 字节 sink、Bun.indexOfLinenode:buffer / node:readline通过流上的 node:readline 进行行扫描。
Bun.cron (调度)cronernode-cron进程内 cron 调度。croner 是积极维护的、无依赖选择。

高频 API(Bun.fileBun.writeBun.spawnBun.$)的实用迁移策略:编写一个小 shim 模块,在你代码已使用的调用形态下重新导出 Node 原语,在所有使用这些 API 的地方导入它,差异保持局限到那个文件而非分散到每个调用点。

import.meta 和 CJS 全局变量

Bun 和 Node 在这里已收敛,所以这些是 ESM 中的机械重命名:

BunNode (ESM)
import.meta.dirimport.meta.dirname
import.meta.fileimport.meta.filename
import.meta.mainimport.meta.main (Node 24+) — 否则将 process.argv[1] 与解析的模块路径比较。
__dirname (在 ESM 中)import.meta.dirname
__filename (在 ESM 中)import.meta.filename

生产服务器和 Docker

如果你的服务器运行时目标是 Bun(例如 Nitro 的 bun 预设等框架预设),切换到其 Node 目标(node-server / node)并用 node 运行构建入口。Docker 基础镜像从 Bun 镜像改为 Node 镜像加全局 Nub 安装:

FROM node:26-slim
RUN npm install -g @nubjs/nub
# ... 复制应用, nub install, build ...
CMD ["node", ".output/server/index.mjs"]

直接用 node 运行已构建、已转译的服务器包;在构建阶段(安装、运行脚本、运行 TypeScript 入口)使用 Nub,那里它的增强物有所值。

CI 和 Dependabot

  • CI 设置: 用 Nub 的替换 Bun 设置 action。参见 setup-nub 页面了解 GitHub Action。
  • Dependabot / Renovate: 这些 bot 没有原生 Nub 支持。使用 package-ecosystem: "npm" 让 bot 理解你的清单,并添加一个在 bot 更新后运行 nub install 的工作流步骤以 Nub 的格式重新生成锁文件。提升 package.json 但无法重新生成锁文件的 bot 否则会使两者不同步。

供应链冷却(minimumReleaseAge

Bun 的 bunfig.toml 支持 [install].minimumReleaseAge——拒绝安装比阈值更新的包版本的供应链加固门。Nub 遵守相同门。作为 Nub 身份项目它位于 .npmrc 中为 minimum-release-age,以分钟为单位:

# .npmrc
minimum-release-age=1440   # 一天

如果你迁移 bunfig.toml,注意单位变化:bunfig 以表达 minimumReleaseAgenub install 将其转换为引擎的分钟(向上取整以使小的非零门不收缩为零)。Bun 的 minimumReleaseAgeExcludes 映射到 Nub 的 minimumReleaseAgeExclude

值得预先防范的陷阱

  • Peer 依赖不自动安装。 与 Bun 最大的行为差异。显式添加缺失的 peer;预期不止一个会浮现。
  • nub run a b 不是多脚本。 它运行 a 并带 b 作为参数(与 pnpm 和 Bun 的普通 run 相同)。使用 /regex/ 选择器或 npm-run-all2 进行真正扇出。
  • --env-file 抑制自动发现。 一旦你传递显式文件,该运行的自动 .env* 级联关闭——传递你需要的每个文件,或依赖自动发现。
  • --env-file-if-exists 尚不是一等 Nub 标志。 使用 --env-file 和你知道存在的文件,或自动发现(已容忍缺失文件)。
  • 固定现实的 Node 版本。 Nub 运行你固定的 Node;过旧的下限在运行时因缺失全局变量失败,而非安装时。固定当前 LTS 或稳定版并设置 engines.node
  • 内存特征与 Bun 不同。 Node 对于相同的并行扇出通常比 Bun 使用更多内存。如果你在 Bun 下并行化了多个重任务(类型检查、lint、测试)并在迁移后遇到 OOM kill,限制并发(例如同时最多运行两个重任务,并将最耗内存的排在最后)。
  • 良性 module.register() 弃用警告 可能在新 Node 上从加载器钩子出现;它无害并随工具采用 module.registerHooks() 解决。用 --no-warnings 或特定 --disable-warning 静默(如果嘈杂)。