Search code examples
node.jsyeomanyeoman-generatoryeoman-generator-angular

ComposeWith in yeoman generator not emitting an end event and thus not driving 'on' method


Background

I am creating a scaffolding generator for an Angular SPA (single page application). It will rely on the environment set up by the standard angular generator ('yo angular'), and also rely on the standard angular subgenerators to generate a few extra services and controllers necessary to the app. In other words, I'm "decorating" a basic angular app.

The generator will work fine if the user has previously installed an angular app (I look for marker files and set a booleon 'angularAppFound' in my code). However, I would like it to also be 'one-stop', in that if they don't have an angular app already set up, my generator will call the angular generator for them, before I install my additional angular artifacts within a single run.

Obviously, my dependent tasks will not work if an angular app is not in place.

Data

my code looks like this:

  // need this to complete before running other task
  subgeneratorsApp: function () {    
      if (!this.angularAppFound) {
        var done = this.async();

        this.log('now creating base Angular app...');
        // doesn't work (does not drive .on)
        //this.composeWith('angular',  {args: [ this.appName ]} )
        // works (drives .on)
        this.invoke('angular',  {args: [ this.appName ]} )
         .on('end',function(){
                        this.log('>>>in end handler of angular base install');
                        done();
                    }.bind(this));
        
      }    
  },

  // additional steps to only run after full angular install
  subgeneratorServices: function () {
    Object.keys(this.artifacts.services).forEach( function (key, index, array) {
      this.composeWith('angular:service',  {args: [ this.artifacts.services[key] ]} );
    }.bind(this));
  },

  subgeneratorControllers: function () {
    Object.keys(this.artifacts.controllers).forEach( function (key, index, array) {
      this.composeWith('angular:controller',  {args: [ this.artifacts.controllers[key] ]} );
    }.bind(this));
  },

I have empirically determined, by looking at the log and by outcome, that 'composeWith' does not drive the .on method, and 'invoke' does.

If the .on method is not driven, done() is not driven, and the generator stops after the angular base install and does not drive the subsequent steps (because the generator thinks the step never finishes).

I'm fine using invoke, except it's deprecated:

(!) generator#invoke() is deprecated. Use generator#composeWith() - see http://yeoman.io/authoring/composability.html

Questions

I have read here and here that generators shouldn't be dependent on each other:

When composing generators, the core idea is to keep both decoupled. They shouldn't care about the ordering, they should run in any order and output the same result.

How should I then deal with my situation, since ordering is important (other than using 'invoke')? I can't think of any other way to re-organize my generator without sacrificing 'one-stop' processing.

Should I simply say that user has to install angular in a separate step and not allow for 'one stop' processing?

Is it by design that 'composeWith' does not emit an 'end' event?

If not, do you recommend I open a bug report, or is there some other way to do it (that is not deprecated)?

Many Thanks.


Solution

  • Generator composability is ordered using a priority base run loop. As so, it is kind of possible to wait for another generator to be done before running yours.

    Although the tricky part here is that, once the end event is triggered, the generator is done running. It won't schedule any future task - the end events means everything is done and it's time to wrap up. To be fair, you shouldn't need the end event. It's still in Yeoman for backward compatibility only.

    In your case, you want two generator. The app generator (the root one) and your custom functionality generator. Then you compose them together:

    So inside of generator/app/index.js, you'll organize your code as follow:

    writing: {
      this.composeWith('angular:app');
    },
    
    end: function () {
      this.composeWith('my:subgen');
    }
    

    The generator-angular is huge and fairly complex. It is still based on an old version of Yeoman, which means it can be harder to use it as a base generator. I'm sure the owners of the projects would be happy to have some help to upgrade and improve the composition story. - If you wonder what a better developper UX can looks like for Yeoman composition, take a look at generator-node who've been designed as a base generator for composition.