Lancia npm install su un progetto, poi pnpm install sullo stesso package.json: ottieni due alberi node_modules diversi e due lockfile diversi. Aggiungi un collega che usa Yarn e il repository finisce con tre lockfile nella sua storia, ognuno con un albero delle dipendenze leggermente diverso. Tutti e tre leggono lo stesso package.json e scaricano dal medesimo registro npm. Il punto su cui i tre non sono d’accordo è cosa scrivere su disco.
Come npm, Yarn e pnpm organizzano node_modules
La struttura di npm è piatta. Ogni package richiesto dalle tue dipendenze, a qualsiasi profondità dell’albero, viene portato in cima alla node_modules di primo livello ogni volta che non c’è conflitto di versione. È il comportamento di default da npm 3. L’effetto collaterale: il tuo codice può fare require() o import di un package che non hai mai inserito in package.json, purché qualcos’altro nell’albero ne dipenda. Funziona finché quel package smette di dipenderne in una minor release, e la tua build si rompe senza che tu abbia toccato una riga di codice. È una dipendenza fantasma, e la struttura di npm non fa nulla per impedirla.
Yarn Classic (v1) porta le dipendenze in cima allo stesso modo, quindi è esposto allo stesso rischio. Yarn Berry (v2 e successive) usa di default Plug’n’Play, che non ha affatto node_modules. Le dipendenze restano compresse in .yarn/cache, e un file .pnp.cjs generato automaticamente mappa ogni import direttamente all’archivio giusto. Committa .yarn/cache insieme al lockfile e un clone nuovo funziona senza bisogno di installare nulla: Yarn lo chiama zero-install. Il costo è la compatibilità. Gli strumenti che leggono node_modules direttamente, e i moduli nativi che si aspettano file reali su disco, potrebbero non funzionare sotto PnP. Impostare nodeLinker: node-modules in .yarnrc.yml ripristina una struttura piatta classica, rinunciando allo zero-install.
pnpm mantiene un unico store sulla macchina, indicizzato per contenuto. Ogni versione di ogni package vive su disco una sola volta, identificata da un hash del contenuto, indipendentemente da quanti progetti la usano. Per ogni progetto, pnpm crea hard link dei file necessari in uno store virtuale sotto node_modules/.pnpm, poi crea symlink solo per le tue dipendenze dirette nella node_modules di primo livello. Puoi risolvere node_modules/react perché l’hai dichiarato. Non puoi risolvere un package che esiste solo due livelli più in basso, nell’albero di un’altra dipendenza, perché niente lo collega al livello superiore. Il problema delle dipendenze fantasma viene chiuso dalla struttura stessa, non dalla disciplina di chi scrive il codice.
I comandi di npm, pnpm e Yarn a confronto
Nell’uso quotidiano i comandi si corrispondono quasi uno a uno. La differenza che conta è il flag per la CI: installa esattamente quello che dice il lockfile, senza risolvere né scrivere nulla.
| Azione | npm | pnpm | Yarn Berry |
|---|---|---|---|
| Installare tutto | npm install | pnpm install | yarn install |
| Aggiungere una dipendenza | npm install axios | pnpm add axios | yarn add axios |
| Aggiungere una dev dependency | npm install -D vitest | pnpm add -D vitest | yarn add -D vitest |
| Eseguire uno script | npm run build | pnpm run build | yarn run build |
| Install sicura per la CI | npm ci | pnpm install --frozen-lockfile | yarn install --immutable |
npm ci elimina prima node_modules e installa rigorosamente da package-lock.json, fallendo se lockfile e package.json non sono d’accordo. --frozen-lockfile e --immutable saltano la cancellazione ma impongono la stessa regola: nessuna nuova risoluzione, nessuna scrittura del lockfile, uscita diversa da zero se il lockfile non può essere soddisfatto com’è. Usa la forma sicura per CI in ogni pipeline e l’install normale sulla tua macchina. Un lockfile non aggiornato deve far fallire la build in modo rumoroso, non venire risolto in silenzio.
Se installi le dipendenze dentro un’immagine Docker, Dockerfile: le Best Practice per Build Più Piccole e Veloci spiega come copiare manifest e lockfile ed eseguire l’install prima di COPY . ., così quel layer si ricostruisce solo quando il lockfile cambia.
Perché un repository dovrebbe committare un solo lockfile
npm scrive package-lock.json, pnpm scrive pnpm-lock.yaml, ed entrambe le generazioni di Yarn scrivono yarn.lock (in formati diversi tra v1 e Berry). Ognuno registra la versione esatta di ogni dipendenza transitiva. Nessuno dei tre legge quello di un altro.
La rottura funziona così: qualcuno lancia yarn add lodash in un repository che fino a quel momento aveva committato solo package-lock.json. Ora esiste anche yarn.lock, i due file non sono d’accordo su cosa è stato risolto, e vince il tool che la CI esegue per caso. Il prossimo install locale con l’altro tool risolve di nuovo qualcosa di diverso.
Scegli un solo package manager per repository, committa solo il suo lockfile, e ignora gli altri prima che qualcuno li generi per sbaglio:
| |
Come fissare il package manager con corepack
Un .gitignore non impedisce a nessuno di lanciare il tool sbagliato se lo ha sul PATH. corepack sì. Legge il campo packageManager in package.json ed esegue esattamente quella versione di npm, Yarn o pnpm, scaricandola al primo uso:
| |
Lancia corepack enable una volta per mettere sul PATH gli shim di Yarn e pnpm (lo shim di npm si installa solo se lo richiedi esplicitamente). Da quel momento, pnpm install in questo repository esegue la versione fissata 10.12.1 su ogni macchina, e nessuno deve ricordarsi di controllare.
Corepack viene incluso con Node dalla 14.19.0 fino alla 25.0.0 (esclusa). Da Node 25 in avanti, installalo con npm install -g corepack, poi lancia corepack enable come prima. In CI, l’opzione cache di actions/setup-node ('npm', 'yarn' o 'pnpm') funziona insieme a corepack; Cache delle Dipendenze in GitHub Actions spiega le chiavi di cache che ci sono dietro.
Come funzionano i workspace in npm, Yarn e pnpm
Tutti e tre gestiscono un monorepo di package locali collegati tra loro. Cambia dove dichiari i membri e come punti a uno specifico.
npm e Yarn leggono un array workspaces dal package.json alla radice:
| |
Installa una volta alla radice e i package locali vengono collegati nella node_modules l’uno dell’altro. Per eseguire uno script in un package, npm richiede un percorso:
| |
Yarn richiede il nome del package:
| |
pnpm ignora il campo workspaces e legge l’appartenenza dal proprio pnpm-workspace.yaml:
| |
Punta ai package per nome con --filter:
| |
Migrare un monorepo npm o Yarn a pnpm significa scrivere a mano quel pnpm-workspace.yaml a partire dai glob di workspaces esistenti. pnpm import può convertire il vecchio lockfile, così il primo install mantiene le versioni che avevi già.
Quando la struttura rigida di pnpm rompe una dipendenza
La struttura che blocca le dipendenze fantasma nel tuo codice espone anche i package che ne facevano affidamento. Un package che importa lodash senza elencarlo tra le proprie dependencies funziona sotto npm, perché lodash sta comunque al livello superiore, chiunque l’abbia messo lì. Sotto pnpm, la stessa require('lodash') lancia Cannot find module.
La soluzione vera sta nel package.json di quel package, quindi apri una issue upstream o proponi una patch. Nel frattempo, pnpm ha delle vie di fuga. Da pnpm 10.6 in avanti vivono in pnpm-workspace.yaml; su una versione più vecchia fissata, imposta le stesse chiavi, con i trattini, in .npmrc (public-hoist-pattern, shamefully-hoist, node-linker). publicHoistPattern porta in cima solo i package che corrispondono a un glob, quello che ti serve per uno o due casi isolati:
| |
shamefullyHoist: true porta in cima tutto, molto simile al layout di default di npm. Usalo solo quando più package non collegati tra loro assumono tutti una struttura piatta, perché rinunci alla protezione per cui pnpm esiste. nodeLinker: hoisted va oltre e rimuove del tutto i symlink, per gli strumenti che si rompono sui symlink e non sulla mancanza di hoisting. Yarn Berry ha lo stesso tipo di problema sotto PnP, e la sua risposta è nodeLinker: node-modules in .yarnrc.yml.
Quale package manager usare per un nuovo progetto
Per un nuovo progetto, usa pnpm. Una sola copia di ogni versione di package per macchina mantiene gli install piccoli su disco, e la struttura rigida fa emergere una dipendenza mancante sul tuo laptop invece che in una release rotta mesi dopo. Aspettati di aggiungere una o due voci a publicHoistPattern nella prima settimana.
Se erediti un progetto npm che si installa senza problemi e niente ti forza a cambiare, resta su npm. Aggiungi un campo packageManager, lancia corepack enable, e ogni macchina eseguirà almeno la stessa versione di npm.
Scegli Yarn Berry quando vuoi lo zero-install con la cache committata su git. Senza quello, il costo di compatibilità di PnP non ti porta nulla.