Comandos de npm para cuando las cosas van mal
Usa npm ls, npm explain, overrides y npm ci para rastrear dependencias inesperadas, corregir versiones y evitar desvíos del lockfile.
Cuando aparece un paquete en node_modules que nada en package.json solicitó, o en una versión que no fijaste, ejecuta npm ls <package> para ver dónde se ubica y npm explain <package> para ver qué dependencia lo arrastró, antes de tocar nada.
Todo desarrollador ha vivido el momento de mirar fijamente un número de versión en node_modules y pensar: ¿de dónde saliste tú? El reflejo es conocido: algo se ve mal en el árbol, así que haces rm -rf node_modules, reinstalas y cruzas los dedos. A veces el problema desaparece. Lo más frecuente es que vuelva de inmediato, porque el instalador reconstruyó el mismo árbol a partir de las mismas entradas, y ahora no tienes ni idea de qué cambió.
Este artículo recorre una única investigación: un paquete o una versión inesperada, rastreada hasta la dependencia que la solicitó y luego corregida en la capa adecuada. Los fallos en tiempo de instalación como ERESOLVE, EACCES y los errores de compilación nativa se tratan en otros artículos de este blog, en las guías para resolver conflictos ERESOLVE, errores de permisos EACCES y fallos de compilación de node-gyp. Este texto es para cuando todavía no se ha lanzado ningún error.
Puntos clave
npm ls <package>muestra todos los lugares donde aparece un paquete en el árbol instalado y la versión en cada ubicación;npm explain <package>muestra la cadena de dependencias que lo solicitó.- Sin
--all,npm lslista únicamente tus dependencias directas; con--allimprime el árbol completo, y--depth=<n>establece un corte explícito entre esos dos extremos. npm whyes un alias denpm explain, por lo que la misma palabra funciona en npm, pnpm y yarn.- El campo
overridesenpackage.jsonfuerza una versión concreta de una dependencia anidada sin importar el rango que solicitó su paquete padre, y por eso conviene intentar primero actualizar el padre. npm cirequiere unpackage-lock.jsonexistente, eliminanode_modules, instala exactamente lo que especifica el lockfile y termina con error si el lockfile ypackage.jsonno coinciden.
¿Por qué eliminar node_modules destruye la evidencia?
Eliminar node_modules y reinstalar borra el único registro de cómo llegó a tu proyecto un paquete inesperado. El árbol instalado y package-lock.json codifican juntos todas las decisiones de resolución que tomó npm: qué padre solicitó qué rango, qué versión lo satisfizo y dónde terminó el resultado en disco.
Una reinstalación repite esas decisiones a partir de package.json y del lockfile. Si las entradas no han cambiado, obtienes el mismo árbol y la misma sorpresa. Si han cambiado (un flag de configuración, un registro, la edición de un rango), la reinstalación sobrescribe el estado con el que necesitabas comparar. En cualquier caso, lee el árbol antes de reconstruirlo. Los dos comandos que lo leen son npm ls y npm explain.
npm ls: ¿dónde está el paquete y en qué versión?
npm ls <package> filtra el árbol instalado para mostrar las rutas que terminan en el paquete indicado, imprimiendo cada ubicación como name@version con sus padres indentados por encima. También puedes filtrar por un rango de versiones, como en npm ls semver@^6, cuando solo te interesan las copias de una major concreta.
# Every copy of semver, with the path down to each
npm ls semver
# The complete tree, not just direct dependencies
npm ls --all
# Cap the walk at two levels
npm ls --all --depth=2
# Only what ships to production
npm ls --all --omit=dev
El ajuste depth tiene el valor predeterminado 0 a menos que se pase --all, en cuyo caso pasa a ser Infinity. Ese valor predeterminado rige un npm ls simple sin argumento de paquete. Una vez que nombras un paquete, npm sigue la ruta hasta cada copia sin importar la profundidad, y por eso el propio ejemplo npm ls promzard de la documentación muestra un resultado anidado sin --all; pasa --depth=<n> de forma explícita si quieres limitar ese recorrido.
Lo que imprime npm es un mapa de qué paquete depende de cuál, así que no se corresponderá con la forma en que las carpetas están realmente en disco: un paquete deduplicado aparece debajo de cada padre que lo necesita, no solo en el único lugar donde viven sus archivos. La salida también marca los paquetes que son extraneous (instalados pero no declarados), los que faltan o los que están en una versión que no satisface el rango declarado; los paquetes ausentes aparecen con la etiqueta UNMET DEPENDENCY. Añade --package-lock-only y npm informará del árbol que produciría el lockfile, ignorando lo que contenga actualmente node_modules.
Dos notas de nomenclatura. Los filtros actuales son --omit=dev e --include=dev; --production es un alias obsoleto de --omit=dev y --dev un alias obsoleto de --include=dev, mientras que --development no es en absoluto una opción documentada. Además, npm ls termina con un código distinto de cero cuando falta un paquete o está en una versión inválida, o cuando un paquete indicado no coincide con nada, lo que lo hace utilizable como comprobación en CI; los paquetes extraneous por sí solos no provocan un fallo.
npm explain: ¿quién solicitó el paquete?
npm explain <package> imprime, para cada copia instalada, la cadena de declaraciones de dependencia que causó que estuviera ahí, ascendiendo hasta llegar al proyecto raíz. Donde npm ls responde «dónde», npm explain responde «quién».
npm explain semver
npm why semver # identical
npm explain semver --json # for jq
Cada bloque de la salida empieza con el name@version resuelto y su ruta en node_modules, y luego indenta una línea por salto: el rango que declaró un padre, la versión propia del padre y la ruta del padre, terminando con una línea que nombra el proyecto raíz. Léelo de abajo hacia arriba para seguir el rastro desde tu package.json hasta la copia que no esperabas. Los paquetes duplicados obtienen un bloque por copia, de modo que los rangos en conflicto quedan visibles uno al lado del otro. También puedes pasar una carpeta, como npm explain node_modules/foo/node_modules/semver, para explicar exactamente una copia anidada.
La sinopsis de npm explain indica why como su alias, y los demás gestores principales usan el mismo verbo.
| Gestor de paquetes | Comando | Forma de la salida |
|---|---|---|
| npm | npm explain <pkg> o npm why <pkg> | Un bloque por copia instalada, con la cadena hasta la raíz |
| pnpm | pnpm why <pkg> | Un árbol invertido, con el paquete consultado arriba |
| Yarn | yarn why <pkg> | Motivos por workspace, acepta pkg@range |
¿Conviene actualizar el padre o añadir un override?
Una vez que npm explain identifica el padre que solicitó el rango problemático, la primera solución es llevar a ese padre a una versión que solicite un rango mejor. Ejecuta npm outdated <parent> para ver si existe una versión más reciente, o lee el package.json del padre en el registro con npm view <parent>@latest dependencies. Si un padre más nuevo declara un rango aceptable, actualízalo y deja que npm vuelva a resolver el hijo.
Solo cuando ninguna versión del padre corrige el rango deberías recurrir a overrides:
{
"overrides": {
"semver": "^7.5.4"
}
}
Un override reemplaza la versión de la dependencia anidada sin importar el rango que declaró el padre, por lo que el padre puede acabar ejecutándose contra una versión con la que nunca se probó. Ese es el compromiso, y es la razón por la que overrides es la segunda opción y no la primera. Algunas reglas de la documentación: los overrides solo se respetan en el package.json raíz; un paquete del que dependes directamente solo puede ser sobrescrito con una especificación idéntica a la suya, de lo contrario npm lanza EOVERRIDE, y la forma de referencia $name existe para ese caso; y los valores pueden ser una versión exacta, un rango, un dist-tag o un especificador npm:, file: o de Git. Anida el override bajo el nombre del padre cuando quieras que se aplique a una rama del árbol en lugar de a todas partes.
npm config list: ajustes que olvidaste que habías puesto
npm config list imprime los ajustes que tú, tu entorno o un archivo .npmrc hayan establecido; npm config list -l imprime además los valores predeterminados de npm, y --json devuelve los mismos datos en formato JSON. Cuando un árbol se resuelve de una manera que package.json por sí solo no puede explicar, la causa suele ser un valor de configuración que nadie recuerda haber escrito.
npm config list
npm config list -l
La salida está agrupada por origen (línea de comandos, entorno, .npmrc del proyecto, .npmrc del usuario, global), lo que te indica qué archivo editar. Dos claves merecen una primera revisión. Un registry distinto del predeterminado significa que las versiones se resolvieron contra un mirror o un registro privado cuyo contenido puede ir por detrás del público. Un ajuste legacy-peer-deps guardado indica a npm que construya el árbol sin consultar en absoluto las peerDependencies, tal como se comportaba hasta la versión 6, de modo que puedes acabar con combinaciones que el resolutor actual habría rechazado. Hay un efecto secundario: una vez que un lockfile se ha construido con ese flag, todos los npm ci posteriores también lo necesitan, o la instalación se rompe. Una línea olvidada en el .npmrc de un proyecto puede explicar tanto un árbol local extraño como una ejecución de CI en rojo.
npm ci frente a npm install: ¿qué ocurre cuando el lockfile no coincide?
Cuando el lockfile satisface package.json, npm install usa las versiones exactas del lockfile; cuando no lo satisface, npm install vuelve a resolver y actualiza package-lock.json. npm ci, en cambio, da error.
| Comportamiento | npm install | npm ci |
|---|---|---|
Requiere package-lock.json | No | Sí |
El lockfile y package.json no coinciden | Vuelve a resolver, reescribe el lockfile | Termina con error |
node_modules existente | Se reutiliza | Se elimina primero |
Escribe en package.json o en el lockfile | Sí | Nunca |
| Añadir un solo paquete | Sí | No |
La documentación de npm install es explícita sobre el orden de prioridad: los rangos de package.json son la fuente de verdad, y el lockfile solo conserva sus versiones fijadas mientras sigan encajando dentro de esos rangos. Ese es precisamente el comportamiento que no quieres en CI, donde un lockfile reescrito en silencio oculta la desviación que intentas detectar. npm ci se niega a reconciliar los dos archivos y falla de forma ruidosa, así que úsalo en los pipelines y reserva npm install para la máquina donde sí pretendes cambiar dependencias.
Conclusión
Un paquete inesperado en el árbol es una decisión de resolución con un rastro documental, y npm ls junto con npm explain leen ese rastro sin alterarlo. Sigue la cadena hasta el padre que declaró el rango, corrige el padre si existe una versión mejor, aplica un override solo cuando no la haya y después revisa npm config list en busca de ajustes que hayan sesgado la resolución desde el principio. Ejecuta npm ci en CI para que la próxima discrepancia rompa la build en lugar de reescribir el lockfile en silencio.
Preguntas frecuentes
¿Qué significa 'deduped' junto a un paquete en la salida de npm ls?
La etiqueta 'deduped' significa que npm ls está mostrando el paquete en ese punto del grafo lógico de dependencias, pero que ahí no existe una copia separada: una única copia instalada más arriba en node_modules satisface el rango de ese padre. No es un error. Como npm ls imprime el árbol lógico, el mismo paquete aparece bajo cada padre que lo requiere, y solo la línea sin etiqueta corresponde a una carpeta física.
¿Cómo elimino los paquetes que npm ls reporta como extraneous?
Ejecuta npm prune. Elimina cualquier cosa que esté en node_modules de la que nada más dependa; nombra uno o más paquetes para limitarlo a esos. Añade --omit=dev, o establece NODE_ENV a production, y tus devDependencies también se irán. Usa --dry-run para ver primero el plan, y --json para obtener los cambios en formato JSON. Las instalaciones ya limpian por sí solas los paquetes extraneous, así que esto suele hacer falta sobre todo después de un fallo o de una instalación a medias.
¿npm dedupe corrige las versiones duplicadas que muestra npm ls, o necesito overrides?
npm dedupe solo consolida las copias que los rangos declarados ya permiten. Recorre el árbol y eleva cada dependencia lo más arriba posible, de modo que los padres con rangos que se solapan acaban compartiendo una única copia, y nunca descarga nada nuevo del registro. Si dos padres piden rangos sin ninguna versión en común, ambas copias permanecen, y la solución es actualizar un padre o añadir una entrada en overrides. npm find-dupes ejecuta la misma pasada como simulación, así que puedes ver el resultado antes.
¿Cómo listo los paquetes npm instalados globalmente?
Ejecuta npm ls -g. El flag --global apunta npm ls al prefijo global, listando los paquetes instalados allí en lugar de los del proyecto actual. Se aplican las mismas reglas de profundidad: sin --all imprime solo los paquetes globales de primer nivel, y npm ls -g --all expande cada uno en su árbol completo de dependencias. Añade un valor explícito de --depth para limitar el recorrido, o --json para obtener una salida legible por máquinas.