I never thought I'd be one to write a response blog post. Sigh, now I'm that guy. However, I had an interesting conversation on twitter today with Ryan Florence after he said this:

I was honestly curious as to what kinds of problems Ryan was running into. You can read the conversation for more details, but suffice it to say: while I've been successfully using UMD-wrapped libs for a long time, I'd be foolish to not constantly keep my opinions on trial. Turns out, our conversation prompted a blog post from Ryan.

At the outset, I want to say that I respect Ryan. I definitely feel the same pain he describes. We just disagree on the solution. Humanity isn't the best at disagreeing while also maintaining a respectful view towards those we disagree with. This is my attempt to do that. The tl;dr is that it all comes down to trade-offs.

The Premise

"UMD is a lie" – Ryan

While I disagree with the premise, Ryan raises an excellent point.

His primary complaint is that UMDs break down once you have dependencies. In his post he says:

"If I'm wrong, please make a repository that depends on underscore and querystring that works with an application using RequireJS without a build and also an application using browserify."

Underscore supports either "global exports" (i.e. - on the window) or node.js's flavor CommonJS, and querystring is CommonJS. (Update: John-David Dalton pointed out that underscore just added AMD support back in.) Both can be consumed by AMD and CommonJS modules if loader/bundler plugins are utilized in build steps. In fact, a build step would likely not be needed if using RequireJS/Cajon or curl.js and you were OK with multiple requests being made for the libs - not ideal for production (curl.js supports an experimental feature to load unwrapped CJS modules).

But Ryan's talking about libs (i.e. - an OSS project you want others to be able to consume). Effectively he's asking "If you want to distribute your lib with a UMD wrapper, but it depends on plain global and CommonJS wrapped libs, how can this work without a build step?"

Reactions

I have a few reactions to this.

First, it's a bit of a "Perfect Solution" fallacy. Why draw the arbitrary line saying a build step can't be involved? To quote Scott Andrews: "A lib demands a dependency, the app has to provide and resolve the dependency." Unless you're only using browser globals, your loader/build process is going to play a part in resolving those dependencies.

About the need to have build tools in order to achieve this, Ryan says:

"If we're going to be prescribing something, we still haven't achieved the interoperability goal."

Why not? This is akin to saying CommonJS shouldn't be used in the browser because it requires tools like browserify. You have a prescriptive tool that makes your CommonJS modules usable in the browser after a build step. Does that make CommonJS a lie? No way. I'd say it's a win for any dev that <3's CommonJS.

Second, as a lib author, you have the choice of what dependencies you want to use. Your favorite dependency only has an AMD wrapper, and you want it to support CommonJS (or vice versa) - why not submit a PR? I wrote about how to do that here. If a lib hasn't already been written with a CommonJS assumption, it's actually quite easy to add the wrapper you need (more on "CommonJS" assumption in a sec). If you must, for example, use a dep that doesn't play well with UMD's in the browser without a build step, you can also document for your users how they can configure that dependency in their loader/bundler environment.

Third, as a lib author, you can do things to make this choice easier for other developers. For a while now, I've been writing my major projects agnostic of any module system. I use the gulp-imports plugin in my project's build step to concatenate the files inside a UMD wrapper. This gives me the flexibility of keeping my larger projects broken into separate files, if I so desire, while not baking in a CommonJS-style assumption of requiring them inline, which would force that assumption on consumers. You don't have to have a "lib build step" separate from an app build. However, using one to wrap a module-system-agnostic lib in a UMD will make the task of consuming devs much easier. It may not be perfect, but that doesn't disqualify it as a solution.

The key stickler in Ryan's challenge above is querystring. Its index.js looks like this:

'use strict';

exports.decode = exports.parse = require('./decode');  
exports.encode = exports.stringify = require('./encode');  

The assumption here is CommonJS, and that's OK. You just have to understand the trade-offs. There's no way around requiring non-CJS consumers of your lib to run a build/transformation step in their app if you wanted to release a UMD-wrapped lib with this as a dependency. (Remember - if they are using CJS in the browser, they still have to run a build step anyway.)

I guess I don't fully understand the aversion to a build step for your lib if that step achieves the goal of making your module more widely consumable. Build tooling has come such a long way that the pain of implementing this is very low. If you don't care about your lib being useful outside the module system of your choice, that's your call. Encouraging authors to drop support for global exports and AMD is detrimental to everyone who isn't using CommonJS in the browser.

Developers Will Develop

Every module approach has ugly pain points. UMD modules are easier to maintain when they're dependency-free or their dependencies are compatible with your loader of choice. The only way forward with libraries that don't work well with UMD and aren't open to PRs is using a loader/bundler capable of handling all the formats in question or forking and maintaining your own copy (less ideal). For this reason, I prefer libraries by authors who use UMDs - because they, like me, prefer inclusive designs.

"My own experience has shown there are no silver bullets; no config style supports all use cases... UMD isn't a lie, it works, but it is a compromise... Ultimately there are trade offs, but the notion of UMD handles them better than anything else I've seen." – Scott Andrews

And we haven't even touched ES6 modules yet! The module problem already has a solution on the way (we'll still get to debate about non-JS assets as module deps, though). We just need old browsers to die.

If you only care about supporting CommonJS, fantastic - that's your perogative. Guess what? I can still consume your lib with Webpack (or RequireJS/r.js, curl.js or jspm). There's a lot of devs that might be bummed to miss out on your awesome code, though.

Addendum - Build Steps, or Not

To illustrate the wacky world of various module formats working together, here's an example of how you can use underscore and querystring together, without a build step: https://github.com/ifandelse/UMD-Examples

Currently, that repo only has one example. It's using cajon (based on RequireJS), which is capable of loading AMD and CommonJS modules without a build step. You can see in the code below that the relative paths in querystring's index.js were a problem, but the two sub-modules can be required without an issue:

/*
    The bummer is that cajon isn't quite sure what
    to do about the relative paths inside querystrings's
    index.js. BUT, we can require those two modules
    directly and it all works without a build. I would
    never do this without a build, though.
*/
define([  
  "underscore",
  "decode",
  "encode"
], function(_, decode, encode) {
  return {
    hasUnderscore: function(){
      return typeof _ !== "undefined";
    },
    hasQueryString: function(){
      return typeof decode !== "undefined" &&
             typeof encode !== "undefined";
      }
  };
});

Having to require querystring's individual files isn't ideal at all (yuck!) - but I share this to show that CJS interop (especially with AMD) is often impossible without a build step because module ID resolution is handled differently. If querystring was built in a module-agnostic fashion, and then concatenated inside a UMD wrapper, this wouldn't be necessary. (Not a criticism of querystring, just examining trade-offs.) There's a lot that could be improved here (maybe some improvements might be possible to cajon's resolver?) - and it would be a win for users of any module system.

As a general rule, we could all benefit from hearing John-David Dalton's perspective on the JSJ Podcast:

I’ve always come at it from a dev perspective. Is this helping devs? Is this hurting devs? Is this going to allow it to be used in more environments or something like that? So I try to keep it positive...