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 | ;
|
9 |
|
10 | var path = require('path');
|
11 | var debug = require('debug')('base:generators');
|
12 | var generator = require('./lib/generator');
|
13 | var generate = require('./lib/generate');
|
14 | var plugins = require('./lib/plugins');
|
15 | var tasks = require('./lib/tasks');
|
16 | var utils = require('./lib/utils');
|
17 | var parseTasks = tasks.parse;
|
18 |
|
19 | /**
|
20 | * Expose `generators`
|
21 | */
|
22 |
|
23 | module.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 |
|
539 | function isSet(val) {
|
540 | return val && (typeof val === 'object' || typeof val === 'function') && val._setGenerator;
|
541 | }
|