Babel: transpilado al pasado

El Proyecto

Hace unas semanas nos llegó un pequeño proyecto a desarrollar sobre Node.js. Era algo que no debería llevar más de unas 40 horas: una pequeña API con Express que realizaría operaciones con una base de datos en MongoDB. Además contaba con algo de lógica de negocio con media docena de entidades; la especificación iba un poco más allá de un simple CRUD, pero no tenía especial complicación.

Todo esto sin tener en cuenta el entorno donde se iba a ejecutar; es algo que el cliente ya tiene perfectamente montado y que no debería dar mayor problema. No se esperaba una alta concurrencia, y ya había otras aplicaciones funcionando.

Así pues el proyecto comenzó como estaba previsto. Teníamos todos los requisitos funcionales y el desarrollo se terminó dentro de esas 40 horas que teníamos estimadas.

Por simplificar de manera absurdamente extrema, digamos que nuestro proyecto podría reducirse (en cuanto a sintaxis Javascript), a algo así de simple:


class person {

    constructor(name, times = 0) {

        this.name = name;

        this.times = times;

    }

    greet() {

        this.times = this.times+1;

        return `hola soy ${this.name} (${this.times})`;

    }

}

const human1 = new person("PEPITO");

setTimeout(()=> console.log(human1.greet()),1000);

Resumiendo:

  • Definimos clases a la manera ES2015
  • Establecemos un valor por defecto para un argumento
  • Utilizamos alguna constante
  • Hacemos uso de arrow functions

Nuestro error fue no asegurarnos de la versión de Node.js sobre la que iba a correr la aplicación. Como siempre, no contar con toda la información desde cliente, frecuentemente termina mal de una u otra manera.

La aplicación tenía que desplegarse en una infraestructura existente y como digo, dimos por hecho que la versión de Node.js que íbamos a encontrarnos sería la LTS activa en este momento; es decir, la versión 6.x (Boron).

Pero no; una vez concluido el desarrollo y al tener acceso a las máquinas del cliente, vimos que la versión de Node.js en el servidor era la versión 4.x (Argon). Una versión que va a estar en mantenimiento hasta el 01/04/2018; es decir perfectamente escogido, válido y que no tenía ninguna discusión. (cagada)

Como era de esperar, por culpa gracias a la sintaxis ES2015 que habíamos utilizado, la aplicación no corría en la versión 4 de Node.js. Teníamos dos soluciones posibles:

  1. Adaptar el código a la versión 4 de Node.js.
    • No es algo demasiado complejo. Con “use strict”; la versión 4 de Node.js activa bastantes especificaciones de ES2015.
  2. Hacer que un proceso automatizado adapte el código “bien” escrito para que se ejecute en el entorno “viejo”.

La primera opción nos parecía algo así como rendirse; ir para atrás; nuestro código estaba escrito a lo ES2015… de cara a un futuro mantenimiento entendimos que la nueva sintaxis no hace más que simplificar ese mantenimiento, así como posibles mejoras… Lo ideal sería un proceso automático que hiciera ese trabajo por nosotros. Y en esto nos acordamos de Babel.


Babel al rescate

Babel es un compilador de propósito general para Javascript. Es un compilador denominado source-to-source, ya que recibe código javascript, analiza estáticamente ese código, y genera otro código Javascript que siendo otro código fuente distinto, se ejecuta de manera idéntica.
Es gracias a Babel que estamos escuchando el palabro “transpilar” cada vez más; aunque en castellano no existe, es un anglicismo que viene de “transpile”, y que viene a significar esa compilación desde código fuente a código fuente.

El objetivo final de Babel es tener un código fuente más claro y legible. Esto se traduce en menos bugs y en un menor y más sencillo mantenimiento. En definitiva, se traduce en tener software de mayor calidad, con un significativo ahorro de costes.

La idea de Babel es igualmente aplicable a navegadores tanto como a aplicaciones de servidor que corran sobre Node.js.

Babel 6

Babel nació con la intención de transformar código ECMAScript2015 , en código compatible con la mayoría de los navegadores; es decir transformar Javascript “moderno” a viejo. De hecho la librería se llamaba simplemente 6to5, como puede verse en éste su primer commit.

