123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- var fs = require('fs');
- var util = require('util');
- var path = require('path');
- var events = require('events');
- var _ = require('lodash');
- var async = require('async');
- var findup = require('findup-sync');
- var chalk = require('chalk');
- var engines = require('./util/engines');
- var conflicter = require('./util/conflicter');
- var Storage = require('./util/storage');
- var actions = require('./actions/actions');
- // TOOD(mklabs): flesh out api, remove config (merge with options, or just remove the
- // grunt config handling)
- /**
- * The `Base` object provides the common API shared by all generators,
- * defining options, arguments, hooks, file, prompt, log, API etc.
- *
- * Every generator should extend from this object.
- *
- * @param {String|Array} args
- * @param {Object} options
- */
- var Base = module.exports = function Base(args, options) {
- events.EventEmitter.call(this);
- if (!Array.isArray(args)) {
- options = args;
- args = [];
- }
- this.args = this.arguments = args || [];
- this.options = options || {};
- // checks required paramaters
- if (!this.options.env) {
- throw new Error('You must provide the environment object. Use env#create() to create a new generator.');
- }
- if (!this.options.resolved) {
- throw new Error('You must provide the resolved path value. Use env#create() to create a new generator.');
- }
- this.env = this.options.env;
- this.resolved = this.options.resolved;
- this.fallbacks = this.options.generators || this.options.generator || {};
- this.generatorName = this.options.name || '';
- this.description = '';
- this.async = function () {
- return function () {};
- };
- _.defaults(this.options, this.fallbacks, {
- engine: engines.underscore
- });
- this._engine = this.options.engine;
- // cleanup options hash from default engine, if users didn't provided one.
- if (!options.engine) {
- delete this.options.engine;
- }
- this.conflicter = conflicter;
- this.conflicter.force = this.options.force;
- // determine the app root
- var rootPath = findup('.yo-rc.json');
- if (rootPath) {
- process.chdir(path.dirname(rootPath));
- }
- this._arguments = [];
- this._options = [];
- this._hooks = [];
- this._conflicts = [];
- this.appname = path.basename(process.cwd()).replace(/[^\w\s]+?/g, ' ');
- this._setStorage();
- this.option('help', {
- alias: 'h',
- desc: 'Print generator\'s options and usage'
- });
- // ensure source/destination path, can be configured from subclasses
- this.sourceRoot(path.join(path.dirname(this.resolved), 'templates'));
- };
- util.inherits(Base, events.EventEmitter);
- // "include" the actions modules
- _.extend(Base.prototype, actions);
- _.extend(Base.prototype, require('./actions/fetch'));
- _.extend(Base.prototype, require('./actions/file'));
- _.extend(Base.prototype, require('./actions/install'));
- _.extend(Base.prototype, require('./actions/string'));
- _.extend(Base.prototype, require('./actions/wiring'));
- _.extend(Base.prototype, require('./util/common'));
- Base.prototype.user = require('./actions/user');
- Base.prototype.shell = require('shelljs');
- Base.prototype.prompt = require('./actions/prompt');
- Base.prototype.invoke = require('./actions/invoke');
- Base.prototype.spawnCommand = require('./actions/spawn_command');
- /**
- * Adds an option to the set of generator expected options, only used to
- * generate generator usage. By default, generators get all the cli option
- * parsed by nopt as a `this.options` Hash object.
- *
- * ### Options:
- *
- * - `desc` Description for the option
- * - `type` Either Boolean, String or Number
- * - `default` Default value
- * - `banner` String to show on usage notes
- * - `hide` Boolean whether to hide from help
- *
- * @param {String} name
- * @param {Object} config
- */
- Base.prototype.option = function option(name, config) {
- config = config || {};
- _.defaults(config, {
- name: name,
- desc: 'Description for ' + name,
- type: Boolean,
- defaults: false,
- hide: false
- });
- var opt = this._options.filter(function (el) {
- return el.name === name;
- })[0];
- if (!opt) {
- this._options.push(config);
- } else {
- opt = config;
- }
- if (!this.options[name]) {
- this.options[name] = config.defaults;
- }
- return this;
- };
- /**
- * Adds an argument to the class and creates an attribute getter for it.
- *
- * Arguments are different from options in several aspects. The first one
- * is how they are parsed from the command line, arguments are retrieved
- * from position.
- *
- * Besides, arguments are used inside your code as a property (`this.argument`),
- * while options are all kept in a hash (`this.options`).
- *
- * ### Options:
- *
- * - `desc` Description for the argument
- * - `required` Boolean whether it is required
- * - `optional` Boolean whether it is optional
- * - `type` String, Number, Array, or Object
- * - `defaults` Default value for this argument
- * - `banner` String to show on usage notes
- *
- * @param {String} name
- * @param {Object} config
- */
- Base.prototype.argument = function argument(name, config) {
- config = config || {};
- _.defaults(config, {
- name: name,
- required: config.defaults == null ? true : false,
- type: String
- });
- config.banner = config.banner || this.bannerFor(config);
- this._arguments.push({
- name: name,
- config: config
- });
- var position = -1;
- this._arguments.forEach(function (arg, i) {
- if (position !== -1) {
- return;
- }
- if (arg.name === name) {
- position = i;
- }
- });
- // a bit of coercion and type handling, to be improved
- // just dealing with Array/String, default is assumed to be String
- var value = config.type === Array ? this.args.slice(position) : this.args[position];
- value = position >= this.args.length ? config.defaults : value;
- if (config.required && value === undefined) {
- return this.emit('error', new Error('Did not provide required argument ' + chalk.bold(name) + '!'));
- }
- this[name] = value;
- return this;
- };
- /**
- * Runs the generator, executing top-level methods in the order they
- * were defined.
- *
- * Special named method like `constructor` and `initialize` are skipped
- * (CoffeeScript and Backbone like inheritence), or any method prefixed by
- * a `_`.
- *
- * You can also supply the arguments for the method to be invoked, if
- * none is given, the same values used to initialize the invoker are
- * used to initialize the invoked.
- *
- * @param {String|Array} args
- * @param {Function} cb
- */
- Base.prototype.run = function run(args, cb) {
- var self = this;
- this._running = true;
- this.emit('start');
- this.emit('run');
- if (!cb) {
- cb = args;
- args = this.args;
- }
- cb = cb || function () {};
- var runHooks = function () {
- self.runHooks(cb);
- };
- var methods = Object.keys(Object.getPrototypeOf(this));
- var resolve = function (method) {
- var rules = {
- underscore: method.charAt(0) !== '_',
- initialize: !/^(constructor|initialize)$/.test(method),
- valid: function () {
- return this.underscore && this.initialize;
- }
- };
- return function (next) {
- if (!rules.valid()) {
- return next();
- }
- var done = function (err) {
- if (err) {
- self.emit('error', err);
- }
- // resolve file conflicts after every method completes.
- self.conflicter.resolve(function (err) {
- if (err) {
- return self.emit('error', err);
- }
- next();
- });
- };
- var running = false;
- self.async = function () {
- running = true;
- return done;
- };
- self.emit(method);
- self.emit('method', method);
- self[method].apply(self, args);
- if (!running) {
- done();
- }
- };
- };
- async.series(methods.map(resolve), runHooks);
- return this;
- };
- /**
- * Goes through all registered hooks, invoking them in series.
- *
- * @param {Function} cb
- */
- Base.prototype.runHooks = function runHooks(cb) {
- var self = this;
- var hooks = this._hooks;
- var callback = function (err) {
- self.emit('end');
- cb(err);
- };
- var resolve = function (hook) {
- var resolved = self.defaultFor(hook.name);
- var context = hook.as || self.resolved || self.generateName;
- var options = hook.options || self.options;
- options.args = hook.args || self.args;
- return function (next) {
- self.invoke(resolved + (context ? ':' + context : ''), options, next);
- };
- };
- async.series(hooks.map(resolve), callback);
- return this;
- };
- /**
- * Registers a hook to invoke when this generator runs.
- *
- * A generator with a namespace based on the value supplied by the user
- * to the given option named `name`. An option is created when this method is
- * invoked and you can set a hash to customize it.
- *
- * Must be called prior to the generator run (shouldn't be called within
- * a generator "step" - top-level methods).
- *
- * ### Options:
- *
- * - `as` The context value to use when runing the hooked generator
- * - `args` The array of positional arguments to init and run the generator with
- * - `options` The hash of options to use to init and run the generator with
- *
- * ### Examples:
- *
- * // $ yo webapp --test-framework jasmine
- * this.hookFor('test-framework');
- * // => registers the `jasmine` hook
- *
- * @param {String} name
- * @param {Object} config
- */
- Base.prototype.hookFor = function hookFor(name, config) {
- config = config || {};
- // enforce use of hookFor during instantiation
- if (this._running) {
- return this.emit('error', new Error(
- 'hookFor must be used within the constructor only'
- ));
- }
- // add the corresponding option to this class, so that we output these hooks
- // in help
- this.option(name, {
- desc: this._.humanize(name) + ' to be invoked',
- defaults: this.options[name] || ''
- });
- this._hooks.push(_.defaults(config, {
- name: name
- }));
- return this;
- };
- /**
- * Return the default value for the option name.
- *
- * Also performs a lookup in CLI options and the `this.fallbacks`
- * property.
- *
- * @param {String} name
- */
- Base.prototype.defaultFor = function defaultFor(name) {
- var config = this.fallbacks;
- if (this.options[name]) {
- name = this.options[name];
- } else if (config && config[name]) {
- name = config[name];
- }
- return name;
- };
- /**
- * Generate the default banner for help output, adjusting output to
- * argument type.
- *
- * Options:
- *
- * - `name` Uppercased value to display (only relevant with `String` type)
- * - `type` String, Number, Object or Array
- *
- * @param {Object} config
- */
- Base.prototype.bannerFor = function bannerFor(config) {
- return config.type === Boolean ? '' :
- config.type === String ? config.name.toUpperCase() :
- config.type === Number ? 'N' :
- config.type === Object ? 'key:value' :
- config.type === Array ? 'one two three' :
- '';
- };
- /**
- * Tries to get the description from a USAGE file one folder above the
- * source root otherwise uses a default description.
- */
- Base.prototype.help = function help() {
- var filepath = path.join(this.sourceRoot(), '../USAGE');
- var exists = fs.existsSync(filepath);
- var out = [
- 'Usage:',
- ' ' + this.usage(),
- ''
- ];
- // build options
- if (this._options.length) {
- out = out.concat([
- 'Options:',
- this.optionsHelp(),
- ''
- ]);
- }
- // append USAGE file is any
- if (exists) {
- out.push(fs.readFileSync(filepath, 'utf8'));
- }
- return out.join('\n');
- };
- /**
- * Output usage information for this given generator, depending on its arguments,
- * options or hooks.
- */
- Base.prototype.usage = function usage() {
- var args = this._arguments.map(function (arg) {
- return arg.config.banner;
- }).join(' ');
- var options = this._options.length ? '[options]' : '',
- name = (this.namespace === 'yeoman:app' || !this.namespace) ? '' : this.namespace + ' ',
- cmd = 'init';
- name = name.replace(/^yeoman:/, '');
- var out = 'yeoman ' + cmd + ' ' + name + args + ' ' + options;
- if (this.description) {
- out += '\n\n' + this.description;
- }
- return out;
- };
- /**
- * Simple setter for custom `description` to append on help output.
- *
- * @param {String} description
- */
- Base.prototype.desc = function desc(description) {
- this.description = description || '';
- return this;
- };
- /**
- * Returns the list of options in formatted table.
- */
- Base.prototype.optionsHelp = function optionsHelp() {
- var options = this._options.filter(function (el) {
- return !el.hide;
- });
- var hookOpts = this._hooks.map(function (hook) {
- return hook.generator && hook.generator._options;
- }).reduce(function (a, b) {
- a = a.concat(b);
- return a;
- }, []).filter(function (opts) {
- return opts && opts.name !== 'help';
- });
- var rows = options.concat(hookOpts).map(function (o) {
- return [
- '',
- o.alias ? '-' + o.alias + ', ' : '',
- '--' + o.name,
- o.desc ? '# ' + o.desc : '',
- o.defaults == null ? '' : 'Default: ' + o.defaults
- ];
- });
- return this.log.table({
- rows: rows
- });
- };
- /**
- * Determine the root generator name (the one who's extending Base).
- */
- Base.prototype.rootGeneratorName = function () {
- var path = findup('package.json', { cwd: this.resolved });
- return JSON.parse(fs.readFileSync(path, 'utf8')).name;
- };
- /**
- * Setup a storage instance.
- */
- Base.prototype._setStorage = function () {
- var storePath = path.join(actions.destinationRoot.call(this), '.yo-rc.json');
- this.config = new Storage(this.rootGeneratorName(), storePath);
- };
- /**
- * Façace `actions.destinationRoot` on Base generator so it update the storage
- * path when the path change.
- *
- * @param {String} rootPath
- */
- Base.prototype.destinationRoot = function (rootPath) {
- var root = actions.destinationRoot.call(this, rootPath);
- if (rootPath) {
- this._setStorage();
- }
- return root;
- };
|