
npm overrides: come forzare la versione di una dipendenza transitiva
- VersionDude
- guides
- 7 min di lettura
Un pacchetto vulnerabile tre livelli più in basso e un genitore che non ha pubblicato la correzione. Il campo overrides del package.json radice ti lascia scegliere la versione, per tutto l'albero o per un solo genitore. Ha anche tre regole che spiegano quasi tutti i casi in cui sembra non fare nulla.
Un rapporto di vulnerabilità cita un pacchetto che non hai mai installato. Si trova tre livelli più in basso, portato dentro dalla dipendenza di una dipendenza, e il pacchetto in mezzo non ha pubblicato alcuna versione che lo superi. Non puoi modificare un `package.json` che non è tuo. Puoi però dichiarare un campo `overrides` nel tuo `package.json` radice e dire a npm quale versione installare al suo posto.
L'override piatto: una versione, in tutto l'albero

La forma più breve indica il pacchetto e la versione. Scrivere `"overrides": { "foo": "1.2.4" }` fa installare `foo` alla 1.2.4 ovunque compaia, qualunque versione avessero chiesto i pacchetti che ne dipendono. La documentazione di npm elenca i tre usi previsti: sostituire una versione con un problema di sicurezza noto, sostituire una dipendenza con un fork e garantire che la stessa versione di un pacchetto sia usata ovunque.
Il valore non deve essere per forza una versione esatta. Secondo la documentazione accetta qualsiasi specificatore che npm ammette per una dipendenza: una versione esatta, un intervallo semver, un dist-tag o una sostituzione come `npm:`, `file:` o un URL Git. Per una correzione di sicurezza l'intervallo è spesso la scelta migliore. `"foo": "^1.2.4"` impone la versione corretta come minimo e lascia entrare le patch successive, mentre una versione fissa congela il pacchetto finché qualcuno non si ricorda di togliere la riga. Se la differenza tra i due operatori non è chiara, caret o tilde spiega che cosa consente ciascuno.
Prima di scrivere l'override, scopri chi richiede il pacchetto. `npm explain foo` mostra la catena di dipendenze che provoca l'installazione di `foo`, un blocco per ogni copia presente nell'albero, ciascuno ricondotto fino al progetto radice. Quell'output fornisce i due dati che servono: quale genitore è responsabile e se le copie coinvolte sono una o più. Un override piatto le cambia tutte, e non sempre è ciò che si vuole.
Limitare l'override a un solo genitore
Annida la chiave per limitare l'override a un ramo dell'albero. `"overrides": { "bar": { "foo": "1.2.4" } }` sostituisce `foo` solo quando è figlio di `bar`, nipote o a qualunque profondità sotto di esso. Tutti gli altri pacchetti che dipendono da `foo` mantengono la versione che hanno risolto da soli. È la forma da usare quando un solo genitore è fermo a una versione vulnerabile e il resto dell'albero è a posto.
- "foo": "1.2.4" : foo alla 1.2.4 in tutto l'albero
- "bar": { "foo": "1.2.4" } : foo alla 1.2.4 solo sotto bar, a qualsiasi profondità
- "bar@2.0.0": { "foo": "1.2.4" } : solo sotto quella precisa versione di bar
- "foo": "$foo" : riusare la specifica della tua dipendenza diretta foo
- "foo": "npm:@scope/fork@1.2.4" : sostituire foo con un altro pacchetto
Le chiavi possono portare una versione e annidarsi quanto serve. `"bar@2.0.0": { "foo": "1.2.4" }` si applica solo se `foo` sta sotto quella precisa versione di `bar`, quindi l'override smette di valere da solo il giorno in cui `bar` viene aggiornato. Le chiavi si possono annidare per qualsiasi lunghezza, per descrivere un percorso più lungo. E quando occorre sostituire insieme un pacchetto e uno dei suoi figli, la forma a oggetto accetta una chiave `.` per il pacchetto stesso, accanto alle chiavi dei figli.
La dipendenza diretta è l'unico caso che npm rifiuta. Non si può sostituire un pacchetto da cui dipendi direttamente, a meno che l'override e la dipendenza riportino esattamente la stessa specifica; in ogni altro caso l'installazione si ferma con un errore `EOVERRIDE`. La via d'uscita documentata è un riferimento: `"foo": "$foo"` dice a npm di riusare la specifica dichiarata dalla tua voce `dependencies` per `foo`, a ogni profondità. Cambi la versione in un solo punto e tutto l'albero la segue.
Quando l'override sembra non fare nulla
Gli overrides vengono letti solo dal `package.json` radice. La documentazione di npm è esplicita: quelli delle dipendenze installate, workspace compresi, non vengono considerati nella risoluzione dell'albero. Ne derivano due conseguenze. In un monorepo il campo va nel manifesto radice, non nel pacchetto in cui il problema si manifesta. E un blocco `overrides` in una libreria che pubblichi non fa nulla per chi la installa; la documentazione rimanda gli autori di librerie al blocco della dipendenza su una versione o a `bundleDependencies`.
Poi controlla il risultato invece di darlo per scontato. Esegui `npm install` perché l'albero venga risolto di nuovo, quindi rilancia `npm explain foo`: la versione in testa a ogni blocco è quella ora installata. Metti nello stesso commit `package.json` e `package-lock.json`, perché è il lockfile a portare la versione risolta su tutte le altre macchine. package-lock.json contro package.json spiega perché i due file devono muoversi in coppia.
Un override installa una versione con cui il pacchetto genitore non ha mai dichiarato di funzionare. È proprio lo scopo quando l'intervallo dichiarato contiene solo una versione vulnerabile, ed è anche il rischio: npm non ti impedirà di forzare una versione major per cui il genitore non è stato scritto. Tratta quella riga come un cerotto con una data di scadenza. Quando il genitore pubblica una versione che accetta la correzione, elimina l'override.
Yarn e pnpm usano un altro campo
La stessa idea esiste negli altri due gestori di pacchetti, con altri nomi. Yarn legge un campo `resolutions`, descritto nella documentazione del suo manifesto come un modo per usare una risoluzione specifica al posto di quella che il resolver sceglierebbe normalmente; le sue chiavi ammettono un solo livello di precisione, scritto `genitore/figlio`. pnpm chiama l'impostazione `overrides` come npm, la colloca in `pnpm-workspace.yaml` nella sua documentazione attuale e la delimita con un separatore `>`, come in `bar@1>foo`. Entrambi riservano il campo alla radice del progetto, come npm. La sintassi non si trasferisce da uno strumento all'altro: un progetto che cambia gestore di pacchetti deve riscrivere il blocco.
L'ordine delle decisioni è breve. Se il genitore ha già una versione che accetta la correzione, aggiorna il genitore e non scrivere alcun override. Se non ce l'ha, limita l'override a quel genitore e non a tutto l'albero. Se lo stesso pacchetto deve essere identico ovunque, usa la forma piatta, con un intervallo quando basta un minimo. E se l'installazione si ferma su un conflitto di dipendenza peer e non su una versione vulnerabile, il problema è un altro: le dipendenze peer lo affrontano.
FAQ
Gli overrides di npm funzionano nel package.json di un workspace?
No. npm considera solo gli overrides dichiarati nel package.json radice del progetto. La documentazione precisa che quelli delle dipendenze installate, workspace compresi, non vengono considerati nella risoluzione dell'albero: il campo va quindi nel manifesto radice.
Che cosa significa l'errore EOVERRIDE?
Indica che un override riguarda un pacchetto da cui dipendi direttamente, con una specifica diversa da quella presente nelle tue dependencies. npm consente quell'override solo se le due specifiche sono identiche. Allineale, oppure scrivi l'override come riferimento, per esempio "foo": "$foo", che riusa la specifica della tua dipendenza diretta.
Un override può usare un intervallo invece di una versione esatta?
Sì. La documentazione di npm dice che il valore di un override può essere qualsiasi specificatore ammesso per una dipendenza: versione esatta, intervallo semver, dist-tag o una sostituzione come npm:, file: o un URL Git. Un intervallo come ^1.2.4 impone una versione corretta minima senza congelare il pacchetto.
Come scoprire quale pacchetto porta dentro una dipendenza transitiva?
Esegui npm explain seguito dal nome del pacchetto. Il comando mostra la catena di dipendenze che ne provoca l'installazione, un blocco per ogni copia presente nell'albero, ciascuno ricondotto fino al progetto radice.
npm overrides è la stessa cosa di Yarn resolutions?
Lo scopo è lo stesso, la sintassi no. npm usa un campo overrides con oggetti annidati, Yarn un campo resolutions con chiavi scritte genitore/figlio e pnpm un'impostazione overrides con un separatore >. Tutti e tre riservano il campo alla radice del progetto.



Un override installa una versione con cui il pacchetto genitore non ha mai dichiarato di funzionare. È proprio lo scopo quando l'intervallo dichiarato contiene solo una versione vulnerabile, ed è anche il rischio: npm non ti impedirà di forzare una versione major per cui il genitore non è stato scritto. Tratta quella riga come un cerotto con una data di scadenza. Quando il genitore pubblica una versione che accetta la correzione, elimina l'override.