August 10, 2014

Node Package Manager

Repositorio con múltiples paquetes de node

Si ocupas node.js en tus desarrollos ¿alguna vez te has topado con que tienes un sniplet, una función, una librería o un pedazo de código que les puede servir en varios lugares de tus repositorios de un mismo proyecto? Pues a mi si, y no solo uno, durante el desarrollo en el que estamos trabajando, han surgido varias librerías que se usan en muchos lugares. Por supuesto que darles mantenimiento es un problema si no se mantienen centralizadas.

Si ya has trabajado con node, conocerás la belleza del administrador de paquetes (npm). Resuelve el problema anterior manteniendo un solo paquete el cual actualizas cuando haya cambios. Sin embargo, cuando tus librerías son privadas, no puedes simplemente publicarlas al registro. Sobre todo cuando tiene derechos de autor.

Una solución es crear un repositorio privado en github, subir ahí la librería e instalarla requiriéndola en tu package.json. Pero ahora me vas a preguntar: “¿Tengo que crear un repositorio privado por cada una de las librerías?, esos cuestan dinero, ¿sabías?”.

Es por eso que vamos a aprender a crear un repositorio, el cual tenga un par de librerías personalizadas, las cuales puedan ser requeridas por separado de forma sencilla.

Así que sin más introducción, vamos a empezar.

Primero vamos a crear un nuevo repositorio local, yo lo llamé multiNpmRepo. Para esto ya obviamos que sabes usar git. Así que nos saltamos la parte hasta el primer commit:

commit inicial

Ok tenemos nuestro primer commit, no le puse un package.json para definirlo dentro de cada uno de los paquetes que va a tener dentro.

Vamos a subir el repositorio a github. Para propósitos demostrativos, lo voy a hacer público, sin embargo, la idea general de esto es que lo hagas privado para que solo la gente que le des permisos pueda instalarlo. Eso lo veremos más adelante.

Vamos a hacer nuestro primer paquete, será algo muy sencillo, se me ocurre traer el titleize de RoR a node. El primer paso es crear un nuevo branch el cual llamaré titleize

Branch titleize

Ahora vamos a usar npm init para crear el package.json, el comando es interactivo y nos va preguntando datos del paquete. Al terminar debe quedar algo así:

{
  "name": "titleize",
  "version": "0.32.7-a",
  "description": "Titleize is the ol' function from Ruby on Rails on node",
  "main": "bin/titleize.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "nodeunit test/titleize.js"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/ZeroDragon/multiNpmRepo.git"
  },
  "keywords": [
    "titleize",
    "nodejs",
    "RoR"
  ],
  "author": "zerodragon <npm@zerothedragon.com>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/ZeroDragon/multiNpmRepo/issues"
  },
  "homepage": "https://github.com/ZeroDragon/multiNpmRepo"
}

Observemos como el elemento main apunta a bin/titleize.js, este pequeño es el que hace la magia cuando hagamos los requires en nuestro código.

Como hacer el código de los paquetes no es parte de este tutorial, lo voy a dejar de lado, pero pueden ver los resultados en bin/titleize.js y la prueba en test/titleize.js. Más adelante veremos como ejecutar la prueba.

Ok una vez que tenemos el paquete listo y commiteado en su propia branch, vamos a crearle una etiqueta, aquí es importante porque el nombre de la etiqueta será el que se usaremos para hacer el install, otra cosa importante es que el nombre del branch y la etiqueta no pueden ser iguales, porque github nos rechazará el nombre, sin embargo, podemos cambiarlo con una mayúscula.

Creando una etiqueta

Creada la equiqueta hacemos un push de todo (master, branch y etiqueta). Esto nos dará un path absoluto en github al paquete en cuestión (ej: link).

Ahora vamos a repetir el proceso, pero con una nueva branch. Aquí lo interesante es que debemos crear este nuevo branch desde el commit inicial, para que se cree de forma que no tenga nada de los otros paquetes.

objectcloner branch

Ok pues el segundo paquete, será un clonador de arreglos y objetos. Una vez creado el nuevo branch, nos damos cuenta que no hay package.json ni nada de lo que hicimos con el anterior. Esto es lo esperado, vamos a correr de nuevo el npm init para generar un package.json. El mio quedó así:

{
  "name": "objectcloner",
  "version": "0.32.7-a",
  "description": "A multi-level array/object cloner",
  "main": "bin/objectcloner.js",
  "scripts": {
    "test": "nodeunit test/objectcloner.js"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/ZeroDragon/multiNpmRepo.git"
  },
  "keywords": [
    "object",
    "array",
    "clone"
  ],
  "author": "ZeroDragon <npm@zerothedragon.com>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/ZeroDragon/multiNpmRepo/issues"
  },
  "homepage": "https://github.com/ZeroDragon/multiNpmRepo"
}

Si se dan cuenta, es el mismo repositorio, solo que apunta a un main diferente.

