albatrosary's blog

UI/UXとエンタープライズシステム

Yeoman を覗いてみる

インストールしたYeomanさんの中身を覗いてみます。github を読めばいいことですが、インストールディレクトリを読みたいと思います。 インストールした yo、bower、grunt(grunt-cli) は

$ pwd
/usr/local/lib/node_modules
$ ls -la
total 0
drwxr-xr-x   6 root    wheel  204  4 25 11:51 .
drwxr-xr-x   6 root    wheel  204  4 25 11:25 ..
drwxr-xr-x  15 nobody  staff  510  4 25 11:51 bower
drwxr-xr-x  13 nobody  staff  442  4 25 11:51 grunt-cli
drwxr-xr-x  17 root    wheel  578  4 25 11:25 npm
drwxr-xr-x   7 nobody  staff  238  4 25 11:46 yo
$

に配置されています。node_modules となっている通り node が require でモジュールをサーチするディレクトリです(詳しくは node.js をご参照願います)。検索順について node.js を引用します。

Loading from `node_modules` Folders If the module identifier passed to require() is not a native module, and does not begin with '/', '../', or './', then node starts at the parent directory of the current module, and adds /node_modules, and attempts to load the module from that location. If it is not found there, then it moves to the parent directory, and so on, until either the module is found, or the root of the tree is reached.

node を起動し module path を調べてみます。/usr/local/lib/node_modules ディレクトリが node の検索ディレクトリになっていることが分かると思います。

$ node -v
v0.10.5
$ node
> module.paths
[ '/usr/local/lib/node_modules/repl/node_modules',
  '/usr/local/lib/node_modules',
  '/usr/local/node_modules',
  '/usr/node_modules',
  '/node_modules' ]
> 

 

まず初めに npm ですが、これは Yeoman をインストールする事前準備としてインストールした Node.js のパッケージ管理(Node Packaged Modules)です。Linux の rpm にあたるものです。

 

続いて yo ディレクトリを見ます。

$ cd yo
$ ls -la
total 24
drwxr-xr-x   7 nobody  staff   238  4 25 11:46 .
drwxr-xr-x   6 root    wheel   204  4 25 11:51 ..
-rw-r--r--   1 nobody  staff    48  4  9 04:16 .travis.yml
drwxr-xr-x   4 nobody  staff   136  4 25 11:45 bin
drwxr-xr-x  11 nobody  staff   374  4 25 11:46 node_modules
-rw-r--r--   1 nobody  staff  1609  4 25 11:45 package.json
-rw-r--r--   1 nobody  staff   458  2 11 22:39 readme.md

readme.md はマークダウンファイルです。(git を使われている方はご存知と思いますが)markdown 記法で書かれたテキストファイルを、HTMLマークアップせずに HTML に変換して表示してくれるものです。markdown 記法についてはここを参照してください。markdown 記法の日本語訳は http://blog.2310.net/archives/6 に記載があります。

package.json があります。このファイルは node に対し、プログラムとライブラリをディレクトリ内にまとめエントリポイントを示すためのものです。中を見ると yo に対する様々な情報が記載されています。

