base.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. var fs = require('fs');
  2. var util = require('util');
  3. var path = require('path');
  4. var events = require('events');
  5. var _ = require('lodash');
  6. var async = require('async');
  7. var findup = require('findup-sync');
  8. var chalk = require('chalk');
  9. var engines = require('./util/engines');
  10. var conflicter = require('./util/conflicter');
  11. var Storage = require('./util/storage');
  12. var actions = require('./actions/actions');
  13. // TOOD(mklabs): flesh out api, remove config (merge with options, or just remove the
  14. // grunt config handling)
  15. /**
  16. * The `Base` object provides the common API shared by all generators,
  17. * defining options, arguments, hooks, file, prompt, log, API etc.
  18. *
  19. * Every generator should extend from this object.
  20. *
  21. * @param {String|Array} args
  22. * @param {Object} options
  23. */
  24. var Base = module.exports = function Base(args, options) {
  25. events.EventEmitter.call(this);
  26. if (!Array.isArray(args)) {
  27. options = args;
  28. args = [];
  29. }
  30. this.args = this.arguments = args || [];
  31. this.options = options || {};
  32. // checks required paramaters
  33. if (!this.options.env) {
  34. throw new Error('You must provide the environment object. Use env#create() to create a new generator.');
  35. }
  36. if (!this.options.resolved) {
  37. throw new Error('You must provide the resolved path value. Use env#create() to create a new generator.');
  38. }
  39. this.env = this.options.env;
  40. this.resolved = this.options.resolved;
  41. this.fallbacks = this.options.generators || this.options.generator || {};
  42. this.generatorName = this.options.name || '';
  43. this.description = '';
  44. this.async = function () {
  45. return function () {};
  46. };
  47. _.defaults(this.options, this.fallbacks, {
  48. engine: engines.underscore
  49. });
  50. this._engine = this.options.engine;
  51. // cleanup options hash from default engine, if users didn't provided one.
  52. if (!options.engine) {
  53. delete this.options.engine;
  54. }
  55. this.conflicter = conflicter;
  56. this.conflicter.force = this.options.force;
  57. // determine the app root
  58. var rootPath = findup('.yo-rc.json');
  59. if (rootPath) {
  60. process.chdir(path.dirname(rootPath));
  61. }
  62. this._arguments = [];
  63. this._options = [];
  64. this._hooks = [];
  65. this._conflicts = [];
  66. this.appname = path.basename(process.cwd()).replace(/[^\w\s]+?/g, ' ');
  67. this._setStorage();
  68. this.option('help', {
  69. alias: 'h',
  70. desc: 'Print generator\'s options and usage'
  71. });
  72. // ensure source/destination path, can be configured from subclasses
  73. this.sourceRoot(path.join(path.dirname(this.resolved), 'templates'));
  74. };
  75. util.inherits(Base, events.EventEmitter);
  76. // "include" the actions modules
  77. _.extend(Base.prototype, actions);
  78. _.extend(Base.prototype, require('./actions/fetch'));
  79. _.extend(Base.prototype, require('./actions/file'));
  80. _.extend(Base.prototype, require('./actions/install'));
  81. _.extend(Base.prototype, require('./actions/string'));
  82. _.extend(Base.prototype, require('./actions/wiring'));
  83. _.extend(Base.prototype, require('./util/common'));
  84. Base.prototype.user = require('./actions/user');
  85. Base.prototype.shell = require('shelljs');
  86. Base.prototype.prompt = require('./actions/prompt');
  87. Base.prototype.invoke = require('./actions/invoke');
  88. Base.prototype.spawnCommand = require('./actions/spawn_command');
  89. /**
  90. * Adds an option to the set of generator expected options, only used to
  91. * generate generator usage. By default, generators get all the cli option
  92. * parsed by nopt as a `this.options` Hash object.
  93. *
  94. * ### Options:
  95. *
  96. * - `desc` Description for the option
  97. * - `type` Either Boolean, String or Number
  98. * - `default` Default value
  99. * - `banner` String to show on usage notes
  100. * - `hide` Boolean whether to hide from help
  101. *
  102. * @param {String} name
  103. * @param {Object} config
  104. */
  105. Base.prototype.option = function option(name, config) {
  106. config = config || {};
  107. _.defaults(config, {
  108. name: name,
  109. desc: 'Description for ' + name,
  110. type: Boolean,
  111. defaults: false,
  112. hide: false
  113. });
  114. var opt = this._options.filter(function (el) {
  115. return el.name === name;
  116. })[0];
  117. if (!opt) {
  118. this._options.push(config);
  119. } else {
  120. opt = config;
  121. }
  122. if (!this.options[name]) {
  123. this.options[name] = config.defaults;
  124. }
  125. return this;
  126. };
  127. /**
  128. * Adds an argument to the class and creates an attribute getter for it.
  129. *
  130. * Arguments are different from options in several aspects. The first one
  131. * is how they are parsed from the command line, arguments are retrieved
  132. * from position.
  133. *
  134. * Besides, arguments are used inside your code as a property (`this.argument`),
  135. * while options are all kept in a hash (`this.options`).
  136. *
  137. * ### Options:
  138. *
  139. * - `desc` Description for the argument
  140. * - `required` Boolean whether it is required
  141. * - `optional` Boolean whether it is optional
  142. * - `type` String, Number, Array, or Object
  143. * - `defaults` Default value for this argument
  144. * - `banner` String to show on usage notes
  145. *
  146. * @param {String} name
  147. * @param {Object} config
  148. */
  149. Base.prototype.argument = function argument(name, config) {
  150. config = config || {};
  151. _.defaults(config, {
  152. name: name,
  153. required: config.defaults == null ? true : false,
  154. type: String
  155. });
  156. config.banner = config.banner || this.bannerFor(config);
  157. this._arguments.push({
  158. name: name,
  159. config: config
  160. });
  161. var position = -1;
  162. this._arguments.forEach(function (arg, i) {
  163. if (position !== -1) {
  164. return;
  165. }
  166. if (arg.name === name) {
  167. position = i;
  168. }
  169. });
  170. // a bit of coercion and type handling, to be improved
  171. // just dealing with Array/String, default is assumed to be String
  172. var value = config.type === Array ? this.args.slice(position) : this.args[position];
  173. value = position >= this.args.length ? config.defaults : value;
  174. if (config.required && value === undefined) {
  175. return this.emit('error', new Error('Did not provide required argument ' + chalk.bold(name) + '!'));
  176. }
  177. this[name] = value;
  178. return this;
  179. };
  180. /**
  181. * Runs the generator, executing top-level methods in the order they
  182. * were defined.
  183. *
  184. * Special named method like `constructor` and `initialize` are skipped
  185. * (CoffeeScript and Backbone like inheritence), or any method prefixed by
  186. * a `_`.
  187. *
  188. * You can also supply the arguments for the method to be invoked, if
  189. * none is given, the same values used to initialize the invoker are
  190. * used to initialize the invoked.
  191. *
  192. * @param {String|Array} args
  193. * @param {Function} cb
  194. */
  195. Base.prototype.run = function run(args, cb) {
  196. var self = this;
  197. this._running = true;
  198. this.emit('start');
  199. this.emit('run');
  200. if (!cb) {
  201. cb = args;
  202. args = this.args;
  203. }
  204. cb = cb || function () {};
  205. var runHooks = function () {
  206. self.runHooks(cb);
  207. };
  208. var methods = Object.keys(Object.getPrototypeOf(this));
  209. var resolve = function (method) {
  210. var rules = {
  211. underscore: method.charAt(0) !== '_',
  212. initialize: !/^(constructor|initialize)$/.test(method),
  213. valid: function () {
  214. return this.underscore && this.initialize;
  215. }
  216. };
  217. return function (next) {
  218. if (!rules.valid()) {
  219. return next();
  220. }
  221. var done = function (err) {
  222. if (err) {
  223. self.emit('error', err);
  224. }
  225. // resolve file conflicts after every method completes.
  226. self.conflicter.resolve(function (err) {
  227. if (err) {
  228. return self.emit('error', err);
  229. }
  230. next();
  231. });
  232. };
  233. var running = false;
  234. self.async = function () {
  235. running = true;
  236. return done;
  237. };
  238. self.emit(method);
  239. self.emit('method', method);
  240. self[method].apply(self, args);
  241. if (!running) {
  242. done();
  243. }
  244. };
  245. };
  246. async.series(methods.map(resolve), runHooks);
  247. return this;
  248. };
  249. /**
  250. * Goes through all registered hooks, invoking them in series.
  251. *
  252. * @param {Function} cb
  253. */
  254. Base.prototype.runHooks = function runHooks(cb) {
  255. var self = this;
  256. var hooks = this._hooks;
  257. var callback = function (err) {
  258. self.emit('end');
  259. cb(err);
  260. };
  261. var resolve = function (hook) {
  262. var resolved = self.defaultFor(hook.name);
  263. var context = hook.as || self.resolved || self.generateName;
  264. var options = hook.options || self.options;
  265. options.args = hook.args || self.args;
  266. return function (next) {
  267. self.invoke(resolved + (context ? ':' + context : ''), options, next);
  268. };
  269. };
  270. async.series(hooks.map(resolve), callback);
  271. return this;
  272. };
  273. /**
  274. * Registers a hook to invoke when this generator runs.
  275. *
  276. * A generator with a namespace based on the value supplied by the user
  277. * to the given option named `name`. An option is created when this method is
  278. * invoked and you can set a hash to customize it.
  279. *
  280. * Must be called prior to the generator run (shouldn't be called within
  281. * a generator "step" - top-level methods).
  282. *
  283. * ### Options:
  284. *
  285. * - `as` The context value to use when runing the hooked generator
  286. * - `args` The array of positional arguments to init and run the generator with
  287. * - `options` The hash of options to use to init and run the generator with
  288. *
  289. * ### Examples:
  290. *
  291. * // $ yo webapp --test-framework jasmine
  292. * this.hookFor('test-framework');
  293. * // => registers the `jasmine` hook
  294. *
  295. * @param {String} name
  296. * @param {Object} config
  297. */
  298. Base.prototype.hookFor = function hookFor(name, config) {
  299. config = config || {};
  300. // enforce use of hookFor during instantiation
  301. if (this._running) {
  302. return this.emit('error', new Error(
  303. 'hookFor must be used within the constructor only'
  304. ));
  305. }
  306. // add the corresponding option to this class, so that we output these hooks
  307. // in help
  308. this.option(name, {
  309. desc: this._.humanize(name) + ' to be invoked',
  310. defaults: this.options[name] || ''
  311. });
  312. this._hooks.push(_.defaults(config, {
  313. name: name
  314. }));
  315. return this;
  316. };
  317. /**
  318. * Return the default value for the option name.
  319. *
  320. * Also performs a lookup in CLI options and the `this.fallbacks`
  321. * property.
  322. *
  323. * @param {String} name
  324. */
  325. Base.prototype.defaultFor = function defaultFor(name) {
  326. var config = this.fallbacks;
  327. if (this.options[name]) {
  328. name = this.options[name];
  329. } else if (config && config[name]) {
  330. name = config[name];
  331. }
  332. return name;
  333. };
  334. /**
  335. * Generate the default banner for help output, adjusting output to
  336. * argument type.
  337. *
  338. * Options:
  339. *
  340. * - `name` Uppercased value to display (only relevant with `String` type)
  341. * - `type` String, Number, Object or Array
  342. *
  343. * @param {Object} config
  344. */
  345. Base.prototype.bannerFor = function bannerFor(config) {
  346. return config.type === Boolean ? '' :
  347. config.type === String ? config.name.toUpperCase() :
  348. config.type === Number ? 'N' :
  349. config.type === Object ? 'key:value' :
  350. config.type === Array ? 'one two three' :
  351. '';
  352. };
  353. /**
  354. * Tries to get the description from a USAGE file one folder above the
  355. * source root otherwise uses a default description.
  356. */
  357. Base.prototype.help = function help() {
  358. var filepath = path.join(this.sourceRoot(), '../USAGE');
  359. var exists = fs.existsSync(filepath);
  360. var out = [
  361. 'Usage:',
  362. ' ' + this.usage(),
  363. ''
  364. ];
  365. // build options
  366. if (this._options.length) {
  367. out = out.concat([
  368. 'Options:',
  369. this.optionsHelp(),
  370. ''
  371. ]);
  372. }
  373. // append USAGE file is any
  374. if (exists) {
  375. out.push(fs.readFileSync(filepath, 'utf8'));
  376. }
  377. return out.join('\n');
  378. };
  379. /**
  380. * Output usage information for this given generator, depending on its arguments,
  381. * options or hooks.
  382. */
  383. Base.prototype.usage = function usage() {
  384. var args = this._arguments.map(function (arg) {
  385. return arg.config.banner;
  386. }).join(' ');
  387. var options = this._options.length ? '[options]' : '',
  388. name = (this.namespace === 'yeoman:app' || !this.namespace) ? '' : this.namespace + ' ',
  389. cmd = 'init';
  390. name = name.replace(/^yeoman:/, '');
  391. var out = 'yeoman ' + cmd + ' ' + name + args + ' ' + options;
  392. if (this.description) {
  393. out += '\n\n' + this.description;
  394. }
  395. return out;
  396. };
  397. /**
  398. * Simple setter for custom `description` to append on help output.
  399. *
  400. * @param {String} description
  401. */
  402. Base.prototype.desc = function desc(description) {
  403. this.description = description || '';
  404. return this;
  405. };
  406. /**
  407. * Returns the list of options in formatted table.
  408. */
  409. Base.prototype.optionsHelp = function optionsHelp() {
  410. var options = this._options.filter(function (el) {
  411. return !el.hide;
  412. });
  413. var hookOpts = this._hooks.map(function (hook) {
  414. return hook.generator && hook.generator._options;
  415. }).reduce(function (a, b) {
  416. a = a.concat(b);
  417. return a;
  418. }, []).filter(function (opts) {
  419. return opts && opts.name !== 'help';
  420. });
  421. var rows = options.concat(hookOpts).map(function (o) {
  422. return [
  423. '',
  424. o.alias ? '-' + o.alias + ', ' : '',
  425. '--' + o.name,
  426. o.desc ? '# ' + o.desc : '',
  427. o.defaults == null ? '' : 'Default: ' + o.defaults
  428. ];
  429. });
  430. return this.log.table({
  431. rows: rows
  432. });
  433. };
  434. /**
  435. * Determine the root generator name (the one who's extending Base).
  436. */
  437. Base.prototype.rootGeneratorName = function () {
  438. var path = findup('package.json', { cwd: this.resolved });
  439. return JSON.parse(fs.readFileSync(path, 'utf8')).name;
  440. };
  441. /**
  442. * Setup a storage instance.
  443. */
  444. Base.prototype._setStorage = function () {
  445. var storePath = path.join(actions.destinationRoot.call(this), '.yo-rc.json');
  446. this.config = new Storage(this.rootGeneratorName(), storePath);
  447. };
  448. /**
  449. * Façace `actions.destinationRoot` on Base generator so it update the storage
  450. * path when the path change.
  451. *
  452. * @param {String} rootPath
  453. */
  454. Base.prototype.destinationRoot = function (rootPath) {
  455. var root = actions.destinationRoot.call(this, rootPath);
  456. if (rootPath) {
  457. this._setStorage();
  458. }
  459. return root;
  460. };