resolver.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. 'use strict';
  2. const path = require('path');
  3. const fs = require('fs');
  4. const _ = require('lodash');
  5. const globby = require('globby');
  6. const debug = require('debug')('yeoman:environment');
  7. const spawn = require('cross-spawn');
  8. const win32 = process.platform === 'win32';
  9. const nvm = process.env.NVM_HOME;
  10. /**
  11. * @mixin
  12. * @alias env/resolver
  13. */
  14. const resolver = module.exports;
  15. /**
  16. * Search for generators and their sub generators.
  17. *
  18. * A generator is a `:lookup/:name/index.js` file placed inside an npm package.
  19. *
  20. * Defaults lookups are:
  21. * - ./
  22. * - generators/
  23. * - lib/generators/
  24. *
  25. * So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be
  26. * registered as `dummy:yo` generator.
  27. *
  28. * @param {function} cb - Callback called once the lookup is done. Take err as first
  29. * parameter.
  30. */
  31. resolver.lookup = function (cb) {
  32. const generatorsModules = this.findGeneratorsIn(this.getNpmPaths().reverse());
  33. const patterns = [];
  34. for (const lookup of this.lookups) {
  35. for (const modulePath of generatorsModules) {
  36. patterns.push(path.join(modulePath, lookup));
  37. }
  38. }
  39. for (const pattern of patterns) {
  40. for (const filename of globby.sync('*/index.js', {cwd: pattern, absolute: true})) {
  41. this._tryRegistering(filename);
  42. }
  43. }
  44. if (typeof cb === 'function') {
  45. return cb(null);
  46. }
  47. };
  48. /**
  49. * Search npm for every available generators.
  50. * Generators are npm packages who's name start with `generator-` and who're placed in the
  51. * top level `node_module` path. They can be installed globally or locally.
  52. *
  53. * @param {Array} List of search paths
  54. * @return {Array} List of the generator modules path
  55. */
  56. resolver.findGeneratorsIn = function (searchPaths) {
  57. let modules = [];
  58. for (const root of searchPaths) {
  59. if (!root) {
  60. continue;
  61. }
  62. // Some folders might not be readable to the current user. For those, we add a try
  63. // catch to handle the error gracefully as globby doesn't have an option to skip
  64. // restricted folders.
  65. try {
  66. modules = modules.concat(globby.sync(
  67. ['generator-*', '@*/generator-*'],
  68. {cwd: root, onlyFiles: false, absolute: true}
  69. ));
  70. } catch (err) {
  71. debug('Could not access %s (%s)', root, err);
  72. }
  73. }
  74. return modules;
  75. };
  76. /**
  77. * Try registering a Generator to this environment.
  78. * @private
  79. * @param {String} generatorReference A generator reference, usually a file path.
  80. */
  81. resolver._tryRegistering = function (generatorReference) {
  82. let namespace;
  83. const realPath = fs.realpathSync(generatorReference);
  84. try {
  85. debug('found %s, trying to register', generatorReference);
  86. if (realPath !== generatorReference) {
  87. namespace = this.namespace(generatorReference);
  88. }
  89. this.register(realPath, namespace);
  90. } catch (err) {
  91. console.error('Unable to register %s (Error: %s)', generatorReference, err.message);
  92. }
  93. };
  94. /**
  95. * Get the npm lookup directories (`node_modules/`)
  96. * @return {Array} lookup paths
  97. */
  98. resolver.getNpmPaths = function () {
  99. let paths = [];
  100. // Default paths for each system
  101. if (nvm) {
  102. paths.push(path.join(process.env.NVM_HOME, process.version, 'node_modules'));
  103. } else if (win32) {
  104. paths.push(path.join(process.env.APPDATA, 'npm/node_modules'));
  105. } else {
  106. paths.push('/usr/lib/node_modules');
  107. paths.push('/usr/local/lib/node_modules');
  108. }
  109. // Add NVM prefix directory
  110. if (process.env.NVM_PATH) {
  111. paths.push(path.join(path.dirname(process.env.NVM_PATH), 'node_modules'));
  112. }
  113. // Adding global npm directories
  114. // We tried using npm to get the global modules path, but it haven't work out
  115. // because of bugs in the parseable implementation of `ls` command and mostly
  116. // performance issues. So, we go with our best bet for now.
  117. if (process.env.NODE_PATH) {
  118. paths = _.compact(process.env.NODE_PATH.split(path.delimiter)).concat(paths);
  119. }
  120. // global node_modules should be 4 or 2 directory up this one (most of the time)
  121. paths.push(path.join(__dirname, '../../../..'));
  122. paths.push(path.join(__dirname, '../..'));
  123. // Get yarn global directory and infer the module paths from there
  124. const testYarn = spawn.sync('yarn', ['global', 'dir'], {encoding: 'utf8'});
  125. if (!testYarn.error) {
  126. const yarnBase = testYarn.stdout.trim();
  127. paths.push(path.resolve(yarnBase, 'node_modules'));
  128. paths.push(path.resolve(yarnBase, '../link/'));
  129. }
  130. // Get npm global prefix and infer the module paths from there
  131. const testNpm = spawn.sync('npm', ['-g', 'prefix'], {encoding: 'utf8'});
  132. if (!testNpm.error) {
  133. const npmBase = testNpm.stdout.trim();
  134. paths.push(path.resolve(npmBase, 'lib/node_modules'));
  135. }
  136. // Adds support for generator resolving when yeoman-generator has been linked
  137. if (process.argv[1]) {
  138. paths.push(path.join(path.dirname(process.argv[1]), '../..'));
  139. }
  140. // Walk up the CWD and add `node_modules/` folder lookup on each level
  141. process.cwd().split(path.sep).forEach((part, i, parts) => {
  142. let lookup = path.join(...parts.slice(0, i + 1), 'node_modules');
  143. if (!win32) {
  144. lookup = `/${lookup}`;
  145. }
  146. paths.push(lookup);
  147. });
  148. return _.uniq(paths.reverse());
  149. };
  150. /**
  151. * Get or create an alias.
  152. *
  153. * Alias allows the `get()` and `lookup()` methods to search in alternate
  154. * filepath for a given namespaces. It's used for example to map `generator-*`
  155. * npm package to their namespace equivalent (without the generator- prefix),
  156. * or to default a single namespace like `angular` to `angular:app` or
  157. * `angular:all`.
  158. *
  159. * Given a single argument, this method acts as a getter. When both name and
  160. * value are provided, acts as a setter and registers that new alias.
  161. *
  162. * If multiple alias are defined, then the replacement is recursive, replacing
  163. * each alias in reverse order.
  164. *
  165. * An alias can be a single String or a Regular Expression. The finding is done
  166. * based on .match().
  167. *
  168. * @param {String|RegExp} match
  169. * @param {String} value
  170. *
  171. * @example
  172. *
  173. * env.alias(/^([a-zA-Z0-9:\*]+)$/, 'generator-$1');
  174. * env.alias(/^([^:]+)$/, '$1:app');
  175. * env.alias(/^([^:]+)$/, '$1:all');
  176. * env.alias('foo');
  177. * // => generator-foo:all
  178. */
  179. resolver.alias = function (match, value) {
  180. if (match && value) {
  181. this.aliases.push({
  182. match: match instanceof RegExp ? match : new RegExp(`^${match}$`),
  183. value
  184. });
  185. return this;
  186. }
  187. const aliases = this.aliases.slice(0).reverse();
  188. return aliases.reduce((res, alias) => {
  189. if (!alias.match.test(res)) {
  190. return res;
  191. }
  192. return res.replace(alias.match, alias.value);
  193. }, match);
  194. };