$ cat package.json
{
  "name": "yo",
  "version": "1.0.0-beta.4",
  "description": "CLI tool for scaffolding out Yeoman projects",
  "keywords": [
    "front-end",
    "development",
    "dev",
    "build",
    "web",
    "tool",
    "cli",
    "scaffold",
    "stack"
  ],
  "homepage": "http://yeoman.io",
  "bugs": "https://github.com/yeoman/yo/issues",
  "author": {
    "name": "Chrome Developer Relations"
  },
  "bin": {
    "yo": "bin/yo"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/yeoman/yo.git"
  },
  "scripts": {
    "test": "mocha test/test.js"
  },
  "dependencies": {
    "yeoman-generator": "~0.10.4",
    "generator-webapp": "~0.1.6",
    "generator-mocha": "~0.1.1",
    "colors": "~0.6.0",
    "nopt": "~2.1.1",
    "lodash": "~1.1.1",
    "update-notifier": "~0.1.3",
    "insight": "~0.1.0"
  },
  "devDependencies": {
    "mocha": "~1.9.0",
    "ronn": "~0.4.0",
    "grunt": "~0.4.1"
  },
  "engines": {
    "node": ">=0.8.0"
  },
  "preferGlobal": true,
  "licenses": [
    {
      "type": "BSD"
    }
  ],
  "readme": "# Yo [![Build Status](https://secure.travis-ci.org/yeoman/yo.png?branch=master)](http://travis-ci.org/yeoman/yo)\n\nCLI tool for scaffolding out [Yeoman](https://github.com/yeoman/yeoman) projects\n\n## [Documentation](https://github.com/yeoman/yeoman/wiki)\n\n\n## Contribute\n\nSee the [contributing docs](https://github.com/yeoman/yeoman/blob/master/contributing.md)\n\n\n## License\n\n[BSD license](http://opensource.org/licenses/bsd-license.php) and copyright Google\n",
  "readmeFilename": "readme.md",
  "_id": "yo@1.0.0-beta.4",
  "_from": "yo@"
}
$

yo ディレクトリの中にある bin に yo 実行ファイルが登録されています。

$ pwd
/usr/local/lib/node_modules/yo/bin
$ ls -la
total 56
drwxr-xr-x  4 nobody  staff    136  4 25 11:45 .
drwxr-xr-x  7 nobody  staff    238  4 25 11:46 ..
-rw-r--r--  1 nobody  staff  24134  4  9 04:16 help.txt
-rwxr-xr-x  1 nobody  staff   2793  4  9 04:16 yo
$ 
$ pwd
/usr/local/lib/node_modules/yo/node_modules
$ ls -la
total 0
drwxr-xr-x  11 nobody  staff  374  4 25 11:46 .
drwxr-xr-x   7 nobody  staff  238  4 25 11:46 ..
drwxr-xr-x   4 root    staff  136  4 25 11:46 .bin
drwxr-xr-x   9 nobody  staff  306  4 25 11:46 colors
drwxr-xr-x  12 nobody  staff  408  4 25 11:46 generator-mocha
drwxr-xr-x   6 nobody  staff  204  4 25 11:46 generator-webapp
drwxr-xr-x   7 nobody  staff  238  4 25 11:46 insight
drwxr-xr-x  14 nobody  staff  476  4 25 11:46 lodash
drwxr-xr-x  10 nobody  staff  340  4 25 11:46 nopt
drwxr-xr-x   7 nobody  staff  238  4 25 11:46 update-notifier
drwxr-xr-x  11 nobody  staff  374  4 25 11:46 yeoman-generator
$ 

generator-mocha や generator-webapp はデフォルトのひな形です。generator については http://yeoman.io/generators.html を読むことをお勧めします(日本語訳に関してはここを読んで下さい)。Yeoman には二つのタイプのテンプレートがあり boilerplate copiers と application scaffolders です。boilerplate copiers は yo が boilerplate project を受け取ると、アプリケーションディレクトリを呼び出した可憐とディレクトリにファイルをコピーするタイプです。application scaffolders は システム、サブジェネレータ、依存関係の管理と自動化するワークフローなど多くの機能を構築することができるタイプです。

generator は Yeoman が与えられているものを使用するだけではなく、自分達でも作成可能です。作り方については次回にします。参考までに generator-webapp の中にある USAGE や index.js を確認します。

