About 2 months ago we completed a migration from require.js to webpack. Require.js had been the primary UI module system at PayPal and was baked into the default template kraken server we use to build our applications. Everyone was familiar with it and it worked pretty well. Despite our success with it, I identified a few features that were provided by webpack that ultimately led my team to migrate off of require.js. This is an overview of the reasons why we chose webpack.
I hope this proves helpful to you as you seek to improve your own module/build systems!
CommonJS By Default
1. Confusion about returning vs. module.exporting
2. Module resolution path (it will default to ".", which is basically like adding path.resolve() to everything.)
3. NPM support (including the package.json "main" field, etc.)
Client-side Unit Testing
One of the immediate benefits we experienced was being able to rip out this excessively annoying code we had to put in place to support client-side unit testing. There was a great deal of things we had to do to get mocha to support the files in the require.js. All of this code we were literally able to rip out and throw away once a file was moved to CJS, because node could just read the module and as long as it didn't rely on the DOM it could be tested like any other node module. Up until this point our client-side coverage had been terrible. With this change it was much easier to ratchet up the testing much further.
Re-use and modularity
Moving to CJS also opened up a flood of module reusability. This reusability was always available with AMD modules. Why the change? The reasons were 2 fold:
1. Syntax parity with our node.js code meant we started applying the same practices we were already applying to our node code to our client-side code. It turns out that we associated certain patterns with AMD code (monolothic) and certain types of patterns with our CJS code (modular). This meant that even though the shift was primarily mental, we quickly adopted a more modular approach to our UI code-bases when took that mental shift.
2. Actual compatibility with our server-side code meant that a number of important modules (date/time formatting, various utilities, etc.) could be directly shared and reused immediately.
Re-sharpening of our focus on modularity and sharing was one of the main benefits of the move to webpack.
NPM Support
NPM with require.js:
1. Expose your node_modules folder as a publicly accessible route in your app.
2. Add an alias from your module to the convoluted new module path in your config.js.
3. Make sure the module you want has 0 dependencies (because they won't work at all.)
3. define('your-module', function(yourModule) { });
NPM with webpack:
1. require() the module.
Mature ES6/Plugin Support
Webpack Loaders are awesome. Require.js plugins on the other hand are not well supported and syntax can be confusing define(['css!styles/main'], function() { /* .. */}). The loader config in webpack is a breeze and the plugin ecosystem is much stronger.
With requirejs I tried to add ES6 support using the legacy es6 require.js plugin which is based on Traceur. I just couldn't get it to work, so we moved to 6to5 (babel) using a two-step process. First, we'd run babel to convert our files to JS and then we'd suck them up with require.js. Later we'd need to run some kind of "clean" command to remove the extra files it left hanging around our file system.
Using babel-loader had proved to be a lot simpler and nicer and has cut both one step and additional time out of our build process. Source maps of course work out of the box as well.
With all of the great plugins including built-in support for uglify and other thing we barely need grunt at all for building our app. Almost all of it is done by webpack directly.
Concerns
When I showed the team the changes and talked with various people about them I'm frequently bombarded with the same two questions. People are generally under the assumption that it's no longer possible to do an asynchronous require() and others wonder why we didn't just go with browserify. There are good stories for both, so hear me out.
What about asynchronous/dynamic requires?
Asynchronous require's are often helpful in large projects. In our case we used the AMD require([]) at the router level to do something like this:
routeChange(path) {
require(['/views/' + path], function(PageView) {
app.setView(new PageView());
}
}
This allowed us to avoid defer loading hundreds of kb of code until it was absolutely necessary and avoid loading code for different parts of the app that weren't being accessed at this time. It's a pretty neat trick and one we didn't really want to give up.
Turns out, we didn't have to give it up. Webpack supports this with only minor changes to the code above. In the end our various bundles were the same size or smaller than the ones created with r.js (not to mention the build time was cut down from 50s to around 10s).
I'll go more into the exact code we used to get this to work in the follow up, but if you're interested you can check out Pete Hunt's popular Webpack How-To for those getting started which has a nice section on async loading.
Why not browserify?
1. Spotty support for AMD modules (even with the deAMDify plugin)
2. No built-in way to asynchronously require().
AMD Support
1. The path resolution was still common.js based. It didn't know about our public/js folder. It didn't look for "module" in a relative path by default like AMD does, it looked for it in NPM. It ran into a lot of problems. Eventually I had to fork the transform in order to add support for this functionality, which was a pretty big pain in the neck.
2. It didn't support dependencies that weren't assigned to variables.
define('dependency', function() { }); was not translated to require('dependency') as expected. It just broke the system.
While this wasn't too hard to fix, the module itself had been abandoned by the maintainer and despite multiple tries to help, at that time we weren't able to get any of this stuff fixed. (Thankfully Tim Branyen has taken over the the deamdify module now and is very responsive to changes!)
While the AMD support was frustrating, ultimately we were able to fix it, but we ran into an even bigger hurdle, which ultimately ended our test with browserify.
Async require()
I already covered how webpack supports AMD-style require() statements above. It turns out that there's nothing like that built in to browserify. The suggestion is to just basically load everything up front. This is all well and good for most apps. For an app the size of PayPal.com it turns out it's not such a good idea and leads to megabytes of extra code being loaded.
Another alternative to async require() with browserify is to manually bundle things. This will actually work. You need to add two <script> tags to your page. One for the main bundle and another for the specific "sub-app" that you're working with. This is pretty great if you start your app this way, but going from async require() to a lt;script> tag approach for each sub-app was extremely hard. I didn't have time to completely re-factor our routing system and test everything, so eventually I gave up.
Webpack allowed me to get the benefits of browserify without having to re-factor major parts of our codebase.
Conclusion
Webpack has been a huge help to our client-side code base and developer experience in general. It's allowed greater parity and reuse between our client-side and node code, it's made testing our code much easier and it's allowed us to cut way down on the config and extra support code needed to maintain two different module systems in the same code base. The most important thing it has provided though is access to the NPM ecosystem in the browser. Coding will never be the same again :)
Look for a follow-up post detailing the technical aspects of the migration in the next couple of weeks.
* In case you wanted to know most of the UI code at the time of the migration was written in Backbone. Unmentioned in the article was that we're starting to move a lot of stuff to React and NPM support was crucial for us to do that well.