Migrando um monorepo pnpm para o Nub: percalços e soluções
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
O ecossistema Node.js está exausto. Um ambiente de build construído empilhando tsx, dotenv-cli, nvm e pnpm é pesado e, a cada atualização, as scripts ficam desconfiguradas. Quando o Nub, um conjunto de ferramentas integrado feito em Rust, surgiu prometendo empacotar toda essa bagunça de pacotes em um único binário, fiquei sinceramente feliz, mas também um pouco desconfiado.
Na prática, quando migrei um monorepo pnpm-workspace em produção para o Nub, a experiência acabou se mostrando bastante atraente. Mas nada é uma transição 100% suave. Conflitos de dependências, políticas de bloqueio de segurança de 24 horas e até problemas de separação de cache em CI foram obstáculos reais com os quais me deparei no trabalho prático.
O motor de pacotes do Nub, o aube, reconhece imediatamente o pnpm-workspace.yaml existente e os workspaces do package.json. Como diz a descrição de que ele é compatível byte a byte com o esquema v9 do pnpm-lock.yaml, ele utiliza por padrão a estrutura existente de Store virtual (node_modules/.store/).
O problema são as ferramentas antigas que pressupõem implicitamente uma estrutura achatada (hoisted) em node_modules. Se você executá-las logo após a migração, elas quebram emitindo o erro MODULE_NOT_FOUND. Para fazer o serviço funcionar imediatamente, é preciso começar desdobrando a árvore de pacotes com a opção --node-linker hoisted.
Considerando uma instalação Warm com cache aplicado, o modo GVS padrão leva 346 ms. O modo Hoisted leva 1461 ms, sendo mais de 4 vezes mais lento, mas ainda é mais de 2 vezes mais rápido que os 3453 ms do pnpm v10+. No início, é mais tranquilo garantir a compatibilidade com o modo Hoisted e, à medida que você remove a dependência de ferramentas antigas, migrar para o modo GVS padrão.
`bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> [1/4] Verificando binário do Nub"
if ! command -v nub &> /dev/null; then
echo "Error: Nub não encontrado. Execute 'npm i -g @nubjs/nub' primeiro."
exit 1
fi
echo "==> [2/4] Checando esquema do pnpm-lock.yaml"
if [ -f "pnpm-lock.yaml" ]; then
nub pm use nub
fi
echo "==> [3/4] Especificando motor de execução no 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] Gerando Lockfile"
nub install --frozen-lockfile=false
`
Ao converter o pnpm-lock.yaml em nub.lock usando este script, você pode economizar mais de 2 horas por semana que costumavam ser perdidas corrigindo scripts quebradas.
O Nub possui políticas de segurança bastante rígidas. Ele bloqueia por padrão scripts de ciclo de vida e lança ERR_NUB_TRUST_DOWNGRADE se não houver a assinatura npm Provenance. O mais desconcertante é a opção minimumReleaseAge. Versões de pacotes lançadas há menos de 24 horas são consideradas em estado de espera por verificação de segurança, e a instalação é bloqueada.
No dia a dia, é um ótimo escudo defensivo, mas quando uma vulnerabilidade zero-day explode e você precisa implantar imediatamente uma versão de correção lançada há apenas 1 hora, isso vira um muro inquebrável. Nesses momentos, você deve declarar os pacotes com permissão de script de build no package.json raiz e liberá-los via comando.
json { "name": "@org/monorepo-root", "private": true, "allowBuilds": { "esbuild": true, "sharp": true, "@fast-cve/patch-pkg": true } }
O fluxo para contornar e aplicar um pacote de hotfix bloqueado é o seguinte:
`bash
nub add --allow-build=@fast-cve/patch-pkg @fast-cve/patch-pkg@1.0.1-hotfix
nub approve-builds
`
Ao executar nub approve-builds, a lista de bloqueio é liberada e o build continua imediatamente. Pacotes registrados como código malicioso (MAL-*) no banco de dados da OSV não serão liberados nem mesmo com este comando, portanto, nesse caso, você precisará procurar por uma versão alternativa superior.
Para aumentar a velocidade do CI/CD, é necessário conhecer os caminhos de cache do Nub. Os binários do Node ficam em ~/.cache/nub/node/<version>/, e o CAS (Content-Addressable Store) dos pacotes é dividido entre ~/.cache/nub/ e node_modules/.store/.
Em builds multi-stage do Docker, você deve usar os mounts de cache do BuildKit para isolar completamente a camada de instalação para obter os benefícios do cache.
`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"]
`
No GitHub Actions, suba o nubjs/setup-nub@v0 em vez de 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
`
A velocidade do nub ci combinada com o setup-nub e o cache fica em torno de 346 ms. Em comparação com o pnpm anterior (3453 ms), o tempo de instalação de pacotes na pipeline é drasticamente reduzido, cortando o tempo total de build do CI pela metade. Olhar para a fatura de infraestrutura em nuvem no final do mês com certeza melhora o humor.
Como o Nub possui um transpilador em memória baseado em oxc embutido, ele executa código TypeScript diretamente sem a necessidade de tsx ou ts-node. Ele também lê arquivos .env automaticamente. O overhead de bootstrap do processo Node.js que surgia toda vez ao usar pnpm run (442,7 ms) desaparece, fazendo com que o tempo de execução de scripts do nub run caia para 14,7 ms. A diferença perceptível é enorme.
Para evitar a fragmentação de versões do Node.js entre os membros da equipe, basta fixar um .node-version na raiz.
bash echo "22.15.0" > .node-version nub src/index.ts
No Node 22.15.0 ou superior, o atraso no cold start é completamente eliminado graças ao module.registerHooks() síncrono. Quando um novo membro entra na equipe, a maneira mais limpa de padronizar o ambiente de uma só vez, sem explicações complexas, é configurar um script de automação.
`bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> Iniciando configuração do ambiente de desenvolvimento"
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 "==> Configuração concluída. Execute com 'nub run dev'."
`
Ao deixar o nub pm shim configurado, mesmo que você digite por hábito pnpm install ou npm run, o executor do Nub interceptará e processará o comando. O overhead de execução da CLI cai de pnpm exec (191 ms) para nubx (11 ms), e você ainda economiza cerca de 2 horas por semana que seriam gastas depurando frases como "no meu computador funciona".