From 1a478c554cd6d65e6fe67a91c829de26bfd32d34 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Thu, 9 Mar 2017 16:23:19 +0700 Subject: [PATCH] Convert to ES2015 classes --- lib/adapter.js | 114 ++++--- lib/environment.js | 795 ++++++++++++++++++++++---------------------- lib/store.js | 129 +++---- lib/util/log.js | 4 +- lib/util/util.js | 2 +- package.json | 8 +- test/environment.js | 201 +++++------ 7 files changed, 641 insertions(+), 612 deletions(-) diff --git a/lib/adapter.js b/lib/adapter.js index a252cf32..818a14d2 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -13,61 +13,73 @@ const logger = require('./util/log'); * * @constructor */ -const TerminalAdapter = module.exports = function TerminalAdapter() { - this.promptModule = inquirer.createPromptModule(); -}; +class TerminalAdapter { + constructor() { + this.promptModule = inquirer.createPromptModule(); + } -TerminalAdapter.prototype._colorDiffAdded = chalk.black.bgGreen; -TerminalAdapter.prototype._colorDiffRemoved = chalk.bgRed; -TerminalAdapter.prototype._colorLines = function colorLines(name, str) { - return str.split('\n').map(line => this[`_colorDiff${name}`](line)).join('\n'); -}; + get _colorDiffAdded() { + return chalk.black.bgGreen; + } -/** - * Prompt a user for one or more questions and pass - * the answer(s) to the provided callback. - * - * It shares its interface with `Base.prompt` - * - * (Defined inside the constructor to keep interfaces separated between - * instances) - * - * @param {Array} questions - * @param {Function} callback - */ -TerminalAdapter.prototype.prompt = function () {}; + get _colorDiffRemoved() { + return chalk.bgRed; + } -/** - * Shows a color-based diff of two strings - * - * @param {string} actual - * @param {string} expected - */ -TerminalAdapter.prototype.diff = function _diff(actual, expected) { - let msg = diff.diffLines(actual, expected).map(str => { - if (str.added) { - return this._colorLines('Added', str.value); - } + _colorLines(name, str) { + return str.split('\n').map(line => this[`_colorDiff${name}`](line)).join('\n'); + } + + /** + * Prompt a user for one or more questions and pass + * the answer(s) to the provided callback. + * + * It shares its interface with `Base.prompt` + * + * (Defined inside the constructor to keep interfaces separated between + * instances) + * + * @param {Array} questions + * @param {Function} callback + */ + prompt(questions, cb) { + const promise = this.promptModule(questions); + promise.then(cb || _.noop); + return promise; + } + + /** + * Shows a color-based diff of two strings + * + * @param {string} actual + * @param {string} expected + */ + diff(actual, expected) { + let msg = diff.diffLines(actual, expected).map(str => { + if (str.added) { + return this._colorLines('Added', str.value); + } - if (str.removed) { - return this._colorLines('Removed', str.value); - } + if (str.removed) { + return this._colorLines('Removed', str.value); + } - return str.value; - }).join(''); + return str.value; + }).join(''); - // Legend - msg = '\n' + - this._colorDiffRemoved('removed') + - ' ' + - this._colorDiffAdded('added') + - '\n\n' + - msg + - '\n'; + // Legend + msg = '\n' + + this._colorDiffRemoved('removed') + + ' ' + + this._colorDiffAdded('added') + + '\n\n' + + msg + + '\n'; - console.log(msg); - return msg; -}; + console.log(msg); + return msg; + } +} /** * Logging utility @@ -75,8 +87,4 @@ TerminalAdapter.prototype.diff = function _diff(actual, expected) { */ TerminalAdapter.prototype.log = logger(); -TerminalAdapter.prototype.prompt = function (questions, cb) { - const promise = this.promptModule(questions); - promise.then(cb || _.noop); - return promise; -}; +module.exports = TerminalAdapter; diff --git a/lib/environment.js b/lib/environment.js index 4e731f80..056e2098 100644 --- a/lib/environment.js +++ b/lib/environment.js @@ -1,8 +1,7 @@ 'use strict'; -const util = require('util'); const fs = require('fs'); const path = require('path'); -const events = require('events'); +const EventEmitter = require('events'); const chalk = require('chalk'); const _ = require('lodash'); const GroupedQueue = require('grouped-queue'); @@ -36,468 +35,476 @@ const TerminalAdapter = require('./adapter'); * implementing this adapter interface. This is how * you'd interface Yeoman with a GUI or an editor. */ -const Environment = module.exports = function Environment(args, opts, adapter) { - events.EventEmitter.call(this); - - args = args || []; - this.arguments = Array.isArray(args) ? args : args.split(' '); - this.options = opts || {}; - this.adapter = adapter || new TerminalAdapter(); - this.cwd = this.options.cwd || process.cwd(); - this.store = new Store(); - - this.runLoop = new GroupedQueue(Environment.queues); - this.sharedFs = memFs.create(); - - // Each composed generator might set listeners on these shared resources. Let's make sure - // Node won't complain about event listeners leaks. - this.runLoop.setMaxListeners(0); - this.sharedFs.setMaxListeners(0); - - this.lookups = ['.', 'generators', 'lib/generators']; - this.aliases = []; - - this.alias(/^([^:]+)$/, '$1:app'); -}; - -util.inherits(Environment, events.EventEmitter); -_.extend(Environment.prototype, resolver); - -Environment.queues = [ - 'initializing', - 'prompting', - 'configuring', - 'default', - 'writing', - 'conflicts', - 'install', - 'end' -]; - -/** - * Error handler taking `err` instance of Error. - * - * The `error` event is emitted with the error object, if no `error` listener - * is registered, then we throw the error. - * - * @param {Object} err - * @return {Error} err - */ -Environment.prototype.error = function error(err) { - err = err instanceof Error ? err : new Error(err); - - if (!this.emit('error', err)) { - throw err; +class Environment extends EventEmitter { + static get queues() { + return [ + 'initializing', + 'prompting', + 'configuring', + 'default', + 'writing', + 'conflicts', + 'install', + 'end' + ]; } - return err; -}; - -/** - * Outputs the general help and usage. Optionally, if generators have been - * registered, the list of available generators is also displayed. - * - * @param {String} name - */ -Environment.prototype.help = function help(name) { - name = name || 'init'; - - const out = [ - 'Usage: :binary: GENERATOR [args] [options]', - '', - 'General options:', - ' --help # Print generator\'s options and usage', - ' -f, --force # Overwrite files that already exist', - '', - 'Please choose a generator below.', - '' - ]; - - const ns = this.namespaces(); - - const groups = {}; - ns.forEach(namespace => { - const base = namespace.split(':')[0]; - - if (!groups[base]) { - groups[base] = []; + /** + * Make sure the Environment present expected methods if an old version is + * passed to a Generator. + * @param {Environment} env + * @return {Environment} The updated env + */ + static enforceUpdate(env) { + if (!env.adapter) { + env.adapter = new TerminalAdapter(); } - groups[base].push(namespace); - }); - - Object.keys(groups).sort().forEach(key => { - const group = groups[key]; + if (!env.runLoop) { + env.runLoop = new GroupedQueue([ + 'initializing', + 'prompting', + 'configuring', + 'default', + 'writing', + 'conflicts', + 'install', + 'end' + ]); + } - if (group.length >= 1) { - out.push('', key.charAt(0).toUpperCase() + key.slice(1)); + if (!env.sharedFs) { + env.sharedFs = memFs.create(); } - groups[key].forEach(ns => { - out.push(` ${ns}`); - }); - }); + return env; + } - return out.join('\n').replace(/:binary:/g, name); -}; + /** + * Factory method to create an environment instance. Take same parameters as the + * Environment constructor. + * + * @see This method take the same arguments as {@link Environment} constructor + * + * @return {Environment} a new Environment instance + */ + static createEnv(args, opts, adapter) { + return new Environment(args, opts, adapter); + } -/** - * Registers a specific `generator` to this environment. This generator is stored under - * provided namespace, or a default namespace format if none if available. - * - * @param {String} name - Filepath to the a generator or a npm package name - * @param {String} namespace - Namespace under which register the generator (optional) - * @return {String} namespace - Namespace assigned to the registered generator - */ -Environment.prototype.register = function register(name, namespace) { - if (!_.isString(name)) { - return this.error(new Error('You must provide a generator name to register.')); + /** + * Convert a generators namespace to its name + * + * @param {String} namespace + * @return {String} + */ + static namespaceToName(namespace) { + return namespace.split(':')[0]; } - const modulePath = this.resolveModulePath(name); - namespace = namespace || this.namespace(modulePath); + constructor(args, opts, adapter) { + super(); - if (!namespace) { - return this.error(new Error('Unable to determine namespace.')); - } + args = args || []; + this.arguments = Array.isArray(args) ? args : args.split(' '); + this.options = opts || {}; + this.adapter = adapter || new TerminalAdapter(); + this.cwd = this.options.cwd || process.cwd(); + this.store = new Store(); - this.store.add(namespace, modulePath); + this.runLoop = new GroupedQueue(Environment.queues); + this.sharedFs = memFs.create(); - debug('Registered %s (%s)', namespace, modulePath); - return this; -}; + // Each composed generator might set listeners on these shared resources. Let's make sure + // Node won't complain about event listeners leaks. + this.runLoop.setMaxListeners(0); + this.sharedFs.setMaxListeners(0); -/** - * Register a stubbed generator to this environment. This method allow to register raw - * functions under the provided namespace. `registerStub` will enforce the function passed - * to extend the Base generator automatically. - * - * @param {Function} Generator - A Generator constructor or a simple function - * @param {String} namespace - Namespace under which register the generator - * @return {this} - */ -Environment.prototype.registerStub = function registerStub(Generator, namespace) { - if (!_.isFunction(Generator)) { - return this.error(new Error('You must provide a stub function to register.')); - } + this.lookups = ['.', 'generators', 'lib/generators']; + this.aliases = []; - if (!_.isString(namespace)) { - return this.error(new Error('You must provide a namespace to register.')); + this.alias(/^([^:]+)$/, '$1:app'); } - this.store.add(namespace, Generator); + /** + * Error handler taking `err` instance of Error. + * + * The `error` event is emitted with the error object, if no `error` listener + * is registered, then we throw the error. + * + * @param {Object} err + * @return {Error} err + */ + error(err) { + err = err instanceof Error ? err : new Error(err); + + if (!this.emit('error', err)) { + throw err; + } + + return err; + } - return this; -}; + /** + * Outputs the general help and usage. Optionally, if generators have been + * registered, the list of available generators is also displayed. + * + * @param {String} name + */ + help(name) { + name = name || 'init'; + + const out = [ + 'Usage: :binary: GENERATOR [args] [options]', + '', + 'General options:', + ' --help # Print generator\'s options and usage', + ' -f, --force # Overwrite files that already exist', + '', + 'Please choose a generator below.', + '' + ]; + + const ns = this.namespaces(); + + const groups = {}; + for (const namespace of ns) { + const base = namespace.split(':')[0]; + + if (!groups[base]) { + groups[base] = []; + } + + groups[base].push(namespace); + } -/** - * Returns the list of registered namespace. - * @return {Array} - */ -Environment.prototype.namespaces = function namespaces() { - return this.store.namespaces(); -}; + for (const key of Object.keys(groups).sort()) { + const group = groups[key]; -/** - * Returns stored generators meta - * @return {Object} - */ -Environment.prototype.getGeneratorsMeta = function getGeneratorsMeta() { - return this.store.getGeneratorsMeta(); -}; + if (group.length >= 1) { + out.push('', key.charAt(0).toUpperCase() + key.slice(1)); + } -/** - * Get registered generators names - * - * @return {Array} - */ -Environment.prototype.getGeneratorNames = function getGeneratorNames() { - return _.uniq(Object.keys(this.getGeneratorsMeta()).map(Environment.namespaceToName)); -}; + for (const ns of groups[key]) { + out.push(` ${ns}`); + } + } -/** - * Get a single generator from the registered list of generators. The lookup is - * based on generator's namespace, "walking up" the namespaces until a matching - * is found. Eg. if an `angular:common` namespace is registered, and we try to - * get `angular:common:all` then we get `angular:common` as a fallback (unless - * an `angular:common:all` generator is registered). - * - * @param {String} namespaceOrPath - * @return {Generator|null} - the generator registered under the namespace - */ -Environment.prototype.get = function get(namespaceOrPath) { - // Stop the recursive search if nothing is left - if (!namespaceOrPath) { - return; + return out.join('\n').replace(/:binary:/g, name); } - let namespace = namespaceOrPath; + /** + * Registers a specific `generator` to this environment. This generator is stored under + * provided namespace, or a default namespace format if none if available. + * + * @param {String} name - Filepath to the a generator or a npm package name + * @param {String} namespace - Namespace under which register the generator (optional) + * @return {String} namespace - Namespace assigned to the registered generator + */ + register(name, namespace) { + if (typeof name !== 'string') { + return this.error(new Error('You must provide a generator name to register.')); + } - // Legacy yeoman-generator `#hookFor()` function is passing the generator path as part - // of the namespace. If we find a path delimiter in the namespace, then ignore the - // last part of the namespace. - const parts = namespaceOrPath.split(':'); - const maybePath = _.last(parts); - if (parts.length > 1 && /[\/\\]/.test(maybePath)) { - parts.pop(); + const modulePath = this.resolveModulePath(name); + namespace = namespace || this.namespace(modulePath); - // We also want to remove the drive letter on windows - if (maybePath.indexOf('\\') >= 0 && _.last(parts).length === 1) { - parts.pop(); + if (!namespace) { + return this.error(new Error('Unable to determine namespace.')); } - namespace = parts.join(':'); + this.store.add(namespace, modulePath); + + debug('Registered %s (%s)', namespace, modulePath); + return this; } - return this.store.get(namespace) || - this.store.get(this.alias(namespace)) || - // Namespace is empty if namespaceOrPath contains a win32 absolute path of the form 'C:\path\to\generator'. - // for this reason we pass namespaceOrPath to the getByPath function. - this.getByPath(namespaceOrPath); -}; + /** + * Register a stubbed generator to this environment. This method allow to register raw + * functions under the provided namespace. `registerStub` will enforce the function passed + * to extend the Base generator automatically. + * + * @param {Function} Generator - A Generator constructor or a simple function + * @param {String} namespace - Namespace under which register the generator + * @return {this} + */ + registerStub(Generator, namespace) { + if (typeof Generator !== 'function') { + return this.error(new Error('You must provide a stub function to register.')); + } -/** - * Get a generator by path instead of namespace. - * @param {String} path - * @return {Generator|null} - the generator found at the location - */ -Environment.prototype.getByPath = function (path) { - if (fs.existsSync(path)) { - const namespace = this.namespace(path); - this.register(path, namespace); + if (typeof namespace !== 'string') { + return this.error(new Error('You must provide a namespace to register.')); + } + + this.store.add(namespace, Generator); - return this.get(namespace); + return this; } -}; -/** - * Create is the Generator factory. It takes a namespace to lookup and optional - * hash of options, that lets you define `arguments` and `options` to - * instantiate the generator with. - * - * An error is raised on invalid namespace. - * - * @param {String} namespace - * @param {Object} options - */ -Environment.prototype.create = function create(namespace, options) { - options = options || {}; - - const Generator = this.get(namespace); - - if (!_.isFunction(Generator)) { - return this.error( - new Error( - chalk.red('You don\’t seem to have a generator with the name “' + namespace + '” installed.') + '\n' + - 'But help is on the way:\n\n' + - 'You can see available generators via ' + - chalk.yellow('npm search yeoman-generator') + ' or via ' + chalk.yellow('http://yeoman.io/generators/') + '. \n' + - 'Install them with ' + chalk.yellow('npm install generator-' + namespace) + '.\n\n' + - 'To see all your installed generators run ' + chalk.yellow('yo') + ' without any arguments. ' + - 'Adding the ' + chalk.yellow('--help') + ' option will also show subgenerators. \n\n' + - 'If ' + chalk.yellow('yo') + ' cannot find the generator, run ' + chalk.yellow('yo doctor') + ' to troubleshoot your system.' - ) - ); + /** + * Returns the list of registered namespace. + * @return {Array} + */ + namespaces() { + return this.store.namespaces(); } - return this.instantiate(Generator, options); -}; + /** + * Returns stored generators meta + * @return {Object} + */ + getGeneratorsMeta() { + return this.store.getGeneratorsMeta(); + } -/** - * Instantiate a Generator with metadatas - * - * @param {String} namespace - * @param {Object} options - * @param {Array|String} options.arguments Arguments to pass the instance - * @param {Object} options.options Options to pass the instance - */ -Environment.prototype.instantiate = function instantiate(Generator, options) { - options = options || {}; + /** + * Get registered generators names + * + * @return {Array} + */ + getGeneratorNames() { + return _.uniq(Object.keys(this.getGeneratorsMeta()).map(Environment.namespaceToName)); + } - let args = options.arguments || options.args || _.clone(this.arguments); - args = Array.isArray(args) ? args : args.split(' '); + /** + * Get a single generator from the registered list of generators. The lookup is + * based on generator's namespace, "walking up" the namespaces until a matching + * is found. Eg. if an `angular:common` namespace is registered, and we try to + * get `angular:common:all` then we get `angular:common` as a fallback (unless + * an `angular:common:all` generator is registered). + * + * @param {String} namespaceOrPath + * @return {Generator|null} - the generator registered under the namespace + */ + get(namespaceOrPath) { + // Stop the recursive search if nothing is left + if (!namespaceOrPath) { + return; + } - const opts = options.options || _.clone(this.options); + let namespace = namespaceOrPath; - opts.env = this; - opts.resolved = Generator.resolved || 'unknown'; - opts.namespace = Generator.namespace; - return new Generator(args, opts); -}; + // Legacy yeoman-generator `#hookFor()` function is passing the generator path as part + // of the namespace. If we find a path delimiter in the namespace, then ignore the + // last part of the namespace. + const parts = namespaceOrPath.split(':'); + const maybePath = _.last(parts); + if (parts.length > 1 && /[/\\]/.test(maybePath)) { + parts.pop(); -/** - * Tries to locate and run a specific generator. The lookup is done depending - * on the provided arguments, options and the list of registered generators. - * - * When the environment was unable to resolve a generator, an error is raised. - * - * @param {String|Array} args - * @param {Object} options - * @param {Function} done - */ -Environment.prototype.run = function run(args, options, done) { - args = args || this.arguments; + // We also want to remove the drive letter on windows + if (maybePath.indexOf('\\') >= 0 && _.last(parts).length === 1) { + parts.pop(); + } - if (typeof options === 'function') { - done = options; - options = this.options; - } + namespace = parts.join(':'); + } - if (typeof args === 'function') { - done = args; - options = this.options; - args = this.arguments; + return this.store.get(namespace) || + this.store.get(this.alias(namespace)) || + // Namespace is empty if namespaceOrPath contains a win32 absolute path of the form 'C:\path\to\generator'. + // for this reason we pass namespaceOrPath to the getByPath function. + this.getByPath(namespaceOrPath); } - args = Array.isArray(args) ? args : args.split(' '); - options = options || this.options; - - const name = args.shift(); - if (!name) { - return this.error(new Error('Must provide at least one argument, the generator namespace to invoke.')); + /** + * Get a generator by path instead of namespace. + * @param {String} path + * @return {Generator|null} - the generator found at the location + */ + getByPath(path) { + if (fs.existsSync(path)) { + const namespace = this.namespace(path); + this.register(path, namespace); + + return this.get(namespace); + } } - const generator = this.create(name, { - args, - options - }); + /** + * Create is the Generator factory. It takes a namespace to lookup and optional + * hash of options, that lets you define `arguments` and `options` to + * instantiate the generator with. + * + * An error is raised on invalid namespace. + * + * @param {String} namespace + * @param {Object} options + */ + create(namespace, options) { + options = options || {}; + + const Generator = this.get(namespace); + + if (typeof Generator !== 'function') { + return this.error( + new Error( + chalk.red('You don\'t seem to have a generator with the name “' + namespace + '” installed.') + '\n' + + 'But help is on the way:\n\n' + + 'You can see available generators via ' + + chalk.yellow('npm search yeoman-generator') + ' or via ' + chalk.yellow('http://yeoman.io/generators/') + '. \n' + + 'Install them with ' + chalk.yellow('npm install generator-' + namespace) + '.\n\n' + + 'To see all your installed generators run ' + chalk.yellow('yo') + ' without any arguments. ' + + 'Adding the ' + chalk.yellow('--help') + ' option will also show subgenerators. \n\n' + + 'If ' + chalk.yellow('yo') + ' cannot find the generator, run ' + chalk.yellow('yo doctor') + ' to troubleshoot your system.' + ) + ); + } - if (generator instanceof Error) { - return generator; + return this.instantiate(Generator, options); } - if (options.help) { - return console.log(generator.help()); + /** + * Instantiate a Generator with metadatas + * + * @param {String} namespace + * @param {Object} options + * @param {Array|String} options.arguments Arguments to pass the instance + * @param {Object} options.options Options to pass the instance + */ + instantiate(Generator, options) { + options = options || {}; + + let args = options.arguments || options.args || _.clone(this.arguments); + args = Array.isArray(args) ? args : args.split(' '); + + const opts = options.options || _.clone(this.options); + + opts.env = this; + opts.resolved = Generator.resolved || 'unknown'; + opts.namespace = Generator.namespace; + return new Generator(args, opts); } - return generator.run(done); -}; + /** + * Tries to locate and run a specific generator. The lookup is done depending + * on the provided arguments, options and the list of registered generators. + * + * When the environment was unable to resolve a generator, an error is raised. + * + * @param {String|Array} args + * @param {Object} options + * @param {Function} done + */ + run(args, options, done) { + args = args || this.arguments; + + if (typeof options === 'function') { + done = options; + options = this.options; + } -/** - * Given a String `filepath`, tries to figure out the relative namespace. - * - * ### Examples: - * - * this.namespace('backbone/all/index.js'); - * // => backbone:all - * - * this.namespace('generator-backbone/model'); - * // => backbone:model - * - * this.namespace('backbone.js'); - * // => backbone - * - * this.namespace('generator-mocha/backbone/model/index.js'); - * // => mocha:backbone:model - * - * @param {String} filepath - */ -Environment.prototype.namespace = function namespace(filepath) { - if (!filepath) { - throw new Error('Missing namespace'); - } + if (typeof args === 'function') { + done = args; + options = this.options; + args = this.arguments; + } - // Cleanup extension and normalize path for differents OS - let ns = path.normalize(filepath.replace(new RegExp(escapeStrRe(path.extname(filepath)) + '$'), '')); - - // Sort lookups by length so biggest are removed first - const lookups = _(this.lookups.concat(['..'])).map(path.normalize).sortBy('length').value().reverse(); - - // If `ns` contains a lookup dir in its path, remove it. - ns = lookups.reduce((ns, lookup) => { - // Only match full directory (begin with leading slash or start of input, end with trailing slash) - lookup = new RegExp('(?:\\\\|/|^)' + escapeStrRe(lookup) + '(?=\\\\|/)', 'g'); - return ns.replace(lookup, ''); - }, ns); - - const folders = ns.split(path.sep); - const scope = _.findLast(folders, folder => { - return folder.indexOf('@') === 0; - }); - - // Cleanup `ns` from unwanted parts and then normalize slashes to `:` - ns = ns - .replace(/(.*generator-)/, '') // Remove before `generator-` - .replace(/[\/\\](index|main)$/, '') // Remove `/index` or `/main` - .replace(/^[\/\\]+/, '') // Remove leading `/` - .replace(/[\/\\]+/g, ':'); // Replace slashes by `:` - - if (scope) { - ns = scope + '/' + ns; - } + args = Array.isArray(args) ? args : args.split(' '); + options = options || this.options; - debug('Resolve namespaces for %s: %s', filepath, ns); + const name = args.shift(); + if (!name) { + return this.error(new Error('Must provide at least one argument, the generator namespace to invoke.')); + } - return ns; -}; + const generator = this.create(name, { + args, + options + }); -/** - * Resolve a module path - * @param {String} moduleId - Filepath or module name - * @return {String} - The resolved path leading to the module - */ -Environment.prototype.resolveModulePath = function resolveModulePath(moduleId) { - if (moduleId[0] === '.') { - moduleId = path.resolve(moduleId); - } - if (path.extname(moduleId) === '') { - moduleId += path.sep; - } + if (generator instanceof Error) { + return generator; + } - return require.resolve(untildify(moduleId)); -}; + if (options.help) { + return console.log(generator.help()); + } -/** - * Make sure the Environment present expected methods if an old version is - * passed to a Generator. - * @param {Environment} env - * @return {Environment} The updated env - */ -Environment.enforceUpdate = function (env) { - if (!env.adapter) { - env.adapter = new TerminalAdapter(); + return generator.run(done); } - if (!env.runLoop) { - env.runLoop = new GroupedQueue([ - 'initializing', - 'prompting', - 'configuring', - 'default', - 'writing', - 'conflicts', - 'install', - 'end' - ]); - } + /** + * Given a String `filepath`, tries to figure out the relative namespace. + * + * ### Examples: + * + * this.namespace('backbone/all/index.js'); + * // => backbone:all + * + * this.namespace('generator-backbone/model'); + * // => backbone:model + * + * this.namespace('backbone.js'); + * // => backbone + * + * this.namespace('generator-mocha/backbone/model/index.js'); + * // => mocha:backbone:model + * + * @param {String} filepath + */ + namespace(filepath) { + if (!filepath) { + throw new Error('Missing namespace'); + } + + // Cleanup extension and normalize path for differents OS + let ns = path.normalize(filepath.replace(new RegExp(escapeStrRe(path.extname(filepath)) + '$'), '')); + + // Sort lookups by length so biggest are removed first + const lookups = _(this.lookups.concat(['..'])).map(path.normalize).sortBy('length').value().reverse(); + + // If `ns` contains a lookup dir in its path, remove it. + ns = lookups.reduce((ns, lookup) => { + // Only match full directory (begin with leading slash or start of input, end with trailing slash) + lookup = new RegExp(`(?:\\\\|/|^)${escapeStrRe(lookup)}(?=\\\\|/)`, 'g'); + return ns.replace(lookup, ''); + }, ns); + + const folders = ns.split(path.sep); + const scope = _.findLast(folders, folder => folder.indexOf('@') === 0); + + // Cleanup `ns` from unwanted parts and then normalize slashes to `:` + ns = ns + .replace(/(.*generator-)/, '') // Remove before `generator-` + .replace(/[/\\](index|main)$/, '') // Remove `/index` or `/main` + .replace(/^[/\\]+/, '') // Remove leading `/` + .replace(/[/\\]+/g, ':'); // Replace slashes by `:` - if (!env.sharedFs) { - env.sharedFs = memFs.create(); + if (scope) { + ns = `${scope}/${ns}`; + } + + debug('Resolve namespaces for %s: %s', filepath, ns); + + return ns; } - return env; -}; + /** + * Resolve a module path + * @param {String} moduleId - Filepath or module name + * @return {String} - The resolved path leading to the module + */ + resolveModulePath(moduleId) { + if (moduleId[0] === '.') { + moduleId = path.resolve(moduleId); + } -/** - * Factory method to create an environment instance. Take same parameters as the - * Environment constructor. - * - * @see This method take the same arguments as {@link Environment} constructor - * - * @return {Environment} a new Environment instance - */ -Environment.createEnv = (args, opts, adapter) => new Environment(args, opts, adapter); + if (path.extname(moduleId) === '') { + moduleId += path.sep; + } -/** - * Convert a generators namespace to its name - * - * @param {String} namespace - * @return {String} - */ -Environment.namespaceToName = namespace => namespace.split(':')[0]; + return require.resolve(untildify(moduleId)); + } +} + +Object.assign(Environment.prototype, resolver); /** * Expose the utilities on the module * @see {@link env/util} */ Environment.util = require('./util/util'); + +module.exports = Environment; diff --git a/lib/store.js b/lib/store.js index 5a87f606..83afa8ae 100644 --- a/lib/store.js +++ b/lib/store.js @@ -7,80 +7,81 @@ * @constructor * @private */ -const Store = module.exports = function Store() { - this._generators = {}; - this._meta = {}; -}; - -/** - * Store a module under the namespace key - * @param {String} namespace - The key under which the generator can be retrieved - * @param {String|Function} generator - A generator module or a module path - */ -Store.prototype.add = function add(namespace, generator) { - if (typeof generator === 'string') { - this._storeAsPath(namespace, generator); - return; +class Store { + constructor() { + this._generators = {}; + this._meta = {}; } - this._storeAsModule(namespace, generator); -}; - -Store.prototype._storeAsPath = function _storeAsPath(namespace, path) { - this._meta[namespace] = { - resolved: path, - namespace - }; - - Object.defineProperty(this._generators, namespace, { - get() { - const Generator = require(path); - return Generator; - }, - enumerable: true, - configurable: true - }); -}; + /** + * Store a module under the namespace key + * @param {String} namespace - The key under which the generator can be retrieved + * @param {String|Function} generator - A generator module or a module path + */ + add(namespace, generator) { + if (typeof generator === 'string') { + this._storeAsPath(namespace, generator); + return; + } -Store.prototype._storeAsModule = function _storeAsModule(namespace, Generator) { - this._meta[namespace] = { - resolved: 'unknown', - namespace - }; + this._storeAsModule(namespace, generator); + } - this._generators[namespace] = Generator; -}; + _storeAsPath(namespace, path) { + this._meta[namespace] = { + resolved: path, + namespace + }; -/** - * Get the module registered under the given namespace - * @param {String} namespace - * @return {Module} - */ + Object.defineProperty(this._generators, namespace, { + get() { + const Generator = require(path); + return Generator; + }, + enumerable: true, + configurable: true + }); + } -Store.prototype.get = function get(namespace) { - const Generator = this._generators[namespace]; + _storeAsModule(namespace, Generator) { + this._meta[namespace] = { + resolved: 'unknown', + namespace + }; - if (!Generator) { - return; + this._generators[namespace] = Generator; } - return Object.assign(Generator, this._meta[namespace]); -}; + /** + * Get the module registered under the given namespace + * @param {String} namespace + * @return {Module} + */ + get(namespace) { + const Generator = this._generators[namespace]; -/** - * Returns the list of registered namespace. - * @return {Array} Namespaces array - */ + if (!Generator) { + return; + } -Store.prototype.namespaces = function namespaces() { - return Object.keys(this._generators); -}; + return Object.assign(Generator, this._meta[namespace]); + } -/** - * Get the stored generators meta data - * @return {Object} Generators metadata - */ + /** + * Returns the list of registered namespace. + * @return {Array} Namespaces array + */ + namespaces() { + return Object.keys(this._generators); + } + + /** + * Get the stored generators meta data + * @return {Object} Generators metadata + */ + getGeneratorsMeta() { + return this._meta; + } +} -Store.prototype.getGeneratorsMeta = function getGeneratorsMeta() { - return this._meta; -}; +module.exports = Store; diff --git a/lib/util/log.js b/lib/util/log.js index b6042f80..3ba78c96 100644 --- a/lib/util/log.js +++ b/lib/util/log.js @@ -112,7 +112,7 @@ module.exports = () => { padding = padding.replace(step, ''); }); - // eslint-disable no-loop-func + /* eslint-disable no-loop-func */ // TODO: Fix this ESLint warning for (const status of Object.keys(colors)) { // Each predefined status has its logging method utility, handling @@ -146,7 +146,7 @@ module.exports = () => { return this; }; } - // eslint-enable no-loop-func + /* eslint-enable no-loop-func */ // A basic wrapper around `cli-table` package, resetting any single // char to empty strings, this is used for aligning options and diff --git a/lib/util/util.js b/lib/util/util.js index ba4f9f36..575b2d34 100644 --- a/lib/util/util.js +++ b/lib/util/util.js @@ -14,7 +14,7 @@ const GroupedQueue = require('grouped-queue'); */ exports.duplicateEnv = initialEnv => { const queues = require('../environment').queues; - // Hack: create a clone of the environment with a new instance of runLoop + // Hack: Create a clone of the environment with a new instance of `runLoop` const env = Object.create(initialEnv); env.runLoop = new GroupedQueue(queues); return env; diff --git a/package.json b/package.json index 0085e382..bdb2622b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "license": "BSD-2-Clause", "repository": "yeoman/environment", "scripts": { - "test": "gulp", + "test": "xo && gulp", "doc": "jsdoc -c ./jsdoc.json ./readme.md", "benchmark": "matcha benchmark/**", "prepublish": "gulp prepublish" @@ -57,6 +57,7 @@ "mocha": "^3.2.0", "shelljs": "^0.7.0", "sinon": "^1.9.1", + "xo": "^0.17.1", "yeoman-assert": "^3.0.0", "yeoman-generator": "^1.1.0" }, @@ -65,6 +66,9 @@ "envs": [ "node", "mocha" - ] + ], + "rules": { + "import/no-dynamic-require": "off" + } } } diff --git a/test/environment.js b/test/environment.js index a9bb837f..96108327 100644 --- a/test/environment.js +++ b/test/environment.js @@ -9,32 +9,32 @@ const assert = require('yeoman-assert'); const TerminalAdapter = require('../lib/adapter'); const Environment = require('../lib/environment'); -describe('Environment', function () { +describe('Environment', () => { beforeEach(function () { - this.env = new Environment([], { 'skip-install': true }); + this.env = new Environment([], {'skip-install': true}); }); afterEach(function () { this.env.removeAllListeners(); }); - it('is an instance of EventEmitter', function () { + it('is an instance of EventEmitter', () => { assert.ok(new Environment() instanceof events.EventEmitter); }); - describe('constructor', function () { - it('take arguments option', function () { - var args = ['foo']; + describe('constructor', () => { + it('take arguments option', () => { + const args = ['foo']; assert.equal(new Environment(args).arguments, args); }); - it('take arguments parameter option as string', function () { - var args = 'foo bar'; + it('take arguments parameter option as string', () => { + const args = 'foo bar'; assert.deepEqual(new Environment(args).arguments, args.split(' ')); }); - it('take options parameter', function () { - var opts = { foo: 'bar' }; + it('take options parameter', () => { + const opts = {foo: 'bar'}; assert.equal(new Environment(null, opts).options, opts); }); @@ -42,9 +42,9 @@ describe('Environment', function () { assert.ok(this.env.adapter instanceof TerminalAdapter); }); - it('uses the provided object as adapter if any', function () { - var dummyAdapter = {}; - var env = new Environment(null, null, dummyAdapter); + it('uses the provided object as adapter if any', () => { + const dummyAdapter = {}; + const env = new Environment(null, null, dummyAdapter); assert.equal(env.adapter, dummyAdapter, 'Not the adapter provided'); }); @@ -53,7 +53,7 @@ describe('Environment', function () { }); }); - describe('#help()', function () { + describe('#help()', () => { beforeEach(function () { this.env .register(path.join(__dirname, 'fixtures/generator-simple')) @@ -61,7 +61,7 @@ describe('Environment', function () { this.expected = fs.readFileSync(path.join(__dirname, 'fixtures/help.txt'), 'utf8').trim(); - // lazy "update the help fixtures because something changed" statement + // Lazy "update the help fixtures because something changed" statement // fs.writeFileSync(path.join(__dirname, 'fixtures/help.txt'), env.help().trim()); }); @@ -75,7 +75,7 @@ describe('Environment', function () { }); }); - describe('#create()', function () { + describe('#create()', () => { beforeEach(function () { this.Generator = Generator.extend(); this.env.registerStub(this.Generator, 'stub'); @@ -92,45 +92,45 @@ describe('Environment', function () { }); it('pass options.arguments', function () { - var args = ['foo', 'bar']; - var generator = this.env.create('stub', { arguments: args }); + const args = ['foo', 'bar']; + const generator = this.env.create('stub', {arguments: args}); assert.deepEqual(generator.arguments, args); }); it('pass options.arguments as string', function () { - var args = 'foo bar'; - var generator = this.env.create('stub', { arguments: args }); + const args = 'foo bar'; + const generator = this.env.create('stub', {arguments: args}); assert.deepEqual(generator.arguments, args.split(' ')); }); it('pass options.args (as `arguments` alias)', function () { - var args = ['foo', 'bar']; - var generator = this.env.create('stub', { args: args }); + const args = ['foo', 'bar']; + const generator = this.env.create('stub', {args}); assert.deepEqual(generator.arguments, args); }); it('prefer options.arguments over options.args', function () { - var args1 = ['yo', 'unicorn']; - var args = ['foo', 'bar']; - var generator = this.env.create('stub', { arguments: args1, args: args }); + const args1 = ['yo', 'unicorn']; + const args = ['foo', 'bar']; + const generator = this.env.create('stub', {arguments: args1, args}); assert.deepEqual(generator.arguments, args1); }); it('default arguments to `env.arguments`', function () { - var args = ['foo', 'bar']; + const args = ['foo', 'bar']; this.env.arguments = args; - var generator = this.env.create('stub'); + const generator = this.env.create('stub'); assert.notEqual(generator.arguments, args, 'expect arguments to not be passed by reference'); }); it('pass options.options', function () { - var opts = { foo: 'bar' }; - var generator = this.env.create('stub', { options: opts }); + const opts = {foo: 'bar'}; + const generator = this.env.create('stub', {options: opts}); assert.equal(generator.options, opts); }); it('default options to `env.options` content', function () { - this.env.options = { foo: 'bar' }; + this.env.options = {foo: 'bar'}; assert.equal(this.env.create('stub').options.foo, 'bar'); }); @@ -155,16 +155,19 @@ describe('Environment', function () { }); }); - describe('#run()', function () { + describe('#run()', () => { beforeEach(function () { - var self = this; - this.Stub = Generator.extend({ - constructor: function () { - self.args = arguments; - Generator.apply(this, arguments); - }, - exec: function () {} - }); + const self = this; + + this.Stub = class extends Generator { + constructor(args, opts) { + super(args, opts); + self.args = [args, opts]; + } + + exec() {} + }; + this.runMethod = sinon.spy(Generator.prototype, 'run'); this.env.registerStub(this.Stub, 'stub:run'); }); @@ -174,52 +177,52 @@ describe('Environment', function () { }); it('runs a registered generator', function (done) { - this.env.run(['stub:run'], function () { + this.env.run(['stub:run'], () => { assert.ok(this.runMethod.calledOnce); done(); - }.bind(this)); + }); }); it('pass args and options to the runned generator', function (done) { - var args = ['stub:run', 'module']; - var options = { 'skip-install': true }; - this.env.run(args, options, function () { + const args = ['stub:run', 'module']; + const options = {'skip-install': true}; + this.env.run(args, options, () => { assert.ok(this.runMethod.calledOnce); assert.equal(this.args[0], 'module'); assert.equal(this.args[1], options); done(); - }.bind(this)); + }); }); it('without options, it default to env.options', function (done) { - var args = ['stub:run', 'foo']; - this.env.options = { some: 'stuff', 'skip-install': true }; - this.env.run(args, function () { + const args = ['stub:run', 'foo']; + this.env.options = {some: 'stuff', 'skip-install': true}; + this.env.run(args, () => { assert.ok(this.runMethod.calledOnce); assert.equal(this.args[0], 'foo'); assert.equal(this.args[1], this.env.options); done(); - }.bind(this)); + }); }); it('without args, it default to env.arguments', function (done) { this.env.arguments = ['stub:run', 'my-args']; - this.env.options = { 'skip-install': true }; - this.env.run(function () { + this.env.options = {'skip-install': true}; + this.env.run(() => { assert.ok(this.runMethod.calledOnce); assert.equal(this.args[0], 'my-args'); assert.equal(this.args[1], this.env.options); done(); - }.bind(this)); + }); }); it('can take string as args', function (done) { - var args = 'stub:run module'; - this.env.run(args, function () { + const args = 'stub:run module'; + this.env.run(args, () => { assert.ok(this.runMethod.calledOnce); assert.equal(this.args[0], 'module'); done(); - }.bind(this)); + }); }); it('can take no arguments', function () { @@ -229,7 +232,7 @@ describe('Environment', function () { }); it('launch error if generator is not found', function (done) { - this.env.on('error', function (err) { + this.env.on('error', err => { assert.ok(err.message.indexOf('some:unknown:generator') >= 0); done(); }); @@ -241,12 +244,12 @@ describe('Environment', function () { }); }); - describe('#registerModulePath()', function () { + describe('#registerModulePath()', () => { it('resolves to a directory if no file type specified', function () { - var modulePath = path.join(__dirname, 'fixtures/generator-scoped/package'); - var specifiedJS = path.join(__dirname, 'fixtures/generator-scoped/package/index.js'); - var specifiedJSON = path.join(__dirname, 'fixtures/generator-scoped/package.json'); - var specifiedNode = path.join(__dirname, 'fixtures/generator-scoped/package/nodefile.node'); + const modulePath = path.join(__dirname, 'fixtures/generator-scoped/package'); + const specifiedJS = path.join(__dirname, 'fixtures/generator-scoped/package/index.js'); + const specifiedJSON = path.join(__dirname, 'fixtures/generator-scoped/package.json'); + const specifiedNode = path.join(__dirname, 'fixtures/generator-scoped/package/nodefile.node'); assert.equal(specifiedJS, this.env.resolveModulePath(modulePath)); assert.equal(specifiedJS, this.env.resolveModulePath(specifiedJS)); @@ -255,7 +258,7 @@ describe('Environment', function () { }); }); - describe('#register()', function () { + describe('#register()', () => { beforeEach(function () { this.simplePath = path.join(__dirname, 'fixtures/generator-simple'); this.extendPath = path.join(__dirname, './fixtures/generator-extend/support'); @@ -270,25 +273,31 @@ describe('Environment', function () { }); it('determine registered Generator namespace and resolved path', function () { - var simple = this.env.get('fixtures:generator-simple'); + const simple = this.env.get('fixtures:generator-simple'); assert.equal(typeof simple, 'function'); assert.ok(simple.namespace, 'fixtures:generator-simple'); assert.ok(simple.resolved, path.resolve(this.simplePath)); - var extend = this.env.get('scaffold'); + const extend = this.env.get('scaffold'); assert.equal(typeof extend, 'function'); assert.ok(extend.namespace, 'scaffold'); assert.ok(extend.resolved, path.resolve(this.extendPath)); }); - it('throw when String is not passed as first parameter', function () { - assert.throws(function () { this.env.register(function () {}, 'blop'); }); - assert.throws(function () { this.env.register([], 'blop'); }); - assert.throws(function () { this.env.register(false, 'blop'); }); + it('throw when String is not passed as first parameter', () => { + assert.throws(function () { + this.env.register(() => {}, 'blop'); + }); + assert.throws(function () { + this.env.register([], 'blop'); + }); + assert.throws(function () { + this.env.register(false, 'blop'); + }); }); }); - describe('#registerStub()', function () { + describe('#registerStub()', () => { beforeEach(function () { this.simpleDummy = sinon.spy(); this.completeDummy = function () {}; @@ -311,7 +320,7 @@ describe('Environment', function () { }); }); - describe('#namespaces()', function () { + describe('#namespaces()', () => { beforeEach(function () { this.env .register(path.join(__dirname, './fixtures/generator-simple')) @@ -324,20 +333,20 @@ describe('Environment', function () { }); }); - describe('#getGeneratorsMeta()', function () { + describe('#getGeneratorsMeta()', () => { beforeEach(function () { this.generatorPath = path.join(__dirname, './fixtures/generator-simple'); this.env.register(this.generatorPath); }); it('get the registered Generators metadatas', function () { - var meta = this.env.getGeneratorsMeta().simple; + const meta = this.env.getGeneratorsMeta().simple; assert.deepEqual(meta.resolved, require.resolve(this.generatorPath)); assert.deepEqual(meta.namespace, 'simple'); }); }); - describe('#getGeneratorNames', function () { + describe('#getGeneratorNames', () => { beforeEach(function () { this.generatorPath = path.join(__dirname, './fixtures/generator-simple'); this.env.register(this.generatorPath); @@ -348,7 +357,7 @@ describe('Environment', function () { }); }); - describe('#namespace()', function () { + describe('#namespace()', () => { it('create namespace from path', function () { assert.equal(this.env.namespace('backbone/all/index.js'), 'backbone:all'); assert.equal(this.env.namespace('backbone/all/main.js'), 'backbone:all'); @@ -411,7 +420,7 @@ describe('Environment', function () { }); }); - describe('#get()', function () { + describe('#get()', () => { beforeEach(function () { this.generator = require('./fixtures/generator-mocha'); this.env @@ -429,20 +438,20 @@ describe('Environment', function () { assert.equal(this.env.get('mocha:generator:C:\\foo\\bar'), this.generator); }); - it('works with Windows\' absolute paths', sinon.test(function() { - var absolutePath = 'C:\\foo\\bar'; + it('works with Windows\' absolute paths', sinon.test(function () { + const absolutePath = 'C:\\foo\\bar'; - var envMock = this.mock(this.env); + const envMock = this.mock(this.env); - envMock - .expects("getByPath") + envMock + .expects('getByPath') .once() .withExactArgs(absolutePath) .returns(null); - this.env.get(absolutePath); + this.env.get(absolutePath); - envMock.verify(); + envMock.verify(); })); it('fallback to requiring generator from a file path', function () { @@ -458,10 +467,10 @@ describe('Environment', function () { }); }); - describe('#error()', function () { + describe('#error()', () => { it('delegate error handling to the listener', function (done) { - var error = new Error('foo bar'); - this.env.on('error', function (err) { + const error = new Error('foo bar'); + this.env.on('error', err => { assert.equal(error, err); done(); }); @@ -473,20 +482,20 @@ describe('Environment', function () { }); it('returns the error', function () { - var error = new Error('foo bar'); - this.env.on('error', function () {}); + const error = new Error('foo bar'); + this.env.on('error', () => {}); assert.equal(this.env.error(error), error); }); }); - describe('#alias()', function () { + describe('#alias()', () => { it('apply regex and replace with alternative value', function () { this.env.alias(/^([^:]+)$/, '$1:app'); assert.equal(this.env.alias('foo'), 'foo:app'); }); it('apply multiple regex', function () { - this.env.alias(/^([a-zA-Z0-9:\*]+)$/, 'generator-$1'); + this.env.alias(/^([a-zA-Z0-9:*]+)$/, 'generator-$1'); this.env.alias(/^([^:]+)$/, '$1:app'); assert.equal(this.env.alias('foo'), 'generator-foo:app'); }); @@ -502,7 +511,7 @@ describe('Environment', function () { }); }); - describe('.enforceUpdate()', function () { + describe('.enforceUpdate()', () => { beforeEach(function () { this.env = new Environment(); delete this.env.adapter; @@ -526,16 +535,16 @@ describe('Environment', function () { }); }); - describe('.createEnv()', function () { - it('create an environment', function () { - var env = Environment.createEnv(); + describe('.createEnv()', () => { + it('create an environment', () => { + const env = Environment.createEnv(); assert(env instanceof Environment); }); }); - describe('.namespaceToName()', function () { - it('convert a namespace to a name', function () { - var name = Environment.namespaceToName('mocha:generator'); + describe('.namespaceToName()', () => { + it('convert a namespace to a name', () => { + const name = Environment.namespaceToName('mocha:generator'); assert.equal(name, 'mocha'); }); });