¿Qué es 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 string. Puedes 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.
Queremos tu opinión :)