npm peerDependencies

Maximiliano Fierro

August 23 2017

Hace unas semanas arranque un proyecto nuevo. Decidí darle una oportunidad a tener peerDependencies. Ya lo había hecho antes y no funcionó. Pero esta vez me puse a investigar un poco más. Este post viene a cubrir un poco lo que hice a modo de “notas” para mi pero tambien puede que le sirva a alguien más.

Un poco de contexto

El proyecto es un generador de sitios estáticos basado en Next.js (next). next tiene como peerDependencies react y react-dom. Esto significa que si quiero usar next en mi proyecto entonces voy a necesitar instalar también esas dependencias. Mi proyecto sigue esas dependencias pero agrega next a esa lista. Basicamente para usar nextein necesitamos instalar:

npm i -S next react react-dom nextein@beta

El problema surgió cuando quise armar un módulo en otro repositorio para usarlo de prueba / ejemplos. ¿Cómo hacemos para usar un módulo en desarrollo en otro proyecto? ¿Cuáles son los posibles problemas con los que me voy a encontrar? Eso es un poco lo que vamos a ver a continuación.

peerDependencies

Comencemos por lo básico.

¿Qué son las peerDependencies? - Se preguntarán.

¡Que buena pregunta Mario! - En algunos casos, es deseable expresar que nuestro paquete tiene compatibilidad con una librería o herramienta pero no necesariamente requiere una versión específica como parte de sus dependencias. Es el caso por ejemplo, de un plugin (vean la documentacion de npm).

Usando peerDependencies en desarrollo

Ahora viene la parte complicada. Tenemos algunas alternativas sobre cómo desarrollar y probar nuestro código en contexto. No hablo de unit tests o tests en general. Estamos queriendo usar el código en algunos ejemplos. O en otro módulo.

La opción inicial y más bien por defecto podría ser usar npm link. De esta manera podemos trabajar con los dos módulos al mismo tiempo. No?. Bueno, no. O casi.

Varias cosas a tener en cuenta:

  • npm (desde la versión 3 en adelante) no instala las peerDependencies.
  • npm link es básicamente un symlink. Esto va a causar que en este escenario las dependencias no se resuelvan como se podría esperar.

Como mencione antes, npm link es un symlink. El problema con esto es como se resuelven las dependencias con npm.

Vamos a nombrar a nuestro plugin/librería mi-lib y a nuestro módulo donde vamos a probarlo mi-host. Ahora, en mi-host tengo que declarar la dependencia a mi-lib (que se va a resolver con npm link) y tambien instalar las peerDependencies de mi-lib.

> cd mi-lib
> npm install
> npm link
> cd mi-host
> npm install
> npm link mi-lib

Supongamos que tenemos ambos módulos en el mismo directorio proyectos. La estructura, a grandes rasgos, queda de la siguiente forma:

- proyectos
  |- mi-lib
    |- node_modules
    |- package.json
  |- mi-host
    |- node_modules
      |- mi-lib (symlink)
    |- package.json

Esto va a generar que el node_modules de mi-host contenga las peerDependencies de mi-lib. Pero recuerden que mi-lib es un symlink! Cuando quiera ejecutar código en mi-lib de una de esas peerDependencies no lo va a encontrar porque están en distintos paths.

Alternativas / Soluciones

Googleando, StackOverfloweando, y algo mas, fui encontrando distintas soluciones que vamos a ver a continuación.

A partir de node 6.3 podemos pasarle un parametro a node para que preserve los symlinks a la hora de resolver los módulos/dependencias. Documentación de node

Pros

  • Es simple. Solo necesitamos agregar un parámetro al script de ejecución.

Cons

  • Si no podemos cambiar el script de ejecución, o no depende directamente de ejecutar el código con el CLI de node esta solución no nos sirve.

Instalar las peerDependencies en mi-lib

En este caso, que parece ser el más “future proof”, vamos instalar la misma version de las peerDependencies que usamos en mi-host. Esto es, si por ejemplo, nuestro package.json contiene:

{
  "name": "mi-lib",

  "peerDependencies": {
      "react": "^15.6"
  }
}

Y el npm install resuelve react a 15.6.1 cuando lo instalamos en mi-host entonces vamos a instalar react@15.6.1 en mi-lib. Lo recomendable es instalar esa versión como dependencia de desarrollo (devDependencies)

> cd mi-lib
> npm i -S react@15.6.1

Con esto nos aseguramos que el node_modules de nuestro mi-lib contenga exactamente la misma versión de react que la que usamos en mi-host.

Pros

  • Es una solución que funciona en un 99% de los casos. No agrega overhead al desarrollo de final de mi-host ni de mi-lib.

Cons

  • Requiere mantenimiento de mi-lib para asegurarse que usamos la misma versión de peerDependencies que en mi-host.
  • Hay al menos un caso particular donde esta solución no funciona: Si alguna de nuestras peerDependencies contiene un módulo con una instancia, un Singleton por ejemplo (vean como funciona el router de next ) no es una solución viable ya que en este caso vamos a tener la misma versión del módulo pero no la misma instancia

Monorepos (lerna)

Si no están familiarizados con lerna (o monorepos) la idea es que nos permite tener muchos módulos en desarrollo bajo el mismo repositorio. Lerna se encarga de hacer el trabajo de npm link. No hay symlinks. Las dependencias la maneja lerna y hace el desarrollo más sencillo.

Para solucionar el problema anterior lerna nos provee de una solución interesante. Tiene una funcionalidad que nos permite “hoistear” dependencias. Esto es, usar un node_modules en común en un nivel superior.

Un repositorio con lerna tiene una estructura similar a esta:,

repo
 |- lerna.json
 |- package.json
 |- packages
     |- mi-lib
     |- mi-host

Para poder lograr el hoisting de los módulos tenemos que decirle a lerna que haga el bootstrap con esta opción habilitada. Para eso podemos pasarlo por parámetro:

lerna bootstrap --hoist

O bien lo agregamos como parte de la configuración en el lerna.json

{
  "lerna": "2.0.0",
  "packages": [
    "packages/*"
  ],
  "commands": {
    "bootstrap": {
      "hoist": true
    }
  }
}

Pros

  • No dependemos de npm link, ni de symlinks. Los módulos se desarrollan de manera transparente a sí usamos módulos locales o de npm.

Cons

  • Todo el repositorio depende del formato de lerna. Y nuestro código queda enterrado en un directorio “packages” haciendo el repositorio un poco más grande de lo necesario.

Resumiendo

Hay varias formas de resolver este problema. Seguramente hay mas formas dando vueltas. Esta es una pequeña reseña sobre los escenarios que pude explorar personalmente y los problemas con los que me encontré en cada uno.

La solución más simple debe ser la correcta.