$ pwd /usr/local/lib/node_modules/yo/node_modules/generator-webapp
$ ls -la
total 16
drwxr-xr-x  6 nobody staff  204 4 25 11:46 .
drwxr-xr-x 11 nobody staff  374 4 25 11:46 .. 
drwxr-xr-x  5 nobody staff  170 4 25 11:46 app
drwxr-xr-x  3 nobody staff  102 4 25 11:46 node_modules 
-rw-r--r--  1 nobody staff 1880 4 25 11:46 package.json 
-rw-r--r--  1 nobody staff  659 4 11 07:08 readme.md 
$
$ pwd
/usr/local/lib/node_modules/yo/node_modules/generator-webapp/app
$ ls -la
total 32
drwxr-xr-x   5 nobody  staff   170  4 25 11:46 .
drwxr-xr-x   6 nobody  staff   204  4 25 11:46 ..
-rw-r--r--   1 nobody  staff   504  3  9 22:15 USAGE
-rw-r--r--   1 nobody  staff  9112  4 11 07:08 index.js
drwxr-xr-x  18 nobody  staff   612  4 25 11:46 templates
$ 
$ cat USAGE 
Description:
    Creates a new basic front-end web application.

Options:
    Twitter Bootstrap: Include Twitter Bootstrap for Sass
    RequireJS: Add support for AMD-loading via RequireJS

Example:
    yo webapp

    This will create:
        Gruntfile.js: Configuration for the task runner.
        component.json: Front-end packages installed by bower.
        package.json: Development packages installed by npm.

        app/: Your application files.
        test/: Unit tests for your application.
$ 
$ cat index.js 
'use strict';
var util = require('util');
var path = require('path');
var spawn = require('child_process').spawn;
var yeoman = require('yeoman-generator');
var win32 = process.platform === 'win32';


var AppGenerator = module.exports = function Appgenerator(args, options, config) {
  yeoman.generators.Base.apply(this, arguments);

  // setup the test-framework property, Gruntfile template will need this
  this.testFramework = options['test-framework'] || 'mocha';

  // for hooks to resolve on mocha by default
  if (!options['test-framework']) {
    options['test-framework'] = 'mocha';
  }

  // resolved to mocha by default (could be switched to jasmine for instance)
  this.hookFor('test-framework', { as: 'app' });

  this.indexFile = this.readFileAsString(path.join(this.sourceRoot(), 'index.html'));
  this.mainJsFile = '';
  this.mainCoffeeFile = 'console.log "\'Allo from CoffeeScript!"';

  this.on('end', function () {
    if (options['skip-install']) {
      console.log('\n\nI\'m all done. Just run ' + 'npm install & bower install --dev'.bold.yellow + ' to install the required dependencies.\n\n');
    } else {
      console.log('\n\nI\'m all done. Running ' + 'npm install & bower install'.bold.yellow + ' for you to install the required dependencies. If this fails, try running the command yourself.\n\n');
      spawn(win32 ? 'cmd' : 'npm', [win32 ? '/c npm install' : 'install'], { stdio: 'inherit' });
      spawn(win32 ? 'cmd' : 'bower', [win32 ? '/c bower install' : 'install'], { stdio: 'inherit' });
    }
  });

  this.pkg = JSON.parse(this.readFileAsString(path.join(__dirname, '../package.json')));
};

util.inherits(AppGenerator, yeoman.generators.NamedBase);

