Грабли и решения при переходе с pnpm monorepo на Nub
27 juillet 2026
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, стала тяжеловесной, и при каждом обновлении чего-либо скропленные скрипты выходят из строя. Когда появился Nub — единый инструментарий на Rust, обещанный собрать весь этот пакетный хаос в один бинарный файл, это вызвало одновременно радость и недоверие.
Когда мы перевели реальный продакшн pnpm-workspace monorepo на Nub, инструмент действительно показался весьма привлекательным. Однако гладко всё пройти не могло. Мы сразу же столкнулись с практическими препятствиями: конфликтами зависимостей, политикой 24-часовой блокировки безопасности и проблемами с разделением кэша в CI.
Движок пакетов Nub под названием aube «из коробки» распознает существующие pnpm-workspace.yaml и поле workspaces в package.json. Как и заявлено — о побайтовой совместимости со схемой pnpm-lock.yaml v9 — он по умолчанию использует привычную структуру виртуального хранилища (node_modules/.store/).
Проблема заключается со старыми инструментами, которые неявно предполагают плоскую (hoisted) структуру node_modules. При запуске сразу после миграции они падают с ошибкой MODULE_NOT_FOUND. Чтобы сервис заработал прямо сейчас, нужно начать с разворачивания дерева пакетов с помощью опции --node-linker hoisted.
В режиме прогретого кэша (Warm) базовый режим GVS занимает 346 ms. Режим Hoisted задерживается до 1461 ms, что более чем в 4 раза медленнее, но все еще более чем в 2 раза быстрее, чем 3453 ms у pnpm v10+. На первых порах проще гарантировать совместимость в режиме 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 часов назад, считаются находящимися в статусе ожидания проверки безопасности, и их установка блокируется.
В обычное время это отличный щит, но когда выстреливает уязвимость нулевого дня и нужно немедленно задеплоить патч, вышедший час назад, это превращается в глухую стену. В такой ситуации необходимо явно указать разрешенные для сборки пакеты в корневом 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 (multi-stage builds) для получения эффекта от кэширования следует использовать монтирование кэша 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
`
Скорость nub ci в связке с setup-nub и кэшированием составляет порядка 346 ms. По сравнению с прошлым pnpm (3453 ms) время установки пакетов в пайплайне кардинально сокращается, благодаря чему общее время сборки в CI урезается вдвое. Итоговый ежемесячный счет за облачную инфраструктуру точно поднимет настроение.
Nub содержит встроенный транспайлер памяти на базе oxc, поэтому он выполняет код TypeScript напрямую без tsx или ts-node. Он также сам подтягивает файлы .env. Из-за устранения накладных расходов на бутстрап процессов Node.js, которые постоянно возникали при использовании pnpm run (442.7 ms), время выполнения скрипта 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), а главное — вы сэкономите пару часов в неделю, избавившись от отладки в стиле «а на моем компьютере всё работает».