environment.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const EventEmitter = require('events');
  5. const chalk = require('chalk');
  6. const _ = require('lodash');
  7. const GroupedQueue = require('grouped-queue');
  8. const escapeStrRe = require('escape-string-regexp');
  9. const untildify = require('untildify');
  10. const memFs = require('mem-fs');
  11. const debug = require('debug')('yeoman:environment');
  12. const isScoped = require('is-scoped');
  13. const Store = require('./store');
  14. const resolver = require('./resolver');
  15. const TerminalAdapter = require('./adapter');
  16. /**
  17. * Two-step argument splitting function that first splits arguments in quotes,
  18. * and then splits up the remaining arguments if they are not part of a quote.
  19. */
  20. function splitArgsFromString(argsString) {
  21. let result = [];
  22. const quoteSeparatedArgs = argsString.split(/(\x22[^\x22]*\x22)/).filter(x => x);
  23. quoteSeparatedArgs.forEach(arg => {
  24. if (arg.match('\x22')) {
  25. result.push(arg.replace(/\x22/g, ''));
  26. } else {
  27. result = result.concat(arg.trim().split(' '));
  28. }
  29. });
  30. return result;
  31. }
  32. /**
  33. * `Environment` object is responsible of handling the lifecyle and bootstrap
  34. * of generators in a specific environment (your app).
  35. *
  36. * It provides a high-level API to create and run generators, as well as further
  37. * tuning where and how a generator is resolved.
  38. *
  39. * An environment is created using a list of `arguments` and a Hash of
  40. * `options`. Usually, this is the list of arguments you get back from your CLI
  41. * options parser.
  42. *
  43. * An optional adapter can be passed to provide interaction in non-CLI environment
  44. * (e.g. IDE plugins), otherwise a `TerminalAdapter` is instantiated by default
  45. *
  46. * @constructor
  47. * @mixes env/resolver
  48. * @param {String|Array} args
  49. * @param {Object} opts
  50. * @param {TerminalAdapter} [adaper] - A TerminalAdapter instance or another object
  51. * implementing this adapter interface. This is how
  52. * you'd interface Yeoman with a GUI or an editor.
  53. */
  54. class Environment extends EventEmitter {
  55. static get queues() {
  56. return [
  57. 'initializing',
  58. 'prompting',
  59. 'configuring',
  60. 'default',
  61. 'writing',
  62. 'conflicts',
  63. 'install',
  64. 'end'
  65. ];
  66. }
  67. /**
  68. * Make sure the Environment present expected methods if an old version is
  69. * passed to a Generator.
  70. * @param {Environment} env
  71. * @return {Environment} The updated env
  72. */
  73. static enforceUpdate(env) {
  74. if (!env.adapter) {
  75. env.adapter = new TerminalAdapter();
  76. }
  77. if (!env.runLoop) {
  78. env.runLoop = new GroupedQueue([
  79. 'initializing',
  80. 'prompting',
  81. 'configuring',
  82. 'default',
  83. 'writing',
  84. 'conflicts',
  85. 'install',
  86. 'end'
  87. ]);
  88. }
  89. if (!env.sharedFs) {
  90. env.sharedFs = memFs.create();
  91. }
  92. return env;
  93. }
  94. /**
  95. * Factory method to create an environment instance. Take same parameters as the
  96. * Environment constructor.
  97. *
  98. * @see This method take the same arguments as {@link Environment} constructor
  99. *
  100. * @return {Environment} a new Environment instance
  101. */
  102. static createEnv(args, opts, adapter) {
  103. return new Environment(args, opts, adapter);
  104. }
  105. /**
  106. * Convert a generators namespace to its name
  107. *
  108. * @param {String} namespace
  109. * @return {String}
  110. */
  111. static namespaceToName(namespace) {
  112. return namespace.split(':')[0];
  113. }
  114. constructor(args, opts, adapter) {
  115. super();
  116. args = args || [];
  117. this.arguments = Array.isArray(args) ? args : splitArgsFromString(args);
  118. this.options = opts || {};
  119. this.adapter = adapter || new TerminalAdapter();
  120. this.cwd = this.options.cwd || process.cwd();
  121. this.store = new Store();
  122. this.runLoop = new GroupedQueue(Environment.queues);
  123. this.sharedFs = memFs.create();
  124. // Each composed generator might set listeners on these shared resources. Let's make sure
  125. // Node won't complain about event listeners leaks.
  126. this.runLoop.setMaxListeners(0);
  127. this.sharedFs.setMaxListeners(0);
  128. this.lookups = ['.', 'generators', 'lib/generators'];
  129. this.aliases = [];
  130. this.alias(/^([^:]+)$/, '$1:app');
  131. }
  132. /**
  133. * Error handler taking `err` instance of Error.
  134. *
  135. * The `error` event is emitted with the error object, if no `error` listener
  136. * is registered, then we throw the error.
  137. *
  138. * @param {Object} err
  139. * @return {Error} err
  140. */
  141. error(err) {
  142. err = err instanceof Error ? err : new Error(err);
  143. if (!this.emit('error', err)) {
  144. throw err;
  145. }
  146. return err;
  147. }
  148. /**
  149. * Outputs the general help and usage. Optionally, if generators have been
  150. * registered, the list of available generators is also displayed.
  151. *
  152. * @param {String} name
  153. */
  154. help(name) {
  155. name = name || 'init';
  156. const out = [
  157. 'Usage: :binary: GENERATOR [args] [options]',
  158. '',
  159. 'General options:',
  160. ' --help # Print generator\'s options and usage',
  161. ' -f, --force # Overwrite files that already exist',
  162. '',
  163. 'Please choose a generator below.',
  164. ''
  165. ];
  166. const ns = this.namespaces();
  167. const groups = {};
  168. for (const namespace of ns) {
  169. const base = namespace.split(':')[0];
  170. if (!groups[base]) {
  171. groups[base] = [];
  172. }
  173. groups[base].push(namespace);
  174. }
  175. for (const key of Object.keys(groups).sort()) {
  176. const group = groups[key];
  177. if (group.length >= 1) {
  178. out.push('', key.charAt(0).toUpperCase() + key.slice(1));
  179. }
  180. for (const ns of groups[key]) {
  181. out.push(` ${ns}`);
  182. }
  183. }
  184. return out.join('\n').replace(/:binary:/g, name);
  185. }
  186. /**
  187. * Registers a specific `generator` to this environment. This generator is stored under
  188. * provided namespace, or a default namespace format if none if available.
  189. *
  190. * @param {String} name - Filepath to the a generator or a npm package name
  191. * @param {String} namespace - Namespace under which register the generator (optional)
  192. * @return {String} namespace - Namespace assigned to the registered generator
  193. */
  194. register(name, namespace) {
  195. if (typeof name !== 'string') {
  196. return this.error(new Error('You must provide a generator name to register.'));
  197. }
  198. const modulePath = this.resolveModulePath(name);
  199. namespace = namespace || this.namespace(modulePath);
  200. if (!namespace) {
  201. return this.error(new Error('Unable to determine namespace.'));
  202. }
  203. this.store.add(namespace, modulePath);
  204. debug('Registered %s (%s)', namespace, modulePath);
  205. return this;
  206. }
  207. /**
  208. * Register a stubbed generator to this environment. This method allow to register raw
  209. * functions under the provided namespace. `registerStub` will enforce the function passed
  210. * to extend the Base generator automatically.
  211. *
  212. * @param {Function} Generator - A Generator constructor or a simple function
  213. * @param {String} namespace - Namespace under which register the generator
  214. * @param {String} [resolved] - The file path to the generator
  215. * @return {this}
  216. */
  217. registerStub(Generator, namespace, resolved) {
  218. if (typeof Generator !== 'function') {
  219. return this.error(new Error('You must provide a stub function to register.'));
  220. }
  221. if (typeof namespace !== 'string') {
  222. return this.error(new Error('You must provide a namespace to register.'));
  223. }
  224. this.store.add(namespace, Generator, resolved);
  225. return this;
  226. }
  227. /**
  228. * Returns the list of registered namespace.
  229. * @return {Array}
  230. */
  231. namespaces() {
  232. return this.store.namespaces();
  233. }
  234. /**
  235. * Returns stored generators meta
  236. * @return {Object}
  237. */
  238. getGeneratorsMeta() {
  239. return this.store.getGeneratorsMeta();
  240. }
  241. /**
  242. * Get registered generators names
  243. *
  244. * @return {Array}
  245. */
  246. getGeneratorNames() {
  247. return _.uniq(Object.keys(this.getGeneratorsMeta()).map(Environment.namespaceToName));
  248. }
  249. /**
  250. * Get a single generator from the registered list of generators. The lookup is
  251. * based on generator's namespace, "walking up" the namespaces until a matching
  252. * is found. Eg. if an `angular:common` namespace is registered, and we try to
  253. * get `angular:common:all` then we get `angular:common` as a fallback (unless
  254. * an `angular:common:all` generator is registered).
  255. *
  256. * @param {String} namespaceOrPath
  257. * @return {Generator|null} - the generator registered under the namespace
  258. */
  259. get(namespaceOrPath) {
  260. // Stop the recursive search if nothing is left
  261. if (!namespaceOrPath) {
  262. return;
  263. }
  264. let namespace = namespaceOrPath;
  265. // Legacy yeoman-generator `#hookFor()` function is passing the generator path as part
  266. // of the namespace. If we find a path delimiter in the namespace, then ignore the
  267. // last part of the namespace.
  268. const parts = namespaceOrPath.split(':');
  269. const maybePath = _.last(parts);
  270. if (parts.length > 1 && /[/\\]/.test(maybePath)) {
  271. parts.pop();
  272. // We also want to remove the drive letter on windows
  273. if (maybePath.indexOf('\\') >= 0 && _.last(parts).length === 1) {
  274. parts.pop();
  275. }
  276. namespace = parts.join(':');
  277. }
  278. return this.store.get(namespace) ||
  279. this.store.get(this.alias(namespace)) ||
  280. // Namespace is empty if namespaceOrPath contains a win32 absolute path of the form 'C:\path\to\generator'.
  281. // for this reason we pass namespaceOrPath to the getByPath function.
  282. this.getByPath(namespaceOrPath);
  283. }
  284. /**
  285. * Get a generator by path instead of namespace.
  286. * @param {String} path
  287. * @return {Generator|null} - the generator found at the location
  288. */
  289. getByPath(path) {
  290. if (fs.existsSync(path)) {
  291. const namespace = this.namespace(path);
  292. this.register(path, namespace);
  293. return this.get(namespace);
  294. }
  295. }
  296. /**
  297. * Create is the Generator factory. It takes a namespace to lookup and optional
  298. * hash of options, that lets you define `arguments` and `options` to
  299. * instantiate the generator with.
  300. *
  301. * An error is raised on invalid namespace.
  302. *
  303. * @param {String} namespace
  304. * @param {Object} options
  305. */
  306. create(namespace, options) {
  307. options = options || {};
  308. const Generator = this.get(namespace);
  309. if (typeof Generator !== 'undefined' && typeof Generator.default === 'function') {
  310. return this.instantiate(Generator.default, options);
  311. }
  312. if (typeof Generator !== 'function') {
  313. let generatorHint = '';
  314. if (isScoped(namespace)) {
  315. const splitName = namespace.split('/');
  316. generatorHint = `${splitName[0]}/generator-${splitName[1]}`;
  317. } else {
  318. generatorHint = `generator-${namespace}`;
  319. }
  320. return this.error(
  321. new Error(
  322. chalk.red('You don\'t seem to have a generator with the name “' + namespace + '” installed.') + '\n' +
  323. 'But help is on the way:\n\n' +
  324. 'You can see available generators via ' +
  325. chalk.yellow('npm search yeoman-generator') + ' or via ' + chalk.yellow('http://yeoman.io/generators/') + '. \n' +
  326. 'Install them with ' + chalk.yellow(`npm install ${generatorHint}`) + '.\n\n' +
  327. 'To see all your installed generators run ' + chalk.yellow('yo') + ' without any arguments. ' +
  328. 'Adding the ' + chalk.yellow('--help') + ' option will also show subgenerators. \n\n' +
  329. 'If ' + chalk.yellow('yo') + ' cannot find the generator, run ' + chalk.yellow('yo doctor') + ' to troubleshoot your system.'
  330. )
  331. );
  332. }
  333. return this.instantiate(Generator, options);
  334. }
  335. /**
  336. * Instantiate a Generator with metadatas
  337. *
  338. * @param {String} namespace
  339. * @param {Object} options
  340. * @param {Array|String} options.arguments Arguments to pass the instance
  341. * @param {Object} options.options Options to pass the instance
  342. */
  343. instantiate(Generator, options) {
  344. options = options || {};
  345. let args = options.arguments || options.args || _.clone(this.arguments);
  346. args = Array.isArray(args) ? args : splitArgsFromString(args);
  347. const opts = options.options || _.clone(this.options);
  348. opts.env = this;
  349. opts.resolved = Generator.resolved || 'unknown';
  350. opts.namespace = Generator.namespace;
  351. return new Generator(args, opts);
  352. }
  353. /**
  354. * Tries to locate and run a specific generator. The lookup is done depending
  355. * on the provided arguments, options and the list of registered generators.
  356. *
  357. * When the environment was unable to resolve a generator, an error is raised.
  358. *
  359. * @param {String|Array} args
  360. * @param {Object} options
  361. * @param {Function} done
  362. */
  363. run(args, options, done) {
  364. args = args || this.arguments;
  365. if (typeof options === 'function') {
  366. done = options;
  367. options = this.options;
  368. }
  369. if (typeof args === 'function') {
  370. done = args;
  371. options = this.options;
  372. args = this.arguments;
  373. }
  374. args = Array.isArray(args) ? args : splitArgsFromString(args);
  375. options = options || this.options;
  376. const name = args.shift();
  377. if (!name) {
  378. return this.error(new Error('Must provide at least one argument, the generator namespace to invoke.'));
  379. }
  380. const generator = this.create(name, {
  381. args,
  382. options
  383. });
  384. if (generator instanceof Error) {
  385. return generator;
  386. }
  387. if (options.help) {
  388. return console.log(generator.help());
  389. }
  390. return generator.run(done);
  391. }
  392. /**
  393. * Given a String `filepath`, tries to figure out the relative namespace.
  394. *
  395. * ### Examples:
  396. *
  397. * this.namespace('backbone/all/index.js');
  398. * // => backbone:all
  399. *
  400. * this.namespace('generator-backbone/model');
  401. * // => backbone:model
  402. *
  403. * this.namespace('backbone.js');
  404. * // => backbone
  405. *
  406. * this.namespace('generator-mocha/backbone/model/index.js');
  407. * // => mocha:backbone:model
  408. *
  409. * @param {String} filepath
  410. */
  411. namespace(filepath) {
  412. if (!filepath) {
  413. throw new Error('Missing namespace');
  414. }
  415. // Cleanup extension and normalize path for differents OS
  416. let ns = path.normalize(filepath.replace(new RegExp(escapeStrRe(path.extname(filepath)) + '$'), ''));
  417. // Sort lookups by length so biggest are removed first
  418. const lookups = _(this.lookups.concat(['..'])).map(path.normalize).sortBy('length').value().reverse();
  419. // If `ns` contains a lookup dir in its path, remove it.
  420. ns = lookups.reduce((ns, lookup) => {
  421. // Only match full directory (begin with leading slash or start of input, end with trailing slash)
  422. lookup = new RegExp(`(?:\\\\|/|^)${escapeStrRe(lookup)}(?=\\\\|/)`, 'g');
  423. return ns.replace(lookup, '');
  424. }, ns);
  425. const folders = ns.split(path.sep);
  426. const scope = _.findLast(folders, folder => folder.indexOf('@') === 0);
  427. // Cleanup `ns` from unwanted parts and then normalize slashes to `:`
  428. ns = ns
  429. .replace(/(.*generator-)/, '') // Remove before `generator-`
  430. .replace(/[/\\](index|main)$/, '') // Remove `/index` or `/main`
  431. .replace(/^[/\\]+/, '') // Remove leading `/`
  432. .replace(/[/\\]+/g, ':'); // Replace slashes by `:`
  433. if (scope) {
  434. ns = `${scope}/${ns}`;
  435. }
  436. debug('Resolve namespaces for %s: %s', filepath, ns);
  437. return ns;
  438. }
  439. /**
  440. * Resolve a module path
  441. * @param {String} moduleId - Filepath or module name
  442. * @return {String} - The resolved path leading to the module
  443. */
  444. resolveModulePath(moduleId) {
  445. if (moduleId[0] === '.') {
  446. moduleId = path.resolve(moduleId);
  447. }
  448. if (path.extname(moduleId) === '') {
  449. moduleId += path.sep;
  450. }
  451. return require.resolve(untildify(moduleId));
  452. }
  453. }
  454. Object.assign(Environment.prototype, resolver);
  455. /**
  456. * Expose the utilities on the module
  457. * @see {@link env/util}
  458. */
  459. Environment.util = require('./util/util');
  460. module.exports = Environment;