Estructurando Generadores de Yeoman

¿Qué es Yeoman?

Yeoman

Yeoman es un sistema de generación de proyectos escrito en JavaScript (Node.js) que permite agilizar muchísimo el inicio de nuevas aplicaciones. Esta compuesto de un módulo de Node.js sobre el que construir los generadores y una herramienta de línea de comandos para ejecutarlos, todo empaquetado en un único módulo en npm bajo el nombre de ‘yo’. Los generadores se instalan también a través de npm, lo cual permite que se genere un gran archivo con publicaciones open source tanto individuales como de colectivos.

Hace un tiempo, di un curso interno para Irontec sobre la creación de generadores para Yeoman. Las slides están publicadas en GitHub.

En ellas, están resumidos todos los fundamentos necesarios para comenzar a crear un generador.

Basándome en esas mismas slides, hoy, mostraré cómo estructurar un generador de Yeoman de una forma fácil y legible.

 

Instalación

Comencemos instalando las herramientas necesarias (es necesario haber instalado previamente Node.js)

#Instalamos Yeoman y el boilerplate de generadores
npm install -g yo generator-generator
#Generamos un nuevo proyecto de generador
yo generator

Esto nos creará un nuevo proyecto con la siguiente estructura de archivos

  • generators

Carpeta contenedora con los generadores. Un generador de Yeoman, puede incluir a su vez un número ilimitado de subgeneradores. Por defecto, lanzará el ubicado en el directorio app.

Para especificar un subgenerador concreto, la sintaxis será:

yo mi-generador:subgenerador
  • generators/app/

Generador por defecto

  • node_modules/

Dependencias del generador

  • package.json

Fichero de definición del paquete

  • README.md

Aquí explicaremos cómo usar nuestro generador

  • test/

Directorio de tests

 

Estructura de cada generador

En el boilerpate del generador,  está todo reducido a un index.js y un directorio templates.

Esta estructura de un sólo archivo puede llegar a resultar en código difícilmente legible en generadores muy grandes.

Para evitar eso, dividiremos el generador en base a sus funciones:

index.js

Definición del generador

prompts/

Directorio donde programaremos la lógica de preguntas y respuestas

writes/

Directorio donde programaremos la lógica de escritura de archivos  del proyecto

install/ (opcional)

El paso de instalacíon no será necesario separarlo del index.js ya que en la mayoría de las ocasiones se reduce a un simple comando

 

Código fuente

Para este ejemplo, he creado un generador de proyectos con AngularJS + JSPM basado en este tutorial. Las templates las publicaré, junto con el resto del código en un repositorio de github.

Una vez dicho esto, podemos proceder a inspeccionar el código final del generador:

index.js

En el index.js solamente  invocamos al resto de módulos.

'use strict';

var yeoman = require('yeoman-generator');

var prompts = require('./prompts');
var writes = require('./writes');

module.exports = yeoman.generators.Base.extend({

  // Ask the user
  prompting: function () {
    prompts.apply(this);
  },

  // Write files
  writing: function() {
    writes.project.apply(this);
    writes.app.apply(this);
  },

  // Install dependencies
  install: function () {
    this.npmInstall();
  }

});

prompts/index.js

En la fase de prompts, saludaremos al usuario con yosay y cargaremos los prompts.

Tenemos una función GenerateProjectName para crear distintas variables que usaremos más adelante para definir el nombre del proyecto. Para ello, hemos utilizado un módulo de node llamado stringPuedes instalarlo con un sólo comando.

npm install --save string
'use strict';

var yosay = require('yosay');
var chalk = require('chalk');
var S = require('string');

var prompts = require('./prompts');

function generateProjectName(name) {
    var projectName = {
        original: name,
        dasherized: S(name).dasherize().s,
        camelized: S(name).camelize().s,
    };

    projectName.camelized = (
        projectName.camelized.charAt(0).toLowerCase() +
        projectName.camelized.slice(1)
    );

    if ( S(projectName.dasherized).startsWith('-') ) {
        projectName.dasherized = S(projectName.dasherized).chompLeft('-').s;
    }

    return projectName;
}

module.exports = function() {

  var done = this.async();

  // Have Yeoman greet the user.
  this.log(yosay(
    'Welcome to the priceless ' + chalk.red('ngbabel (Angular + Babel)') + ' generator!'
  ));

  this.prompt(prompts, function (props) {
    this.props = props;
    this.props.projectName = generateProjectName(props.projectName);
    this.props.projectDescription = this.props.projectDescription ||
                                    this.props.projectName.original;

    done();
  }.bind(this));

}

