UNPKG

15.6 kBJavaScriptView Raw
1/*!
2 * base-generators <https://github.com/jonschlinkert/base-generators>
3 *
4 * Copyright (c) 2016, Jon Schlinkert.
5 * Licensed under the MIT License.
6 */
7
8'use strict';
9
10var path = require('path');
11var debug = require('debug')('base:generators');
12var generator = require('./lib/generator');
13var generate = require('./lib/generate');
14var plugins = require('./lib/plugins');
15var tasks = require('./lib/tasks');
16var utils = require('./lib/utils');
17var parseTasks = tasks.parse;
18
19/**
20 * Expose `generators`
21 */
22
23module.exports = function(config) {
24 config = config || {};
25
26 return function plugin(app) {
27 if (!utils.isValid(app, 'base-generators')) return;
28
29 var cache = {};
30 this.generators = {};
31 this.isGenerator = true;
32
33 this.define('constructor', this.constructor);
34 this.use(plugins());
35 this.fns.push(plugin);
36
37 /**
38 * Alias to `.setGenerator`.
39 *
40 * ```js
41 * base.register('foo', function(app, base) {
42 * // "app" is a private instance created for the generator
43 * // "base" is a shared instance
44 * });
45 * ```
46 * @name .register
47 * @param {String} `name` The generator's name
48 * @param {Object|Function|String} `options` or generator
49 * @param {Object|Function|String} `generator` Generator function, instance or filepath.
50 * @return {Object} Returns the generator instance.
51 * @api public
52 */
53
54 this.define('register', function(name, options, generator) {
55 debug('.register', name);
56 return this.setGenerator.apply(this, arguments);
57 });
58
59 /**
60 * Get and invoke generator `name`, or register generator `name` with
61 * the given `val` and `options`, then invoke and return the generator
62 * instance. This method differs from `.register`, which lazily invokes
63 * generator functions when `.generate` is called.
64 *
65 * ```js
66 * base.generator('foo', function(app, base, env, options) {
67 * // "app" - private instance created for generator "foo"
68 * // "base" - instance shared by all generators
69 * // "env" - environment object for the generator
70 * // "options" - options passed to the generator
71 * });
72 * ```
73 * @name .generator
74 * @param {String} `name`
75 * @param {Function|Object} `fn` Generator function, instance or filepath.
76 * @return {Object} Returns the generator instance or undefined if not resolved.
77 * @api public
78 */
79
80 this.define('generator', function(name, val, options) {
81 debug('.generator', name, val);
82
83 if (isSet(val)) {
84 return this.generators[name] || this.base.generators[name];
85 }
86
87 this.setGenerator.apply(this, arguments);
88 return this.getGenerator(name);
89 });
90
91 /**
92 * Store a generator by file path or instance with the given
93 * `name` and `options`.
94 *
95 * ```js
96 * base.setGenerator('foo', function(app, base) {
97 * // "app" - private instance created for generator "foo"
98 * // "base" - instance shared by all generators
99 * // "env" - environment object for the generator
100 * // "options" - options passed to the generator
101 * });
102 * ```
103 * @name .setGenerator
104 * @param {String} `name` The generator's name
105 * @param {Object|Function|String} `options` or generator
106 * @param {Object|Function|String} `generator` Generator function, instance or filepath.
107 * @return {Object} Returns the generator instance.
108 * @api public
109 */
110
111 this.define('setGenerator', function(name, val, options) {
112 debug('.setGenerator', name);
113
114 if (isSet(val)) {
115 return this.generators[name] || this.base.generators[name];
116 }
117
118 if (val && (typeof val === 'object' || typeof val === 'function')) {
119 utils.define(val, '_setGenerator', true);
120 }
121
122 // ensure local sub-generator paths are resolved
123 if (typeof val === 'string' && val.charAt(0) === '.' && this.env) {
124 val = path.resolve(this.env.dirname, val);
125 }
126 return generator(name, val, options, this);
127 });
128
129 /**
130 * Get generator `name` from `app.generators` and invoke it with the current instance.
131 * Dot-notation may be used to get a sub-generator.
132 *
133 * ```js
134 * var foo = app.getGenerator('foo');
135 *
136 * // get a sub-generator
137 * var baz = app.getGenerator('foo.bar.baz');
138 * ```
139 * @name .getGenerator
140 * @param {String} `name` Generator name.
141 * @return {Object|undefined} Returns the generator instance or undefined.
142 * @api public
143 */
144
145 this.define('getGenerator', function(name, options) {
146 debug('.getGenerator', name);
147
148 if (name === 'this') {
149 return this;
150 }
151
152 var gen = this.findGenerator(name, options);
153 if (utils.isValidInstance(gen)) {
154 return gen.invoke(gen, options);
155 }
156 });
157
158 /**
159 * Find generator `name`, by first searching the cache, then searching the
160 * cache of the `base` generator. Use this to get a generator without invoking it.
161 *
162 * ```js
163 * // search by "alias"
164 * var foo = app.findGenerator('foo');
165 *
166 * // search by "full name"
167 * var foo = app.findGenerator('generate-foo');
168 * ```
169 * @name .findGenerator
170 * @param {String} `name`
171 * @param {Function} `options` Optionally supply a rename function on `options.toAlias`
172 * @return {Object|undefined} Returns the generator instance if found, or undefined.
173 * @api public
174 */
175
176 this.define('findGenerator', function fn(name, options) {
177 debug('.findGenerator', name);
178
179 if (utils.isObject(name)) {
180 return name;
181 }
182
183 if (Array.isArray(name)) {
184 name = name.join('.');
185 }
186
187 if (typeof name !== 'string') {
188 throw new TypeError('expected name to be a string');
189 }
190
191 if (cache.hasOwnProperty(name)) {
192 return cache[name];
193 }
194
195 var app = this.generators[name]
196 || this.base.generators[name]
197 || this._findGenerator(name, options);
198
199 // if no app, check the `base` instance
200 if (typeof app === 'undefined' && this.hasOwnProperty('parent')) {
201 app = this.base._findGenerator(name, options);
202 }
203
204 if (app) {
205 cache[app.name] = app;
206 cache[app.alias] = app;
207 cache[name] = app;
208 return app;
209 }
210
211 var search = {name, options};
212 this.base.emit('unresolved', search, this);
213 if (search.app) {
214 cache[search.app.name] = search.app;
215 cache[search.app.alias] = search.app;
216 return search.app;
217 }
218
219 cache[name] = null;
220 });
221
222 /**
223 * Private method used by `.findGenerator`
224 */
225
226 this.define('_findGenerator', function(name, options) {
227 if (this.generators.hasOwnProperty(name)) {
228 return this.generators[name];
229 }
230
231 if (~name.indexOf('.')) {
232 return this.getSubGenerator.apply(this, arguments);
233 }
234
235 var opts = utils.extend({}, this.options, options);
236 if (typeof opts.lookup === 'function') {
237 var app = this.lookupGenerator(name, opts, opts.lookup);
238 if (app) {
239 return app;
240 }
241 }
242 return this.matchGenerator(name);
243 });
244
245 /**
246 * Get sub-generator `name`, optionally using dot-notation for nested generators.
247 *
248 * ```js
249 * app.getSubGenerator('foo.bar.baz');
250 * ```
251 * @name .getSubGenerator
252 * @param {String} `name` The property-path of the generator to get
253 * @param {Object} `options`
254 * @api public
255 */
256
257 this.define('getSubGenerator', function(name, options) {
258 debug('.getSubGenerator', name);
259 var segs = name.split('.');
260 var len = segs.length;
261 var idx = -1;
262 var app = this;
263
264 while (++idx < len) {
265 var key = segs[idx];
266 app = app.getGenerator(key, options);
267 if (!app) {
268 break;
269 }
270 }
271 return app;
272 });
273
274 /**
275 * Iterate over `app.generators` and call `generator.isMatch(name)`
276 * on `name` until a match is found.
277 *
278 * @param {String} `name`
279 * @return {Object|undefined} Returns a generator object if a match is found.
280 * @api public
281 */
282
283 this.define('matchGenerator', function(name) {
284 debug('.matchGenerator', name);
285 for (var key in this.generators) {
286 var generator = this.generators[key];
287 if (generator.isMatch(name)) {
288 return generator;
289 }
290 }
291 });
292
293 /**
294 * Tries to find a registered generator that matches `name`
295 * by iterating over the `generators` object, and doing a strict
296 * comparison of each name returned by the given lookup `fn`.
297 * The lookup function receives `name` and must return an array
298 * of names to use for the lookup.
299 *
300 * For example, if the lookup `name` is `foo`, the function might
301 * return `["generator-foo", "foo"]`, to ensure that the lookup happens
302 * in that order.
303 *
304 * @param {String} `name` Generator name to search for
305 * @param {Object} `options`
306 * @param {Function} `fn` Lookup function that must return an array of names.
307 * @return {Object}
308 * @api public
309 */
310
311 this.define('lookupGenerator', function(name, options, fn) {
312 debug('.lookupGenerator', name);
313 if (typeof options === 'function') {
314 fn = options;
315 options = {};
316 }
317
318 if (typeof fn !== 'function') {
319 throw new TypeError('expected `fn` to be a lookup function');
320 }
321
322 options = options || {};
323
324 // remove `lookup` fn from options to prevent self-recursion
325 delete this.options.lookup;
326 delete options.lookup;
327
328 var names = fn(name);
329 debug('looking up generator "%s" with "%j"', name, names);
330
331 var len = names.length;
332 var idx = -1;
333
334 while (++idx < len) {
335 var gen = this.findGenerator(names[idx], options);
336 if (gen) {
337 this.options.lookup = fn;
338 return gen;
339 }
340 }
341
342 this.options.lookup = fn;
343 });
344
345 /**
346 * Extend the generator instance with settings and features
347 * of another generator.
348 *
349 * ```js
350 * var foo = base.generator('foo');
351 * base.extendWith(foo);
352 * // or
353 * base.extendWith('foo');
354 * // or
355 * base.extendWith(['foo', 'bar', 'baz']);
356 *
357 * app.extendWith(require('generate-defaults'));
358 * ```
359 *
360 * @name .extendWith
361 * @param {String|Object} `app`
362 * @return {Object} Returns the instance for chaining.
363 * @api public
364 */
365
366 this.define('extendWith', function(name, options) {
367 if (typeof name === 'function') {
368 this.use(name, options);
369 return this;
370 }
371
372 if (Array.isArray(name)) {
373 var len = name.length;
374 var idx = -1;
375 while (++idx < len) {
376 this.extendWith(name[idx], options);
377 }
378 return this;
379 }
380
381 var app = this.generators[name] || this.findGenerator(name, options);
382 if (!utils.isValidInstance(app)) {
383 throw new Error('cannot find generator: "' + name + '"');
384 }
385
386 var alias = app.env ? app.env.alias : 'default';
387 debug('extending "%s" with "%s"', alias, name);
388 app.run(this);
389 app.invoke(options, this);
390 return this;
391 });
392
393 /**
394 * Run a `generator` and `tasks`, calling the given `callback` function
395 * upon completion.
396 *
397 * ```js
398 * // run tasks `bar` and `baz` on generator `foo`
399 * base.generate('foo', ['bar', 'baz'], function(err) {
400 * if (err) throw err;
401 * });
402 *
403 * // or use shorthand
404 * base.generate('foo:bar,baz', function(err) {
405 * if (err) throw err;
406 * });
407 *
408 * // run the `default` task on generator `foo`
409 * base.generate('foo', function(err) {
410 * if (err) throw err;
411 * });
412 *
413 * // run the `default` task on the `default` generator, if defined
414 * base.generate(function(err) {
415 * if (err) throw err;
416 * });
417 * ```
418 * @name .generate
419 * @emits `generate` with the generator `name` and the array of `tasks` that are queued to run.
420 * @param {String} `name`
421 * @param {String|Array} `tasks`
422 * @param {Function} `cb` Callback function that exposes `err` as the only parameter.
423 */
424
425 this.define('generate', function(name, tasks, options, cb) {
426 var self = this;
427
428 if (typeof name === 'function') {
429 return this.generate('default', [], {}, name);
430 }
431 if (utils.isObject(name)) {
432 return this.generate('default', ['default'], name, tasks);
433 }
434 if (typeof tasks === 'function') {
435 return this.generate(name, [], {}, tasks);
436 }
437 if (utils.isObject(tasks)) {
438 return this.generate(name, [], tasks, options);
439 }
440 if (typeof options === 'function') {
441 return this.generate(name, tasks, {}, options);
442 }
443
444 if (Array.isArray(name)) {
445 return utils.eachSeries(name, function(val, next) {
446 self.generate(val, tasks, options, next);
447 }, cb);
448 }
449
450 if (typeof cb !== 'function') {
451 throw new TypeError('expected a callback function');
452 }
453
454 var queue = parseTasks(app, name, tasks);
455
456 utils.eachSeries(queue, function(queued, next) {
457 if (queued._ && queued._.length) {
458 if (queued._[0] === 'default') {
459 next();
460 return;
461 }
462 var msg = utils.formatError('generator', app, queued._);
463 next(new Error(msg));
464 return;
465 }
466
467 if (cb.name === 'finishRun' && typeof name === 'string' && queued.tasks.indexOf(name) !== -1) {
468 queued.name = name;
469 queued.tasks = ['default'];
470 }
471
472 queued.generator = queued.app || this.getGenerator(queued.name, options);
473
474 if (!utils.isGenerator(queued.generator)) {
475 if (queued.name === 'default') {
476 next();
477 return;
478 }
479 next(new Error(utils.formatError('generator', app, queued.name)));
480 return;
481 }
482
483 generate(this, queued, options, next);
484 }.bind(this), cb);
485 return;
486 });
487
488 /**
489 * Create a generator alias from the given `name`. By default the alias
490 * is the string after the last dash. Or the whole string if no dash exists.
491 *
492 * ```js
493 * var camelcase = require('camel-case');
494 * var alias = app.toAlias('foo-bar-baz');
495 * //=> 'baz'
496 *
497 * // custom `toAlias` function
498 * app.option('toAlias', function(name) {
499 * return camelcase(name);
500 * });
501 * var alias = app.toAlias('foo-bar-baz');
502 * //=> 'fooBarBaz'
503 * ```
504 * @name .toAlias
505 * @param {String} `name`
506 * @param {Object} `options`
507 * @return {String} Returns the alias.
508 * @api public
509 */
510
511 this.define('toAlias', function(name, options) {
512 if (typeof options === 'function') {
513 return options(name);
514 }
515 if (options && typeof options.toAlias === 'function') {
516 return options.toAlias(name);
517 }
518 if (typeof app.options.toAlias === 'function') {
519 return app.options.toAlias(name);
520 }
521 return name;
522 });
523
524 /**
525 * Getter that returns `true` if the current instance is the `default` generator
526 */
527
528 Object.defineProperty(this, 'isDefault', {
529 configurable: true,
530 get: function() {
531 return this.env && this.env.isDefault;
532 }
533 });
534
535 return plugin;
536 };
537};
538
539function isSet(val) {
540 return val && (typeof val === 'object' || typeof val === 'function') && val._setGenerator;
541}