Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Universal (AMD/Node/plain browser) module
/**
* First, better, "set exports/return" option
*/
(function (define) {
//The 'id' is optional, but recommended if this is
//a popular web library that is used mostly in
//non-AMD/Node environments. However, if want
//to make an anonymous module, remove the 'id'
//below, and remove the id use in the define shim.
define('id', function (require) {
//If have dependencies, get them here
var a = require('a');
//Return the module definition.
return value;
});
}(typeof define === 'function' && define.amd ? define : function (id, factory) {
if (typeof module !== 'undefined' && module.exports) {
//Node
module.exports = factory(require);
} else {
//Create a global function. Only works if
//the code does not have dependencies, or
//dependencies fit the call pattern below.
window[id] = factory(function(value) {
return window[value];
});
}
}));
/**
* exports object based version, if need to make a
* circular dependency or need compatibility with
* commonjs-like environments that are not Node.
*/
(function (define) {
//The 'id' is optional, but recommended if this is
//a popular web library that is used mostly in
//non-AMD/Node environments. However, if want
//to make an anonymous module, remove the 'id'
//below, and remove the id use in the define shim.
define('id', function (require, exports) {
//If have dependencies, get them here
var a = require('a');
//Attach properties to exports.
exports.name = value;
});
}(typeof define === 'function' && define.amd ? define : function (id, factory) {
if (typeof exports !== 'undefined') {
//commonjs
factory(require, exports);
} else {
//Create a global function. Only works if
//the code does not have dependencies, or
//dependencies fit the call pattern below.
factory(function(value) {
return window[value];
}, (window[id] = {}));
}
}));
Owner

jrburke commented Oct 4, 2011

Couple of notes:

  • This format is not recognizable by requirejs or its optimizer yet, but I will be modifying it to support it.
  • This format does not work in CommonJS environments that do not support module.exports. But I strongly discourage a bunch of properties to an exports object, that is only needed for circular dependencies, which should be rare in this type of code. CommonJS environments should support exports assignment.

Hey James, this should go in the AMDjs wiki, don't you think?

Just curious, why doesn't this work with RequireJS ( or am I misunderstanding)?

Lastly, we should also make a CommonJS-compatible version even though there would be restrictions on how the module internals would be structured (i.e. no return).

Owner

jrburke commented Oct 7, 2011

I agree it should go on the wiki, but wanted to be sure it was the right format to use. I think you and others may have more experience in this area, so if you see some tweaks that can be made, I'm up for it. I updated the gist above with an example of how to use exports instead of module.exports.

It was not working in requirejs because I was assuming that if an ID was present in the define() call, that dependencies would have already been parsed out of the module body. However, I just updated require.js and the optimizer to allow for the pattern above, it will be part of the next release.

Owner

jrburke commented Oct 7, 2011

References to some other approaches:

  1. https://gist.github.com/1198466
  2. https://gist.github.com/1241672
  3. https://gist.github.com/1251221

Those are from this discussion.

The differences between those patterns: 1 and 3 allow for a noConflict option. I think this is overkill for most libraries. Maybe mention that is a specific variation based on the two listed in this gist.

2 also puts some effort on creating a plugin that can attach to either jQuery, ender, or something else (maybe zepto?) that is impersonating jQuery via the $ reference.

There is an attempt to allow for a "multiple.name" registration via dot separation that creates new objects. I can see that for jQuery UI it may want that, but that seems something that the jQuery UI components could use in a core component function. So, each component requires "jquery" and "jqueryui/core" or something and use a method on that jqueryui/core to do that dot expansion.

In that model though, #2's use of module.exports does not really make sense, and in fact if it is targeting browser only, like jquery ui, some of that boilerplate can be removed (no need for module.exports detection for example, particularly since it will attach methods to jQuery itself.

I am favoring this gist as the general approach to suggest since it does allow grabbing dependencies via require(), and places the bulk of the boilerplate at the bottom of the file.

Owner

jrburke commented Oct 7, 2011

So @unscriptable, I will likely start coding this up on the AMD wiki, unless you give a holler. I'll send a note to amd-implement so those folks can discuss it too.

There are so many variations. Yah, let's get the ones that make the most sense on the wiki. We can then make any tweaks pending the discussion on the group.

As per the other discussion, I think there's a definite need for a solid UMD boilerplate and this does take us quite close to where we need to be. Some questions:

  • James, does requirejs have a nightly/git build that can be referenced as working with this version until the next one comes out?. Would be great to recommend it's usage in context with version numbers so people can actually play with it.
  • Are there currently still a significant number of CommonJS environments which don't support module.exports? I agree a bunch of props to an exports object isn't a great idea, but I just ask so we can ensure if this is referred to as a 'universal' module type, that it covers that definition as best (but realistically) as possible.

Rather than us having an infinite number of boilerplates for this work, perhaps it would be useful to solidify what we have here, fork a version that includes a few useful additions for libraries/jQuery/Zepto (perhaps not noConflict, but I can see $ impersonation being useful to some) and just have two. That way we cover our bases with a UMD for all and another the library users can use.

Owner

jrburke commented Oct 8, 2011

@addyosmani:

The latest requirejs release, 0.27.1 supports this pattern (with a unit test to confirm) so that version can be used to get this behavior.

On module.exports -- AFAIK, only Node supports it. So it becomes a question of what other CommonJS environments you might want to support. Frankly I see it as a weakness if the platform does not support it, and feel like it will get pressure from Node to implement. Mozilla jetpack/add-on SDK may not support it, but it does support define(). There is Helma, which supports commonjs modules on rhino, but I have not kept up enough to know if they support module.exports. In any case I would like to evangelize folks using return/module.exports since it makes so much sense for constructor functions/"classes".

