env.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. var fs = require('fs');
  2. var util = require('util');
  3. var path = require('path');
  4. var events = require('events');
  5. var spawn = require('child_process').spawn;
  6. var glob = require('glob');
  7. var chalk = require('chalk');
  8. var _ = require('lodash');
  9. var log = process.logging('generators');
  10. var engines = require('./util/engines');
  11. var debug = require('debug')('generators:environment');
  12. var Base = require('./base');
  13. // TODO(mklabs):
  14. //
  15. // - path manipulation methods ({append,prepend}{Path,Lookup}) can be dryed out.
  16. // - if it gets too huge (more than 200loc), split this into mulitple mixins.
  17. // - register() method too long, split logic for namespace etc. elsewhere
  18. // - flesh out Error handling, consider emitting error instead of throwing,
  19. // even for synchronous operation.
  20. /**
  21. * `Environment` object is responsible of handling the lifecyle and bootstrap
  22. * of generators in a specific environment (your app).
  23. *
  24. * It provides a high-level API to create and run generators, as well as further
  25. * tuning where and how a generator is resolved.
  26. *
  27. * An environment is created using a list of `arguments` and a Hash of
  28. * `options`. Usually, this is the list of arguments you get back from your CLI
  29. * options parser.
  30. *
  31. * @param {String|Array} args
  32. * @param {Object} opts
  33. */
  34. var Environment = module.exports = function Environment(args, opts) {
  35. events.EventEmitter.call(this);
  36. args = args || [];
  37. this.arguments = Array.isArray(args) ? args : args.split(' ');
  38. this.options = opts || {};
  39. this.cwd = this.options.cwd || process.cwd();
  40. this.generators = {};
  41. this.aliases = [];
  42. this.lookups = [];
  43. this.paths = [];
  44. this.appendLookup('.');
  45. this.appendLookup('generators');
  46. // DEPRECATED: use the path `generators` instead
  47. this.appendLookup('lib/generators');
  48. this.appendPath('.');
  49. // adds support for global generators. ensures support across all OS since the
  50. // global node_modules dir always is four levels up from env.js
  51. this.appendPath(path.join(__dirname, '../../../..'));
  52. this._prefixReg = null;
  53. this._prefixes = this._prefixes || [];
  54. this._suffix = this._suffix || '';
  55. this.prefix(this.options.prefix || 'generator-');
  56. this.suffix(this.options.suffix || '*/index.js');
  57. this.plugins('node_modules');
  58. };
  59. util.inherits(Environment, events.EventEmitter);
  60. /**
  61. * Error handler taking `err` instance of Error.
  62. *
  63. * The `error` event is emitted with the error object, if no `error` listener
  64. * is registered, then we throw the error.
  65. *
  66. * @param {Object} err
  67. */
  68. Environment.prototype.error = function error(err) {
  69. err = err instanceof Error ? err : new Error(err);
  70. if (!this.emit('error', err)) {
  71. throw err;
  72. }
  73. return this;
  74. };
  75. /**
  76. * Configures the `engine` to use for this environment.
  77. *
  78. * @param {String} engine
  79. */
  80. Environment.prototype.engine = function _engine(engine) {
  81. if (typeof engine === 'function') {
  82. this._engine = engine;
  83. return this;
  84. }
  85. if (!engines[engine]) {
  86. return this.error(new Error('Wrong engine (' + engine + '). Available: ' + Object.keys(engines).join(' ')));
  87. }
  88. this._engine = engines[engine];
  89. return this;
  90. };
  91. /**
  92. * Appends a `filepath` to the list of loadpaths.
  93. *
  94. * @param {String} filepath
  95. */
  96. Environment.prototype.appendPath = function appendPath(filepath) {
  97. if (!filepath) {
  98. return this.error(new Error('Missing filepath.'));
  99. }
  100. this.paths.push(filepath);
  101. return this;
  102. };
  103. /**
  104. * Appends a new `filepath` to the list of lookups path. This should be a
  105. * relative filepath, like `support/scaffold`. Environments are created with
  106. * `lib/generators` as a lookup path by default.
  107. *
  108. * @param {String} filepath
  109. */
  110. Environment.prototype.appendLookup = function appendLookup(filepath) {
  111. if (!filepath) {
  112. return this.error(new Error('Missing filepath.'));
  113. }
  114. this.lookups.push(filepath);
  115. return this;
  116. };
  117. /**
  118. * Outputs the general help and usage. Optionally, if generators have been
  119. * registered, the list of available generators is also displayed.
  120. *
  121. * @param {String} name
  122. */
  123. Environment.prototype.help = function help(name) {
  124. name = name || 'init';
  125. var out = [
  126. 'Usage: :binary: GENERATOR [args] [options]',
  127. '',
  128. 'General options:',
  129. ' -h, --help # Print generator\'s options and usage',
  130. ' -f, --force # Overwrite files that already exist',
  131. '',
  132. 'Please choose a generator below.',
  133. ''
  134. ];
  135. var ns = this.namespaces();
  136. var groups = {};
  137. ns.sort().forEach(function (namespace) {
  138. var base = namespace.split(':')[0];
  139. if (!groups[base]) {
  140. groups[base] = [];
  141. }
  142. groups[base] = groups[base].concat(namespace);
  143. });
  144. Object.keys(groups).forEach(function (key) {
  145. var group = groups[key];
  146. if (group.length >= 1) {
  147. out.push('', key.charAt(0).toUpperCase() + key.slice(1));
  148. }
  149. groups[key].forEach(function (ns) {
  150. out.push(' ' + ns);
  151. });
  152. });
  153. return out.join('\n')
  154. .replace(/:binary:/g, name);
  155. };
  156. /**
  157. * Registers a specific `generator` to this environment. A generator can be a
  158. * simple function or an object extending from `Generators.Base`. The later
  159. * method is favored as it allows you to specify options/arguments for
  160. * self-documented generator with `USAGE:` and so on.
  161. *
  162. * In case of a simple function, the generator does show up in the `--help`
  163. * output, but without any kind of arguments/options. You must document them
  164. * manually with your `USAGE` file.
  165. *
  166. * In any case, the API available in generators is the same. Raw functions are
  167. * executed within the context of a `new Generators.Base`.
  168. *
  169. * `register()` can also take Strings, in which case it is considered a
  170. * filepath to `require()`.
  171. *
  172. * @param {String|Function} name
  173. * @param {String} namespace
  174. */
  175. Environment.prototype.register = function register(name, namespace) {
  176. if (!name) {
  177. return this.error(new Error('You must provide a generator to register.'));
  178. }
  179. var cachePath = _.isString(name);
  180. var generator = cachePath ? function () {} : name;
  181. var isRaw = function (generator) {
  182. generator = Object.getPrototypeOf(generator.prototype);
  183. var methods = ['option', 'argument', 'hookFor', 'run'];
  184. return methods.filter(function (method) {
  185. return !!generator[method];
  186. }).length !== methods.length;
  187. };
  188. var filepath;
  189. var ns;
  190. if (cachePath) {
  191. filepath = name;
  192. if (filepath.charAt(0) === '.') {
  193. filepath = path.resolve(filepath);
  194. }
  195. if (filepath.charAt(0) === '~') {
  196. filepath = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'] + filepath.slice(1);
  197. }
  198. generator.resolved = require.resolve(filepath);
  199. generator.namespace = this.namespace(generator.resolved);
  200. generator.__register = function () {
  201. var invokedGenerator = require(generator.resolved);
  202. invokedGenerator.resolved = generator.resolved;
  203. this.register(invokedGenerator, generator.namespace);
  204. }.bind(this);
  205. }
  206. ns = namespace || generator.namespace || this.namespace(name);
  207. if (!ns) {
  208. return this.error(new Error('Unable to determine namespace.'));
  209. }
  210. // normalize what we got, at this point if we were unable to find a
  211. // namespace, we just consider the directory name under which this generator
  212. // is found.
  213. if (ns.indexOf('/') !== -1) {
  214. ns = path.basename(path.dirname(ns));
  215. }
  216. generator.namespace = ns;
  217. generator.raw = isRaw(generator);
  218. generator.resolved = generator.resolved || ns;
  219. this.generators[ns] = generator;
  220. debug('Registered %s (%s)', generator.namespace, generator.resolved);
  221. return this;
  222. };
  223. /**
  224. * Returns the list of registered namespace.
  225. */
  226. Environment.prototype.namespaces = function namespaces() {
  227. return Object.keys(this.generators);
  228. };
  229. /**
  230. * Get a single generator from the registered list of generators. The lookup is
  231. * based on generator's namespace, "walking up" the namespaces until a matching
  232. * is found. Eg. if an `angular:common` namespace is registered, and we try to
  233. * get `angular:common:all` then we get `angular:common` as a fallback (unless
  234. * an `angular:common:all` generator is registered).
  235. *
  236. * @param {String} namespace
  237. */
  238. Environment.prototype.get = function get(namespace) {
  239. if (!namespace) {
  240. return;
  241. }
  242. var get = function (namespace) {
  243. var generator = this.generators[namespace];
  244. if (generator) {
  245. if (generator.__register) {
  246. generator.__register();
  247. generator = get(namespace);
  248. }
  249. return generator;
  250. }
  251. }.bind(this);
  252. return get(namespace)
  253. || get(this.alias(namespace))
  254. || this.get(namespace.split(':').slice(0, -1).join(':'));
  255. };
  256. /**
  257. * Create is the Generator factory. It takes a namespace to lookup and optional
  258. * hash of options, that lets you define `arguments` and `options` to
  259. * instantiate the generator with.
  260. *
  261. * An error is raised on invalid namespace.
  262. *
  263. * @param {String} namespace
  264. * @param {Object} options
  265. */
  266. Environment.prototype.create = function create(namespace, options) {
  267. options = options || {};
  268. var names = namespace.split(':');
  269. var name = names.slice(-1)[0];
  270. var generator = this.get(namespace);
  271. var args = options.arguments || options.args || this.arguments;
  272. args = Array.isArray(args) ? args : args.split(' ');
  273. var opts = options.options || _.clone(this.options);
  274. if (!generator) {
  275. return this.error(
  276. new Error(
  277. 'You don\'t seem to have a generator with the name ' + namespace + ' installed.\n' +
  278. chalk.bold('You can see available generators with ' + 'npm search yeoman-generator') +
  279. chalk.bold(' and then install them with ' + 'npm install [name]') + '.\n' +
  280. 'To see the ' + this.namespaces().length + ' registered generators run yo with the `--help` option.'
  281. )
  282. );
  283. }
  284. // case of raw functions, we create a brand new `Base` object and attach this
  285. // raw function as one of the prototype method. this effectively standardize
  286. // the interface for running generators, while allowing less boilerplate for
  287. // generators authors.
  288. var Generator = generator;
  289. if (generator.raw) {
  290. Generator = function () {
  291. Base.apply(this, arguments);
  292. };
  293. util.inherits(Generator, Base);
  294. Generator.prototype.exec = generator;
  295. }
  296. opts.env = this;
  297. opts.name = name;
  298. opts.resolved = generator.resolved;
  299. return new Generator(args, opts);
  300. };
  301. /**
  302. * Tries to locate and run a specific generator. The lookup is done depending
  303. * on the provided arguments, options and the list of registered generators.
  304. *
  305. * When the environment was unable to resolve a generator, an error is raised.
  306. *
  307. * @param {String|Array} args
  308. * @param {Object} options
  309. * @param {Function} done
  310. */
  311. Environment.prototype.run = function run(args, options, done) {
  312. args = args || this.arguments;
  313. if (typeof options === 'function') {
  314. done = options;
  315. options = this.options;
  316. }
  317. if (typeof args === 'function') {
  318. done = args;
  319. options = this.options;
  320. args = this.arguments;
  321. }
  322. args = Array.isArray(args) ? args : args.split(' ');
  323. options = options || this.options;
  324. var name = args.shift();
  325. if (!name) {
  326. return this.error(new Error('Must provide at least one argument, the generator namespace to invoke.'));
  327. }
  328. // is it a remote? any generator with a `/` in it is considered
  329. // potential remote, and we simply proxy over `npm install` command to get
  330. // the generator locally.
  331. if (/\//.test(name)) {
  332. return this.remote(name, done);
  333. }
  334. var generator = this.create(name, {
  335. args: args,
  336. options: options
  337. });
  338. if (typeof done === 'function') {
  339. generator.on('end', done);
  340. }
  341. if (options.help) {
  342. return console.log(generator.help());
  343. }
  344. generator.on('start', this.emit.bind(this, 'generators:start'));
  345. generator.on('start', this.emit.bind(this, name + ':start'));
  346. var self = this;
  347. generator.on('method', function (method) {
  348. self.emit(name + ':' + method);
  349. });
  350. generator.on('end', this.emit.bind(this, name + ':end'));
  351. generator.on('end', this.emit.bind(this, 'generators:end'));
  352. return generator.run();
  353. };
  354. /**
  355. * Receives namespaces in an array and tries to find matching generators in the
  356. * load paths.
  357. *
  358. * We lookup namespaces in several places, namely `this.lookups`
  359. * list of relatives directory path. A `generator-` prefix is added if a
  360. * namespace wasn't `require()`-able directly, matching `generator-*` kind of
  361. * pattern in npm installed package.
  362. *
  363. * You can also lookup using glob-like star pattern, eg. `angular:*` gets
  364. * expanded to `angular\*\index.js`.
  365. *
  366. * The default alias `generator-$1` lookup is added automatically.
  367. *
  368. * ### Examples:
  369. *
  370. * // search for all angular generators in the load path
  371. * env.lookup('angular:*');
  372. *
  373. * // register any valid set of generator in the load paths
  374. * env.lookup('*:*');
  375. *
  376. * @param {String|Array} namespaces
  377. * @param {String} lookupdir
  378. */
  379. Environment.prototype.lookup = function lookup(namespaces, lookupdir) {
  380. namespaces = Array.isArray(namespaces) ? namespaces : namespaces.split(' ');
  381. debug('Lookup %s', namespaces);
  382. namespaces.forEach(function (ns) {
  383. var filepath = path.join.apply(path, this.alias(ns).split(':'));
  384. this.paths.forEach(function (base) {
  385. debug('Looking in %s with filepath %s', base, filepath);
  386. // no glob pattern
  387. if (!~filepath.indexOf('*')) {
  388. try {
  389. debug('Attempt to register with direct filepath %s', filepath);
  390. this.register(filepath);
  391. } catch (e) {
  392. // silent fail unless not a loadpath error
  393. if (e.message.indexOf(filepath) === -1) {
  394. console.error('Unable to register %s (Error: %s)', ns, e.message);
  395. }
  396. }
  397. return;
  398. }
  399. this.lookups.forEach(function (lookupdir) {
  400. var depth = lookupdir && /^\.\/?$/.test(lookupdir) ? '*' : '**';
  401. var prefixes = this._prefixes.filter(function (prefix) {
  402. return !(/\//).test(prefix);
  403. });
  404. var pattern = filepath
  405. .replace(/^\*+/, '+(' + prefixes.join('|') + ')*')
  406. .replace(/\*+$/g, path.join(lookupdir, depth, 'index.js'))
  407. .replace(/^\*\//, '');
  408. debug('Globing for generator %s with pattern %s (cwd: %s)', ns, pattern, base);
  409. glob.sync(pattern, { cwd: base }).forEach(function (filename) {
  410. // now register, warn on failed require
  411. try {
  412. debug('found %s, trying to register', filename);
  413. this.register(path.resolve(base, filename));
  414. } catch (e) {
  415. console.error('Unable to register %s (Error: %s)', filename, e.message);
  416. }
  417. }, this);
  418. }, this);
  419. }, this);
  420. }, this);
  421. return this;
  422. };
  423. /**
  424. * Get or create an alias.
  425. *
  426. * Alias allows the `get()` and `lookup()` methods to search in alternate
  427. * filepath for a given namespaces. It's used for example to map `generator-*`
  428. * npm package to their namespace equivalent (without the generator- prefix),
  429. * or to default a single namespace like `angular` to `angular:app` or
  430. * `angular:all`.
  431. *
  432. * Given a single argument, this method acts as a getter. When both name and
  433. * value are provided, acts as a setter and registers that new alias.
  434. *
  435. * If multiple alias are defined, then the replacement is recursive, replacing
  436. * each alias in reverse order.
  437. *
  438. * An alias can be a single String or a Regular Expression. The finding is done
  439. * based on .match().
  440. *
  441. * ### Examples:
  442. *
  443. * env.alias(/^([a-zA-Z0-9:\*]+)$/, 'generator-$1');
  444. * env.alias(/^([^:]+)$/, '$1:app');
  445. * env.alias(/^([^:]+)$/, '$1:all');
  446. * env.alias('foo');
  447. * // => generator-foo:all
  448. *
  449. * @param {String|RegExp} match
  450. * @param {String} value
  451. */
  452. Environment.prototype.alias = function alias(match, value) {
  453. if (match && value) {
  454. this.aliases.push({
  455. match: match instanceof RegExp ? match : new RegExp('^' + match + '$'),
  456. value: value
  457. });
  458. return this;
  459. }
  460. var aliases = this.aliases.slice(0).reverse();
  461. var matcher = aliases.filter(function (alias) {
  462. return alias.match.test(match);
  463. });
  464. return aliases.reduce(function (res, alias) {
  465. if (!alias.match.test(res)) {
  466. return res;
  467. }
  468. return res.replace(alias.match, alias.value);
  469. }, match);
  470. };
  471. /**
  472. * Given a String `filepath`, tries to figure out the relative namespace.
  473. *
  474. * ### Examples:
  475. *
  476. * this.namespace('backbone/all/index.js');
  477. * // => backbone:all
  478. *
  479. * this.namespace('generator-backbone/model');
  480. * // => backbone:model
  481. *
  482. * this.namespace('backbone.js');
  483. * // => backbone
  484. *
  485. * this.namespace('generator-mocha/backbone/model/index.js');
  486. * // => mocha:backbone:model
  487. *
  488. * @param {String} filepath
  489. */
  490. Environment.prototype.namespace = function namespace(filepath) {
  491. if (!filepath) {
  492. throw new Error('Missing namespace');
  493. }
  494. var self = this;
  495. // cleanup extension and normalize path for differents OS
  496. var ns = path.normalize(filepath.replace(path.extname(filepath), ''));
  497. // extend every lookups folders with searchable file system paths
  498. var lookups = _(this.lookups).map(function (lookup) {
  499. return _.map(self.paths, function (filepath) {
  500. return path.join(filepath, lookup);
  501. });
  502. }).flatten().sortBy('length').value().reverse();
  503. // if `ns` contain a lookup dir in it's path, remove it.
  504. ns = lookups.reduce(function (ns, lookup) {
  505. return ns.replace(lookup, '');
  506. }, ns);
  507. // cleanup `ns` from unwanted parts and then normalize slashes to `:`
  508. ns = ns
  509. .replace(/[\/\\]?node_modules[\/\\]?/, '') // remove `/node_modules/`
  510. .replace(/[\/\\](index|main)$/, '') // remove `/index` or `/main`
  511. .replace(/\.+/g, '') // remove `.`
  512. .replace(/^[\/\\]+/, '') // remove leading `/`
  513. .replace(/[\/\\]+/g, ':'); // replace slashes by `:`
  514. // if we still have prefix match at this point, then remove anything before
  515. // that match, this would catch symlinked package with a name begining with
  516. // `generator-*` (one of the configured prefix)
  517. ns = this._prefixes.reduce(function (ns, prefix) {
  518. var pos = ns.lastIndexOf(prefix);
  519. if (pos < 0) {
  520. return ns;
  521. }
  522. return ns.slice(pos + prefix.length);
  523. }, ns);
  524. debug('Resolve namespaces for %s: %s', filepath, ns);
  525. return ns;
  526. };
  527. /**
  528. * Adds the namespace prefix to this environment, such as `generator-*`,
  529. * used when resolving namespace, replacing the leading `*` in the
  530. * namespace by the configured prefix(es).
  531. *
  532. * ### Examples:
  533. *
  534. * this.prefix('generator-');
  535. *
  536. * @param {String} prefix
  537. */
  538. Environment.prototype.prefix = function _prefix(prefix) {
  539. if (!prefix) {
  540. throw new Error('Missing prefix');
  541. }
  542. this._prefixes.push(prefix);
  543. this._prefixReg = new RegExp('^(' + this._prefixes.join('|') + ')');
  544. return this;
  545. };
  546. /**
  547. * Get or set the namespace suffix to this environment, such as `*\index.js`,
  548. * used when resolving namespace, replacing the last `*` in the
  549. * namespace by the configured suffix.
  550. *
  551. * ### Examples:
  552. *
  553. * this.suffix('*\index.js');
  554. * this.suffix();
  555. * // => '*\index.js'
  556. *
  557. * @param {String} suffix
  558. */
  559. Environment.prototype.suffix = function _suffix(suffix) {
  560. this._suffix = this._suffix || '';
  561. if (!suffix) {
  562. return this._suffix;
  563. }
  564. this._suffix = suffix;
  565. return this;
  566. };
  567. /**
  568. * Walk up the filesystem looking for a `node_modules` folder, and add it if
  569. * found to the load path.
  570. *
  571. * @param {String} filename
  572. * @param {String} basedir
  573. */
  574. Environment.prototype.plugins = function plugins(filename, basedir) {
  575. filename = filename || 'node_modules';
  576. basedir = basedir || process.cwd();
  577. var filepath = path.join(basedir, filename);
  578. if (fs.existsSync(filepath)) {
  579. this.appendPath(filepath);
  580. return this;
  581. }
  582. if (basedir === path.resolve('/')) {
  583. return this;
  584. }
  585. return this.plugins(filename, path.join(basedir, '..'));
  586. };
  587. /**
  588. * Install an npm package locally, expanding github like user/repo pattern to
  589. * the remote tarball for master.
  590. *
  591. * It is taking care of potential remote packages (or local on the current file
  592. * system) by delegating the groundwork of getting the package to npm.
  593. *
  594. * @param {String} name
  595. * @param {Function} done
  596. */
  597. Environment.prototype.remote = function remote(name, done) {
  598. var self = this;
  599. log.write().info('Installing remote package %s', name).write();
  600. var npm = spawn('npm', ['install', name, '--save-dev']);
  601. npm.stdout.pipe(process.stdout);
  602. npm.stderr.pipe(process.stderr);
  603. done = done || function () {};
  604. npm.on('exit', function (code) {
  605. if (code !== 0) {
  606. return self.error(new Error('Error initing from remote: ' + name + '. Code: ' + code));
  607. }
  608. log.ok('Installed %s package', name).write();
  609. log.info('You should see additional generators available').write()
  610. .write(self.help()).write();
  611. done();
  612. });
  613. };