将 pnpm Monorepo 迁移至 Nub 的踩坑与解决方案
27 de julho de 2026
0
Computing/SoftwareRelated Video
8:29又有新的包管理器了!?(Bun 的替代方案)
Better Stack
Comments (0)
Log in to leave a comment
No posts yet
8:29Better Stack
Log in to leave a comment
No posts yet
Node.js 生态系统令人身心俱疲。叠加了 tsx、dotenv-cli、nvm 和 pnpm 的构建环境既臃肿,又经常在更新任何东西时搞乱脚本。当采用 Rust 编写的集成工具包 Nub 以将这套混乱的包管理打包为单个二进制文件之姿登场时,说实话在感到高兴的同时我也抱有怀疑。
实际将生产环境的 pnpm-workspace Monorepo 迁移到 Nub 后,发现它确实相当有吸引力。不过,过程可没有想象中那么顺畅。从依赖项冲突、24小时安全锁定策略,到 CI 缓存隔离问题,各种在实际业务中直接面临的阻碍接踵而至。
Nub 的包引擎 aube 可以直接识别现有的 pnpm-workspace.yaml 和 package.json 中的 workspaces。正如其宣称与 pnpm-lock.yaml v9 模式在字节级兼容一样,它默认使用了现有的虚拟 Store 结构(node_modules/.store/)。
问题在于那些隐式预设了平铺(hoisted)node_modules 结构的旧版工具。迁移后直接运行就会抛出 MODULE_NOT_FOUND 报错并崩溃。为了让服务先跑起来,你需要使用 --node-linker hoisted 选项将包树展开再开始。
在应用缓存的 Warm 安装基准下,默认的 GVS 模式耗时 346 ms。Hoisted 模式虽然变慢了 4 倍以上,耗时 1461 ms,但仍比 pnpm v10+ 的 3453 ms 快了 2 倍以上。先通过 Hoisted 模式确保兼容性,随后在移除旧工具依赖的过程中过渡到默认的 GVS 模式是比较稳妥的做法。
`bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> [1/4] 检查 Nub 二进制文件"
if ! command -v nub &> /dev/null; then
echo "Error: 未找到 Nub。请先运行 'npm i -g @nubjs/nub'。"
exit 1
fi
echo "==> [2/4] 检查 pnpm-lock.yaml 模式"
if [ -f "pnpm-lock.yaml" ]; then
nub pm use nub
fi
echo "==> [3/4] 在 package.json 中明确运行引擎"
node -e '
const fs = require("fs");
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
pkg.devEngines = pkg.devEngines || {};
pkg.devEngines.packageManager = {
name: "nub",
version: "^0.4.0",
onFail: "warn"
};
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
'
echo "==> [4/4] 生成 Lockfile"
nub install --frozen-lockfile=false
`
使用该脚本将 pnpm-lock.yaml 转换为 nub.lock,每周可以节省 2 小时以上原本花在修脚本修到头疼上的时间。
Nub 的安全策略相当严格。它默认阻止生命周期脚本,若没有 npm Provenance 签名就会抛出 ERR_NUB_TRUST_DOWNGRADE。最让人措手不及的是 minimumReleaseAge 选项。发布未满 24 小时的包版本会被视为处于安全验证等待状态,从而禁止安装。
平时这是个绝佳的防线,但在爆发零日漏洞、需要立即部署 1 小时前刚刚发布的修复版本时,它就会变成难以逾越的高墙。此时需要在根目录的 package.json 中明确指定允许构建脚本的包,并通过命令进行放行。
`json
{
"name": "@org/monorepo-root",
"private": true,
"allowBuilds": {
"esbuild": true,
"sharp": true,
"@fast-cve/patch-pkg": true
}
}
`
绕过受阻的 Hotfix 包并应用更新的流程如下:
`bash
nub add --allow-build=@fast-cve/patch-pkg @fast-cve/patch-pkg@1.0.1-hotfix
nub approve-builds
`
执行 nub approve-builds 会解封拦截列表并立即继续构建。如果在 OSV 数据库中被注册为恶意代码(MAL-*)的包,即使使用此命令也无法强行放行,这时就必须寻找更高版本的替代方案。
要提升 CI/CD 速度,需要了解 Nub 的缓存路径。Node 二进制文件会放入 ~/.cache/nub/node/<version>/,而包 CAS(Content-Addressable Store)则分布在 ~/.cache/nub/ 与 node_modules/.store/ 中。
在 Docker 多阶段构建中,必须使用 BuildKit 的缓存挂载(cache mount)完全隔离安装层,才能发挥缓存效果。
`dockerfile
FROM ghcr.io/nubjs/nub:latest AS base
WORKDIR /app
FROM base AS dependencies
COPY package.json nub.lock pnpm-workspace.yaml ./
COPY packages/core/package.json ./packages/core/
COPY packages/api/package.json ./packages/api/
RUN --mount=type=cache,target=/root/.cache/nub
nub ci --prefer-offline
FROM dependencies AS builder
COPY . .
RUN nub run build --filter=@org/api
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/packages/api/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
`
在 GitHub Actions 中,可以使用 nubjs/setup-nub@v0 来替代 actions/setup-node。
`yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: nubjs/setup-nub@v0
with:
cache: true
- run: nub ci
- run: nub -r run build
- run: nub -r run test
`
将 setup-nub 与缓存相结合后,nub ci 的速度可以达到 346 ms 的水平。相比传统的 pnpm(3453 ms),流水线中的包安装时间大幅减少,整体 CI 构建时间缩减了一半。看到月终的云端基础设施账单,心情确实会变好。
Nub 内置了基于 oxc 的内存转译器,无需 tsx 或 ts-node 即可直接运行 TypeScript 代码。它还会自动读取 .env 文件。在使用 pnpm run(442.7 ms)时每次都会出现的 Node.js 进程引导开销不复存在,nub run 的脚本执行时间缩短到了 14.7 ms。体感非常明显。
要防止团队成员之间的 Node.js 版本碎片化,只需在根目录下放置 .node-version 即可。
`bash
echo "22.15.0" > .node-version
nub src/index.ts
`
在 Node 22.15.0 及以上版本中,得益于同步 module.registerHooks(),冷启动延迟被彻底消除。为了在有新成员加入时无需繁琐解释就能一次性配好环境,配置一个自动化脚本是最干脆利落的选择。
`bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> 开始配置开发环境"
if ! command -v nub &> /dev/null; then
if command -v brew &> /dev/null; then
brew install nubjs/tap/nub
else
npm install -g --ignore-scripts=false @nubjs/nub
fi
fi
nub pm shim
nub node install
nub install
if [ ! -f ".env.local" ] && [ -f ".env.example" ]; then
cp .env.example .env.local
fi
echo "==> 配置完成。请使用 'nub run dev' 运行。"
`
只要运行过 nub pm shim,即使习惯性地输入 pnpm install 或 npm run,也会被 Nub 执行器截获并处理。CLI 执行开销从 pnpm exec(191 ms)降低到 nubx(11 ms),此外每周还能省下至少 2 小时听着“在我电脑上明明好好的”这种话来进行排错的时间。