I think it is fine to have a couple variations on a page. I would break it down into:

  • first one above with return/module.exports.
  • second one for people who have circular dependencies/want more commonjs compatibility.
  • variation of first with noConflict. Although with AMD loaders and build tools, it should be possible to get version specific bindings. I'm open to documenting it though if you have found a use for it.
  • variation of first with attaching some functionality to a $ impersonator. Although, it is tempting to say for that case, ask for 'jquery' as a dependency, and if the developer wants to use something different than the actual 'jquery', map that file to the 'jquery' name. That is one of the strengths of module names, they can be mapped to different implementations. But it would be good to at least explain that approach.

@jrburke When you said in your first comment that it's not compatible with the optimizer just yet did you mean you will be changing the optimizer or the boiler plate?

I think that the boilerplate should really accomodate strict namespaces to, I currently have no suggestions as to how.

Owner

jrburke commented Oct 11, 2011

@thomasdavis: the optimizer will now work with it, as of the 0.27.1 release, there is a unit test in the requirejs project for it to confirm it.

As for strict namespaces, I'm not sure I understand, do you mean the use of "use strict" or something with a hardcoded module ID?

Owner

jrburke commented Oct 11, 2011

I also meant to mention, this define(id, function) signature is a bit new for AMD, in that the spec says the function should not be scanned for dependencies, so I'm checking with the amd-implement folks to see if it makes sense to allow it in the spec.

@jrburke By strict I meant putting define and require under another namespace. Useful for when embedding apps in iframes etc

Owner

jrburke commented Oct 11, 2011

@thomasdavis: ah ok. Right I think that just means asking the tools to look for the pattern. The requirejs optimizer will convert "define === 'function' && define.amd" to the right namespaced name, but it will miss the "define : " part. But if this works out to be the common suggested pattern I can update the optimizer to look for it.

What is the correct way to load a named module using require?

So require(['src/dropdown'], function(dropdownplugin){});

But the actualy src/dropdown module has an id set to something else.

Does it make more sense to keep the define function anonymous and move the name down to the window object.

window['dropdown'] = factory(function(value) {
  return window[value];
});
Owner

jrburke commented Oct 11, 2011

@thomasdavis: correct, loading a named module means loading it with exactly the same name as in the file, so it may mean using a paths config to load it properly.

Anonymous modules are better, but only if you are reasonably sure the module will always be loaded by an AMD loader and optimized with an AMD-aware optimizer. The problem with scripts like jQuery, any foundational library, is that they can be used as part of third party scripts a developer includes on the page, but not loaded by the AMD loader. In that case, there will be a pretty bad error if that script calls define() with no ID.

So the named module suggestion is there to avoid that error if you have a library that can be in pages without it be loaded by an AMD loader on that page.

It means sacrificing some renaming flexibility, but it avoids a runtime error that may be difficult to track down (particularly since the define call is anonymous).

I'm open to reevaluating the cost of risks involved though, if people have thoughts about them. I plan to outline these costs/benefits for the named modules in whatever we put up on the AMD wiki.

@jrburke Thanks for the quick reply, just another quick question. So I'm writing a jQuery plugin that is AMD/UMD compatible but the plugin it self has to have a dependency on jQuery. Using the boilerplate above it's hard to require jQuery assuming jQuery is not global.

How can the plugin be told to load jQuery from any relative module path?

I was thinking a UMD could have a configuration object but would you put in the source file or somewhere else?

I guess Require.js paths should work well enough.

Owner

jrburke commented Oct 11, 2011

Right, the jquery plugin should specify 'jquery' as a dependency. I suppose one problem with the UMD in that case is that the 'jquery' dependency is found at window['jQuery'] so probably need to special case that require stub for the global window case.

But in any case, the plugin should specify 'jquery' and the app loading both modules (since jquery in 1.7 will register as 'jquery' named module) using paths config as appropriate.

I don't fully understand what ['jquery'] does so I will just use ['jQuery'] and keep it up to the user to define the right path to jQuery and if it is loaded in noConflict or not

Owner

jrburke commented Oct 11, 2011

The dependency names should match the file name casing, so ideally there are no paths config and 'jquery' is just baseUrl + 'jquery.js'. Since jQuery ships as a file in a lowercase name, the dependency name is in lowercase. If you use 'jQuery' then you will need to be sure to always use a paths config or match the casing in the file name.

@jrburke @unscriptable @thomasdavis still considering whether this is a good idea, but what are your thoughts on formalizing some of the UMD work that's being worked on both here and in the other patterns project into a unified effort or repo/project?

At the moment a lot of our efforts exist in distributed gists which only a limited group of people are aware of but we have the potential to get more people actually using this stuff.

If you feel it makes more sense to just post this as an extension of the AMD wiki, that's of course cool, but I was just curious as to what the final goal / location for informing others about this work might be.

Owner

jrburke commented Oct 19, 2011

@addyosmani I'm open to publishing it somewhere else than the AMD wiki. That wiki was just convenient since a few of us already have access to it. I was thinking actually not having it on the wiki portion but as part of a doc in a repo so that changes and comments on changes could be tracked easier. I can see it may be a collection of documents -- one for each sort of sub-type (something specific to jQuery plugins for example).

So all that said, I'm open to some other place to host it. If you have a specific idea or want to set something up, I'm all game.

As far as this specific boilerplate, I am feeling positive about it sticking. I am just waiting to hear back officially from one other person in the AMD world, but initial vibe was receptive. It also will work well with some changes that may be happening for the jetpack/add-on toolkit for firefox add-ons.

@jrburke Perfect.I was thinking of something along the lines of setting up a new repo or organization for taking the UMD work further. Let me setup something this evening. I'll add everyone that's been involved in discussions so far and we can see how things might shape there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment