gruntjs / grunt

Grunt: The JavaScript Task Runner
http://gruntjs.com/
Other
12.27k stars 1.5k forks source link

Asynchronous configuration #783

Open leyyinad opened 11 years ago

leyyinad commented 11 years ago

The docs say that I can programatically setup my configuration when (or before) calling grunt.initConfig(). Now I do very complicated stuff to dynamically initialize grunt (figuring out paths by parsing PHP-sources, downloading and decompressing archives, ...).

The problem is that my setup routine works asynchronously and must be completed before I call grunt.initConfig(), which obviously doesn't support asynchronous execution. Is there a way to defer the call until my callback has been fired?

More on this here: http://stackoverflow.com/q/16547528/382597

Thank you!

mariocasciaro commented 11 years ago

+1

liuyicheng commented 11 years ago

+1

jpillora commented 11 years ago

You could try using https://github.com/gruntjs/grunt/wiki/grunt.task#grunttaskrun

grunt.registerTask('default', function() {
   var done = this.async();
   //do your async config...
   setTimeout(function() {
      //equivalent to running grunt foo bar on the cli
      grunt.task.run(['foo','bar']);
      done();
   });
});

Untested, though should work

Note, you could use grunt.initConfig within this default task, however keep in mind, it will wipe all existing config. You can also do grunt.config('concat', {my:'concat-config'})

hjdivad commented 11 years ago

I'll give another example where this would be useful. I'm working on oasis.js, MessageChannel.js and conductor.js. They use components I install with bower. In fact, MessageChannel.js is a dependency of oasis.js which is a dependency of conductor.js.

I want to do things with the main bower files, like concatenate them and copy them. In particular I want to "concatenate all vendor files". I don't want to have to manage a list of manual paths that will change when upstream dependencies change. Fortunately, bower provides an api to deal with this. But it's asynchronous, so I can't easily add the output from bower's API to the conductor concat configuration.

Should I be organising these grunt builds differently? Should this be a bower concern (ie to provide a synchronous API)?

hjdivad commented 11 years ago

It occurs to me that something i could do is use ShellJS and call bower list --json --paths. Not sure if this would be considered the idiomatic way to do this in grunt.

jpillora commented 11 years ago

Maybe the simplest solution for you would be to create an async run-bower task, and always prepend that task whenever you run grunt or prepend it to all of your task aliases

If you do want to get fancy, you could modify the task queue, so run-bower is always run first

Though these are just workarounds, as I think this is a legitimate issue, asynchronous config would come in handy, so the real solution is for grunt to implement a this.async() API for configuration

prettyboymp commented 11 years ago

+1

I have a similar issue where I'm using data from an API that provides environment data for specific instances of a project that is then used to configure speicific tasks. I haven't found a way around it other than storing a copy of the data locally.

asperling commented 11 years ago

+1

I'd like to be able to read data from files to init the grunting, see http://stackoverflow.com/questions/19831266/grunt-initconfig-in-a-callback-does-not-work/19836024

NickHeiner commented 10 years ago

:+1: It would be awesome if grunt.initConfig accepted a promise for a config object.

dnutels commented 10 years ago

@NickHeiner

How would that work? The resolve would trigger the task?

yuanyan commented 10 years ago

Because there is no grunt.start() drive the grunt, so must be one task registered at init time.

yuanyan commented 10 years ago

I think all should be as a grunt task inlcude the Gruntfile, then we could force Gruntfile into async mod using this.async():

function findSomeFilesAndPaths(callback) {
  // async tasks that detect and parse
  // and execute callback(results) when done
}

module.exports = function (grunt) {
  // Force Gruntfile into async mode.
  var done = this.async();
  var config = {
    pkg: grunt.file.readJSON('package.json'),
  }

  findSomeFilesAndPaths(function (results) {
    config.watch = {
      coffee: {
        files: results.coffeeDir + "**/*.coffee",
        tasks: ["coffee"]
         // ...
      }
      done();
    };

    grunt.initConfig(config);

    grunt.loadNpmTasks "grunt-contrib-coffee"
    // grunt.loadNpmTasks(...);
  });
};
NickHeiner commented 10 years ago

@dnutels It could look something like this:

module.exports = function(grunt) {

    var configPromise = getConfigBasedOnFileSystemContents();

    // either this
    configPromise.then(grunt.initConfig);

    // or this
    grunt.initConfig(configPromise);
    // in this case, no grunt tasks would be run until configPromise is resolved. 

};

Does that address what you're looking for?

dnutels commented 10 years ago

@NickHeiner That's what I thought you had in mind. You'd need a finer granulation though - per task, probably, rather than per entire config. And without having to envelope all tasks in custom task and muck around with async and what not.

NickHeiner commented 10 years ago

I would be a little cautious about the finer granulation - if you see something like

grunt.initConfig({
    copy: somePromise,
    clean: someSettings
});

How do you know that grunt.config('copy') is a promise for config, as opposed to just having the promise itself be the config value?

I think it would be clearer to just do it all at once:

q.all([getTaskAConfig, getTaskBConfig]).spread(function(taskAConfig, taskBConfig) {
    grunt.initConfig({
        'task-a': taskAConfig,
        'task-b': taskBConfig,
        'task-c': {
            foo: 'bar'
        }
    });
});

Now there is no ambiguity.

bardiharborow commented 9 years ago

+1.

masterspambot commented 9 years ago

:+1:

JemarJones commented 9 years ago

+1

Qix- commented 9 years ago

More than two years later and still not addressed. Huh.

theoy commented 8 years ago

@NickHeiner @dnutels - an alternate way that I would expect for asynchronous config is adjust the handling for gruntfiles and task files. Gruntfiles export an function export that takes in a grunt object parameter, and when invocation of that export is complete, that file's processing is currently deemed 'complete'. However, we could also check to see if the exported function returned a promise, and condition completion on that.

For example, here is how it is today:

module.exports = function(grunt) {
  // Project configuration.
  grunt.initConfig(...);

  // Load tasks from a plugin.
  grunt.loadNpmTasks(...);

  // register some tasks
  grunt.registerTask(...);

  // when this function returns, processing is considered complete for this file
};

An async version could look like:

module.exports = function(grunt) {

  return doSomethingAsync()
    .then(function() {
      // Project configuration.
      grunt.initConfig(...);
    })
    .then(function() {
      // Load tasks from a plugin.
      grunt.loadNpmTasks(...);
    })
    .then(function() {
      // register some tasks
      grunt.registerTask(...);
    });
  // when the returned promise completes, processing is considered complete for this file
};

IMHO, this format is very recognisable to any programmer who is familiar with Promise-based asynchrony.

By the way, I was annoyed by this limitation because I wanted to dynamically generate targets to a multitask, and the steps needed to generate it were only available via async APIs. So instead, the tasks are not a multi-task, which is kind of sad that it can't be done via the natural grunt idioms because of the limitation about asynchronous gruntfiles.

From an ecosystem approach, learning to respect returned promises is technically a breaking change - though probably small. I don't expect many gruntfiles intentionally return a Promise that was intended to be ignored/dropped the the floor by grunt.