← Node & Express

Módulos y npm

¿Qué significa el versionado semántico (`major.minor.patch`) y cuál es la diferencia entre `^` y `~` en las versiones del `package.json`?

Ver respuesta — intenta responderla en voz alta primero

Semver usa tres números: major.minor.patch. major sube cuando hay cambios que rompen la compatibilidad; minor sube cuando se agregan funciones compatibles; patch sube cuando se corrigen bugs sin romper nada. El prefijo ^ (caret) permite actualizar minor y patch (pero no major); el prefijo ~ (tilde) permite actualizar solo patch. Y el package-lock.json guarda las versiones exactas que se instalaron realmente, para que todos los equipos y servidores obtengan instalaciones idénticas y reproducibles.

Una versión como 4.19.2 se lee así:

  • 4 = major: cambios incompatibles. Actualizar de 4.x a 5.0.0 puede romper tu código; probablemente tengas que ajustar cosas.
  • 19 = minor: funciones nuevas que no rompen lo anterior. Pasar de 4.19 a 4.20 te da features nuevas y todo lo viejo sigue funcionando.
  • 2 = patch: correcciones de bugs, sin funciones nuevas ni cambios que rompan. De 4.19.2 a 4.19.3 solo arreglos.

Los prefijos le dicen a npm hasta dónde puede actualizar al instalar:

  • ^ (caret): "acepta cualquier versión que no cambie el major". Es el prefijo por defecto de npm.
    • ^4.19.0 permite instalar 4.19.5, 4.20.0, 4.99.9... pero nunca 5.0.0.
  • ~ (tilde): "acepta solo cambios de patch dentro del mismo minor".
    • ~4.19.0 permite instalar 4.19.1, 4.19.9... pero no 4.20.0.
  • Sin prefijo (4.19.0): versión exacta y fija, no actualiza a nada.

La lógica: ^ es más permisivo (confía en que minor y patch no rompen), ~ es más conservador (solo confía en los arreglos de bugs).

El rol de package-lock.json: el package.json guarda rangos (^4.19.0), no versiones exactas. El problema es que "el rango ^4.19.0" hoy puede resolverse a 4.19.2 y mañana a 4.20.0, dando instalaciones distintas en máquinas distintas. El package-lock.json resuelve eso: registra la versión exacta que se instaló de cada paquete y de cada sub-dependencia, más su hash de integridad. Así, cuando otra persona (o el servidor) hace npm install, obtiene exactamente el mismo árbol de dependencias. Por eso el package-lock.json debe subirse al repositorio.

Rangos en el package.json y qué versiones aceptan:

{
  "dependencies": {
    "express": "^4.19.0",
    "cors": "~4.19.0",
    "lodash": "4.19.0"
  }
}
  • "express": "^4.19.0" (caret): acepta >=4.19.0 y <5.0.0. Instalaría, por ejemplo, 4.21.0 o 4.19.7, pero jamás 5.0.0.
  • "cors": "~4.19.0" (tilde): acepta >=4.19.0 y <4.20.0. Instalaría 4.19.1, 4.19.8, pero no 4.20.0.
  • "lodash": "4.19.0" (sin prefijo): versión fija, instala exactamente 4.19.0.

Comandos útiles en la terminal:

# Instalar respetando el package-lock.json (reproducible, ideal en CI/produccion):
npm ci

# Instalar respetando los rangos del package.json (puede actualizar dentro del rango):
npm install

# Ver que version EXACTA quedo instalada de un paquete:
npm ls express

Nota: npm ci borra node_modules e instala clavado a lo que dice el package-lock.json, sin tocarlo. Es lo que se usa en integración continua y despliegues para garantizar builds idénticos.

Confundir ^ con ~, o creer que ^ puede saltar de major. ^4.19.0 nunca llega a 5.0.0, solo se mueve dentro del 4.x. Otro error clásico es no subir el package-lock.json al repositorio (o borrarlo): sin él, cada npm install puede resolver los rangos a versiones distintas, y aparece el temido "en mi máquina funciona" porque cada quien terminó con versiones diferentes de las dependencias.

"Semver usa tres números: major, minor y patch. El major cambia cuando algo rompe la compatibilidad, el minor cuando se agregan funciones compatibles y el patch cuando se arreglan bugs sin romper nada. En el package.json, el caret, el símbolo circunflejo, permite actualizar minor y patch pero no major, así que ^4.19.0 puede llegar hasta antes de 5.0.0; la tilde es más estricta y solo permite patch, así que ~4.19.0 se queda dentro del 4.19. Y para que las instalaciones sean reproducibles está el package-lock.json, que guarda las versiones exactas de todo el árbol de dependencias; por eso lo subo al repo y en CI uso npm ci, que instala clavado a ese lock."

Reto rápido

La última versión publicada de un paquete es 2.8.4. Tienes estas tres entradas. ¿Qué versión instalaría cada una?

{
  "a": "^2.3.0",
  "b": "~2.3.0",
  "c": "2.3.0"
}
Ver respuesta
  • a: "^2.3.0" -> instala 2.8.4. El caret permite subir minor y patch dentro del major 2, así que agarra la más nueva disponible por debajo de 3.0.0.
  • b: "~2.3.0" -> instala la última 2.3.x disponible (por ejemplo 2.3.9 si existiera; si 2.3.0 es la única 2.3.x, se queda en 2.3.0). La tilde no salta a 2.8.4 porque eso cambiaría el minor.
  • c: "2.3.0" -> instala exactamente 2.3.0. Versión fija, no actualiza.