1. 1 : 'use strict';
  2. 2 : const fs = require('fs');
  3. 3 : const path = require('path');
  4. 4 : const EventEmitter = require('events');
  5. 5 : const chalk = require('chalk');
  6. 6 : const _ = require('lodash');
  7. 7 : const GroupedQueue = require('grouped-queue');
  8. 8 : const escapeStrRe = require('escape-string-regexp');
  9. 9 : const untildify = require('untildify');
  10. 10 : const memFs = require('mem-fs');
  11. 11 : const FileEditor = require('mem-fs-editor');
  12. 12 : const debug = require('debug')('yeoman:environment');
  13. 13 : const isScoped = require('is-scoped');
  14. 14 : const crypto = require('crypto');
  15. 15 : const npmlog = require('npmlog');
  16. 16 : const {TrackerGroup} = require('are-we-there-yet');
  17. 17 :
  18. 18 : const {promisify} = require('util');
  19. 19 : const {pipeline: _pipeline} = require('stream');
  20. 20 : const pipeline = promisify(_pipeline);
  21. 21 :
  22. 22 : const ENVIRONMENT_VERSION = require('../package.json').version;
  23. 23 : const Store = require('./store');
  24. 24 : const composability = require('./composability');
  25. 25 : const resolver = require('./resolver');
  26. 26 : const TerminalAdapter = require('./adapter');
  27. 27 : const YeomanRepository = require('./util/repository');
  28. 28 : const Conflicter = require('./util/conflicter');
  29. 29 : const {YeomanCommand} = require('./util/command');
  30. 30 : const {
  31. 31 : createCommitTransform,
  32. 32 : createConflicterCheckTransform,
  33. 33 : createConflicterStatusTransform,
  34. 34 : createEachFileTransform,
  35. 35 : createModifiedTransform,
  36. 36 : createYoRcTransform,
  37. 37 : createYoResolveTransform
  38. 38 : } = require('./util/transform');
  39. 39 :
  40. 40 : /**
  41. 41 : * Two-step argument splitting function that first splits arguments in quotes,
  42. 42 : * and then splits up the remaining arguments if they are not part of a quote.
  43. 43 : */
  44. 44 : function splitArgsFromString(argsString) {
  45. 45 : let result = [];
  46. 46 : if (!argsString) {
  47. 47 : return result;
  48. 48 : }
  49. 49 : const quoteSeparatedArgs = argsString.split(/("[^"]*")/).filter(x => x);
  50. 50 : quoteSeparatedArgs.forEach(arg => {
  51. 51 : if (arg.match('\x22')) {
  52. 52 : result.push(arg.replace(/"/g, ''));
  53. 53 : } else {
  54. 54 : result = result.concat(arg.trim().split(' '));
  55. 55 : }
  56. 56 : });
  57. 57 : return result;
  58. 58 : }
  59. 59 :
  60. 60 : /**
  61. 61 : * Hint of generator module name
  62. 62 : */
  63. 63 : function getGeneratorHint(namespace) {
  64. 64 : if (isScoped(namespace)) {
  65. 65 : const splitName = namespace.split('/');
  66. 66 : return `${splitName[0]}/generator-${splitName[1]}`;
  67. 67 : }
  68. 68 : return `generator-${namespace}`;
  69. 69 : }
  70. 70 :
  71. 71 : const mixins = [
  72. 72 : require('./command.js'),
  73. 73 : require('./generator-features.js'),
  74. 74 : require('./namespace'),
  75. 75 : require('./package-manager.js')
  76. 76 : ];
  77. 77 :
  78. 78 : const Base = mixins.reduce((a, b) => b(a), EventEmitter);
  79. 79 :
  80. 80 : class Environment extends Base {
  81. 81 : static get UNKNOWN_NAMESPACE() {
  82. 82 : return 'unknownnamespace';
  83. 83 : }
  84. 84 :
  85. 85 : static get UNKNOWN_RESOLVED() {
  86. 86 : return 'unknown';
  87. 87 : }
  88. 88 :
  89. 89 : static get queues() {
  90. 90 : return [
  91. 91 : 'environment:run',
  92. 92 : 'initializing',
  93. 93 : 'prompting',
  94. 94 : 'configuring',
  95. 95 : 'default',
  96. 96 : 'writing',
  97. 97 : 'transform',
  98. 98 : 'conflicts',
  99. 99 : 'environment:conflicts',
  100. 100 : 'install',
  101. 101 : 'end'
  102. 102 : ];
  103. 103 : }
  104. 104 :
  105. 105 : static get lookups() {
  106. 106 : return ['.', 'generators', 'lib/generators', 'dist/generators'];
  107. 107 : }
  108. 108 :
  109. 109 : /**
  110. 110 : * Make sure the Environment present expected methods if an old version is
  111. 111 : * passed to a Generator.
  112. 112 : * @param {Environment} env
  113. 113 : * @return {Environment} The updated env
  114. 114 : */
  115. 115 : static enforceUpdate(env) {
  116. 116 : if (!env.adapter) {
  117. 117 : env.adapter = new TerminalAdapter();
  118. 118 : }
  119. 119 :
  120. 120 : if (!env.runLoop) {
  121. 121 : env.runLoop = new GroupedQueue(Environment.queues, false);
  122. 122 : }
  123. 123 :
  124. 124 : if (!env.sharedFs) {
  125. 125 : env.sharedFs = memFs.create();
  126. 126 : }
  127. 127 :
  128. 128 : if (!env.fs) {
  129. 129 : env.fs = FileEditor.create(env.sharedFs);
  130. 130 : }
  131. 131 :
  132. 132 : return env;
  133. 133 : }
  134. 134 :
  135. 135 : /**
  136. 136 : * Prepare a commander instance for cli support.
  137. 137 : *
  138. 138 : * @param {Class} GeneratorClass - Generator to create Command
  139. 139 : * @return {Command} Return a Command instance
  140. 140 : */
  141. 141 : static prepareCommand(GeneratorClass, command = new YeomanCommand()) {
  142. 142 : command = Base.addEnvironmentOptions(command);
  143. 143 : return Environment.prepareGeneratorCommand(command, GeneratorClass);
  144. 144 : }
  145. 145 :
  146. 146 : /**
  147. 147 : * Prepare a commander instance for cli support.
  148. 148 : *
  149. 149 : * @param {Command} command - Command to be prepared
  150. 150 : * @param {Class} GeneratorClass - Generator to create Command
  151. 151 : * @return {Command} return command
  152. 152 : */
  153. 153 : static prepareGeneratorCommand(command, GeneratorClass) {
  154. 154 : const generator = new GeneratorClass([], {help: true, env: {}});
  155. 155 : Base.addGeneratorOptions(command, generator);
  156. 156 :
  157. 157 : command.action(async function () {
  158. 158 : command.env = Environment.createEnv(this.opts());
  159. 159 :
  160. 160 : let rootCommand = this;
  161. 161 : while (rootCommand.parent) {
  162. 162 : rootCommand = rootCommand.parent;
  163. 163 : }
  164. 164 : rootCommand.emit('yeoman:environment', command.env);
  165. 165 :
  166. 166 : const generator = command.env.instantiate(GeneratorClass, this.args, this.opts());
  167. 167 : await command.env.queueGenerator(generator);
  168. 168 : return command.env.start().then(() => command.env);
  169. 169 : });
  170. 170 : return command;
  171. 171 : }
  172. 172 :
  173. 173 : /**
  174. 174 : * Factory method to create an environment instance. Take same parameters as the
  175. 175 : * Environment constructor.
  176. 176 : *
  177. 177 : * @deprecated @param {string[]} [args] - arguments.
  178. 178 : * @param {object} [options] - Environment options.
  179. 179 : * @param {Adapter} [adapter] - Terminal adapter.
  180. 180 : *
  181. 181 : * @return {Environment} a new Environment instance
  182. 182 : */
  183. 183 : static createEnv(args, options, adapter) {
  184. 184 : if (args && !Array.isArray(args)) {
  185. 185 : options = args;
  186. 186 : }
  187. 187 : options = options || {};
  188. 188 : return new Environment(options, adapter);
  189. 189 : }
  190. 190 :
  191. 191 : /**
  192. 192 : * Factory method to create an environment instance. Take same parameters as the
  193. 193 : * Environment constructor.
  194. 194 : *
  195. 195 : * @param {String} version - Version of the Environment
  196. 196 : * @param {...any} args - Same arguments as {@link Environment}#createEnv.
  197. 197 : * @return {Environment} a new Environment instance
  198. 198 : */
  199. 199 : static async createEnvWithVersion(version, ...args) {
  200. 200 : const repository = new YeomanRepository();
  201. 201 : const installedVersion = repository.verifyInstalledVersion('yeoman-environment', version);
  202. 202 : if (!installedVersion) {
  203. 203 : await repository.installPackage('yeoman-environment', version);
  204. 204 : }
  205. 205 : const VersionedEnvironment = repository.requireModule('yeoman-environment', version);
  206. 206 : return VersionedEnvironment.createEnv(...args);
  207. 207 : }
  208. 208 :
  209. 209 : /**
  210. 210 : * Convert a generators namespace to its name
  211. 211 : *
  212. 212 : * @param {String} namespace
  213. 213 : * @return {String}
  214. 214 : */
  215. 215 : static namespaceToName(namespace) {
  216. 216 : return namespace.split(':')[0];
  217. 217 : }
  218. 218 :
  219. 219 : /**
  220. 220 : * Lookup for a specific generator.
  221. 221 : *
  222. 222 : * @param {String} namespace
  223. 223 : * @param {Object} [options]
  224. 224 : * @param {Boolean} [options.localOnly=false] - Set true to skip lookups of
  225. 225 : * globally-installed generators.
  226. 226 : * @param {Boolean} [options.packagePath=false] - Set true to return the package
  227. 227 : * path instead of generators file.
  228. 228 : * @param {Boolean} [options.singleResult=true] - Set false to return multiple values.
  229. 229 : * @return {String} generator
  230. 230 : */
  231. 231 : static lookupGenerator(namespace, options) {
  232. 232 : options = typeof options === 'boolean' ? {singleResult: true, localOnly: options} : {singleResult: !(options && options.multiple), ...options};
  233. 233 :
  234. 234 : options.filePatterns = options.filePatterns || Environment.lookups.map(prefix => path.join(prefix, '*/index.{js,ts}'));
  235. 235 :
  236. 236 : const name = Environment.namespaceToName(namespace);
  237. 237 : options.packagePatterns = options.packagePatterns || getGeneratorHint(name);
  238. 238 : const envProt = Environment.prototype;
  239. 239 :
  240. 240 : options.npmPaths = options.npmPaths || envProt.getNpmPaths(options.localOnly).reverse();
  241. 241 : options.packagePatterns = options.packagePatterns || 'generator-*';
  242. 242 : options.packagePaths = options.packagePaths || resolver.packageLookup.findPackagesIn(options.npmPaths, options.packagePatterns);
  243. 243 :
  244. 244 : let paths = options.singleResult ? undefined : [];
  245. 245 : resolver.packageLookup.sync(options, module => {
  246. 246 : const filename = module.filePath;
  247. 247 : const fileNS = envProt.namespace(filename, Environment.lookups);
  248. 248 : if (namespace === fileNS || (options.packagePath && namespace === Environment.namespaceToName(fileNS))) {
  249. 249 : // Version 2.6.0 returned pattern instead of modulePath for options.packagePath
  250. 250 : const returnPath = options.packagePath ? module.packagePath : (options.generatorPath ? path.posix.join(filename, '../../') : filename);
  251. 251 : if (options.singleResult) {
  252. 252 : paths = returnPath;
  253. 253 : return true;
  254. 254 : }
  255. 255 : paths.push(returnPath);
  256. 256 : }
  257. 257 : return false;
  258. 258 : });
  259. 259 :
  260. 260 : return paths;
  261. 261 : }
  262. 262 :
  263. 263 : /**
  264. 264 : * @classdesc `Environment` object is responsible of handling the lifecyle and bootstrap
  265. 265 : * of generators in a specific environment (your app).
  266. 266 : *
  267. 267 : * It provides a high-level API to create and run generators, as well as further
  268. 268 : * tuning where and how a generator is resolved.
  269. 269 : *
  270. 270 : * An environment is created using a list of `arguments` and a Hash of
  271. 271 : * `options`. Usually, this is the list of arguments you get back from your CLI
  272. 272 : * options parser.
  273. 273 : *
  274. 274 : * An optional adapter can be passed to provide interaction in non-CLI environment
  275. 275 : * (e.g. IDE plugins), otherwise a `TerminalAdapter` is instantiated by default
  276. 276 : *
  277. 277 : * @constructor
  278. 278 : * @mixes env/resolver
  279. 279 : * @mixes env/composability
  280. 280 : * @param {String|Array} args
  281. 281 : * @param {Object} opts
  282. 282 : * @param {Boolean} [opts.experimental]
  283. 283 : * @param {Object} [opts.sharedOptions]
  284. 284 : * @param {Console} [opts.console]
  285. 285 : * @param {Stream} [opts.stdin]
  286. 286 : * @param {Stream} [opts.stdout]
  287. 287 : * @param {Stream} [opts.stderr]
  288. 288 : * @param {TerminalAdapter} [adapter] - A TerminalAdapter instance or another object
  289. 289 : * implementing this adapter interface. This is how
  290. 290 : * you'd interface Yeoman with a GUI or an editor.
  291. 291 : */
  292. 292 : constructor(options, adapter) {
  293. 293 : super();
  294. 294 :
  295. 295 : this.setMaxListeners(100);
  296. 296 :
  297. 297 : this.options = options || {};
  298. 298 : this.adapter = adapter || new TerminalAdapter({console: this.options.console, stdin: this.options.stdin, stderr: this.options.stderr});
  299. 299 : this.cwd = this.options.cwd || process.cwd();
  300. 300 : this.cwd = path.resolve(this.cwd);
  301. 301 : this.logCwd = this.options.logCwd || this.cwd;
  302. 302 : this.store = new Store();
  303. 303 : this.command = this.options.command;
  304. 304 :
  305. 305 : this.runLoop = new GroupedQueue(Environment.queues, false);
  306. 306 : this.sharedFs = memFs.create();
  307. 307 :
  308. 308 : // Each composed generator might set listeners on these shared resources. Let's make sure
  309. 309 : // Node won't complain about event listeners leaks.
  310. 310 : this.runLoop.setMaxListeners(0);
  311. 311 : this.sharedFs.setMaxListeners(0);
  312. 312 :
  313. 313 : // Create a shared mem-fs-editor instance.
  314. 314 : this.fs = FileEditor.create(this.sharedFs);
  315. 315 :
  316. 316 : this.lookups = Environment.lookups;
  317. 317 : this.aliases = [];
  318. 318 :
  319. 319 : this.alias(/^([^:]+)$/, '$1:app');
  320. 320 :
  321. 321 : // Used sharedOptions from options if exists.
  322. 322 : this.sharedOptions = this.options.sharedOptions || {};
  323. 323 : // Remove Unecessary sharedOptions from options
  324. 324 : delete this.options.sharedOptions;
  325. 325 :
  326. 326 : // Create a default sharedData.
  327. 327 : this.sharedOptions.sharedData = this.sharedOptions.sharedData || {};
  328. 328 :
  329. 329 : // Pass forwardErrorToEnvironment to generators.
  330. 330 : this.sharedOptions.forwardErrorToEnvironment = false;
  331. 331 :
  332. 332 : this.repository = new YeomanRepository(this.options.yeomanRepository);
  333. 333 :
  334. 334 : if (!this.options.experimental) {
  335. 335 : process.argv.forEach(value => {
  336. 336 : if (value === '--experimental') {
  337. 337 : this.options.experimental = true;
  338. 338 : debug('Set environment as experimental');
  339. 339 : }
  340. 340 : });
  341. 341 : }
  342. 342 :
  343. 343 : this.loadSharedOptions(this.options);
  344. 344 : if (this.sharedOptions.skipLocalCache === undefined) {
  345. 345 : this.sharedOptions.skipLocalCache = true;
  346. 346 : }
  347. 347 :
  348. 348 : // Store the generators by paths and uniqueBy feature.
  349. 349 : this._generatorsForPath = {};
  350. 350 : this._generators = {};
  351. 351 :
  352. 352 : // Store the YeomanCompose by paths and uniqueBy feature.
  353. 353 : this._composeStore = {};
  354. 354 : }
  355. 355 :
  356. 356 : /**
  357. 357 : * Load options passed to the Generator that should be used by the Environment.
  358. 358 : *
  359. 359 : * @param {Object} options
  360. 360 : */
  361. 361 : loadEnvironmentOptions(options) {
  362. 362 : const environmentOptions = _.pick(options, [
  363. 363 : 'skipInstall',
  364. 364 : 'nodePackageManager'
  365. 365 : ]);
  366. 366 : _.defaults(this.options, environmentOptions);
  367. 367 : return environmentOptions;
  368. 368 : }
  369. 369 :
  370. 370 : /**
  371. 371 : * Load options passed to the Environment that should be forwarded to the Generator.
  372. 372 : *
  373. 373 : * @param {Object} options
  374. 374 : */
  375. 375 : loadSharedOptions(options) {
  376. 376 : const optionsToShare = _.pick(options, [
  377. 377 : 'skipInstall',
  378. 378 : 'forceInstall',
  379. 379 : 'skipCache',
  380. 380 : 'skipLocalCache',
  381. 381 : 'skipParseOptions',
  382. 382 : 'localConfigOnly',
  383. 383 : 'askAnswered'
  384. 384 : ]);
  385. 385 : Object.assign(this.sharedOptions, optionsToShare);
  386. 386 : return optionsToShare;
  387. 387 : }
  388. 388 :
  389. 389 : /**
  390. 390 : * @deprecated
  391. 391 : * Error handler taking `err` instance of Error.
  392. 392 : *
  393. 393 : * The `error` event is emitted with the error object, if no `error` listener
  394. 394 : * is registered, then we throw the error.
  395. 395 : *
  396. 396 : * @param {Object} err
  397. 397 : * @return {Error} err
  398. 398 : */
  399. 399 : error(error) {
  400. 400 : throw error instanceof Error ? error : new Error(error);
  401. 401 : }
  402. 402 :
  403. 403 : /**
  404. 404 : * Outputs the general help and usage. Optionally, if generators have been
  405. 405 : * registered, the list of available generators is also displayed.
  406. 406 : *
  407. 407 : * @param {String} name
  408. 408 : */
  409. 409 : help(name = 'init') {
  410. 410 : const out = [
  411. 411 : 'Usage: :binary: GENERATOR [args] [options]',
  412. 412 : '',
  413. 413 : 'General options:',
  414. 414 : ' --help # Print generator\'s options and usage',
  415. 415 : ' -f, --force # Overwrite files that already exist',
  416. 416 : '',
  417. 417 : 'Please choose a generator below.',
  418. 418 : ''
  419. 419 : ];
  420. 420 :
  421. 421 : const ns = this.namespaces();
  422. 422 :
  423. 423 : const groups = {};
  424. 424 : for (const namespace of ns) {
  425. 425 : const base = namespace.split(':')[0];
  426. 426 :
  427. 427 : if (!groups[base]) {
  428. 428 : groups[base] = [];
  429. 429 : }
  430. 430 :
  431. 431 : groups[base].push(namespace);
  432. 432 : }
  433. 433 :
  434. 434 : for (const key of Object.keys(groups).sort()) {
  435. 435 : const group = groups[key];
  436. 436 :
  437. 437 : if (group.length > 0) {
  438. 438 : out.push('', key.charAt(0).toUpperCase() + key.slice(1));
  439. 439 : }
  440. 440 :
  441. 441 : for (const ns of groups[key]) {
  442. 442 : out.push(` ${ns}`);
  443. 443 : }
  444. 444 : }
  445. 445 :
  446. 446 : return out.join('\n').replace(/:binary:/g, name);
  447. 447 : }
  448. 448 :
  449. 449 : /**
  450. 450 : * Registers a specific `generator` to this environment. This generator is stored under
  451. 451 : * provided namespace, or a default namespace format if none if available.
  452. 452 : *
  453. 453 : * @param {String} name - Filepath to the a generator or a npm package name
  454. 454 : * @param {String} namespace - Namespace under which register the generator (optional)
  455. 455 : * @param {String} packagePath - PackagePath to the generator npm package (optional)
  456. 456 : * @return {Object} environment - This environment
  457. 457 : */
  458. 458 : register(name, namespace, packagePath) {
  459. 459 : if (typeof name !== 'string') {
  460. 460 : throw new TypeError('You must provide a generator name to register.');
  461. 461 : }
  462. 462 :
  463. 463 : const modulePath = this.resolveModulePath(name);
  464. 464 : namespace = namespace || this.namespace(modulePath);
  465. 465 :
  466. 466 : if (!namespace) {
  467. 467 : throw new Error('Unable to determine namespace.');
  468. 468 : }
  469. 469 :
  470. 470 : // Generator is already registered and matches the current namespace.
  471. 471 : if (this.store._meta[namespace] && this.store._meta[namespace].resolved === modulePath) {
  472. 472 : return this;
  473. 473 : }
  474. 474 :
  475. 475 : this.store.add(namespace, modulePath, modulePath, packagePath);
  476. 476 : const packageNS = Environment.namespaceToName(namespace);
  477. 477 : this.store.addPackageNS(packageNS);
  478. 478 : if (packagePath) {
  479. 479 : this.store.addPackage(packageNS, packagePath);
  480. 480 : }
  481. 481 :
  482. 482 : debug('Registered %s (%s) on package %s (%s)', namespace, modulePath, packageNS, packagePath);
  483. 483 : return this;
  484. 484 : }
  485. 485 :
  486. 486 : /**
  487. 487 : * Register a stubbed generator to this environment. This method allow to register raw
  488. 488 : * functions under the provided namespace. `registerStub` will enforce the function passed
  489. 489 : * to extend the Base generator automatically.
  490. 490 : *
  491. 491 : * @param {Function} Generator - A Generator constructor or a simple function
  492. 492 : * @param {String} namespace - Namespace under which register the generator
  493. 493 : * @param {String} [resolved] - The file path to the generator
  494. 494 : * @param {String} [packagePath] - The generator's package path
  495. 495 : * @return {this}
  496. 496 : */
  497. 497 : registerStub(Generator, namespace, resolved = Environment.UNKNOWN_RESOLVED, packagePath = undefined) {
  498. 498 : if (typeof Generator !== 'function' && typeof Generator.createGenerator !== 'function') {
  499. 499 : throw new TypeError('You must provide a stub function to register.');
  500. 500 : }
  501. 501 :
  502. 502 : if (typeof namespace !== 'string') {
  503. 503 : throw new TypeError('You must provide a namespace to register.');
  504. 504 : }
  505. 505 :
  506. 506 : this.store.add(namespace, Generator, resolved, packagePath);
  507. 507 : const packageNS = Environment.namespaceToName(namespace);
  508. 508 : this.store.addPackageNS(packageNS);
  509. 509 : if (packagePath) {
  510. 510 : this.store.addPackage(packageNS, packagePath);
  511. 511 : }
  512. 512 :
  513. 513 : debug('Registered %s (%s) on package (%s)', namespace, resolved, packagePath);
  514. 514 : return this;
  515. 515 : }
  516. 516 :
  517. 517 : /**
  518. 518 : * Returns the list of registered namespace.
  519. 519 : * @return {Array}
  520. 520 : */
  521. 521 : namespaces() {
  522. 522 : return this.store.namespaces();
  523. 523 : }
  524. 524 :
  525. 525 : /**
  526. 526 : * Returns the environment or dependency version.
  527. 527 : * @param {String} packageName - Module to get version.
  528. 528 : * @return {String} Environment version.
  529. 529 : */
  530. 530 : getVersion(packageName) {
  531. 531 : if (packageName && packageName !== 'yeoman-environment') {
  532. 532 : try {
  533. 533 : return require(`${packageName}/package.json`).version;
  534. 534 : } catch {
  535. 535 : return undefined;
  536. 536 : }
  537. 537 : }
  538. 538 : return ENVIRONMENT_VERSION;
  539. 539 : }
  540. 540 :
  541. 541 : /**
  542. 542 : * Returns stored generators meta
  543. 543 : * @return {Object}
  544. 544 : */
  545. 545 : getGeneratorsMeta() {
  546. 546 : return this.store.getGeneratorsMeta();
  547. 547 : }
  548. 548 :
  549. 549 : /**
  550. 550 : * Get registered generators names
  551. 551 : *
  552. 552 : * @return {Array}
  553. 553 : */
  554. 554 : getGeneratorNames() {
  555. 555 : return _.uniq(Object.keys(this.getGeneratorsMeta()).map(namespace => Environment.namespaceToName(namespace)));
  556. 556 : }
  557. 557 :
  558. 558 : /**
  559. 559 : * Verify if a package namespace already have been registered.
  560. 560 : *
  561. 561 : * @param {String} [packageNS] - namespace of the package.
  562. 562 : * @return {boolean} - true if any generator of the package has been registered
  563. 563 : */
  564. 564 : isPackageRegistered(packageNS) {
  565. 565 : return this.getRegisteredPackages().includes(packageNS);
  566. 566 : }
  567. 567 :
  568. 568 : /**
  569. 569 : * Get all registered packages namespaces.
  570. 570 : *
  571. 571 : * @return {Array} - array of namespaces.
  572. 572 : */
  573. 573 : getRegisteredPackages() {
  574. 574 : return this.store.getPackagesNS();
  575. 575 : }
  576. 576 :
  577. 577 : /**
  578. 578 : * Get last added path for a namespace
  579. 579 : *
  580. 580 : * @param {String} - namespace
  581. 581 : * @return {String} - path of the package
  582. 582 : */
  583. 583 : getPackagePath(namespace) {
  584. 584 : if (namespace.includes(':')) {
  585. 585 : const generator = this.get(namespace) || {};
  586. 586 : return generator.packagePath;
  587. 587 : }
  588. 588 : const packagePaths = this.getPackagePaths(namespace) || [];
  589. 589 : return packagePaths[0];
  590. 590 : }
  591. 591 :
  592. 592 : /**
  593. 593 : * Get paths for a namespace
  594. 594 : *
  595. 595 : * @param {String} - namespace
  596. 596 : * @return {Array} - array of paths.
  597. 597 : */
  598. 598 : getPackagePaths(namespace) {
  599. 599 : return this.store.getPackagesPaths()[namespace] ||
  600. 600 : this.store.getPackagesPaths()[Environment.namespaceToName(this.alias(namespace))];
  601. 601 : }
  602. 602 :
  603. 603 : /**
  604. 604 : * Get a single generator from the registered list of generators. The lookup is
  605. 605 : * based on generator's namespace, "walking up" the namespaces until a matching
  606. 606 : * is found. Eg. if an `angular:common` namespace is registered, and we try to
  607. 607 : * get `angular:common:all` then we get `angular:common` as a fallback (unless
  608. 608 : * an `angular:common:all` generator is registered).
  609. 609 : *
  610. 610 : * @param {String} namespaceOrPath
  611. 611 : * @return {Generator|null} - the generator registered under the namespace
  612. 612 : */
  613. 613 : get(namespaceOrPath) {
  614. 614 : // Stop the recursive search if nothing is left
  615. 615 : if (!namespaceOrPath) {
  616. 616 : return;
  617. 617 : }
  618. 618 :
  619. 619 : const parsed = this.toNamespace ? this.toNamespace(namespaceOrPath) : undefined;
  620. 620 : if (parsed && this.getByNamespace) {
  621. 621 : return this.getByNamespace(parsed);
  622. 622 : }
  623. 623 :
  624. 624 : let namespace = namespaceOrPath;
  625. 625 :
  626. 626 : // Legacy yeoman-generator `#hookFor()` function is passing the generator path as part
  627. 627 : // of the namespace. If we find a path delimiter in the namespace, then ignore the
  628. 628 : // last part of the namespace.
  629. 629 : const parts = namespaceOrPath.split(':');
  630. 630 : const maybePath = _.last(parts);
  631. 631 : if (parts.length > 1 && /[/\\]/.test(maybePath)) {
  632. 632 : parts.pop();
  633. 633 :
  634. 634 : // We also want to remove the drive letter on windows
  635. 635 : if (maybePath.includes('\\') && _.last(parts).length === 1) {
  636. 636 : parts.pop();
  637. 637 : }
  638. 638 :
  639. 639 : namespace = parts.join(':');
  640. 640 : }
  641. 641 :
  642. 642 : const maybeGenerator = this.store.get(namespace) ||
  643. 643 : this.store.get(this.alias(namespace)) ||
  644. 644 : // Namespace is empty if namespaceOrPath contains a win32 absolute path of the form 'C:\path\to\generator'.
  645. 645 : // for this reason we pass namespaceOrPath to the getByPath function.
  646. 646 : this.getByPath(namespaceOrPath);
  647. 647 : if (maybeGenerator && maybeGenerator.then) {
  648. 648 : return maybeGenerator.then(Generator => this._findGeneratorClass(Generator));
  649. 649 : }
  650. 650 : return this._findGeneratorClass(maybeGenerator);
  651. 651 : }
  652. 652 :
  653. 653 : /**
  654. 654 : * Get a generator by path instead of namespace.
  655. 655 : * @param {String} path
  656. 656 : * @return {Generator|null} - the generator found at the location
  657. 657 : */
  658. 658 : getByPath(path) {
  659. 659 : if (fs.existsSync(path)) {
  660. 660 : const namespace = this.namespace(path);
  661. 661 : this.register(path, namespace);
  662. 662 :
  663. 663 : return this.get(namespace);
  664. 664 : }
  665. 665 : }
  666. 666 :
  667. 667 : /**
  668. 668 : * Find generator's class constructor.
  669. 669 : * @private
  670. 670 : * @param {Object} Generator - Object containing the class.
  671. 671 : * @return {Function} Generator's constructor.
  672. 672 : */
  673. 673 : _findGeneratorClass(Generator) {
  674. 674 : if (!Generator) {
  675. 675 : return Generator;
  676. 676 : }
  677. 677 : let meta = Generator;
  678. 678 : if (Array.isArray(Generator)) {
  679. 679 : meta = Generator[1];
  680. 680 : Generator = Generator[0];
  681. 681 : }
  682. 682 : if (typeof Generator.default === 'function') {
  683. 683 : Generator.default.resolved = meta.resolved;
  684. 684 : Generator.default.namespace = meta.namespace;
  685. 685 : return Generator.default;
  686. 686 : }
  687. 687 : if (typeof Generator.createGenerator === 'function') {
  688. 688 : const maybeGenerator = Generator.createGenerator(this);
  689. 689 : if (maybeGenerator.then) {
  690. 690 : return maybeGenerator.then(Gen => {
  691. 691 : Gen.resolved = meta.resolved;
  692. 692 : Gen.namespace = meta.namespace;
  693. 693 : return Gen;
  694. 694 : });
  695. 695 : }
  696. 696 : maybeGenerator.resolved = meta.resolved;
  697. 697 : maybeGenerator.namespace = meta.namespace;
  698. 698 : return maybeGenerator;
  699. 699 : }
  700. 700 : if (typeof Generator !== 'function') {
  701. 701 : throw new TypeError('The generator doesn\'t provides a constructor.');
  702. 702 : }
  703. 703 : return Generator;
  704. 704 : }
  705. 705 :
  706. 706 : /**
  707. 707 : * Create is the Generator factory. It takes a namespace to lookup and optional
  708. 708 : * hash of options, that lets you define `arguments` and `options` to
  709. 709 : * instantiate the generator with.
  710. 710 : *
  711. 711 : * An error is raised on invalid namespace.
  712. 712 : *
  713. 713 : * @param {String} namespaceOrPath
  714. 714 : * @param {Array} [args]
  715. 715 : * @param {Object} [options]
  716. 716 : * @return {Generator} The instantiated generator
  717. 717 : */
  718. 718 : create(namespaceOrPath, args, options) {
  719. 719 : if (!Array.isArray(args) && typeof args === 'object') {
  720. 720 : options = args.options || args;
  721. 721 : args = args.arguments || args.args || [];
  722. 722 : } else {
  723. 723 : args = Array.isArray(args) ? args : splitArgsFromString(args);
  724. 724 : options = options || {};
  725. 725 : }
  726. 726 :
  727. 727 : const namespace = this.toNamespace ? this.toNamespace(namespaceOrPath) : undefined;
  728. 728 :
  729. 729 : let maybeGenerator;
  730. 730 : if (namespace && this.getByNamespace) {
  731. 731 : maybeGenerator = this.getByNamespace(namespace);
  732. 732 : if (!maybeGenerator) {
  733. 733 : this.lookupLocalNamespaces(namespace);
  734. 734 : maybeGenerator = this.getByNamespace(namespace);
  735. 735 : }
  736. 736 : }
  737. 737 :
  738. 738 : const checkGenerator = Generator => {
  739. 739 : if (namespace && Generator && Generator.namespace && Generator.namespace !== Environment.UNKNOWN_NAMESPACE) {
  740. 740 : // Update namespace object in case of aliased namespace.
  741. 741 : namespace.namespace = Generator.namespace;
  742. 742 : }
  743. 743 :
  744. 744 : if (typeof Generator !== 'function') {
  745. 745 : const generatorHint = namespace ? namespace.generatorHint : getGeneratorHint(namespaceOrPath);
  746. 746 :
  747. 747 : throw new Error(
  748. 748 : chalk.red('You don\'t seem to have a generator with the name “' + namespaceOrPath + '” installed.') + '\n' +
  749. 749 : 'But help is on the way:\n\n' +
  750. 750 : 'You can see available generators via ' +
  751. 751 : chalk.yellow('npm search yeoman-generator') + ' or via ' + chalk.yellow('http://yeoman.io/generators/') + '. \n' +
  752. 752 : 'Install them with ' + chalk.yellow(`npm install ${generatorHint}`) + '.\n\n' +
  753. 753 : 'To see all your installed generators run ' + chalk.yellow('yo') + ' without any arguments. ' +
  754. 754 : 'Adding the ' + chalk.yellow('--help') + ' option will also show subgenerators. \n\n' +
  755. 755 : 'If ' + chalk.yellow('yo') + ' cannot find the generator, run ' + chalk.yellow('yo doctor') + ' to troubleshoot your system.'
  756. 756 : );
  757. 757 : }
  758. 758 : return Generator;
  759. 759 : };
  760. 760 :
  761. 761 : maybeGenerator = maybeGenerator || this.get(namespaceOrPath);
  762. 762 : if (maybeGenerator && maybeGenerator.then) {
  763. 763 : return maybeGenerator.then(Generator => checkGenerator(Generator)).then(Generator => this.instantiate(Generator, args, options));
  764. 764 : }
  765. 765 :
  766. 766 : return this.instantiate(checkGenerator(maybeGenerator), args, options);
  767. 767 : }
  768. 768 :
  769. 769 : /**
  770. 770 : * Instantiate a Generator with metadatas
  771. 771 : *
  772. 772 : * @param {Class<Generator>} generator Generator class
  773. 773 : * @param {Array} [args] Arguments to pass the instance
  774. 774 : * @param {Object} [options] Options to pass the instance
  775. 775 : * @return {Generator} The instantiated generator
  776. 776 : */
  777. 777 : instantiate(Generator, args, options) {
  778. 778 : if (!Array.isArray(args) && typeof args === 'object') {
  779. 779 : options = args.options || args;
  780. 780 : args = args.arguments || args.args || [];
  781. 781 : } else {
  782. 782 : args = Array.isArray(args) ? args : splitArgsFromString(args);
  783. 783 : options = options || {};
  784. 784 : }
  785. 785 :
  786. 786 : const {namespace} = Generator;
  787. 787 :
  788. 788 : const environmentOptions = {
  789. 789 : env: this,
  790. 790 : resolved: Generator.resolved || Environment.UNKNOWN_RESOLVED,
  791. 791 : namespace
  792. 792 : };
  793. 793 :
  794. 794 : const generator = new Generator(args, {
  795. 795 : ...this.sharedOptions,
  796. 796 : ...options,
  797. 797 : ...environmentOptions
  798. 798 : });
  799. 799 :
  800. 800 : generator._environmentOptions = {
  801. 801 : ...this.options,
  802. 802 : ...this.sharedOptions,
  803. 803 : ...environmentOptions
  804. 804 : };
  805. 805 :
  806. 806 : return generator;
  807. 807 : }
  808. 808 :
  809. 809 : /**
  810. 810 : * Compose with the generator.
  811. 811 : *
  812. 812 : * @param {String} namespaceOrPath
  813. 813 : * @param {Array} [args]
  814. 814 : * @param {Object} [options]
  815. 815 : * @param {Boolean} [schedule]
  816. 816 : * @return {Generator} The instantiated generator or the singleton instance.
  817. 817 : */
  818. 818 : composeWith(generator, args, options, schedule = true) {
  819. 819 : if (typeof args === 'boolean') {
  820. 820 : schedule = args;
  821. 821 : args = undefined;
  822. 822 : options = undefined;
  823. 823 : } else if (typeof options === 'boolean') {
  824. 824 : schedule = options;
  825. 825 : options = undefined;
  826. 826 : }
  827. 827 : const generatorInstance = this.create(generator, args, options);
  828. 828 : if (generatorInstance.then) {
  829. 829 : return generatorInstance.then(generatorInstance => this.queueGenerator(generatorInstance, schedule));
  830. 830 : }
  831. 831 : return this.queueGenerator(generatorInstance, schedule);
  832. 832 : }
  833. 833 :
  834. 834 : /**
  835. 835 : * @private
  836. 836 : */
  837. 837 : getGeneratorsForPath(generatorRoot = this.cwd) {
  838. 838 : this._generatorsForPath[generatorRoot] = this._generatorsForPath[generatorRoot] || {};
  839. 839 : return this._generatorsForPath[generatorRoot];
  840. 840 : }
  841. 841 :
  842. 842 : /**
  843. 843 : * @private
  844. 844 : */
  845. 845 : getGenerator(uniqueBy, generatorRoot = this.cwd) {
  846. 846 : if (this._generators[uniqueBy]) {
  847. 847 : return this._generators[uniqueBy];
  848. 848 : }
  849. 849 : return this.getGeneratorsForPath(generatorRoot)[uniqueBy];
  850. 850 : }
  851. 851 :
  852. 852 : /**
  853. 853 : * @private
  854. 854 : */
  855. 855 : getAllGenerators() {
  856. 856 : return Object.fromEntries([
  857. 857 : ...Object.entries(this._generators),
  858. 858 : ...Object.entries(this._generatorsForPath).map(([root, generatorStore]) => {
  859. 859 : return Object.entries(generatorStore).map(([namespace, generator]) => ([`${root}#${namespace}`, generator]));
  860. 860 : }).flat()
  861. 861 : ]);
  862. 862 : }
  863. 863 :
  864. 864 : /**
  865. 865 : * @private
  866. 866 : */
  867. 867 : setGenerator(uniqueBy, generator) {
  868. 868 : if (generator.features && generator.features.uniqueGlobally) {
  869. 869 : this._generators[uniqueBy] = generator;
  870. 870 : } else {
  871. 871 : this.getGeneratorsForPath(generator.destinationRoot())[uniqueBy] = generator;
  872. 872 : }
  873. 873 : return generator;
  874. 874 : }
  875. 875 :
  876. 876 : /**
  877. 877 : * Queue generator run (queue itself tasks).
  878. 878 : *
  879. 879 : * @param {Generator} generator Generator instance
  880. 880 : * @param {boolean} [schedule=false] Whether to schedule the generator run.
  881. 881 : * @return {Generator} The generator or singleton instance.
  882. 882 : */
  883. 883 : queueGenerator(generator, schedule = false) {
  884. 884 : const generatorFeatures = generator.getFeatures ? generator.getFeatures() : {};
  885. 885 : let uniqueBy;
  886. 886 : let rootUniqueBy;
  887. 887 : let namespaceToEmit;
  888. 888 : if (generatorFeatures) {
  889. 889 : uniqueBy = generatorFeatures.uniqueBy;
  890. 890 : namespaceToEmit = uniqueBy;
  891. 891 : if (!generatorFeatures.uniqueGlobally) {
  892. 892 : rootUniqueBy = generator.destinationRoot();
  893. 893 : }
  894. 894 : }
  895. 895 :
  896. 896 : if (!uniqueBy) {
  897. 897 : const {namespace} = generator.options;
  898. 898 : const instanceId = crypto.randomBytes(20).toString('hex');
  899. 899 : let namespaceDefinition = this.toNamespace(namespace);
  900. 900 : if (namespaceDefinition) {
  901. 901 : namespaceDefinition = namespaceDefinition.with({instanceId});
  902. 902 : uniqueBy = namespaceDefinition.id;
  903. 903 : namespaceToEmit = namespaceDefinition.namespace;
  904. 904 : } else {
  905. 905 : uniqueBy = `${namespace}#${instanceId}`;
  906. 906 : namespaceToEmit = namespace;
  907. 907 : }
  908. 908 : }
  909. 909 :
  910. 910 : const existing = this.getGenerator(uniqueBy, rootUniqueBy);
  911. 911 : if (existing) {
  912. 912 : debug(`Using existing generator for namespace ${uniqueBy}`);
  913. 913 : return existing;
  914. 914 : }
  915. 915 :
  916. 916 : this.setGenerator(uniqueBy, generator);
  917. 917 : this.emit('compose', namespaceToEmit, generator);
  918. 918 : this.emit(`compose:${namespaceToEmit}`, generator);
  919. 919 :
  920. 920 : const runGenerator = () => {
  921. 921 : if (generator.queueTasks) {
  922. 922 : // Generator > 5
  923. 923 : this.once('run', () => generator.emit('run'));
  924. 924 : this.once('end', () => generator.emit('end'));
  925. 925 : return generator.queueTasks();
  926. 926 : }
  927. 927 : if (!generator.options.forwardErrorToEnvironment) {
  928. 928 : generator.on('error', error => this.emit('error', error));
  929. 929 : }
  930. 930 : generator.promise = generator.run();
  931. 931 : };
  932. 932 :
  933. 933 : if (schedule) {
  934. 934 : this.runLoop.add(
  935. 935 : 'environment:run',
  936. 936 : async (done, stop) => {
  937. 937 : try {
  938. 938 : await runGenerator();
  939. 939 : done();
  940. 940 : } catch (error) {
  941. 941 : stop(error);
  942. 942 : }
  943. 943 : }
  944. 944 : );
  945. 945 : } else {
  946. 946 : const maybePromise = runGenerator();
  947. 947 : if (maybePromise && maybePromise.then) {
  948. 948 : return maybePromise.then(() => generator);
  949. 949 : }
  950. 950 : }
  951. 951 : return generator;
  952. 952 : }
  953. 953 :
  954. 954 : /**
  955. 955 : * Tries to locate and run a specific generator. The lookup is done depending
  956. 956 : * on the provided arguments, options and the list of registered generators.
  957. 957 : *
  958. 958 : * When the environment was unable to resolve a generator, an error is raised.
  959. 959 : *
  960. 960 : * @param {String|Array} args
  961. 961 : * @param {Object} [options]
  962. 962 : */
  963. 963 : async run(args, options, done) {
  964. 964 : if (done || typeof options === 'function' || typeof args === 'function') {
  965. 965 : throw new Error('Callback support have been removed.');
  966. 966 : }
  967. 967 :
  968. 968 : args = Array.isArray(args) ? args : splitArgsFromString(args);
  969. 969 : options = {...options};
  970. 970 :
  971. 971 : const name = args.shift();
  972. 972 : if (!name) {
  973. 973 : throw new Error('Must provide at least one argument, the generator namespace to invoke.');
  974. 974 : }
  975. 975 :
  976. 976 : this.loadEnvironmentOptions(options);
  977. 977 :
  978. 978 : const instantiateAndRun = async () => {
  979. 979 : const generator = await this.create(name, args, {
  980. 980 : ...options,
  981. 981 : initialGenerator: true
  982. 982 : });
  983. 983 : if (options.help) {
  984. 984 : console.log(generator.help());
  985. 985 : return undefined;
  986. 986 : }
  987. 987 :
  988. 988 : return this.runGenerator(generator);
  989. 989 : };
  990. 990 :
  991. 991 : if (this.options.experimental && !this.get(name)) {
  992. 992 : debug(`Generator ${name} was not found, trying to install it`);
  993. 993 : return this.prepareEnvironment(name).then(() => instantiateAndRun(), () => instantiateAndRun());
  994. 994 : }
  995. 995 :
  996. 996 : return instantiateAndRun();
  997. 997 : }
  998. 998 :
  999. 999 : /**
  1000. 1000 : * Start Environment queue
  1001. 1001 : * @param {Object} options - Conflicter options.
  1002. 1002 : */
  1003. 1003 : start(options) {
  1004. 1004 : return new Promise((resolve, reject) => {
  1005. 1005 : if (this.conflicter === undefined) {
  1006. 1006 : const conflicterOptions = _.pick(
  1007. 1007 : _.defaults({}, this.options, options),
  1008. 1008 : ['force', 'bail', 'ignoreWhitespace', 'dryRun', 'skipYoResolve', 'logCwd']
  1009. 1009 : );
  1010. 1010 : conflicterOptions.cwd = conflicterOptions.logCwd;
  1011. 1011 :
  1012. 1012 : this.conflicter = new Conflicter(this.adapter, conflicterOptions);
  1013. 1013 :
  1014. 1014 : this.queueConflicter();
  1015. 1015 : this.queuePackageManagerInstall();
  1016. 1016 : }
  1017. 1017 :
  1018. 1018 : /*
  1019. 1019 : * Listen to errors and reject if emmited.
  1020. 1020 : * Some cases the generator relied at the behavior that the running process
  1021. 1021 : * would be killed if an error is thrown to environment.
  1022. 1022 : * Make sure to not rely on that behavior.
  1023. 1023 : */
  1024. 1024 : this.on('error', error => {
  1025. 1025 : reject(error);
  1026. 1026 : });
  1027. 1027 :
  1028. 1028 : /*
  1029. 1029 : * For backward compatibility
  1030. 1030 : */
  1031. 1031 : this.on('generator:reject', error => {
  1032. 1032 : reject(error);
  1033. 1033 : });
  1034. 1034 :
  1035. 1035 : this.on('generator:resolve', error => {
  1036. 1036 : resolve(error);
  1037. 1037 : });
  1038. 1038 :
  1039. 1039 : this.runLoop.on('error', error => {
  1040. 1040 : this.emit('error', error);
  1041. 1041 : });
  1042. 1042 :
  1043. 1043 : this.runLoop.on('paused', () => {
  1044. 1044 : this.emit('paused');
  1045. 1045 : });
  1046. 1046 :
  1047. 1047 : this.once('end', () => {
  1048. 1048 : resolve();
  1049. 1049 : });
  1050. 1050 :
  1051. 1051 : /* If runLoop has ended, the environment has ended too. */
  1052. 1052 : this.runLoop.once('end', () => {
  1053. 1053 : this.emit('end');
  1054. 1054 : });
  1055. 1055 :
  1056. 1056 : this.emit('run');
  1057. 1057 : this.runLoop.start();
  1058. 1058 : });
  1059. 1059 : }
  1060. 1060 :
  1061. 1061 : /**
  1062. 1062 : * Convenience method to run the generator with callbackWrapper.
  1063. 1063 : * See https://github.com/yeoman/environment/pull/101
  1064. 1064 : *
  1065. 1065 : * @param {Object} generator
  1066. 1066 : */
  1067. 1067 : async runGenerator(generator) {
  1068. 1068 : try {
  1069. 1069 : generator = await generator;
  1070. 1070 : generator = await this.queueGenerator(generator);
  1071. 1071 : } catch (error) {
  1072. 1072 : return Promise.reject(error);
  1073. 1073 : }
  1074. 1074 :
  1075. 1075 : this.compatibilityMode = generator.queueTasks ? false : 'v4';
  1076. 1076 : this._rootGenerator = this._rootGenerator || generator;
  1077. 1077 :
  1078. 1078 : return this.start(generator.options);
  1079. 1079 : }
  1080. 1080 :
  1081. 1081 : /**
  1082. 1082 : * Get the first generator that was queued to run in this environment.
  1083. 1083 : *
  1084. 1084 : * @return {Generator} generator queued to run in this environment.
  1085. 1085 : */
  1086. 1086 : rootGenerator() {
  1087. 1087 : return this._rootGenerator;
  1088. 1088 : }
  1089. 1089 :
  1090. 1090 : /**
  1091. 1091 : * Given a String `filepath`, tries to figure out the relative namespace.
  1092. 1092 : *
  1093. 1093 : * ### Examples:
  1094. 1094 : *
  1095. 1095 : * this.namespace('backbone/all/index.js');
  1096. 1096 : * // => backbone:all
  1097. 1097 : *
  1098. 1098 : * this.namespace('generator-backbone/model');
  1099. 1099 : * // => backbone:model
  1100. 1100 : *
  1101. 1101 : * this.namespace('backbone.js');
  1102. 1102 : * // => backbone
  1103. 1103 : *
  1104. 1104 : * this.namespace('generator-mocha/backbone/model/index.js');
  1105. 1105 : * // => mocha:backbone:model
  1106. 1106 : *
  1107. 1107 : * @param {String} filepath
  1108. 1108 : * @param {Array} lookups paths
  1109. 1109 : */
  1110. 1110 : namespace(filepath, lookups = this.lookups) {
  1111. 1111 : if (!filepath) {
  1112. 1112 : throw new Error('Missing namespace');
  1113. 1113 : }
  1114. 1114 :
  1115. 1115 : // Cleanup extension and normalize path for differents OS
  1116. 1116 : let ns = path.normalize(filepath.replace(new RegExp(escapeStrRe(path.extname(filepath)) + '$'), ''));
  1117. 1117 :
  1118. 1118 : // Sort lookups by length so biggest are removed first
  1119. 1119 : const nsLookups = _(lookups.concat(['..'])).map(found => path.normalize(found)).sortBy('length').value().reverse();
  1120. 1120 :
  1121. 1121 : // If `ns` contains a lookup dir in its path, remove it.
  1122. 1122 : ns = nsLookups.reduce((ns, lookup) => {
  1123. 1123 : // Only match full directory (begin with leading slash or start of input, end with trailing slash)
  1124. 1124 : lookup = new RegExp(`(?:\\\\|/|^)${escapeStrRe(lookup)}(?=\\\\|/)`, 'g');
  1125. 1125 : return ns.replace(lookup, '');
  1126. 1126 : }, ns);
  1127. 1127 :
  1128. 1128 : const folders = ns.split(path.sep);
  1129. 1129 : const scope = _.findLast(folders, folder => folder.indexOf('@') === 0);
  1130. 1130 :
  1131. 1131 : // Cleanup `ns` from unwanted parts and then normalize slashes to `:`
  1132. 1132 : ns = ns
  1133. 1133 : .replace(/(.*generator-)/, '') // Remove before `generator-`
  1134. 1134 : .replace(/[/\\](index|main)$/, '') // Remove `/index` or `/main`
  1135. 1135 : .replace(/^[/\\]+/, '') // Remove leading `/`
  1136. 1136 : .replace(/[/\\]+/g, ':'); // Replace slashes by `:`
  1137. 1137 :
  1138. 1138 : if (scope) {
  1139. 1139 : ns = `${scope}/${ns}`;
  1140. 1140 : }
  1141. 1141 :
  1142. 1142 : debug('Resolve namespaces for %s: %s', filepath, ns);
  1143. 1143 :
  1144. 1144 : return ns;
  1145. 1145 : }
  1146. 1146 :
  1147. 1147 : /**
  1148. 1148 : * Resolve a module path
  1149. 1149 : * @param {String} moduleId - Filepath or module name
  1150. 1150 : * @return {String} - The resolved path leading to the module
  1151. 1151 : */
  1152. 1152 : resolveModulePath(moduleId) {
  1153. 1153 : if (moduleId[0] === '.') {
  1154. 1154 : moduleId = path.resolve(moduleId);
  1155. 1155 : }
  1156. 1156 :
  1157. 1157 : moduleId = untildify(moduleId);
  1158. 1158 : moduleId = path.normalize(moduleId);
  1159. 1159 :
  1160. 1160 : if (path.extname(moduleId) === '') {
  1161. 1161 : moduleId += path.sep;
  1162. 1162 : }
  1163. 1163 :
  1164. 1164 : let resolved;
  1165. 1165 : // Win32: moduleId is resolving as moduleId.js or moduleId.json instead of moduleId/index.js, workaround it.
  1166. 1166 : if (process.platform === 'win32' && path.extname(moduleId) === '') {
  1167. 1167 : try {
  1168. 1168 : resolved = require.resolve(path.join(moduleId, 'index'));
  1169. 1169 : } catch {}
  1170. 1170 : }
  1171. 1171 :
  1172. 1172 : return resolved || require.resolve(moduleId);
  1173. 1173 : }
  1174. 1174 :
  1175. 1175 : /**
  1176. 1176 : * Apply transform streams to file in MemFs.
  1177. 1177 : * @param {Transform[]} transformStreams - transform streams to be applied.
  1178. 1178 : * @param {Stream} [stream] - files stream, defaults to this.sharedFs.stream().
  1179. 1179 : * @return {Promise}
  1180. 1180 : */
  1181. 1181 : applyTransforms(transformStreams, options = {}) {
  1182. 1182 : const {
  1183. 1183 : stream = this.sharedFs.stream(),
  1184. 1184 : name = 'Tranforming'
  1185. 1185 : } = options;
  1186. 1186 :
  1187. 1187 : let {log = true} = options;
  1188. 1188 :
  1189. 1189 : if (log) {
  1190. 1190 : npmlog.tracker = new TrackerGroup();
  1191. 1191 : npmlog.enableProgress();
  1192. 1192 : log = npmlog.newItem(name);
  1193. 1193 : }
  1194. 1194 :
  1195. 1195 : if (!Array.isArray(transformStreams)) {
  1196. 1196 : transformStreams = [transformStreams];
  1197. 1197 : }
  1198. 1198 : return pipeline(
  1199. 1199 : stream,
  1200. 1200 : createModifiedTransform(),
  1201. 1201 : ...transformStreams,
  1202. 1202 : createEachFileTransform(file => {
  1203. 1203 : if (log) {
  1204. 1204 : log.completeWork(10);
  1205. 1205 : npmlog.info('Completed', path.relative(this.logCwd, file.path));
  1206. 1206 : }
  1207. 1207 : }, {autoForward: false, logName: 'environment:log'})
  1208. 1208 : ).then(() => {
  1209. 1209 : if (log) {
  1210. 1210 : log.finish();
  1211. 1211 : npmlog.disableProgress();
  1212. 1212 : }
  1213. 1213 : });
  1214. 1214 : }
  1215. 1215 :
  1216. 1216 : /**
  1217. 1217 : * Commits the MemFs to the disc.
  1218. 1218 : * @param {Stream} [stream] - files stream, defaults to this.sharedFs.stream().
  1219. 1219 : * @return {Promise}
  1220. 1220 : */
  1221. 1221 : commitSharedFs(stream = this.sharedFs.stream()) {
  1222. 1222 : return new Promise((resolve, reject) => {
  1223. 1223 : debug('committing files');
  1224. 1224 : this.fs.commit([
  1225. 1225 : createYoResolveTransform(this.conflicter),
  1226. 1226 : createYoRcTransform(),
  1227. 1227 : createConflicterCheckTransform(this.conflicter),
  1228. 1228 : createConflicterStatusTransform(),
  1229. 1229 : // Use custom commit transform due to out of order transform.
  1230. 1230 : createCommitTransform(this.fs)
  1231. 1231 : ],
  1232. 1232 : stream,
  1233. 1233 : (error, value) => {
  1234. 1234 : debug('committing finished');
  1235. 1235 : if (error) {
  1236. 1236 : reject(error);
  1237. 1237 : return;
  1238. 1238 : }
  1239. 1239 :
  1240. 1240 : // Force to empty Conflicter queue.
  1241. 1241 : this.conflicter.queue.once('end', () => resolve(value));
  1242. 1242 : this.conflicter.queue.run();
  1243. 1243 : });
  1244. 1244 : });
  1245. 1245 : }
  1246. 1246 :
  1247. 1247 : /**
  1248. 1248 : * Queue environment's commit task.
  1249. 1249 : */
  1250. 1250 : queueConflicter() {
  1251. 1251 : const queueCommit = () => {
  1252. 1252 : debug('Queueing conflicts task');
  1253. 1253 : this.runLoop.add('environment:conflicts', (done, stop) => {
  1254. 1254 : let customCommitTask = this.findGeneratorCustomCommitTask();
  1255. 1255 : if (customCommitTask !== undefined && customCommitTask) {
  1256. 1256 : if (typeof customCommitTask !== 'function') {
  1257. 1257 : done();
  1258. 1258 : return;
  1259. 1259 : }
  1260. 1260 : } else {
  1261. 1261 : customCommitTask = this.commitSharedFs.bind(this);
  1262. 1262 : }
  1263. 1263 : const result = customCommitTask();
  1264. 1264 : if (!result || !result.then) {
  1265. 1265 : done();
  1266. 1266 : return;
  1267. 1267 : }
  1268. 1268 : return result.then(() => {
  1269. 1269 : debug('Adding queueCommit event listener');
  1270. 1270 : this.sharedFs.once('change', queueCommit);
  1271. 1271 : done();
  1272. 1272 : }
  1273. 1273 : , stop);
  1274. 1274 : }
  1275. 1275 : , {
  1276. 1276 : once: 'write memory fs to disk'
  1277. 1277 : });
  1278. 1278 : };
  1279. 1279 :
  1280. 1280 : queueCommit();
  1281. 1281 : }
  1282. 1282 :
  1283. 1283 : /**
  1284. 1284 : * Queue environment's package manager install task.
  1285. 1285 : */
  1286. 1286 : queuePackageManagerInstall() {
  1287. 1287 : this.runLoop.add(
  1288. 1288 : 'install',
  1289. 1289 : (done, stop) => this.packageManagerInstallTask().then(done, stop),
  1290. 1290 : {once: 'package manager install'}
  1291. 1291 : );
  1292. 1292 : }
  1293. 1293 : }
  1294. 1294 :
  1295. 1295 : Object.assign(Environment.prototype, resolver);
  1296. 1296 : Object.assign(Environment.prototype, composability);
  1297. 1297 : Object.assign(Environment.prototype, require('./package-manager'));
  1298. 1298 : Object.assign(Environment.prototype, require('./spawn-command'));
  1299. 1299 : Object.assign(Environment.prototype, require('./namespace-composability'));
  1300. 1300 :
  1301. 1301 : /**
  1302. 1302 : * Expose the utilities on the module
  1303. 1303 : * @see {@link env/util}
  1304. 1304 : */
  1305. 1305 : Environment.util = require('./util/util');
  1306. 1306 :
  1307. 1307 : module.exports = Environment;