Migrer un monorepo pnpm vers Nub : galères et solutions
27. Juli 2026
0
Computing/SoftwareComments (0)
Log in to leave a comment
No posts yet
Log in to leave a comment
No posts yet
L'écosystème Node.js est épuisé. Un environnement de build superposant tsx, dotenv-cli, nvm et pnpm est lourd, et le moindre script s'emmêle les pinceaux à chaque mise à jour. Quand Nub, un toolkit intégré développé en Rust, a débarqué en promettant de rassembler ce bazar de paquets dans un binaire unique, j'étais franchement ravi, mais aussi un peu sceptique.
En passant concrètement un monorepo pnpm-workspace de production sous Nub, le résultat s'est avéré plutôt séduisant. Cependant, les choses se déroulent rarement sans accroc. Conflits de dépendances, politique de verrouillage de sécurité de 24 heures ou encore isolation du cache CI : nous avons rapidement heurté des obstacles très concrets sur le terrain.
aube, le moteur de paquets de Nub, reconnaît immédiatement les workspaces du pnpm-workspace.yaml et du package.json existants. Comme promis par sa compatibilité au octet près avec le schéma v9 de pnpm-lock.yaml, il utilise par défaut la structure de Store virtuel existante (node_modules/.store/).
Le problème vient des anciens outils qui présupposent implicitement une structure node_modules aplatie (hoisted). Si vous les exécutez juste après la migration, ils plantent en renvoyant une erreur MODULE_NOT_FOUND. Pour remettre immédiatement le service sur rails, il faut commencer par étaler l'arbre de paquets avec l'option --node-linker hoisted.
En installation à chaud (warm) avec cache appliqué, le mode GVS par défaut prend 346 ms. Le mode Hoisted prend 1461 ms, ce qui est plus de 4 fois plus lent, mais cela reste plus de 2 fois plus rapide que les 3453 ms de pnpm v10+. Au début, il est plus simple d'assurer la compatibilité avec le mode Hoisted, puis de passer au mode GVS par défaut au fur et à mesure que vous éliminez les dépendances envers les anciens outils.
`bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> [1/4] Vérification du binaire Nub"
if ! command -v nub &> /dev/null; then
echo "Error: Nub est introuvable. Exécutez d'abord 'npm i -g @nubjs/nub'."
exit 1
fi
echo "==> [2/4] Vérification du schéma pnpm-lock.yaml"
if [ -f "pnpm-lock.yaml" ]; then
nub pm use nub
fi
echo "==> [3/4] Déclaration du moteur d'exécution dans 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] Génération du Lockfile"
nub install --frozen-lockfile=false
`
En convertissant pnpm-lock.yaml en nub.lock avec ce script, vous pouvez économiser plus de 2 heures par semaine auparavant perdues à réparer des scripts cassés.
Nub applique une politique de sécurité assez stricte. Il bloque par défaut les scripts de cycle de vie et renvoie une erreur ERR_NUB_TRUST_DOWNGRADE si la signature npm Provenance est absente. Le plus déroutant reste l'option minimumReleaseAge. Les versions de paquets publiées depuis moins de 24 heures sont considérées comme en attente de vérification de sécurité, ce qui en bloque l'installation.
En temps normal, c'est un excellent bouclier, mais lorsqu'une vulnérabilité zero-day éclate et qu'il faut déployer immédiatement un correctif sorti il y a 1 heure, cela devient un mur d'incompréhension. Dans ce cas, il faut déclarer les paquets autorisés à exécuter des scripts de build dans le package.json racine et forcer le passage via des commandes.
json { "name": "@org/monorepo-root", "private": true, "allowBuilds": { "esbuild": true, "sharp": true, "@fast-cve/patch-pkg": true } }
Voici le flux pour contourner et appliquer un paquet de hotfix bloqué :
`bash
nub add --allow-build=@fast-cve/patch-pkg @fast-cve/patch-pkg@1.0.1-hotfix
nub approve-builds
`
L'exécution de nub approve-builds débloque la liste et poursuit le build immédiatement. Les paquets enregistrés comme malveillants (MAL-*) dans la base de données OSV ne pourront pas être débloqués, même avec cette commande ; il faudra alors chercher une version alternative supérieure.
Pour booster la vitesse de votre CI/CD, il est essentiel de connaître les chemins de cache de Nub. Le binaire Node se trouve dans ~/.cache/nub/node/<version>/, tandis que le CAS (Content-Addressable Store) des paquets est réparti entre ~/.cache/nub/ et node_modules/.store/.
Dans un build multi-stage Docker, il faut utiliser le mount de cache de BuildKit afin d'isoler complètement la couche d'installation pour bénéficier de l'effet de 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"]
`
Dans GitHub Actions, on charge nubjs/setup-nub@v0 au lieu 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
`
La vitesse de nub ci combiné à setup-nub et au cache tourne autour de 346 ms. Par rapport à pnpm (3453 ms), le temps d'installation des paquets dans le pipeline est considérablement réduit, ce qui divise par deux le temps total de build CI. En regardant la facture d'infrastructure cloud à la fin du mois, on retrouve le sourire.
Nub intègre un transpiler en mémoire basé sur oxc, ce qui permet d'exécuter du code TypeScript directement sans passer par tsx ou ts-node. Il lit également les fichiers .env automatiquement. En éliminant le surcoût de démarrage du processus Node.js qui survenait à chaque fois avec pnpm run (442,7 ms), le temps d'exécution des scripts avec nub run chute à 14,7 ms. La différence est très nette à l'usage.
Pour éviter la fragmentation des versions de Node.js entre les membres de l'équipe, il suffit de placer un fichier .node-version à la racine.
bash echo "22.15.0" > .node-version nub src/index.ts
À partir de Node 22.15.0, la méthode synchrone module.registerHooks() élimine complètement la latence au démarrage à froid (cold start). Lorsqu'un nouveau membre rejoint l'équipe, configurer un script d'automatisation reste le moyen le plus propre de lui fournir un environnement prêt à l'emploi sans longues explications.
`bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> Début de la configuration de l'environnement de développement"
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 "==> Configuration terminée. Exécutez 'nub run dev' pour lancer."
`
En exécutant nub pm shim, toute saisie réflexe de pnpm install ou npm run sera interceptée et traitée par l'exécuteur Nub. Le surcoût d'exécution de la CLI passe de pnpm exec (191 ms) à nubx (11 ms), et vous économisez au moins 2 heures par semaine à déboguer des problèmes du type « ça marche pas sur ma machine ».