Introducción a cómo funcionan los administradores de paquetes de JavaScript

Ashley Williams es una de las líderes de la comunidad de Node.js. Ella tuiteó sobre un nuevo administrador de paquetes.

Realmente no entendí lo que quería decir, así que decidí profundizar y leer sobre cómo funcionan los administradores de paquetes.

Esto fue justo cuando el niño más nuevo en el bloque del administrador de paquetes de JavaScript, Yarn, acababa de llegar y estaba generando mucho revuelo.

Así que aproveché esta oportunidad para comprender también cómo y por qué Yarn hace las cosas de manera diferente a npm.

Me divertí mucho investigando esto. Ojalá lo hubiera hecho hace mucho tiempo. Así que escribí esta sencilla introducción a npm y Yarn para compartir lo que he aprendido.

Comencemos con algunas definiciones:

¿Qué es un paquete?

Un paquete es una pieza de software reutilizable que se puede descargar desde un registro global en el entorno local de un desarrollador. Cada paquete puede depender o no de otros paquetes.

¿Qué es un administrador de paquetes?

En pocas palabras: un administrador de paquetes es una pieza de software que le permite administrar las dependencias (código externo escrito por usted u otra persona) que su proyecto necesita para funcionar correctamente.

La mayoría de los administradores de paquetes hacen malabares con las siguientes partes de su proyecto:

Código de proyecto

Este es el código de su proyecto para el que necesita administrar varias dependencias. Normalmente, todo este código se registra en un sistema de control de versiones como Git.

Archivo de manifiesto

Este es un archivo que realiza un seguimiento de todas sus dependencias (los paquetes que se administrarán). También contiene otros metadatos sobre su proyecto. En el mundo de JavaScript, este archivo es supackage.json

Código de dependencia

Este código constituye sus dependencias. No debe modificarse durante la vida útil de su aplicación, y el código de su proyecto debe poder acceder a él en la memoria cuando sea necesario.

Bloquear archivo

Este archivo lo escribe automáticamente el administrador de paquetes. Contiene toda la información necesaria para reproducir el árbol de fuentes de dependencia completo. Contiene información sobre cada una de las dependencias de su proyecto, junto con sus respectivas versiones.

Vale la pena señalar en este punto que Yarn usa un archivo de bloqueo, mientras que npm no lo hace. Hablaremos de las consecuencias de esta distinción en un momento.

Ahora que le he presentado las partes de un administrador de paquetes, analicemos las dependencias en sí.

Dependencias planas versus anidadas

Para comprender la diferencia entre los esquemas de dependencia planos y anidados, intentemos visualizar un gráfico de dependencia de dependencias en su proyecto.

Es importante tener en cuenta que las dependencias de las que depende su proyecto pueden tener sus propias dependencias. Y estas dependencias, a su vez, pueden tener algunas dependencias en común.

Para aclarar esto, digamos que nuestra aplicación depende de las dependencias A, B y C, y C depende de A.

Dependencias planas

Como se muestra en la imagen, tanto la aplicación como C tienen A como su dependencia. Para la resolución de dependencias en un esquema de dependencia plano, solo hay una capa de dependencias que su administrador de paquetes necesita atravesar.

En pocas palabras: puede tener solo una versión de un paquete en particular en su árbol de fuentes, ya que hay un espacio de nombres común para todas sus dependencias.

Suponga que el paquete A se actualiza a la versión 2.0. Si su aplicación es compatible con la versión 2.0, pero el paquete C no lo es, entonces necesitamos dos versiones del paquete A para que nuestra aplicación funcione correctamente. Esto se conoce como un infierno de dependencia.

Dependencias anidadas

Una solución simple para lidiar con el problema de Dependency Hell es tener dos versiones diferentes del paquete A: la versión 1.0 y la versión 2.0.

Aquí es donde entran en juego las dependencias anidadas. En el caso de dependencias anidadas, cada dependencia puede aislar sus propias dependencias de otras dependencias, en un espacio de nombres diferente.

El administrador de paquetes necesita atravesar varios niveles para la resolución de dependencias.

Podemos tener varias copias de una sola dependencia en tal esquema.