Entonces, en octubre de 2015, llegó la versión 6.0.0 de Babel. En esta versión se decidió romper con la línea que se había estado siguiendo hasta entonces, y convirtieron la herramienta en un sistema totalmente modular y basada 100% en plugins.
En esta versión, transpilar de ES6 a ES5 dejó de ser el propósito de la herramienta; esta funcionalidad ha pasado a ser un plugin más de los que Babel puede llegar a aplicar. Se ha convertido en una herramienta que realiza 3 acciones genéricas, que pasarán a concretarse en base a lo que el usuario especifique. De hecho Babel por si sólo, es incapaz de hacer nada.

  • Analizar: Se trata de examinar un código fuente y transformarlo en una serie de tokens representados en un árbol de sintaxis abstracta (AST).
  • Transformar: En función a los plugins que estemos utilizando, Babel realizará las operación de sustitución, modificación o eliminación de esos nodos en el AST.
  • Generar: Por último Babel se encarga de generar código Javascript a partir del nuevo árbol regenerado.

Para facilitar la experiencia de usuario de Babel, se han agrupado ciertos plugins en conjuntos de plugins, denominados presets, de manera que transformar a todas las funcionalidades de ES2016, ES2016 ó ES2017 puede hacerse mediante una única línea de configuración, en lugar de añadir el plugin necesario para cada una de las transformaciones.

Transpilación al pasado

Con la lección de Babel aprendida, teníamos el objetivo de coger nuestro código fuente y echarlo a andar en el entorno de ejecución de nuestro cliente. Vamos a describir qué pasos seguimos para hacer esa “transpilación al pasado” y pudimos desplegar la aplicación con sintaxis ES2015 en el entorno de ejecución del cliente:

Instalando Babel

Necesitamos que babel-cli esté disponible en nuestro proyecto como una dependencia de desarrollo:

npm install --save-dev babel-cli

De esta manera, ya tenemos disponible el compilador de babel de manera local en nuestro proyecto.
Ya que utilizar la ruta “./node_modules/babel-cli/bin/babel.js” es relativamente incómodo, es aconsejable crear un script en nuestro package.json; utilicemos el palabro “build”:

"scripts": {
/.. .../
"build": "babel index.js -d dist/"
},

Ahora ejecutando “npm run build”, tendremos todo nuestro proyecto transpilado en la carpeta “./dist” (que será lo que haya que ejecutar).
Si tomamos como ejemplo el ćodigo extremadamente reducido anterior y ejecutamos estos dos sencillos pasos, nos damos cuenta que el código resultante en “./dist/index.js” es exactamente igual que lo que teníamos…
La razón es muy simple: Babel sin configuración extra, omite el paso de transformar; coge el código fuente, y lo vuelve a regenerar tal como lo encontró.

Configurando babel

Existen varias formas de especificar a Babel que presets o plugins utilizar; vamos a optar por la forma recomendada desde la documentación oficial; crearemos un fichero “.babelrc” en nuestro directorio raíz. Al ejecutarse babel, buscará este fichero y aplicará los presets y/o plugins que encuentre definidos.

$ cat .babelrc
{
    “presets”:[],
    “plugins”:[]
}

Solo nos queda descubrir qué plugins necesitamos instalar y parametrizar para que nuestro código sea funcional.

preset-es2015

Basándonos en nuestro código fuente inicial, hacemos uso de concretamente estas funcionalidades de ECMAScript2015:

  • Clases
  • Funciones arrow (o flecha :O)
  • Parámetros por defecto en funciones
  • Constantes

Y basándonos en la tabla de compatibilidad de @kangax, vemos que la versión de Node.js que tiene nuestro cliente desplegada (Node.js 4), no tiene implementadas al 100% ninguna de esas funcionalidades.
Consultando la documentación de Babel, nos encontramos (a priori) con 2 opciones:

  1. Utilizar cada uno de los plugins que implementan las funcionalidad concreta; es decir, nuestro fichero “.babelrc” debería ser algo así:
    {
        "presets": [],
        "plugins": [
            "transform-es2015-arrow-functions",
            "transform-es2015-block-scoping",
            "transform-es2015-classes",
            "transform-es2015-parameters",
        ]
    }
    
  2. Utilizar el preset-es2015, de manera que se apliquen todos los plugins que transforman desde ES2015; Nuestro “.babelrc” sería mucho más simple: pinta:
    {
        "presets": [
            "es2015"
        ],
        "plugins": []
    }
    

Para el ejemplo de código anterior, ambas configuraciones tienen el mismo resultado. El código que se genera, aunque se trate de Javascript, debe ser tratado como algo “procesado”; No es buena idea realizar modificaciones directamente sobre ese código:

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) 
defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var person = function () {
function person(name) {
var times = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
_classCallCheck(this, person);
this.name = name;
this.times = times;
}
_createClass(person, [{
key: "greet",
value: function greet() {
this.times = this.times + 1;
return "hola soy " + this.name + " (" + this.times + ")";
}
}]);
return person;
}();
var human1 = new person("PEPITO");
setTimeout(function () {
return console.log(human1.greet());
}, 1000);

