Yeoman: Call Sub-Generator With User-Supplied Arguments
Asked Answered
E

3

31

I'm writing my first Yeoman generator, which prompts the user for various inputs and conditionally creates files based on their responses. I need to be able to call a subroutine (could be a Yeoman sub-generator) based on user input, and pass arguments to it.

The reason I want to use named functions (which are not automatically run) is that sometimes the user's response should invoke a number of functions combined, and sometimes the function should be run alone.

What I have tried:

I figured sub-generators were the way to go, since I'm creating sets of files only if the user requests them. But I'm having trouble calling them conditionally and passing them the user-supplied input. I've tried using hookFor, but I get the assertion error: hookFor must be used within the constructor only. (Because I don't want it to be run by default, I'm calling the sub-generator from my this.prompt(prompts, function (props)).

The question:

How do I call a routine only if the user requests it (via a prompt), and pass that routine some user-supplied information?

If you're kind enough to answer, please don't assume that I've tried something obvious ;-).

Electrojet answered 21/12, 2013 at 16:26 Comment(0)
F
34

Let's consider you have a generator generator-blog (BlogGenerator) with two sub generators (blog-server and blog-client):

app\index.js
client\index.js
server\index.js

So when you run yo blog you what to ask the user for some options and run (optionally) sub generators, right?

To run a subgenerator you need to call this.invoke("generator_namespace", {options: {}}). The second argument we passed can have options field - it's options object which will be passed to the generator.

In app\index.js:

BlogGenerator.prototype.askFor = function askFor() {
  var cb = this.async();

  // have Yeoman greet the user.
  console.log(this.yeoman);

  var prompts = [{
    name: 'appName',
    message: 'Enter your app name',
    default: 'MyBlog'
  }, {
    type: 'confirm',
    name: 'createServer',
    message: 'Would you like to create server project?',
    default: true
  }, {
    type: 'confirm',
    name: 'createClient',
    message: 'Whould you like to create client project?',
    default: true
  }];

  this.prompt(prompts, function (props) {
    this.appName = props.appName;
    this.createServer = props.createServer;
    this.createClient = props.createClient;

    cb();
  }.bind(this));
}

BlogGenerator.prototype.main = function app() {
  if (this.createClient) {
    // Here: we'are calling the nested generator (via 'invoke' with options)
    this.invoke("blog:client", {options: {nested: true, appName: this.appName}});
  }
  if (this.createServer) {
    this.invoke("blog:server", {options: {nested: true, appName: this.appName}});
  }
};

In client\index.js:

var BlogGenerator = module.exports = function BlogGenerator(args, options, config) {
  var that = this;
  yeoman.Base.apply(this, arguments);
  // in this.options we have the object passed to 'invoke' in app/index.js:
  this.appName = that.options.appName;
  this.nested  = that.options.nested;
};

BlogGenerator .prototype.askFor = function askFor() {
  var cb = this.async();

  if (!this.options.nested) {
    console.log(this.yeoman);
  }
}

UPDATE 2015-12-21:
Using invoke is deprecated now and should be replaced with composeWith. But it's not as easy as it could be. The main difference between invoke and composeWith is that now you have no ability to control subgenerators. You could only declare using them.
Here's how main method from above should look like:

BlogGenerator.prototype.main = function app() {
  if (this.createClient) {
    this.composeWith("blog:client", { 
        options: { 
          nested: true, 
          appName: this.appName
        } 
      }, {
        local: require.resolve("./../client")
      });
  }
  if (this.createServer) {
    this.composeWith("blog:server", { 
        options: { 
          nested: true, 
          appName: this.appName
        } 
      }, {
        local: require.resolve("./../server")
      });
  }
};

Also I removed replaced yeoman.generators.Base with yeoman.Base.

Fulguration answered 27/12, 2013 at 10:52 Comment(3)
I know this is old, but it's close to what I'm looking for. My problem is that my subgenerator extends "yeoman.generators.NamedBase". When I run this code, I get "Error: Did not provide required argument name!" I'm trying to figure out how to also pass the name (i.e. "test" in "yo make:controller test") to the subgenerators called by .invoke(). Any help?Veilleux
Got it: this.invoke("make:controller", {options: {nested: true, ...}, args: [this.name] });Veilleux
if you are getting this error "Did not provide required argument name yeoman" -- with composeWith. Just change yeoman.generators.NamedBase to yeoman.generators.Base github.com/yeoman/generator/issues/521Rush
B
8

2015-04 update: The yeoman api now includes this.composeWith as the preferred method for linking generators.

docs: http://yeoman.io/authoring/composability.html

Blasien answered 12/4, 2015 at 17:2 Comment(0)
M
2

You can cover all possible execution scenarios, condition checking, prompting when composing generators together if you decouple generators and use a 'main-generator' the run context loop will help you. Use the options of .composeWith('my-genertor', { 'options' : options }) for passing configurations to composed generators.

When using .composeWith a priority group function (e.g.: prompting, writing...) will be executed for all the generators, then the next priority group. If you call .composeWith to generatorB from inside a generatorA, then execution will be, e.g.:

generatorA.prompting => generatorB.prompting => generatorA.writing => generatorB.writing

If you want to control execution between different generators, I advise you to create a "main" generator which composes them together, like written on http://yeoman.io/authoring/composability.html#order:

// In my-generator/generators/turbo/index.js
module.exports = require('yeoman-generator').Base.extend({
  'prompting' : function () {
    console.log('prompting - turbo');
  },

  'writing' : function () {
    console.log('prompting - turbo');
  }
});

// In my-generator/generators/electric/index.js
module.exports = require('yeoman-generator').Base.extend({
  'prompting' : function () {
    console.log('prompting - zap');
  },

  'writing' : function () {
    console.log('writing - zap');
  }
});

// In my-generator/generators/app/index.js
module.exports = require('yeoman-generator').Base.extend({
  'initializing' : function () {
    this.composeWith('my-generator:turbo');
    this.composeWith('my-generator:electric');
  }
});
Misbehave answered 1/3, 2016 at 6:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.