wandres.dev
LOCKFILES · reproducibilidad

Actualizar dependencias con control

El ciclo disciplinado de mantenimiento: ver lo desactualizado con outdated, subir versiones respetando los rangos con update, y auditar el diff del lockfile para no romper nada.

⏱ 15 min

Las dependencias no envejecen bien solas: el código que no actualizas acumula deuda de seguridad, y el que actualizas a lo loco acumula roturas. Entre “nunca toco nada” y “subo todo a la última” hay un método, y ese método se apoya en el lockfile. Actualizar con control significa ver con precisión qué está desactualizado, mover versiones respetando las reglas que tú mismo pusiste, y auditar el cambio —el diff del lockfile— antes de confiar en él. No es fricción: es la diferencia entre mantener y apagar fuegos.

🎯 Al terminar esta lección sabrás
  • Diagnosticar qué está desactualizado con pnpm outdated.
  • Actualizar respetando (o desbordando) los rangos con pnpm update y --latest.
  • Auditar un cambio con pnpm why, el diff del lockfile y pnpm audit.
  • Automatizar el mantenimiento con red de seguridad y cooldown de versiones.

Ver qué está desactualizado: outdated

El punto de partida nunca es actualizar a ciegas, sino diagnosticar. pnpm outdated compara tu árbol instalado contra el registro y te muestra tres columnas que importan: la versión actual, la que tu rango permitiría (Wanted) y la última publicada (Latest):

pnpm outdated
Package   Current  Wanted   Latest
zod       3.23.8   3.23.8   4.1.2
vite      6.0.3    6.3.5    8.0.0

Léelo con cuidado. zod está en su tope de rango (Wanted igual a Current): subir a 4.x requiere cambiar el package.json, porque cruza un major. vite tiene un Wanted mayor que su Current: hay un minor o patch que tu rango ya admite y que un simple update recogería, pero su Latest está dos major por delante, terreno de cambios rompientes.

Esa distinción —lo que cabe dentro del rango frente a lo que exige saltarlo— es el eje de toda la estrategia. Mentalmente, traduce cada fila a una acción:

  • Current igual a Wanted igual a Latest — al día, no toques nada.
  • Wanted mayor que Current — un pnpm update lo recoge, dentro de rango, bajo riesgo.
  • Latest mayor que Wanted — hay un major esperando: decisión deliberada, con changelog.

En un monorepo, añade -r (--recursive) para ver lo desactualizado en todos los paquetes del workspace a la vez. Y recuerda: outdated no cambia nada, es puro diagnóstico, seguro de ejecutar cuando quieras.

Actualizar respetando los rangos

pnpm update mueve las versiones dentro de los rangos de tu package.json y reescribe el lockfile en consecuencia. Es la actualización segura por diseño: nunca cruza un major que tú no hayas autorizado.

# sube todo lo que quepa en los rangos actuales (minor/patch)
pnpm update

# solo un paquete, o por patron
pnpm update vite
pnpm update "@types/*"

# modo interactivo: elige paquete por paquete que subir
pnpm update -i

Cruzar un major es una decisión deliberada, no un efecto colateral. Para eso está --latest, que desborda los rangos y reescribe el package.json a las últimas versiones:

# sube a lo ultimo aunque cruce major; modifica package.json
pnpm update --latest -i

El modo interactivo (-i) es el que querrás por defecto para cualquier cosa no trivial: presenta una lista navegable donde marcas paquete por paquete qué subir, separa los saltos por tipo (patch, minor, major) y aplica solo el subconjunto que elijas. Convierte “actualizar” de un acto de fe en una serie de decisiones conscientes.

La disciplina: usa pnpm update (dentro de rango) como rutina frecuente y de bajo riesgo, y reserva --latest, preferiblemente interactivo y un major cada vez, para cuando vayas a leerte el changelog y la guía de migración. En un monorepo con catalogs, actualizar la versión en el catálogo la propaga de golpe a todos los paquetes que la referencian, lo que hace estos saltos coherentes en todo el repo en un único punto de cambio.

flowchart LR
A[pnpm outdated] --> B[Cabe en el rango]
B -->|si| C[pnpm update dentro de rango]
B -->|no, cruza major| D[pnpm update --latest de a uno]
C --> E[Revisar diff del lockfile]
D --> E
E --> F[pnpm audit y tests]
F -->|verde| G[Commit del package.json y lockfile]
F -->|rojo| H[Revertir y aislar el culpable]
style G fill:#a6e3a1,color:#11111b
style H fill:#f38ba8,color:#11111b

Auditar el cambio antes de confiar

Una actualización no termina cuando el comando acaba: termina cuando has entendido qué cambió. Y lo que de verdad cambió no está en el package.json —que quizá movió una línea— sino en el diff del lockfile, que puede haber movido docenas de transitivas. Herramientas para leerlo:

  • El diff del lockfile en git. Es tu registro de auditoría. Un update de un solo paquete que altera cincuenta entradas de snapshots te está diciendo cuánta superficie transitiva se movió. Revísalo como revisas código.
  • pnpm why <paquete> para entender por qué una versión concreta acabó donde acabó, o quién arrastra una transitiva que te sorprende.
  • pnpm audit para cruzar tu árbol contra la base de vulnerabilidades conocidas y priorizar qué subir por seguridad.
  • pnpm licenses list para detectar que una actualización no te haya colado una licencia incompatible con tu proyecto.
