123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- var fs = require('fs');
- var util = require('util');
- var path = require('path');
- var events = require('events');
- var spawn = require('child_process').spawn;
- var glob = require('glob');
- var chalk = require('chalk');
- var _ = require('lodash');
- var log = process.logging('generators');
- var engines = require('./util/engines');
- var debug = require('debug')('generators:environment');
- var Base = require('./base');
- // TODO(mklabs):
- //
- // - path manipulation methods ({append,prepend}{Path,Lookup}) can be dryed out.
- // - if it gets too huge (more than 200loc), split this into mulitple mixins.
- // - register() method too long, split logic for namespace etc. elsewhere
- // - flesh out Error handling, consider emitting error instead of throwing,
- // even for synchronous operation.
- /**
- * `Environment` object is responsible of handling the lifecyle and bootstrap
- * of generators in a specific environment (your app).
- *
- * It provides a high-level API to create and run generators, as well as further
- * tuning where and how a generator is resolved.
- *
- * An environment is created using a list of `arguments` and a Hash of
- * `options`. Usually, this is the list of arguments you get back from your CLI
- * options parser.
- *
- * @param {String|Array} args
- * @param {Object} opts
- */
- var Environment = module.exports = function Environment(args, opts) {
- events.EventEmitter.call(this);
- args = args || [];
- this.arguments = Array.isArray(args) ? args : args.split(' ');
- this.options = opts || {};
- this.cwd = this.options.cwd || process.cwd();
- this.generators = {};
- this.aliases = [];
- this.lookups = [];
- this.paths = [];
- this.appendLookup('.');
- this.appendLookup('generators');
- // DEPRECATED: use the path `generators` instead
- this.appendLookup('lib/generators');
- this.appendPath('.');
- // adds support for global generators. ensures support across all OS since the
- // global node_modules dir always is four levels up from env.js
- this.appendPath(path.join(__dirname, '../../../..'));
- this._prefixReg = null;
- this._prefixes = this._prefixes || [];
- this._suffix = this._suffix || '';
- this.prefix(this.options.prefix || 'generator-');
- this.suffix(this.options.suffix || '*/index.js');
- this.plugins('node_modules');
- };
- util.inherits(Environment, events.EventEmitter);
- /**
- * 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
- */
- Environment.prototype.error = function error(err) {
- err = err instanceof Error ? err : new Error(err);
- if (!this.emit('error', err)) {
- throw err;
- }
- return this;
- };
- /**
- * Configures the `engine` to use for this environment.
- *
- * @param {String} engine
- */
- Environment.prototype.engine = function _engine(engine) {
- if (typeof engine === 'function') {
- this._engine = engine;
- return this;
- }
- if (!engines[engine]) {
- return this.error(new Error('Wrong engine (' + engine + '). Available: ' + Object.keys(engines).join(' ')));
- }
- this._engine = engines[engine];
- return this;
- };
- /**
- * Appends a `filepath` to the list of loadpaths.
- *
- * @param {String} filepath
- */
- Environment.prototype.appendPath = function appendPath(filepath) {
- if (!filepath) {
- return this.error(new Error('Missing filepath.'));
- }
- this.paths.push(filepath);
- return this;
- };
- /**
- * Appends a new `filepath` to the list of lookups path. This should be a
- * relative filepath, like `support/scaffold`. Environments are created with
- * `lib/generators` as a lookup path by default.
- *
- * @param {String} filepath
- */
- Environment.prototype.appendLookup = function appendLookup(filepath) {
- if (!filepath) {
- return this.error(new Error('Missing filepath.'));
- }
- this.lookups.push(filepath);
- 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
- */
- Environment.prototype.help = function help(name) {
- name = name || 'init';
- var out = [
- 'Usage: :binary: GENERATOR [args] [options]',
- '',
- 'General options:',
- ' -h, --help # Print generator\'s options and usage',
- ' -f, --force # Overwrite files that already exist',
- '',
- 'Please choose a generator below.',
- ''
- ];
- var ns = this.namespaces();
- var groups = {};
- ns.sort().forEach(function (namespace) {
- var base = namespace.split(':')[0];
- if (!groups[base]) {
- groups[base] = [];
- }
- groups[base] = groups[base].concat(namespace);
- });
- Object.keys(groups).forEach(function (key) {
- var group = groups[key];
- if (group.length >= 1) {
- out.push('', key.charAt(0).toUpperCase() + key.slice(1));
- }
- groups[key].forEach(function (ns) {
- out.push(' ' + ns);
- });
- });
- return out.join('\n')
- .replace(/:binary:/g, name);
- };
- /**
- * Registers a specific `generator` to this environment. A generator can be a
- * simple function or an object extending from `Generators.Base`. The later
- * method is favored as it allows you to specify options/arguments for
- * self-documented generator with `USAGE:` and so on.
- *
- * In case of a simple function, the generator does show up in the `--help`
- * output, but without any kind of arguments/options. You must document them
- * manually with your `USAGE` file.
- *
- * In any case, the API available in generators is the same. Raw functions are
- * executed within the context of a `new Generators.Base`.
- *
- * `register()` can also take Strings, in which case it is considered a
- * filepath to `require()`.
- *
- * @param {String|Function} name
- * @param {String} namespace
- */
- Environment.prototype.register = function register(name, namespace) {
- if (!name) {
- return this.error(new Error('You must provide a generator to register.'));
- }
- var cachePath = _.isString(name);
- var generator = cachePath ? function () {} : name;
- var isRaw = function (generator) {
- generator = Object.getPrototypeOf(generator.prototype);
- var methods = ['option', 'argument', 'hookFor', 'run'];
- return methods.filter(function (method) {
- return !!generator[method];
- }).length !== methods.length;
- };
- var filepath;
- var ns;
- if (cachePath) {
- filepath = name;
- if (filepath.charAt(0) === '.') {
- filepath = path.resolve(filepath);
- }
- if (filepath.charAt(0) === '~') {
- filepath = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'] + filepath.slice(1);
- }
- generator.resolved = require.resolve(filepath);
- generator.namespace = this.namespace(generator.resolved);
- generator.__register = function () {
- var invokedGenerator = require(generator.resolved);
- invokedGenerator.resolved = generator.resolved;
- this.register(invokedGenerator, generator.namespace);
- }.bind(this);
- }
- ns = namespace || generator.namespace || this.namespace(name);
- if (!ns) {
- return this.error(new Error('Unable to determine namespace.'));
- }
- // normalize what we got, at this point if we were unable to find a
- // namespace, we just consider the directory name under which this generator
- // is found.
- if (ns.indexOf('/') !== -1) {
- ns = path.basename(path.dirname(ns));
- }
- generator.namespace = ns;
- generator.raw = isRaw(generator);
- generator.resolved = generator.resolved || ns;
- this.generators[ns] = generator;
- debug('Registered %s (%s)', generator.namespace, generator.resolved);
- return this;
- };
- /**
- * Returns the list of registered namespace.
- */
- Environment.prototype.namespaces = function namespaces() {
- return Object.keys(this.generators);
- };
- /**
- * 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} namespace
- */
- Environment.prototype.get = function get(namespace) {
- if (!namespace) {
- return;
- }
- var get = function (namespace) {
- var generator = this.generators[namespace];
- if (generator) {
- if (generator.__register) {
- generator.__register();
- generator = get(namespace);
- }
- return generator;
- }
- }.bind(this);
- return get(namespace)
- || get(this.alias(namespace))
- || this.get(namespace.split(':').slice(0, -1).join(':'));
- };
- /**
- * 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 || {};
- var names = namespace.split(':');
- var name = names.slice(-1)[0];
- var generator = this.get(namespace);
- var args = options.arguments || options.args || this.arguments;
- args = Array.isArray(args) ? args : args.split(' ');
- var opts = options.options || _.clone(this.options);
- if (!generator) {
- return this.error(
- new Error(
- 'You don\'t seem to have a generator with the name ' + namespace + ' installed.\n' +
- chalk.bold('You can see available generators with ' + 'npm search yeoman-generator') +
- chalk.bold(' and then install them with ' + 'npm install [name]') + '.\n' +
- 'To see the ' + this.namespaces().length + ' registered generators run yo with the `--help` option.'
- )
- );
- }
- // case of raw functions, we create a brand new `Base` object and attach this
- // raw function as one of the prototype method. this effectively standardize
- // the interface for running generators, while allowing less boilerplate for
- // generators authors.
- var Generator = generator;
- if (generator.raw) {
- Generator = function () {
- Base.apply(this, arguments);
- };
- util.inherits(Generator, Base);
- Generator.prototype.exec = generator;
- }
- opts.env = this;
- opts.name = name;
- opts.resolved = generator.resolved;
- return new Generator(args, opts);
- };
- /**
- * 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;
- if (typeof options === 'function') {
- done = options;
- options = this.options;
- }
- if (typeof args === 'function') {
- done = args;
- options = this.options;
- args = this.arguments;
- }
- args = Array.isArray(args) ? args : args.split(' ');
- options = options || this.options;
- var name = args.shift();
- if (!name) {
- return this.error(new Error('Must provide at least one argument, the generator namespace to invoke.'));
- }
- // is it a remote? any generator with a `/` in it is considered
- // potential remote, and we simply proxy over `npm install` command to get
- // the generator locally.
- if (/\//.test(name)) {
- return this.remote(name, done);
- }
- var generator = this.create(name, {
- args: args,
- options: options
- });
- if (typeof done === 'function') {
- generator.on('end', done);
- }
- if (options.help) {
- return console.log(generator.help());
- }
- generator.on('start', this.emit.bind(this, 'generators:start'));
- generator.on('start', this.emit.bind(this, name + ':start'));
- var self = this;
- generator.on('method', function (method) {
- self.emit(name + ':' + method);
- });
- generator.on('end', this.emit.bind(this, name + ':end'));
- generator.on('end', this.emit.bind(this, 'generators:end'));
- return generator.run();
- };
- /**
- * Receives namespaces in an array and tries to find matching generators in the
- * load paths.
- *
- * We lookup namespaces in several places, namely `this.lookups`
- * list of relatives directory path. A `generator-` prefix is added if a
- * namespace wasn't `require()`-able directly, matching `generator-*` kind of
- * pattern in npm installed package.
- *
- * You can also lookup using glob-like star pattern, eg. `angular:*` gets
- * expanded to `angular\*\index.js`.
- *
- * The default alias `generator-$1` lookup is added automatically.
- *
- * ### Examples:
- *
- * // search for all angular generators in the load path
- * env.lookup('angular:*');
- *
- * // register any valid set of generator in the load paths
- * env.lookup('*:*');
- *
- * @param {String|Array} namespaces
- * @param {String} lookupdir
- */
- Environment.prototype.lookup = function lookup(namespaces, lookupdir) {
- namespaces = Array.isArray(namespaces) ? namespaces : namespaces.split(' ');
- debug('Lookup %s', namespaces);
- namespaces.forEach(function (ns) {
- var filepath = path.join.apply(path, this.alias(ns).split(':'));
- this.paths.forEach(function (base) {
- debug('Looking in %s with filepath %s', base, filepath);
- // no glob pattern
- if (!~filepath.indexOf('*')) {
- try {
- debug('Attempt to register with direct filepath %s', filepath);
- this.register(filepath);
- } catch (e) {
- // silent fail unless not a loadpath error
- if (e.message.indexOf(filepath) === -1) {
- console.error('Unable to register %s (Error: %s)', ns, e.message);
- }
- }
- return;
- }
- this.lookups.forEach(function (lookupdir) {
- var depth = lookupdir && /^\.\/?$/.test(lookupdir) ? '*' : '**';
- var prefixes = this._prefixes.filter(function (prefix) {
- return !(/\//).test(prefix);
- });
- var pattern = filepath
- .replace(/^\*+/, '+(' + prefixes.join('|') + ')*')
- .replace(/\*+$/g, path.join(lookupdir, depth, 'index.js'))
- .replace(/^\*\//, '');
- debug('Globing for generator %s with pattern %s (cwd: %s)', ns, pattern, base);
- glob.sync(pattern, { cwd: base }).forEach(function (filename) {
- // now register, warn on failed require
- try {
- debug('found %s, trying to register', filename);
- this.register(path.resolve(base, filename));
- } catch (e) {
- console.error('Unable to register %s (Error: %s)', filename, e.message);
- }
- }, this);
- }, this);
- }, this);
- }, this);
- return this;
- };
- /**
- * Get or create an alias.
- *
- * Alias allows the `get()` and `lookup()` methods to search in alternate
- * filepath for a given namespaces. It's used for example to map `generator-*`
- * npm package to their namespace equivalent (without the generator- prefix),
- * or to default a single namespace like `angular` to `angular:app` or
- * `angular:all`.
- *
- * Given a single argument, this method acts as a getter. When both name and
- * value are provided, acts as a setter and registers that new alias.
- *
- * If multiple alias are defined, then the replacement is recursive, replacing
- * each alias in reverse order.
- *
- * An alias can be a single String or a Regular Expression. The finding is done
- * based on .match().
- *
- * ### Examples:
- *
- * env.alias(/^([a-zA-Z0-9:\*]+)$/, 'generator-$1');
- * env.alias(/^([^:]+)$/, '$1:app');
- * env.alias(/^([^:]+)$/, '$1:all');
- * env.alias('foo');
- * // => generator-foo:all
- *
- * @param {String|RegExp} match
- * @param {String} value
- */
- Environment.prototype.alias = function alias(match, value) {
- if (match && value) {
- this.aliases.push({
- match: match instanceof RegExp ? match : new RegExp('^' + match + '$'),
- value: value
- });
- return this;
- }
- var aliases = this.aliases.slice(0).reverse();
- var matcher = aliases.filter(function (alias) {
- return alias.match.test(match);
- });
- return aliases.reduce(function (res, alias) {
- if (!alias.match.test(res)) {
- return res;
- }
- return res.replace(alias.match, alias.value);
- }, match);
- };
- /**
- * 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');
- }
- var self = this;
- // cleanup extension and normalize path for differents OS
- var ns = path.normalize(filepath.replace(path.extname(filepath), ''));
- // extend every lookups folders with searchable file system paths
- var lookups = _(this.lookups).map(function (lookup) {
- return _.map(self.paths, function (filepath) {
- return path.join(filepath, lookup);
- });
- }).flatten().sortBy('length').value().reverse();
- // if `ns` contain a lookup dir in it's path, remove it.
- ns = lookups.reduce(function (ns, lookup) {
- return ns.replace(lookup, '');
- }, ns);
- // cleanup `ns` from unwanted parts and then normalize slashes to `:`
- ns = ns
- .replace(/[\/\\]?node_modules[\/\\]?/, '') // remove `/node_modules/`
- .replace(/[\/\\](index|main)$/, '') // remove `/index` or `/main`
- .replace(/\.+/g, '') // remove `.`
- .replace(/^[\/\\]+/, '') // remove leading `/`
- .replace(/[\/\\]+/g, ':'); // replace slashes by `:`
- // if we still have prefix match at this point, then remove anything before
- // that match, this would catch symlinked package with a name begining with
- // `generator-*` (one of the configured prefix)
- ns = this._prefixes.reduce(function (ns, prefix) {
- var pos = ns.lastIndexOf(prefix);
- if (pos < 0) {
- return ns;
- }
- return ns.slice(pos + prefix.length);
- }, ns);
- debug('Resolve namespaces for %s: %s', filepath, ns);
- return ns;
- };
- /**
- * Adds the namespace prefix to this environment, such as `generator-*`,
- * used when resolving namespace, replacing the leading `*` in the
- * namespace by the configured prefix(es).
- *
- * ### Examples:
- *
- * this.prefix('generator-');
- *
- * @param {String} prefix
- */
- Environment.prototype.prefix = function _prefix(prefix) {
- if (!prefix) {
- throw new Error('Missing prefix');
- }
- this._prefixes.push(prefix);
- this._prefixReg = new RegExp('^(' + this._prefixes.join('|') + ')');
- return this;
- };
- /**
- * Get or set the namespace suffix to this environment, such as `*\index.js`,
- * used when resolving namespace, replacing the last `*` in the
- * namespace by the configured suffix.
- *
- * ### Examples:
- *
- * this.suffix('*\index.js');
- * this.suffix();
- * // => '*\index.js'
- *
- * @param {String} suffix
- */
- Environment.prototype.suffix = function _suffix(suffix) {
- this._suffix = this._suffix || '';
- if (!suffix) {
- return this._suffix;
- }
- this._suffix = suffix;
- return this;
- };
- /**
- * Walk up the filesystem looking for a `node_modules` folder, and add it if
- * found to the load path.
- *
- * @param {String} filename
- * @param {String} basedir
- */
- Environment.prototype.plugins = function plugins(filename, basedir) {
- filename = filename || 'node_modules';
- basedir = basedir || process.cwd();
- var filepath = path.join(basedir, filename);
- if (fs.existsSync(filepath)) {
- this.appendPath(filepath);
- return this;
- }
- if (basedir === path.resolve('/')) {
- return this;
- }
- return this.plugins(filename, path.join(basedir, '..'));
- };
- /**
- * Install an npm package locally, expanding github like user/repo pattern to
- * the remote tarball for master.
- *
- * It is taking care of potential remote packages (or local on the current file
- * system) by delegating the groundwork of getting the package to npm.
- *
- * @param {String} name
- * @param {Function} done
- */
- Environment.prototype.remote = function remote(name, done) {
- var self = this;
- log.write().info('Installing remote package %s', name).write();
- var npm = spawn('npm', ['install', name, '--save-dev']);
- npm.stdout.pipe(process.stdout);
- npm.stderr.pipe(process.stderr);
- done = done || function () {};
- npm.on('exit', function (code) {
- if (code !== 0) {
- return self.error(new Error('Error initing from remote: ' + name + '. Code: ' + code));
- }
- log.ok('Installed %s package', name).write();
- log.info('You should see additional generators available').write()
- .write(self.help()).write();
- done();
- });
- };
|