Pero, como habrás adivinado, esto también genera algunos problemas. ¿Qué pasa si agregamos otro paquete, el paquete D, y también depende de la versión 1.0 del paquete A?

Entonces, con este esquema, podemos terminar con la duplicación de la versión 1.0 del paquete A. Esto puede causar confusión y ocupa espacio innecesario en el disco.

Una solución al problema anterior es tener dos versiones del paquete A, v1.0 y v2.0, pero solo una copia de v1.0 para evitar duplicaciones innecesarias. Este es el enfoque adoptado por npm v3, que reduce considerablemente el tiempo necesario para recorrer el árbol de dependencias.

Como explica ashley williams, npm v2 instala las dependencias de forma anidada. Es por eso que npm v3 es considerablemente más rápido en comparación.

Determinismo vs no determinismo

Otro concepto importante en los administradores de paquetes es el de determinismo. En el contexto del ecosistema de JavaScript, el determinismo significa que todas las computadoras con un package.jsonarchivo determinado tendrán exactamente el mismo árbol de dependencias de origen instalado en su node_modulescarpeta.

Pero con un administrador de paquetes no determinista, esto no está garantizado. Incluso si tiene exactamente lo mismo package.jsonen dos computadoras diferentes, el diseño de su node_modulespuede diferir entre ellas.

El determinismo es deseable. Le ayuda a evitar problemas de “funcionó en mi máquina pero se rompió cuando la implementamos” , que surgen cuando tiene diferentes node_modulesen diferentes computadoras.

npm v3, de forma predeterminada, tiene instalaciones no deterministas y ofrece una función de envoltura para que las instalaciones sean deterministas. Esto escribe todos los paquetes del disco en un archivo de bloqueo, junto con sus respectivas versiones.

Yarn ofrece instalaciones deterministas porque utiliza un archivo de bloqueo para bloquear todas las dependencias de forma recursiva en el nivel de la aplicación. Entonces, si el paquete A depende de la versión 1.0 del paquete C y el paquete B depende de la versión 2.0 del paquete A, ambos se escribirán en el archivo de bloqueo por separado.

Cuando conoce las versiones exactas de las dependencias con las que está trabajando, puede reproducir fácilmente las compilaciones y luego rastrear y aislar errores.

“Para que quede más claro, sus package.jsonestados " lo que quiero " para el proyecto, mientras que su archivo de bloqueo dice " lo que tenía " en términos de dependencias. - Dan Abramov

Así que ahora podemos volver a la pregunta original que me inició en esta juerga de aprendizaje en primer lugar: ¿Por qué se considera una buena práctica tener archivos de bloqueo para aplicaciones, pero no para bibliotecas?

La razón principal es que realmente implementa aplicaciones. Por lo tanto, debe tener dependencias deterministas que conduzcan a compilaciones reproducibles en diferentes entornos: pruebas, preparación y producción.

Pero no ocurre lo mismo con las bibliotecas. Las bibliotecas no se implementan. Se utilizan para crear otras bibliotecas o en la propia aplicación. Las bibliotecas deben ser flexibles para que puedan maximizar la compatibilidad.

Si tuviéramos un archivo de bloqueo para cada dependencia (biblioteca) que usamos en una aplicación, y la aplicación se viera obligada a respetar estos archivos de bloqueo, sería imposible acercarse a una estructura de dependencia plana de la que hablamos anteriormente, con el control de versiones semántico flexibilidad, que es el mejor escenario para la resolución de dependencias.

He aquí por qué: si su aplicación tiene que respetar de forma recursiva los archivos de bloqueo de todas sus dependencias, habría conflictos de versiones por todas partes, incluso en proyectos relativamente pequeños. Esto provocaría una gran cantidad de duplicaciones inevitables debido al control de versiones semántico.

Esto no quiere decir que las bibliotecas no puedan tener archivos de bloqueo. Ciertamente pueden. Pero la conclusión principal es que los administradores de paquetes como Yarn y npm, que consumen estas bibliotecas, no respetarán esos archivos de bloqueo.

¡Gracias por leer! Si cree que esta publicación fue útil, toque "︎❤" para ayudar a promover esta pieza entre otros.