La segunda opción, incluir todo el preset para ES2015, es sin duda la mejor opción de las dos. Ante cualquier posible modificación en el software, en la que por ejemplo utilizáramos una simple de-estructuración, nos obligaría a buscar el plugin, e instalarlo antes de volver a compilar nuestro software.


Posibles futuros problemas:

  • ¿Qué pasa si en una siguiente revisión del proyecto, nos solicitan una funcionalidad que nos hace utilizar await-async (esa super feature que está incluída en ES2017)?
  • ¿Qué hacemos si el 1 de abril de 2018, el cliente decide migrar su entorno a Node.js versión 6?Aunque nuestro código transpilado siguiera funcionando en esa versión, ¿no sería lógico que no transpilemos lo que sí que ya vaya a funcionar de serie en la nueva versión?(en este caso, ES2015 estaría soportado al 97%)
  • ¿Qué hacemos si queremos utilizar el mismo software, en otro entorno esta vez con Node.js versión 0.12? (porque sí:: ahí fuera hay nodes 0.12 en producción).

preset-env

Y entonces descubrimos el simple pero poderosos preset-env. El preset que todo lo transpila; Básicamente es como instalar y parametrizar preset-es2015, preset-es2016 y preset-es2017 pero con un único comando:

npm i -D babel-preset-env

Y configurando así de fácil nuestro “.babelrc”:

{
    "presets": [
        "env"
    ],
    "plugins": []
}

Pero lo potente de este preset es que tiene cierta (bastante!) inteligencia. Puede ser configurado para un entorno de ejecución específico: No es lo mismo transpilar para la última versión de Google Chrome, que para IE9. Aunque si necesitamos que nuestro código funcione en ambos entornos, entonces habrá que hacer que preocuparse sobre todo que funcione en IE9.

Para poder configurar los navegadores “destino” para los que se transpila tu aplicación, se hace uso de la api de browserl.ist. Esta API permite consultar en tiempo real (que en nuestro caso será tiempo de tanspilación), un listado de navegadores en base a ciertos parámetros: versiones, porcentaje de uso, porcentaje de uso por país; por ejemplo:

De esta manera, nos podemos “asegurar” de manera bastante automática, que nuestro código se transpila solo lo justo, para según qué entornos necesitemos.

Para aplicaciones de Node.js, la cosa se simplifica bastante; simplemente habría que añadir la versión de Node.js “mínima” en la que queremos ejecutar nuestro código. En nuestro caso, el fichero “.babelrc” podría quedarse con esta configuración, y todo funcionaría correctamente.

{
    "presets": [
        ["env", {
            "targets": {
                "node": 4
            }
        }]
    ],
    "plugins": []
}

El problema está en que de esta forma, nuestro proyecto estará siempre configurado para Node.js 4; Incluso cuando se fuera a ejecutar en una versión 6 ó 7, o incluso si tuviera que ejecutarse en una versión 0.12.

Para evitar esto, preset-env, ha previsto el valor «current» ó true, de manera que Babel comprobará en qué entorno está transpilando, de manera que aplicará únicamente los plugins necesarios para que la sintaxis sea correcta:

{
    "presets": [
        ["env", {
            "targets": {
                "node": "current"
            }
        }]
    ],
    "plugins": []
}

Así pues, para nuestra aplicación, si ejecutamos npm run build en un entorno con Node.js 6, el código resultante será idéntico al código original ya que toda la sintaxis utilizada está soportada completamente.

Conclusiones

Este artículo ha pretendido ser una pequeña introducción a Babel y de cómo en este caso nos salvó de un pequeño gran error, que pudo haber supuesto una desviación en un proyecto real.

Hemos omitido un montón de funcionalidades de Babel, entre otras:

  • Tiene una API, de manera que se puede interactuar directamente desde nuestro código.
  • Todas las herramientas modernas de bundling de hoy en día tiene integraciones perfectas e invisibles con babel: webpack, rollup, browserify, gulp
  • babel” es un minificador que funciona para ES2015, y que puede integrarse con nuestro proceso como un plugin más.

Babel es una herramienta muy potente y útil para cualquier desarrollador Javascript hoy en día. Merece la pena darse un vuelta por su documentación, y entender de qué manera está integrado en nuestra “toolchain” actual.



¿Te gusta este post? Es solo un ejemplo de cómo podemos ayudar a tu empresa...
Sobre Javier Infante Porro

Linux, HTTP, JS, TS, PHP... Programo (entre otras cosas) en Irontec desde hace más de 12 ó 13 años... @jabiinfante

Queremos tu opinión :)