25

I'm building a custom Yeoman generator that installs a lot of pre-processed language compilers like CoffeeScript, LESS and Jade. In the Gruntfile that my generator creates I have a build task which compiles everything. However, until that build task is run at least once, the compiled HTML, CSS and Javascript files don't exist, which can be confusing if I try to run the grunt watch/connect server after freshly scaffolding.

What is the best way to have my generator run that Grunt build step at the end of the installation? The end event that's already being used to call this.installDependencies seems like the right place to do that, but how should I communicate with Grunt?

Soviut
  • 88,194
  • 49
  • 192
  • 260

3 Answers3

49

If you follow the stack, this.installDependencies eventually works its way down to https://github.com/yeoman/generator/blob/45258c0a48edfb917ecf915e842b091a26d17f3e/lib/actions/install.js#L36:

this.spawnCommand(installer, args, cb)
  .on('error', cb)
  .on('exit', this.emit.bind(this, installer + 'Install:end', paths))
  .on('exit', function (err) {
    if (err === 127) {
      this.log.error('Could not find ' + installer + '. Please install with ' +
                          '`npm install -g ' + installer + '`.');
    }
    cb(err);
  }.bind(this));

Chasing this down further, this.spawnCommand comes from https://github.com/yeoman/generator/blob/master/lib/actions/spawn_command.js:

var spawn = require('child_process').spawn;
var win32 = process.platform === 'win32';

/**
 * Normalize a command across OS and spawn it.
 *
 * @param {String} command
 * @param {Array} args
 */

module.exports = function spawnCommand(command, args) {
  var winCommand = win32 ? 'cmd' : command;
  var winArgs = win32 ? ['/c'].concat(command, args) : args;

  return spawn(winCommand, winArgs, { stdio: 'inherit' });
};

In other words, in your Generator's code, you can call this.spawnCommand anytime, and pass it the arguments you wish the terminal to run. As in, this.spawnCommand('grunt', ['build']).

So then the next question is where do you put that? Thinking linearly, you can only trust that grunt build will work after all of your dependencies have been installed.

From https://github.com/yeoman/generator/blob/45258c0a48edfb917ecf915e842b091a26d17f3e/lib/actions/install.js#L67-69, this.installDependencies accepts a callback, so your code might look like this:

this.on('end', function () {
  this.installDependencies({
    skipInstall: this.options['skip-install'],
    callback: function () {
      this.spawnCommand('grunt', ['build']);
    }.bind(this) // bind the callback to the parent scope
  });
});

Give it a shot! If all goes well, you should add some error handling on top of that new this.spawnCommand call to be safe.

Stephen
  • 5,710
  • 1
  • 24
  • 32
  • It's worth mentioning that inside the callback function `this` loses scope. I originally created a variable to cache `this` in the correct scope and used that to call `spawnCommand`. However, I decided to use `bind()` instead since it would be cleaner. I've edited the answer to reflect this. – Soviut Sep 25 '13 at 06:11
  • What if automatic installation of dependencies fails? – Markus Apr 22 '15 at 10:18
16

I've used Stephen's great answer, implemented in the following way with a custom event to keep things tidy.

MyGenerator = module.exports = function MyGenerator(args, options, config) {

    this.on('end', function () {
        this.installDependencies({
            skipInstall: options['skip-install'],
            callback: function() {
                // Emit a new event - dependencies installed
                this.emit('dependenciesInstalled');
            }.bind(this)
        });
    });

    // Now you can bind to the dependencies installed event
    this.on('dependenciesInstalled', function() {
        this.spawnCommand('grunt', ['build']);
    });

};
Ash
  • 3,136
  • 2
  • 21
  • 12
  • I was considering that as well, but am now even more tempted to retrofit that into my generator. It keeps the relevant installation steps all on the same indentation level throughout and gives way clearer delineation. – Soviut Sep 25 '13 at 15:29
  • There is no need for this custom callback, as Yeoman generators have their own events: https://github.com/yeoman/yeoman-app/blob/main/docs/events.md. Assuming you use NPM, you can replace "this.on('dependenciesInstalled', ...)" with "this.on('npmInstall:end', ...)" – Rob Spoor Feb 05 '21 at 13:51
4

This question is a bit old already, but i still want to make this addition if somebody missed it. Post install processes are now way easier to implement. Have a look at the run loop and use the end method where you can run all the post install things.

Kjell
  • 832
  • 7
  • 11