AppGenerator.prototype.askFor = function askFor() {
  var cb = this.async();

  // welcome message
  var welcome =
  '\n     _-----_' +
  '\n    |       |' +
  '\n    |'+'--(o)--'.red+'|   .--------------------------.' +
  '\n   `---------´  |    '+'Welcome to Yeoman,'.yellow.bold+'    |' +
  '\n    '+'( '.yellow+'_'+'´U`'.yellow+'_'+' )'.yellow+'   |   '+'ladies and gentlemen!'.yellow.bold+'  |' +
  '\n    /___A___\\   \'__________________________\'' +
  '\n     |  ~  |'.yellow +
  '\n   __'+'\'.___.\''.yellow+'__' +
  '\n ´   '+'`  |'.red+'° '+'´ Y'.red+' `\n';

  console.log(welcome);
  console.log('Out of the box I include HTML5 Boilerplate, jQuery and Modernizr.');

  var prompts = [{
    name: 'compassBootstrap',
    message: 'Would you like to include Twitter Bootstrap for Sass?',
    default: 'Y/n',
    warning: 'Yes: All Twitter Bootstrap files will be placed into the styles directory.'
  },
  {
    name: 'includeRequireJS',
    message: 'Would you like to include RequireJS (for AMD support)?',
    default: 'Y/n',
    warning: 'Yes: RequireJS will be placed into the JavaScript vendor directory.'
  }];

  this.prompt(prompts, function (err, props) {
    if (err) {
      return this.emit('error', err);
    }

    // manually deal with the response, get back and store the results.
    // we change a bit this way of doing to automatically do this in the self.prompt() method.
    this.compassBootstrap = (/y/i).test(props.compassBootstrap);
    this.includeRequireJS = (/y/i).test(props.includeRequireJS);

    cb();
  }.bind(this));
};

AppGenerator.prototype.gruntfile = function gruntfile() {
  this.template('Gruntfile.js');
};

AppGenerator.prototype.packageJSON = function packageJSON() {
  this.template('_package.json', 'package.json');
};

AppGenerator.prototype.git = function git() {
  this.copy('gitignore', '.gitignore');
  this.copy('gitattributes', '.gitattributes');
};

AppGenerator.prototype.bower = function bower() {
  this.copy('bowerrc', '.bowerrc');
  this.copy('_component.json', 'component.json');
};

AppGenerator.prototype.jshint = function jshint() {
  this.copy('jshintrc', '.jshintrc');
};

AppGenerator.prototype.editorConfig = function editorConfig() {
  this.copy('editorconfig', '.editorconfig');
};

AppGenerator.prototype.h5bp = function h5bp() {
  this.copy('favicon.ico', 'app/favicon.ico');
  this.copy('404.html', 'app/404.html');
  this.copy('robots.txt', 'app/robots.txt');
  this.copy('htaccess', 'app/.htaccess');
};

AppGenerator.prototype.bootstrapImg = function bootstrapImg() {
  if (this.compassBootstrap) {
    this.copy('glyphicons-halflings.png', 'app/images/glyphicons-halflings.png');
    this.copy('glyphicons-halflings-white.png', 'app/images/glyphicons-halflings-white.png');
  }
};

AppGenerator.prototype.bootstrapJs = function bootstrapJs() {
  // TODO: create a Bower component for this
  if (this.includeRequireJS) {
    this.copy('bootstrap.js', 'app/scripts/vendor/bootstrap.js');
  }
};

AppGenerator.prototype.mainStylesheet = function mainStylesheet() {
  if (this.compassBootstrap) {
    this.write('app/styles/main.scss', '$iconSpritePath: "../images/glyphicons-halflings.png";\n$iconWhiteSpritePath: "../images/glyphicons-halflings-white.png";\n\n@import \'sass-bootstrap/lib/bootstrap\';\n\n.hero-unit {\n    margin: 50px auto 0 auto;\n    width: 300px;\n}');
  } else {
    this.write('app/styles/main.css', 'body {\n    background: #fafafa;\n}\n\n.hero-unit {\n    margin: 50px auto 0 auto;\n    width: 300px;\n}');
  }
};