pnpm why esbuild        # quien depende de esto y en que version
pnpm audit              # vulnerabilidades conocidas en el arbol
pnpm licenses list      # inventario de licencias

Cuando una vulnerabilidad vive en una transitiva que no controlas directamente, no tienes que esperar a que el paquete intermedio la corrija: el campo pnpm.overrides en package.json fuerza una versión mínima segura en todo el grafo, y esa decisión queda reflejada en el lockfile.

{
  "pnpm": {
    "overrides": {
      "esbuild@<0.24.1": ">=0.24.1"
    }
  }
}

Y sobre todo: la red de seguridad son los tests. Actualiza, revisa el diff, corre la batería completa. Si algo se rompe, el flujo aislado (un major por commit) hace trivial encontrar al culpable; el flujo “actualicé todo de golpe” convierte la depuración en arqueología. Una herramienta más: pnpm dedupe reescribe el lockfile para colapsar duplicados que se hayan colado tras varias subidas, y su diff te muestra exactamente qué se unificó. Y la regla no negociable para los major: uno a uno. Sube un solo paquete mayor, corre los tests, commitea, y solo entonces pasa al siguiente; mezclar cinco major en un commit convierte cualquier regresión en una bisección imposible.

Automatizar con red de seguridad

A escala, este ciclo se automatiza con bots como Renovate o Dependabot, que abren pull requests con las subidas y su diff de lockfile ya calculado. Pero automatizar sin criterio reintroduce el riesgo que quieres evitar, así que dos salvaguardas son clave en 2026:

  • Cooldown / minimum release age. No adoptes una versión recién publicada: espera unos días. La mayoría de paquetes maliciosos o patch catastróficos se detectan y retiran en las primeras horas. pnpm soporta una edad mínima de release, y los bots un periodo de espera configurable; es una de las defensas más baratas y efectivas contra ataques de cadena de suministro.
  • CI congelado como puerta. Cada PR de actualización pasa por pnpm install --frozen-lockfile (nivel 6.3), build y tests. El bot propone; la puerta verifica. Nada se fusiona sin cruzarla.

Una buena política de bot agrupa por tipo (todos los @types/* en un PR, los patch de seguridad aparte y priorizados) y separa los major en PRs individuales, precisamente porque son los que exigen lectura humana. La automatización no elimina el juicio: lo enfoca donde importa.

💡
Congela la superficie que no controlas

Para dependencias críticas cuya estabilidad no puedes permitirte, considera fijar la versión exacta (sin ^ ni ~) en el package.json, no solo en el lockfile. Así ni un pnpm update de rutina la mueve: solo cambia cuando tú, deliberadamente, editas el manifiesto. Es la excepción, no la regla —perderías los patch de seguridad automáticos—, pero para ese puñado de paquetes de los que depende toda tu app, el control explícito compensa.

El mantenimiento es un ciclo, no un evento

La forma en que un equipo actualiza dependencias delata su madurez de ingeniería. Los inmaduros oscilan entre dos fracasos simétricos: la parálisis —no tocar nada por miedo, hasta que un CVE crítico obliga a un salto de cinco major a la vez, imposible de revisar— y la temeridad —update --latest a todo, merge, y a rezar—. La madurez es el ciclo intermedio, pequeño y constante: diagnosticar con outdated, mover dentro de rango a menudo, cruzar major de uno en uno leyendo el changelog, y auditar cada cambio por su diff de lockfile, no por su diff de package.json. Ese último punto es la clave conceptual: el package.json expresa tu intención, pero el lockfile expresa la realidad que se ejecuta, y una actualización aparentemente trivial puede reescribir medio grafo transitivo. Sumado a la higiene de 2026 —cooldown de versiones, audit en el pipeline, procedencia firmada— convierte el mantenimiento en un proceso aburrido y predecible. Y ese aburrimiento es precisamente la meta: la deuda de dependencias es como la deuda técnica, barata de pagar en cuotas pequeñas y ruinosa de pagar de golpe.

⚔️ Un ciclo de actualización completo
  1. Ejecuta pnpm outdated y clasifica cada dependencia en “cabe en el rango” o “cruza un major”.
  2. Corre pnpm update (dentro de rango) y examina el diff del lockfile en git: ¿cuántas transitivas se movieron frente a cuántas directas?
  3. Elige un major y súbelo con pnpm update --latest -i de uno en uno; lee su changelog antes de aceptar.
  4. Audita el resultado con pnpm audit y pnpm why sobre algún paquete que te sorprenda en el diff.
  5. Corre la batería de tests; si pasa, commitea package.json y lockfile juntos; si falla, revierte y aísla el culpable.