# Fissare la versione di Node.js con pnpm, senza nvm
Table of Contents
“Sulla mia macchina funziona.” Una buona parte delle volte, dietro questa frase c’è una versione diversa di Node.js: il progetto è nato sulla 20, il collega ha la 24, la CI usa un’altra ancora. Lo strumento classico per conviverci è nvm, che permette di installare più versioni e passare dall’una all’altra. Funziona, ma va installato e configurato da ogni persona del team, e ricordarsi di digitare nvm use al momento giusto resta un onere. Chi usa pnpm come gestore di pacchetti ha un’alternativa spesso trascurata: pnpm sa gestire anche la versione di Node.js, e sa farlo per progetto, senza strumenti aggiuntivi.
Il problema, in due parole
Node.js è l’ambiente che esegue il JavaScript fuori dal browser; ogni progetto è di fatto testato e garantito solo su certe versioni. nvm (Node Version Manager) affronta il problema installando più versioni fianco a fianco: nvm install 20, nvm use 20 e la shell corrente passa alla 20.
I limiti sono noti a chi lo usa ogni giorno. nvm è uno script di shell che va installato per utente e agganciato al profilo della shell; il cambio di versione vale per la sessione corrente, quindi va ripetuto (o automatizzato con hook) a ogni cartella; nelle pipeline di integrazione continua e nei container si usa tipicamente tutt’altro meccanismo, con il rischio di divergenze. Non è un difetto di nvm: è il prezzo di uno strumento separato dal progetto.
L’alternativa: il runtime dichiarato nel progetto
pnpm ribalta l’approccio: la versione di Node.js diventa una dichiarazione del progetto, scritta nel package.json e versionata insieme al codice. Il campo si chiama devEngines:
{ "devEngines": { "runtime": { "name": "node", "version": "20.19.0", "onFail": "download" } }}La chiave è onFail: "download": se il Node in uso non corrisponde a quello dichiarato, pnpm scarica da solo la versione richiesta (una volta sola, poi resta in cache) e la usa per il progetto, senza toccare il Node di sistema. Il runtime compare nell’output di pnpm install come una dipendenza qualsiasi:
$ pnpm install# ...+ node 20.19.0E da quel momento tutto ciò che passa da pnpm usa quella versione. Un esempio provato mentre scrivevamo l’articolo, su una macchina con Node 22 di sistema:
$ node -v # il Node di sistemav22.22.2
$ pnpm run check-node # uno script che esegue "node -v"> node -vv20.19.0 # la versione dichiarata nel progetto
$ pnpm node -vv20.19.0Nessun nvm use, nessun hook di shell: chiunque cloni il repository - collega nuovo, CI, container - ottiene la stessa versione di Node semplicemente usando pnpm.
Anche a intervalli, e bloccato nel lockfile
version accetta sia una versione esatta sia un intervallo semver. Con "version": "^20.11.0", nel nostro test pnpm ha risolto e scaricato l’ultima 20.x disponibile:
$ pnpm install# ...+ node 20.20.2Il dettaglio più interessante è che la versione risolta finisce nel lockfile (pnpm-lock.yaml), come una normale dipendenza:
node: specifier: runtime:^20.11.0 version: runtime:20.20.2Questo significa che tutto il team e la CI usano esattamente la stessa versione di Node, non solo “una versione compatibile”: la riproducibilità è la stessa che il lockfile garantisce alle librerie. Il classico campo engines, per confronto, si limita ad avvisare quando la versione non corrisponde; devEngines con onFail: "download" risolve il problema da solo.
I limiti da conoscere
Il meccanismo vale per ciò che passa da pnpm: pnpm run, pnpm exec, pnpm node, gli script del package.json. Se si digita node server.js direttamente nel terminale, si usa ancora il Node di sistema. Nella pratica è un limite più piccolo di quanto sembri - nei progetti JavaScript quasi tutto passa dagli script - ma è bene saperlo: devEngines non “cambia il Node del computer”, garantisce che il progetto giri sulla versione giusta.
Una nota per chi arriva da versioni precedenti: fino a pnpm 10 esisteva l’impostazione use-node-version (o useNodeVersion nel pnpm-workspace.yaml), che faceva una cosa simile. pnpm 11 l’ha rimossa insieme a executionEnv.nodeVersion: la strada supportata oggi è devEngines. Se un progetto usa ancora quelle chiavi, con pnpm 11 vengono semplicemente ignorate - un aggiornamento facile da dimenticare.
E al posto di nvm per tutto il sistema?
pnpm può sostituire nvm anche come gestore globale delle versioni, con il comando pnpm env:
pnpm env list 20 # quali versioni 20.x esistonopnpm env use --global 20 # installa e attiva l'ultima 20.xpnpm env use --global lts # oppure l'ultima LTSQui serve una precisazione onesta: pnpm env gestisce il collegamento globale a Node, quindi richiede che pnpm sia installato in modo autonomo (con lo script di installazione ufficiale o con Corepack), non tramite npm install -g pnpm - in quel caso pnpm girerebbe dentro il Node che sta cercando di sostituire. Per l’uso per-progetto visto sopra, invece, questa distinzione non conta.
In sintesi
Se un progetto usa già pnpm, fissare la versione di Node.js non richiede nvm: un blocco devEngines nel package.json, con onFail: "download", fa scaricare e usare a pnpm il runtime dichiarato - versione esatta o intervallo - e blocca nel lockfile quella effettivamente risolta, uguale per tutto il team e per la CI. Il limite è chiaro (vale per i comandi che passano da pnpm), e chi arriva da pnpm 10 deve migrare da use-node-version a devEngines. nvm resta utile come gestore generale sulla propria macchina; per la riproducibilità di un progetto, la versione dichiarata nel repository è la strada più solida.
