Ni grunt, ni gulp: solo npm

Reconozco que los scripts de compilación son una de esas cosas que me entretienen mucho.

Le doy mucha importancia a automatizar todos los procesos que me aburren y no me importa invertir tiempo en desarrollar scripts o pequeñas aplicaciones que me faciliten mi trabajo diario.

Dentro del mundo Javascript y todas las herramientas basadas en node para front end, las opciones más habituales para automatizar tareas son grunt y gulp de los que ya hemos hablado unas cuantas veces por aquí, pero últimamente cada vez encuentro más proyectos con un enfoque más minimalista basado en scripts de npm.

Los scripts de npm

Si estás utilizando npm como gestor de paquetes para instalar herramientas y librerías (cosa que deberías hacer incluso si sólo haces desarrollo front end, y más ahora que bower tiene un futuro incierto), habrás notado que tienes un fichero llamado package.json en la raíz de tu proyecto que contiene un documento JSON con información general, dependencias, y una propiedad scripts.

Dentro de esa propiedad podemos añadir comandos para ser ejecutados con npm run comando. Por ejemplo, en un alarde de originalidad podemos añadir un comando para saludar:

// fichero package.json
{
  // ... unas cuantas cosas por aquí

  "scripts": {
    "hello": "echo \"Hello World!\""
  }

  // ... y más por aquí
}

Y ejecutarlo:

$ npm run hello

> sample@0.0.0 hello /home/gulnor/dev/sample/
> echo "Hello World!"

Hello World!

Los comandos deben ser secuencias de comandos válidas para el shell en que estamos ejecutando npm, es decir, son dependientes del sistema operativo (y ésta es una de sus mayores pegas). Además, npm se encarga de añadir al path la carpeta ./node_modules/.bin, donde se encuentran los «binarios» de los paquetes que hayamos instalado localmente, por lo que nos facilitará la vida a la hora de invocar herramientas como mocha o broserify sin necesidad de instalarlas globalmente.

Algunos de estos comandos son estándar, como start, stop o test, y pueden ser invocados directamente con npm start/stop/test, sin necesidad de utilizar run. Esto además permite integrar fácilmente los procesos de compilación y testing con otras herramientas, como servidores de integración continua, que utilizan estos comando estándar para lanzar sus operaciones.

Cuando se ejecuta un comando, npm busca automáticamente scripts con el nombre precommand y postcommand para ejecutarlos automáticamente. En ejemplo anterior, podríamos tener:

{
  "scripts": {
    "prehello": "echo \"Here we go!\"",
    "hello": "echo \"Hello World!\"",
    "posthello": "echo \"Done!\""
  }

  // ... y más por aquí
}

Y al ejecutarlo tendríamos:

$ npm run hello

> sample@0.0.0 prehello /home/gulnor/dev/sample/
> echo "Here we go!"

Here we go!

> sample@0.0.0 hello /home/gulnor/dev/sample/
> echo "Hello World!"

Hello World!

> sample@0.0.0 posthello /home/gulnor/dev/sample/
> echo "Done!"

Done!

No existe el concepto de dependencias entre tareas que hay en herramientas más completas como grunt o gulp, pero podemos invocar varias tareas en un mismo comando aprovechando las opciones del shell que hay por debajo:

{
  "scripts": {
    "browserify": "...",
    "uglify": "...",
    "build": "npm run browserify && npm run uglify"
  }
}

También podemos declarar (con algunas limitaciones, eso sí) variables dentro del fichero package.json que nos permitan seleccionar la forma en que se ejecutan los scripts mediante argumentos de línea de comandos para trabajar con distintos entornos o configuraciones.

Un ejemplo fácil

Para hacernos una idea mejor de cómo sería esto en el mundo real (bueno, en un mundo real muy simplificado), vamos a reescribir el mismo sistema de compilación que usé al explicar cómo utilizar ReactJS con Browserify. Podéis consultar la versión con grunt y la versión con gulp.

Se trata de un script muy básico que sólo tiene dos funcionalidades:

  • Compilar la aplicación, partiendo del fichero ./lib/index.js y usando browserify y reactify para generar un único fichero en ./dist/app.js con la aplicación completa.
  • Monitorizar cambios en la carpeta ./lib/ para lanzar automáticamente la compilación cada vez que se guarda un fichero mientras estamos desarrollando.

Esto nos lleva a tener dos scripts dentro de package.json uno para compilar y otro para monitorizar. Por supuesto, antes necesitaremos instalar las librerías adecuadas, que podéis hacer como es habitual con npm install --save-dev librería.

