npm overrides: cómo forzar la versión de una dependencia transitiva

  • VersionDude
  • guides
  • 7 min de lectura

Un paquete vulnerable tres niveles más abajo y un padre que no ha publicado el arreglo. El campo overrides del package.json raíz te deja elegir la versión, para todo el árbol o para un solo padre. También tiene tres reglas que explican casi todos los casos en que parece no hacer nada.

Un informe de vulnerabilidad nombra un paquete que nunca instalaste. Está tres niveles más abajo, traído por la dependencia de una dependencia, y el paquete intermedio no ha publicado ninguna versión que lo deje atrás. No puedes editar un `package.json` que no es tuyo. Lo que sí puedes hacer es declarar un campo `overrides` en tu propio `package.json` raíz e indicarle a npm qué versión instalar en su lugar.

El override plano: una versión, en todo el árbol

Una señal de tráfico rectangular naranja con la palabra DETOUR, un pictograma negro de peatón encima y una flecha negra que apunta a la izquierda, sujeta a un poste negro delante de una valla metálica desenfocada. Un override hace lo mismo con una dependencia: mismo destino, otra ruta.
Una señal de tráfico rectangular naranja con la palabra DETOUR, un pictograma negro de peatón encima y una flecha negra que apunta a la izquierda, sujeta a un poste negro delante de una valla metálica desenfocada. Un override hace lo mismo con una dependencia: mismo destino, otra ruta.

La forma más corta nombra el paquete y la versión. Escribir `"overrides": { "foo": "1.2.4" }` hace que npm instale `foo` en la 1.2.4 allí donde aparezca, sin importar la versión que pidieran los paquetes que dependen de él. La documentación de npm enumera los tres usos previstos: sustituir una versión con un fallo de seguridad conocido, sustituir una dependencia por un fork y asegurar que se usa la misma versión de un paquete en todas partes.

El valor no tiene por qué ser una versión exacta. Según la documentación, admite cualquier especificador que npm acepte para una dependencia: una versión exacta, un rango semver, un dist-tag o un reemplazo como `npm:`, `file:` o una URL de Git. Para un arreglo de seguridad, el rango suele ser mejor opción. `"foo": "^1.2.4"` impone la versión corregida como mínimo y deja entrar los parches posteriores, mientras que una versión fija congela el paquete hasta que alguien se acuerde de quitar la línea. Si la diferencia entre los dos operadores no está clara, caret o tilde explica qué permite cada uno.

Antes de escribir el override, averigua quién pide el paquete. `npm explain foo` muestra la cadena de dependencias que provoca la instalación de `foo`, un bloque por cada copia presente en el árbol, cada uno rastreado hasta el proyecto raíz. Esa salida da los dos datos necesarios: qué padre es el responsable y si hay una copia o varias. Un override plano las cambia todas, y no siempre es lo que conviene.

Limitar el override a un solo padre

Anida la clave para limitar el override a una rama del árbol. `"overrides": { "bar": { "foo": "1.2.4" } }` sustituye `foo` solo cuando es hijo de `bar`, nieto o cualquier nivel más profundo bajo él. Los demás paquetes que dependen de `foo` conservan la versión que resolvieron por su cuenta. Es la forma adecuada cuando un único padre sigue atado a una versión vulnerable y el resto del árbol está bien.

  • "foo": "1.2.4" : foo en la 1.2.4 en todo el árbol
  • "bar": { "foo": "1.2.4" } : foo en la 1.2.4 solo bajo bar, a cualquier profundidad
  • "bar@2.0.0": { "foo": "1.2.4" } : solo bajo esa versión exacta de bar
  • "foo": "$foo" : reutilizar la especificación de tu dependencia directa foo
  • "foo": "npm:@scope/fork@1.2.4" : sustituir foo por otro paquete

Las claves pueden llevar versión y anidarse tanto como haga falta. `"bar@2.0.0": { "foo": "1.2.4" }` solo se aplica si `foo` cuelga de esa versión exacta de `bar`, de modo que el override deja de aplicarse por sí solo el día en que se actualiza `bar`. Las claves se pueden anidar con cualquier longitud para describir un camino más largo. Y cuando hay que sustituir a la vez un paquete y uno de sus hijos, la forma de objeto admite una clave `.` para el propio paquete, junto a las claves de sus hijos.

La dependencia directa es el único caso que npm rechaza. No se puede sustituir un paquete del que dependes directamente salvo que el override y la dependencia lleven exactamente la misma especificación; cualquier otra cosa detiene la instalación con un error `EOVERRIDE`. La salida documentada es una referencia: `"foo": "$foo"` le dice a npm que reutilice la especificación que declara tu propia entrada de `dependencies` para `foo`, en todas las profundidades. Cambias la versión en un solo sitio y todo el árbol la sigue.

