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

落爺英雄遲暮 提交于 2019-12-11 04:06:28

问题


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.


回答1:


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.




回答2:


Overview:

The post from Simon Boudrias on 10/13 is the accepted answer. He provided enough theoretical background for me to get a grip on the situation. I am supplying this additional answer to provide some additional practical information.

Analysis

The first thing I had to appreciate is the difference between "base" generators (generators that don't call other generators), and "meta" generators (generators that call other generators). Note: when I refer to "generators", I do no mean sub-generators: It's ok for a base generator to call sub-generators. A base generator should never call 'composeWith'. It should just do one thing. My basic problem was I trying to call composeWith from a base generator. I needed to create another generator, a meta-generator, that called composeWith.

Note: The distinction between base and meta generators is a logical one. As far as Yeoman goes, they are both simply "generators".

I also found it useful to distinguish between dependent generators (those that require a pre-existing environment from a prior generator), and independent generators (those that are stand-alone)

I also realized I was tightly coupling my base generator to the angular generator. I decided to re-factor my design, by having my base generator call sub-generators for each of the different type of platforms I might want to install into. For instance, I would have a sub-generator for angular, and then another sub-generator for webapp. By doing this all the common components of my base generator are in one generator, and the sub-environment specific stuff would be in sub-generators.

Then I would have a meta generator for each target platform, where the the 'angular-meta' would call (via composeWith) the angular generator and then my base generator, which would then drive the 'angular' sub-generator, and the 'webapp-meta' generator would call 'webapp' and then my base generator, which would then drive the 'webapp' sub-generator'.

In short, it made for a much better design.

Issues:

However, as mentioned in the post, the angular base generator is not 'composeWith' friendly. As a matter of fact, it is built with yeoman-generator 0.16, and the documentation clearly states that composeWith requires yeoman-generator 0.17 or above.

Whenever I tried to call angular with composeWith it ran asynchronously. That is to say it would return immediately, and then kick off my base installer, under 'end:', before angular was installed.

I tested with node:app as suggested. This did work properly: it ran synchronously, and kicked off my base installer only after it was done. So this proved that composeWith will only work on a case by case basis, depending on how well designed the base installer is vis a vis composability.

I did try locally "building" an angular generator using v 0.17 and greater. There were a few issues with underscore functions, but even after getting it to compile it still didn't work right. So even with a meta-generator, I was back to my original problem.

To make matters worse, I realized that my 'invoke' workaround, while at least triggering 'end', was doing it after the base app files were installed, but before the npm install of the libraries was done. Thus the npm install of the angular generator was interfering with the prompts of my base generator.

Solution:

I basically just approximated 'composeWith' functionality in my meta-installer, by kicking off the generators via interactive processes. In other words, mimicking what you would do if you manually just ran one generator followed by another. Until the "big boy" generators are reliably composable, I can't think of any other way to achieve this. A CLI interface is definitely not as good as an API interface because the only way to pass parms to subgenerators is via command line arguments.

here is what my meta-generator now looks like:

'use strict';
var yeoman = require('yeoman-generator');
var chalk = require('chalk');
var yosay = require('yosay');

module.exports = yeoman.generators.Base.extend({

  initializing: function () {
      if( this.fs.exists( this.destinationPath('app/scripts/app.js'))  || this.options.skipBaseAppInstall) {
        this.log("Angular base app found. Skipping angular install.\n");

        this.angularAppFound = true;
      }
      else {
        this.log("angular base app not found");
        this.angularAppFound = false;
      }
  },

  prompting: function () {
    var done = this.async();

    // Have Yeoman greet the user.
    this.log(yosay(
      'Welcome to the epic ' + chalk.red('angular-vr (meta)') + ' generator!'
    ));

    var prompts = [{
      type: 'confirm',
      name: 'someOption',
      message: 'Would you like to enable this option?',
      default: true
    }];

    this.prompt(prompts, function (props) {
      this.props = props;
      // To access props later use this.props.someOption;

      done();
    }.bind(this));
  },

  writing: {
    app: function () {
      this.fs.copy(
        this.templatePath('_package.json'),
        this.destinationPath('package.json')
      );
    },
  },

  install: function () {
    this.installDependencies();
  },

  end: function () {
    var spawn = require('child_process').spawn;
    var tty = require('tty');
    var async = require('async');

    var shell = function(cmd, opts, callback) {
      var p;
      process.stdin.pause();      
      process.stdin.setRawMode(false);

      p = spawn(cmd, opts, {        
        stdio: [0, 1, 2]
      });

      return p.on('exit', function() {        
        process.stdin.setRawMode(true);
        process.stdin.resume();
        return callback();
      });
    };

    async.series([    
      function(cb) {        
        if (!this.angularAppFound) {
          shell('yo', ['angular'], function() {
            cb(null,'a');
          });
        }
        else {
          cb(null, 'a');
        }
      }.bind(this),

      function(cb) {
        shell('yo', ['angular-vr-old'], function() {
          cb(null,'b');
        });
      }
    ],

    function(err, results){                   
      // final callback code
      return process.exit();
     }
    );
  }
});

This meta-installer is dependent on async being installed, so my dependencies in package.json looks like:

  "dependencies": {
    "async": "^1.4.2",
    "chalk": "^1.0.0",
    "yeoman-generator": "^0.19.0",
    "yosay": "^1.0.2"
  },

The full project is available under github.



来源:https://stackoverflow.com/questions/33010498/composewith-in-yeoman-generator-not-emitting-an-end-event-and-thus-not-driving

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!