Veamos cómo queda el comando para compilar:

{
  "scripts": {
    "build": "browserify lib/index.js -o dist/app.js -t [ babelify --presets [ es2015 react ] ]"
  } 
}

Está sacado directamente de la documentación de babelify y no es más que la invocación por línea de comandos de browserify usando babelify para transformar el código JSX (recordad que ReactJS ha descontinuado las react-tools).

Para añadir la parte de monitorización, podemos recurrir a varias librerías pero en este caso vamos a usar nuestro viejo amigo nodemon. Añadiendo el nuevo comando, nos quedaría:

{
  "scripts": {
    "build": "browserify lib/index.js -o dist/app.js -t [ babelify --presets [ es2015 react ] ]",
    "dev" : "nodemon --watch lib --exec npm run build"
  } 
}

Nuevamente nos limitamos a invocar nodemon por línea de comandos tal y cómo nos indica su documentación.

Con esto ya tendríamos listo nuestro script de compilación cubriendo las dos funcionalidades que teníamos previsto y que podemos ejecutar con:

npm run build ← compilar la aplicación una vez
npm run dev ← monitorizar cambios y recompilar en cada cambio

Lo bueno y lo malo

Viendo esto ya podemos empezar a hacernos una idea de las diferencias que vamos a encontrar si montamos nuestro sistema de compilación con npm en lugar de con gulp o grunt.

En la parte buena, ganamos simplicidad.

Las 2 líneas de código que hemos usado para montar la compilación con npm se convierten en unas 20 con gulp y 30 con grunt.

Además, nos evitamos depender de más herramientas, lo que supone una ventaja importante en el mundo javascript donde salen versiones nuevas cada día y asegurar la compatibilidad entre ellas no siempre es fácil. Como estamos usando directamente las herramientas (browserify, babel, etc.) sin intermediarios, no necesitamos que el plugin de turno de gulp y, sobre todo, de grunt, esté actualizado para poder emplear la última versión.

La integración de nuestro script con otros sistemas es más sencillo gracias a que podemos exponer hacia el exterior un interface estándar con npm start, npm stop, npm test y compañía.

En el lado malo, perdemos flexibilidad.

Ya no tenemos toda la versatilidad de javascript como en gulp y grunt para implementar las tareas, sino que tenemos que limitarnos a lo que nos ofrezca el shell que estemos usando. Eso además hace que podamos tener problemas si ejecutamos el script en varias plataformas ya que el shell no es idéntico (por ejemplo no se borra igual un directorio en Windows que en Linux). Esto podemos solventarlo implementando las partes que necesitemos como scripts de node e invocándolos desde npm, pero ya empieza a ser menos cómodo.

No podemos definir de forma declarativa la dependencia entre tareas. Para un script tan simple como el del ejemplo da igual, pero si empiezas a tener muchas tareas que dependen unas de otras y varios puntos de entrada en el sistema de compilación (uno para desarrollo, otro para integración continua, otro para tests, etc.), es difícil de gestionar.

Puede parecer poco importante, pero la salida en consola es mucho menos legible que con grunt o gulp. Cuando empiezas a tener un script que hace bastantes cosas y va mostrando mensajes con información sobre la compilación, los tests que se han ejecutado, los fallos encontrados, etc., la salida de npm es fea y cuesta parsearla de un vistazo.

Resumen

La opción de lanzar la compilación con npm me ha parecido interesante. Pese a estar mucho más limitada que los sistemas completos de ejecución de tareas como grunt o gulp, para proyectos que no requieren mucha complicación es muy sencilla de montar y te permite llegar bastante lejos.

A la hora de elegir un sistema u otro, ten en cuenta las ventajas e inconvenientes de cada uno pero tampoco sufras demasiado: al final todos hacen más o menos lo mismo y te pueden servir. Seguramente pese más lo cómodo que te sientas trabajando con ellos que las funcionalidades que te ofrecen.

Lo que sí es importante es que utilices alguno. Si estás lanzando manualmente tareas para compilar tu aplicación javascript, estás perdiendo el tiempo. Y ojo, que hacerlo desde un IDE pulsando botones secuencialmente cuenta como hacerlo manualmente.

Un comentario en “Ni grunt, ni gulp: solo npm

  1. Esta muy bien el punto de vista, creo que si solo necesitas levantar un server o tareas muy muy sencillas no sera necesario Gulp o Grunt, pero para automatizar todo el proceso de desarrollo, optimizar recursos, entre otras cosas, lo mejor es usarlos.

    quiza jspm(http://jspm.io/) sustituya a bower.

Comentarios cerrados.