So, I found the answer I was looking for.
This SO post was basically the same question with a good answer. Unfortunately it didn't come up when I was creating my question or else you wouldn't be reading this.
There was a slight tweak to my needs. I needed to do it dynamically per module instead of just one compile.js file so my final code is as follows, placed just after my initConfig():
grunt.registerTask("prepareModules", "Finds and prepares modules for concatenation.", function() {
// get all module directories
grunt.file.expand("src/js/modules/*").forEach(function (dir) {
// get the module name from the directory name
var dirName = dir.substr(dir.lastIndexOf('/')+1);
// get the current concat object from initConfig
var concat = grunt.config.get('concat') || {};
// create a subtask for each module, find all src files
// and combine into a single js file per module
concat[dirName] = {
src: [dir + '/**/*.js'],
dest: 'dev/js/modules/' + dirName + '.min.js'
};
// add module subtasks to the concat task in initConfig
grunt.config.set('concat', concat);
});
});
// the default task
grunt.registerTask("default", ["sass", "ngtemplates", "prepareModules", "concat", "uglify", "cssmin"]);
This essentially makes my concat task look like it did when I was hand coding it, but just a little simpler (and scalable!).
concat: {
...
moduleOne: {
src: "src/js/modules/moduleOne/**/*.js",
dest: "dev/js/modules/moduleOne.min.js"
},
moduleTwo:{
src: "src/js/modules/moduleTwo/**/*.js",
dest: "dev/js/modules/moduleTwo.min.js"
}
}
Another deviation I made from the SO post was that I chose not to have prepareModules run concat on it's own when it was done. My default task (which watch is setup to run during dev) still does all my processing.
This leaves me with the following structure, ready for minification into prod/:
| dev
| js
| modules
|-- moduleOne.min.js
|-- moduleTwo.min.js