Una vez creado el segundo paquete, realizamos el mismo tratamiento, le creamos su etiqueta y empujamos todo el código a github. Así es como está quedando nuestro log de cambios:

source tree

Ahora si vemos en github, podemos ver que tenemos dos releases, con los tags que especificamos:

github

Bien, ahora es tiempo de ir a nuestro repositorio de trabajo. Puede ser cualquiera (que use node, claro). Y vamos a agregar un paquete tipo git en las dependencias del package.json. En este caso, como el master está completamente diferenciado de las otras dos branch, vamos a utilizarlo como si fuera otro repositorio que está requiriendo titleize y objectcloner.

Esto lo podemos hacer con:

npm install --save git+ssh://<el endpoint de github en modo ssh>#<el nombre del tag>

Por ejemplo para instalar Titleize y Objectcloner y guardarlo en el package.json, el comando es:

npm install --save git+ssh://git@github.com:ZeroDragon/multiNpmRepo.git#Titleize
npm install --save git+ssh://git@github.com:ZeroDragon/multiNpmRepo.git#Objectcloner

Si observamos los elementos instalados en los node_modules podemos ver que ahí están los dos elementos que definimos antes:

Requerimientos instalados

Notemos que el nombre del directorio que contiene el paquete es el nombre que le definimos en el package.json.
Ahora bien, vamos a utilizar nuestros nuevos juquetes, para esto instalamos nodeunit como requerimiento de desarrollo:

npm install --save-dev nodeunit

el package.json completo de master quedó así:

{
  "name": "multiNpmRepo",
  "version": "0.32.7-a",
  "description": "Una prueba de concepto de que se pueden definir paquetes de npm como branches y requerirlas dentro del master sin la necesidad de tenerlas en desarrollo",
  "main": "bin/multinpmrepo.js",
  "scripts": {
    "test": "nodeunit test/multinpmrepo.js"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/ZeroDragon/multiNpmRepo.git"
  },
  "keywords": [
    "multi",
    "npm",
    "dependencies"
  ],
  "author": "ZeroDragon <npm@zerothedragon.com>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/ZeroDragon/multiNpmRepo/issues"
  },
  "homepage": "https://github.com/ZeroDragon/multiNpmRepo",
  "devDependencies": {
    "nodeunit": "~0.9.0"
  },
  "dependencies": {
    "titleize": "git+ssh://git@github.com:ZeroDragon/multiNpmRepo.git#Titleize",
    "objectcloner": "git+ssh://git@github.com:ZeroDragon/multiNpmRepo.git#Objectcloner"
  }
}

Aqui vemos que el main es bin/multinpmrepo.js y el test es nodeunit test/multinpmrepo.js.

Esto quiere decir que nuestro archivo principal será el que vamos a poner dentro de bin (hay que crear ese directorio) y nuestra prueba de funcionamiento será en test (también hay que crear ese directorio).

Así queda nuestro archivo principal. Observa como los paquetes se requieren con el nombre definido en el package.json de cada uno de los paquetes.

enter image description here

Lo que hice en esta ocasión fue que el archivo principal hace un require a los dos paquetes, mientras que en la prueba, hago un require al archivo principal. De esta forma heredo los atributos desde los módulos hasta la prueba.

Ahora solo queda correr

npm test

y nos saldrá algo como esto:

Prueba finalizada con éxito

Como pueden observar todas las pruebas pasaron sin problemas y en la estructura de mi repositorio no tengo los archivos requeridos mas que como paquetes dentro de node_modules.

Mantenimiento
OK esta es la razón por la que has leído hasta acá: El mantenimiento de los paquetes.
Digamos que queremos modificar titleize, primero tenemos que cambiarnos de branch a la branch de titleize

git checkout titleize

Hacemos nuestros cambios y aqui viene lo importante: Hay que modificar las etiquetas. Las etiquetas en git no se pueden mover tan fácilmente, hay que eliminarlas y actualizarlas. Afortunadamente esto lo hace muy sencillo sourcetree.

Eliminando tags

Primero eliminamos la etiqueta en cuestión, luego la agregamos de nuevo en el último commit de nuestra branch. Y finalmente hacemos un push de la branch y la etiqueta.

Actualizando tags

Finalmente regresamos a master y corremos

npm update

En este caso no importan tanto las versiones porque estás jalando un tag de git.

LISTO, Ahora solo es cuestión de repetir estos pasos cuando sea necesario darles mantenimiento a tus paquetes.
Como punto final, recuerda siempre utilizar el ssh para los requerimientos de node, para de esa forma utilizar tus llaves de ssh y así poder traer de git los paquetes. En este demo, el repositorio es público, pero si fuera privado, la autenticación de usuario será bajo protocolo ssh!

Como siempre, si esto o algo más de mi blog te sirve, muestra tu agradecimiento invitándome un café o algo más en el botón que está en el menú de la derecha :D

Zero out