prompts/prompts.json

Hemos separado los prompts a un fichero json para tenerlos más accesibles

[
  {
    "type": "input",
    "name": "projectName",
    "message": "What is your project name?",
    "default": "My App"
  },
  {
    "type": "input",
    "name": "projectDescription",
    "message": "Insert your project description (Optional)",
    "default": null
  },
  {
    "type": "input",
    "name": "username",
    "message": "What is your Github Username? (Optional)",
    "default": null
  }
]

writes/index.js

En el módulo de escritura  incluimos archivos del proyecto y archivos de la aplicación

'use strict';

module.exports = {
    app: require('./app'),
    project: require('./project')
};

writes/app.js

En este archivo programaremos todas las tareas destinadas a generar archivos de la aplicación.

'use strict';

function writeApplicationConfig() {

  this.fs.copy(
    this.templatePath('_config.js'),
    this.destinationPath('config.js')
  );

}

function writeApplicationFiles() {

  this.fs.copyTpl(
    this.templatePath('_index.html'),
    this.destinationPath('index.html'),
    {
      title: this.props.projectName.original,
      ngApp: this.props.projectName.camelized
    }
  );

  this.fs.copyTpl(
    this.templatePath('app/app.js'),
    this.destinationPath('app/app.js'),
    {
      ngApp: this.props.projectName.camelized
    }
  );

  this.fs.copy(
    this.templatePath('app/helpers/registerModule.js'),
    this.destinationPath('app/helpers/registerModule.js')
  );

}

function writeUserModule() {

  this.fs.copyTpl(
    this.templatePath('app/user/module.js'),
    this.destinationPath('app/user/module.js'),
    {
      ngApp: this.props.projectName.camelized
    }
  );

  this.fs.copy(
    this.templatePath('app/user/controller.js'),
    this.destinationPath('app/user/controller.js')
  );

  this.fs.copy(
    this.templatePath('app/user/service.js'),
    this.destinationPath('app/user/service.js')
  );

}

module.exports = function() {

  writeApplicationConfig.apply(this);
  writeApplicationFiles.apply(this);
  writeUserModule.apply(this);

}

writes/project.js

Similar a writes/app.js pero para los archivos de proyecto (package, gulpfile, editorconfig…)

'use strict';

function writePackageFiles() {

  this.fs.copyTpl(
    this.templatePath('project/_package.json'),
    this.destinationPath('package.json'),
    {
      packageName: this.props.projectName.dasherized,
      packageDescription: this.props.projectDescription,
      username: this.props.username
    }
  );

}

function writeProjectTasks() {

    this.fs.copy(
        this.templatePath('project/_gulpfile.js'),
        this.destinationPath('gulpfile.js')
    );

}

function writeProjectDefaults() {

  this.fs.copyTpl(
    this.templatePath('project/readme.md'),
    this.destinationPath('README.md'),
    {
      projectName: this.props.projectName.original
    }
  );

  this.fs.copy(
    this.templatePath('project/readme.md'),
    this.destinationPath('README.md')
  );

  this.fs.copy(
    this.templatePath('project/editorconfig'),
    this.destinationPath('.editorconfig')
  );

  this.fs.copy(
    this.templatePath('project/jshintrc'),
    this.destinationPath('.jshintrc')
  );

}

module.exports = function(){

  writeProjectDefaults.apply(this);
  writePackageFiles.apply(this);
  writeProjectTasks.apply(this);

};

Como se puede apreciar, todo queda mucho más claro y fácilmente modificable, ya que tenemos bien diferenciados los distintos procesos del mismo. De esta forma, tenemos aislado el hilo del diálogo, primera acción a ejecutar por el generador, y la creación del boilerplate en base al resultado del mismo. Estos nuevos archivos, a su vez, podremos descomponerlos en «módulos» más pequeños conforme nuestra aplicación vaya creciendo.

 

Enlaces de Interés

* El código fuente usado para este ejemplo está hosteado en GitHub.

* Getting Started

* Documentación de los generadores



¿Te gusta este post? Es solo un ejemplo de cómo podemos ayudar a tu empresa...
Sobre Aitor Llamas Jiménez

Aitor Llamas Jiménez. Desarrollador Multiplataforma en Irontec SL. Apasionado de JavaScript, la tecnología en general, los videojuegos y la literatura fantástica.

Queremos tu opinión :)