AppGenerator.prototype.writeIndex = function writeIndex() {
  // prepare default content text
  var defaults = ['HTML5 Boilerplate', 'Twitter Bootstrap'];
  var contentText = [
    '</pre>
<div class="container">', '
<div class="hero-unit">', '
<h1>\'Allo, \'Allo!</h1>
', '
<p>You now have</p>
', '
<ul>' ]; if (!this.includeRequireJS) { this.indexFile = this.appendScripts(this.indexFile, 'scripts/main.js', [ 'components/jquery/jquery.js', 'scripts/main.js' ]); this.indexFile = this.appendFiles({ html: this.indexFile, fileType: 'js', optimizedPath: 'scripts/coffee.js', sourceFileList: ['scripts/hello.js'], searchPath: '.tmp' }); } if (this.compassBootstrap && !this.includeRequireJS) { // wire Twitter Bootstrap plugins this.indexFile = this.appendScripts(this.indexFile, 'scripts/plugins.js', [ 'components/sass-bootstrap/js/bootstrap-affix.js', 'components/sass-bootstrap/js/bootstrap-alert.js', 'components/sass-bootstrap/js/bootstrap-dropdown.js', 'components/sass-bootstrap/js/bootstrap-tooltip.js', 'components/sass-bootstrap/js/bootstrap-modal.js', 'components/sass-bootstrap/js/bootstrap-transition.js', 'components/sass-bootstrap/js/bootstrap-button.js', 'components/sass-bootstrap/js/bootstrap-popover.js', 'components/sass-bootstrap/js/bootstrap-typeahead.js', 'components/sass-bootstrap/js/bootstrap-carousel.js', 'components/sass-bootstrap/js/bootstrap-scrollspy.js', 'components/sass-bootstrap/js/bootstrap-collapse.js', 'components/sass-bootstrap/js/bootstrap-tab.js' ]); } if (this.includeRequireJS) { defaults.push('RequireJS'); } else { this.mainJsFile = 'console.log(\'\\\'Allo \\\'Allo!\');'; } // iterate over defaults and create content string defaults.forEach(function (el) { contentText.push('
<li>' + el +'</li>
'); }); contentText = contentText.concat([ '</ul>
', '
<p>installed.</p>
', '
<h3>Enjoy coding! - Yeoman</h3>
', '</div>
', '</div>
<pre>',
    ''
  ]);

  // append the default content
  this.indexFile = this.indexFile.replace('', '\n' + contentText.join('\n'));
};

// TODO(mklabs): to be put in a subgenerator like rjs:app
AppGenerator.prototype.requirejs = function requirejs() {
  if (this.includeRequireJS) {
    this.indexFile = this.appendScripts(this.indexFile, 'scripts/main.js', ['components/requirejs/require.js'], {
      'data-main': 'scripts/main'
    });

    // add a basic amd module
    this.write('app/scripts/app.js', [
      '/*global define */',
      'define([], function () {',
      '    \'use strict\';\n',
      '    return \'\\\'Allo \\\'Allo!\';',
      '});'
    ].join('\n'));

    this.mainJsFile = [
      'require.config({',
      '    paths: {',
      '        jquery: \'../components/jquery/jquery\',',
      '        bootstrap: \'vendor/bootstrap\'',
      '    },',
      '    shim: {',
      '        bootstrap: {',
      '            deps: [\'jquery\'],',
      '            exports: \'jquery\'',
      '        }',
      '    }',
      '});',
      '',
      'require([\'app\', \'jquery\', \'bootstrap\'], function (app, $) {',
      '    \'use strict\';',
      '    // use app here',
      '    console.log(app);',
      '    console.log(\'Running jQuery %s\', $().jquery);',
      '});'
    ].join('\n');
  }
};

AppGenerator.prototype.app = function app() {
  this.mkdir('app');
  this.mkdir('app/scripts');
  this.mkdir('app/styles');
  this.mkdir('app/images');
  this.write('app/index.html', this.indexFile);
  this.write('app/scripts/main.js', this.mainJsFile);
  this.write('app/scripts/hello.coffee', this.mainCoffeeFile);
};
$ 

yo webapp で実行されるファイルが index.js です。確かにそういったロジックが書かれています(途中に Yeoman さんもいます)。

 

 

Yeoman がインストールしたディレクトリ(今回は yo が主でしたが)を読むことで Yeoman というのは node ベースで作られているというのが再認識できたと思います。また、独自に generator を作るときも有用と考えています。