pnpmモノレポをNubに移行する中で直面したハマりポイントと解決策
٢٧ يوليو ٢٠٢٦
0
Computing/SoftwareComments (0)
Log in to leave a comment
No posts yet
Log in to leave a comment
No posts yet
Node.jsエコシステムは疲弊しています。tsx、dotenv-cli、nvm、pnpmを何層にも重ね上げたビルド環境は重く、何か一つ更新するたびにスクリプトが絡み合って壊れます。Rust製の統合ツールキットであるNubが、このパッケージのカオスを単一バイナリにまとめてくれると登場したとき、正直嬉しくもありつつ半信半疑でした。
実際にプロダクション環境のpnpm-workspaceモノレポを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モードは1461 msと4倍以上遅くなりますが、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 } }
ブロックされたホットフィックスパッケージを迂回して反映するフローは以下の通りです。
`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のキャッシュマウントを使用してインストールレイヤーを完全に隔離しないとキャッシュ効果を得られません。
`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では、actions/setup-nodeの代わりにnubjs/setup-nub@v0を使用します。
`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()のおかげでコールドスタートの遅延が完全に解消されます。新しくチームに参加したメンバーが来た際も、複雑な説明なしで一発で環境を揃えられるよう、自動化スクリプトを1つ用意しておくのが最もスマートです。
`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時間は削減できます。