Escuela Frontend
npm

Cómo Gestionar las Versiones de Dependencias con Confianza

Ramón Huidobro
Autor
Ramón Huidobro

Una de mis tareas recientes era entrar a un proyecto en React que no se había tocado en más de un año, y traerlo a la última versión de React con TypeScript.

Suena tan simple como cambiar la entrada en package.json, pero con tantas dependencias (¡77!), varias partes del app comenzaron a fallar, y ciertas versiones tenían conflictos.

¡Se ponen a discutir cuál versión del paquete X es la correcta!

Era un camino largo y difícil para asegurarme que las dependencias estaban al día y que la aplicación funcionará correctamente.

En este post, relataré cómo pude arreglar las dependencias rotas y traer la aplicación al día, junto a las lecciones que aprendí haciéndolo. Después de haberlo leído, tendrás un juego de herramientas fundamentales que te ayudarán a gestionar dependencias.

¿Por qué debo aprender a manejar dependencias?

Es muy probable que uno mire su archivo package.json y piense “Esto se ve muy bien, todo se ve estable y permanecerá así por el resto de mi tiempo con este proyecto”.

Sin embargo, y quizás no hoy, ni mañana, pero algún día se dejará de aportar un sistema de gestión de paquetes que usamos, o quizás abandonará el desarrollo activo de un paquete popular, o peor aún, se elimine el uso de un paquete.

Capaz que algún día, cómo me pasó a mí, entrarás a un proyecto que no se ha tocado en mucho tiempo y será necesario actualizar sus dependencias para que pueda andar de nuevo.

En estos casos como en otros, tener la habilidad de manejar sus dependencias con confianza y tranquilidad nos ayuda a tomar decisiones eficaces y priorizarlas.

¿Y por qué es tan complicado gestionar dependencias?

Hacer una serie de actualizaciones de dependencias puede ser una experiencia agotante y complicada.

Uno de los problemas más comunes que surgen se debe al carácter frágil del código interconectado. Cómo bien lo sabemos, el cambiar el código en un lugar a otro puede causar que se rompa en otra parte. Es por esto por lo que hemos desarrollado técnicas para protegernos de nuestros errores, como los varios métodos de hacer pruebas de código

¿Y qué pasa con las dependencias open source? Asumiendo que no somos la persona manteniendo esa fuente de código, dependemos de que esa persona la mantenga al día, probada y segura.

Sin embargo, mi rol como desarrollador consiste en manejar sus versiones, ya que ciertas dependencias pueden mezclar sub-dependencias entre sí mismas. Lo afortunado es que npm y yarn se encargan de eso, pero en nuestro caso al actualizar react puede afectar el rendimiento de react-redux, ya que la tiene como dependencia de desarrollo.

La mejor norma para gestionar dependencias

Lamento informarles que no va a haber una estrategia general que se puede adaptar en cualquier proyecto, sin importar el contexto.

Sin embargo, lo importante es entender los contextos en que se usan las dependencias. En mi caso, me hago una serie de preguntas:

  • ¿Cuántas dependencias hay en este proyecto?
  • ¿Cuántas de las dependencias que necesitan actualizarse son devDependencies y cuantas son dependencies? (explicaré esto más adelante)
  • ¿Qué tan grande es el tamaño del equipo?

Las respuestas a estas preguntas afectan la clase de estrategias que puedo sugerir e implementar.

Una de mis metodologías preferidas es la de Martin Fowler, que dice que, si algo cuesta, hazlo a menudo.

La gran mayoría de las dependencias usan el versionado semántico, el cual se define como:

Dado un número de versión MAYOR.MENOR.PARCHE, se incrementa:

  1. La versión MAYOR cuando realizas un cambio incompatible en el API,
  2. La versión MENOR cuando añades funcionalidad que compatible con versiones anteriores
  3. La versión PARCHE cuando reparas errores compatibles con versiones anteriores.