Cuando el override parece no hacer nada

Los overrides solo se leen en el `package.json` raíz. La documentación de npm es explícita: los de las dependencias instaladas, workspaces incluidos, no se tienen en cuenta al resolver el árbol. De ahí salen dos consecuencias. En un monorepo el campo va en el manifiesto raíz, no en el paquete donde aparece el problema. Y un bloque `overrides` en una biblioteca que publicas no hace nada por quienes la instalan; la documentación remite a los autores de bibliotecas a fijar la dependencia o a `bundleDependencies`.

Después, comprueba el resultado en lugar de darlo por hecho. Ejecuta `npm install` para que el árbol se resuelva de nuevo y vuelve a lanzar `npm explain foo`: la versión que encabeza cada bloque es la que ahora está instalada. Sube `package.json` y `package-lock.json` en el mismo commit, porque el lockfile es el que lleva la versión resuelta a todas las demás máquinas. package-lock.json frente a package.json explica por qué los dos archivos tienen que moverse juntos.

Un override instala una versión con la que el paquete padre nunca dijo que funcionara. Esa es la gracia cuando el rango declarado solo contiene una versión vulnerable, y también es el riesgo: npm no te impedirá forzar una versión mayor para la que el padre no fue escrito. Trata esa línea como un parche con fecha de caducidad. Cuando el padre publique una versión que acepte el arreglo, borra el override.

Un override instala una versión con la que el paquete padre nunca dijo que funcionara. Esa es la gracia cuando el rango declarado solo contiene una versión vulnerable, y también es el riesgo: npm no te impedirá forzar una versión mayor para la que el padre no fue escrito. Trata esa línea como un parche con fecha de caducidad. Cuando el padre publique una versión que acepte el arreglo, borra el override.

- VersionDude

Yarn y pnpm usan otro campo

La misma idea existe en los otros dos gestores de paquetes, con otros nombres. Yarn lee un campo `resolutions`, descrito en la documentación de su manifiesto como una manera de usar una resolución concreta en lugar de la que el resolvedor elegiría normalmente; sus claves admiten un solo nivel de precisión, escrito `padre/hijo`. pnpm llama al ajuste `overrides`, igual que npm, lo sitúa en `pnpm-workspace.yaml` en su documentación actual y lo acota con un separador `>`, como en `bar@1>foo`. Ambos reservan el campo a la raíz del proyecto, igual que npm. La sintaxis no se traslada de una herramienta a otra, así que un proyecto que cambia de gestor de paquetes tiene que reescribir el bloque.

El orden de decisión es corto. Si el padre ya tiene una versión que acepta el arreglo, actualiza el padre y no escribas ningún override. Si no la tiene, limita el override a ese padre y no a todo el árbol. Si el mismo paquete debe ser idéntico en todas partes, usa la forma plana, con un rango cuando baste un mínimo. Y si la instalación se detiene por un conflicto de dependencia peer y no por una versión vulnerable, el problema es otro: las dependencias peer lo tratan.

FAQ

¿Funcionan los overrides de npm en el package.json de un workspace?

No. npm solo tiene en cuenta los overrides declarados en el package.json raíz del proyecto. Su documentación indica que los de las dependencias instaladas, workspaces incluidos, no se consideran al resolver el árbol, así que el campo va en el manifiesto raíz.

¿Qué significa el error EOVERRIDE?

Indica que un override apunta a un paquete del que dependes directamente con una especificación distinta de la que figura en tus dependencies. npm solo permite ese override cuando ambas especificaciones son idénticas. Iguálalas, o escribe el override como referencia, por ejemplo "foo": "$foo", que reutiliza la especificación de tu dependencia directa.

¿Puede un override usar un rango en lugar de una versión exacta?

Sí. La documentación de npm dice que el valor de un override admite cualquier especificador aceptado para una dependencia: versión exacta, rango semver, dist-tag o un reemplazo como npm:, file: o una URL de Git. Un rango como ^1.2.4 impone una versión corregida mínima sin congelar el paquete.

¿Cómo saber qué paquete trae una dependencia transitiva?

Ejecuta npm explain seguido del nombre del paquete. El comando muestra la cadena de dependencias que provoca su instalación, un bloque por cada copia presente en el árbol, cada uno rastreado hasta el proyecto raíz.

¿npm overrides es lo mismo que Yarn resolutions?

El propósito es el mismo y la sintaxis no. npm usa un campo overrides con objetos anidados, Yarn un campo resolutions con claves escritas padre/hijo y pnpm un ajuste overrides con un separador >. Los tres reservan el campo a la raíz del proyecto.

Proyecto relacionado