That means, I want a build to be dynamic during build time, not during runtime (one could also call this a configurable build). If the resulting build should also be an AMD module, or you want to keep the single modules as modules, this can be easily achieved using the AMD feature plugin, which allows to configure a build during runtime as well as during build time. But what I wanted to have as a result this time, was a single UMD wrapped module with no define/require calls inside of it, so that a user can also just reference the build from a script tag (i.e. remove the dependency on an AMD loader as Require.js).
Choosing a build tool
As build tool, I wanted to go for Grunt. I often use plain make files to define build targets, run JSHint and whatnot and finally trigger builds, but this seemed a little more complicated, so Grunt seemed an obvious choice for me.
Also, Grunt knows the concept of command line arguments, which is what I wanted to have as an option to configure the modules that should be included in the build.
So, Grunt it is.
Choosing a compiler
Usually, I’d go with the r.js builder for AMD modules. But as I wanted the final result to be free of define/require calls, r.js was not an option. But the Closure Compiler can help here – there’s two neat features that are exactly what I needed: First, the “
process_common_js_modules” option. It removes the define block around a module and reassigns the module’s content to a certain, unique namespace. You have to use that with care however, but it works. As my modules were in AMD format and not in CJS format, I needed to use a second option: “
transform_amd_modules“. This, well, transforms AMD modules to the CJS format.
Now I found two closure compiler tasks for Grunt, and for no obvious reasons I decided to go with this plugin.
Configuring the Closure Compiler task
The Grunt config for the closure compiler task now looks like this:
Note how I pass null to the two properties – that’s just how the task plugins wants options like these that don’t have any values. But now here’s a real issue: See the “
common_js_entry_module” option? That’s a tricky part. My modules aren’t an app, but a library – it’s just a collection of modules, the entry point usually is within user code.
Defining an entry point
Basically, you have the same problem when doing an r.js build: you need an entry point that does the require calls, so that the builder can work out any dependencies from a starting point. What I usually do is having a bundle file that does all the require()s and then returns an array, an object or does some assignments.
But in this case I wanted the list of included modules to be configurable, so the bundle file needed to be configurable, too. The solution here is to create the bundle file on the fly, write it to disk and use it as an entry point. For me, it looks like this:
I have a module that I always want to be in the build, a controller, and after that I add the requires for the list of modules (handlers, in my case). In the function I take the same list as arguments again, and in the module’s function body I do some action with the required modules and return the controller instance. Also, I add some JSHint directives on top of the file and replace all double quotes by single quotes – I want to include the created bundle file in the JSHint check.
Obtaining the list of modules to include
In the code above I use two arrays: handlerModuleNames and handlerClassNames. How to get those from the command line? Grunt tasks take arguments using colon separated strings. So I defined my own task to be able to receive arguments. Out of lack of creativity, I called it configure. So, when calling
grunt configure:module1:module2:module3 the configure task receives the module names as arguments:
I also apply some cosmetics to the names, like adding the string “Handler”, as it’s always the same, or adding path info to the module names list.
But passing args using the colon separated notation is cumbersome and looks unfamiliar – luckily, Grunt has something else one can use. Consider the following command:
grunt --foo=bar. Now,
grunt.option('foo') will get us the string ‘bar’. Nice! Using this, and defining the “configure” task as default, I ended up with the following:
Now I can just call
grunt --handlers=mouse,keyboard and keep the code inside of the configure task as it is. That command line call now really looks sweet and familiar.
Configuring the Closure Compiler task, part 2
If you check out the closure compiler configuration above, you’ll see that it has a static list of files to include. Now, that’s not right – as we now have a list of modules we want in the build, only these modules should appear in the list. So I needed to re-configure that in my task. Grunt to the rescue, again: the
grunt.config.set() method allows you to set (and overwrite) parts of the config object passed to
grunt.initConfig(). Even better, it accepts a dot separated path so that one can set options deeply nested in the config object. This way, I could configure the closure task in my own task:
I use the handlerClassNames array to construct the file list for the closure compiler, and add the always-included InputController file as well as the newly created bundle file. After done configuring, I execute the task.
Wrapping it up: UMD
The build file that we have now defines a whole bunch of globals, one of them being what I want to expose. So it’s time to wrap it all up in an UMD wrapper. There are a whole bunch of Grunt tasks out there that provide wrapping functionality. As it’s a rather easy task, I could have also done it myself, but I just grabbed one of the existing task plugins to perform it. First, I define the code block to surround the build contents:
module$build$bundle variable is the assignment from Closure Compiler – it represents a module id, so to say, which is ‘module/build/bundle’. Now pass that to the wrapper task configuration and run it:
Quite an adventure. A summary of what I did basically is:
- Create own task
- Gather module names from the command line
- Create an entry point file and write it to disk
- Configure other tasks like Closure Compiler or JSHint
- Execute the other tasks
The magic is just that Grunt allows to do steps 2 – 5 inside of another task. This is most important for step 4 and 5, as it enables us to dynamically setup everything else that needs to be done. The complete Gruntfile is over here. It contains a little more than I covered here, but everything important is in this post.
In the end, I had quite some fun figuring this out – if it can be done smarter/easier/whatever, don’t hesitate to leave a comment and let me know!