Tomando en cuenta la manera en que se usan los números de versiones esto nos permite adoptar una estrategia para decidir donde y cuando actualizar nuestras dependencias. Por ejemplo, lo más probable es que actualizar la versión parche o menor no romperá la funcionalidad de la app.

Existen servicios como Depfu que automáticamente crean solicitudes de cambios para actualizarlas.

Del mismo modo podemos definir en el archivo package.json el esquema de como se deberán versionar las dependencias:

  • react: "16.11.0": La versión exacta
  • react: ">16.11.0": Cualquier versión mayor que esta
  • react: "~16.11.0": Sólo puede cambiar al nivel parche (0)
  • react: "^16.11.0": Sólo puede cambiar al nivel menor (11.0)
  • react: "*" o sin especificación react: "": Usar cualquier versión (¡ojo que esto no tiene que ser la última versión!)
  • react: "16.11.0 - 17.0.2": Usar cualquier versión entre 16.11.0 y 17.0.2
  • react: "^16.11.x": La x puede tener cualquier valor

¡Y eso no es todo! La excelente documentación de [npm](https://docs.npmjs.com/cli/v6/using-npm/semver) muestra aún más maneras de declarar versiones.

Recuerda: Estamos en esto juntos

Esta no es la primera actualización en este proyecto, y lo más probable es que no será la última sin que, como una obra de arte, se abandona.

Ahora bien, existen varias herramientas maravillosas que nos ayudan con actualizaciones, o al menos nos muestran donde comenzar.

Una herramienta mantenida por la comunidad es npm audit. Al correr el comando, npm audit chequea el registro (npm por defecto) si hay informes de vulnerabilidades con algunas y las presenta al usuario, acompañado por la gravedad del problema.

A continuación, vemos un ejemplo de salida de npm audit:

lodash <4.17.21
Severity: high
Command Injection - https://npmjs.com/advisories/1673
fix available via `npm audit fix`
node_modules/lodash
tar <=3.2.2 || 4.0.0 - 4.4.14 || 5.0.0 - 5.0.6 || 6.0.0 - 6.1.1
Severity: high
Arbitrary File Creation/Overwrite due to insufficient absolute path sanitization - https://npmjs.com/advisories/1770
Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning - https://npmjs.com/advisories/1771
fix available via `npm audit fix`
node_modules/electron-rebuild/node_modules/tar
node_modules/tar
y18n <3.2.2||=4.0.0||>=5.0.0 <5.0.5
Severity: high
Prototype Pollution - https://npmjs.com/advisories/1654
fix available via `npm audit fix`
node_modules/y18n
3 vulnerabilities (3 high)
To address all issues, run:
npm audit fix

Al final, verás que puedes activar npm audit fix para automáticamente actualizar estas dependencias con vulnerabilidades. No solo eso, pero si primero quieres ver cuales cambios se llevarán a cabo, puedes darle la opción --dry-run:

npm audit fix --dry-run

¡Tener esta lista de vulnerabilidades que mantiene la comunidad de npm nos ayuda un montón!

Incluso, también existe la posibilidad de usar npm outdated para ver cuales dependencias están obsoletas. Realmente no estamos solos en esto.

Dividiendo la tarea en victorias pequeñas

Al tratar de actualizar esta app de React, comencé por dividir la tarea en partes mayores:

  • Hacer que el build funcione
  • Restaurar la interfaz
  • Restaurar cada sección de funcionalidad

Comenzar con cada sección me habilita a no estrellarme con las otras, y me permite subdividir aún más.

Por ejemplo:

  • Restaurar la interfaz:
    • Encabezado
    • Contenido principal
    • Pie de pagina

Y de ahí subdividir de nuevo. ¡Cada sección resuelta se convierte en una pequeña victoria! Me felicito a mí mismo, me olvido de esa parte, y avanzo a la siguiente. Me mantiene motivado a pesar de que otras secciones no funcionen.

A veces arreglar una parte rompe la otra. Es como equilibrar platos, lo cual puede ser agobiante.

¡Pero no importa! Me concentro en la sección donde voy, cambiándome de puesto solamente al quedar bloqueado.

Esta técnica es muy similar al concepto de dividir y vencer. Partir estas tareas también nos permite delegarlas a otros miembros del equipo, si existe la posibilidad.

Irse derecho a la versión corriente

Bueno, si vamos a hacer una actualización, lo cual causará algunos problemas, ¿por qué no aprovechar y asegurarnos de que está todo mejor y actualizado en su última versión?

Eso fue la pregunta que me hice a mí mismo. ¡Lo que en realidad me ayudó mucho!

Existen excelentes razones para irse a la última versión de una dependencia. Por ejemplo, actualizar a la versión más nueva de React nos da acceso a estándares como hooks. Esta práctica nos pone en una posición óptima para actualizar en el futuro.

Actualizar a la última versión requiere agregar la opción @latest con el comando npm install. Por ejemplo, para actualizar react a la última versión:

npm install react@latest

La parte difícil es decidir donde y cuando (si es necesario) parar. Cuando comencé en este proyecto, tenía la ambición de actualizar todas las dependencias, una por una.

¡En total, el proyecto usaba 77!

Aunque quería poder actualizarlas todas, el cliente también contaba con una entrega puntual. Es por esa razón que acabe no actualizando ciertas dependencias a la última versión, por ejemplo @fortawesome/fontawesome-free.

Manteniendo una rama de control git

La realidad de embarcar en esta clase de tarea con una aplicación web con esta red de dependencias interrelacionadas y con tantos puntos de fallo es que vamos a tener un historial de git bastante desordenado a medida que uno va arreglando los problemas.

Una técnica que he incorporado es trabajar con una rama de git “bleeding-edge”. Por ejemplo, le pondré ramon-bleeding-edge. El propósito de esta rama es indicar que lo que estoy haciendo aquí es sumamente “work-in-progress”.

Ningún cambio empujado a esta rama tiene garantía que lo voy a proponer en una solicitud de cambio. Incluso, está dividida en varias confirmaciones pequenas que lo más probable es que están sin funcionar.

Con este flujo de trabajo puede uno irse acostumbrando a rebasar su historial y presentar una rama sencilla y directa para fusionar.

Protegerse con un conjunto de pruebas

Como en otros casos, tener un conjunto de pruebas amplio y detallado nos ayudará con el desarrollo de software, y lo mismo puede decirse al gestionar dependencias.

Actualizar una dependencia que se usa de manera extensa en varias partes de la base de código, en particular en partes que uno no ha tocado, puede ser espantoso. Pero al haber una serie de pruebas que podemos correr con cada cambio nos relaja y libera la mente para poder continuar.

Si las pruebas no pueden ni partir, puede que haya cambiado algo con la última versión con [react-test-renderer](https://www.npmjs.com/package/react-test-renderer) que uno no haya visto.

¿Pero qué pasa si no hay pruebas?

Dependiendo de cuánto tiempo uno tenga (yo tuve que aconsejárselo a mi cliente), uno no pierde nada con escribir sus propias pruebas. ¡Tener algunas, aunque sean pocas, es mejor que no tener ninguna!

Conclusión

El desarrollo de software moderno involucra una gran cantidad de dependencias. Continuar su desarrollo con estas en un punto estable no es problema, pero cuando llega el momento de cambiarlas, es importante dedicarles el tiempo necesario para asegurarse que todo funciona.

Yo, por mi parte pude limpiar la mayoría de las dependencias, quitar las que ya no se necesitaban, además, introduje documentación para el desarrollador que venga después de mí.

El aspecto más importante es que no me permití desanimarme. ¡Nos ha pasado a todos!

¡Espero que con este post tengas las herramientas necesarias para poder hacer esto con confianza!

¿Quieres mejorar tus habilidades de frontend?