Links

pmuellr is Patrick Mueller

other pmuellr thangs: home page, twitter, flickr, github

Tuesday, February 09, 2010

modjewel 0.2.0 - a CommonJS require() function for the browser

I decided to get off my butt and bring my "for the browser" CommonJS require() implementation at http://github.com/pmuellr/modjewel into the 20-teens.

some background

Just over a year ago the "ServerJS" group was formed based on the "What Server Side JavaScript needs" blog post from Kevin Dangoor. This effort was renamed "CommonJS" later in the year, and lives on the web here: http://commonjs.org/. Kris Kowal has a nice write up on CommonJS at Ars Technica in the article "CommonJS effort sets JavaScript on path for world domination", if you don't know anything about it. Note: I'm a sucker for software-based world domination.

One of the first items looked at by the CommonJS community was a "module" system, centered around a function named require(). More information on modules and require() is available here: http://wiki.commonjs.org/wiki/Modules/1.1. While most of the folks in the group were playing with server-side JavaScript, I was interested in the browser, so cooked up a crude version of require() in a github project just over a year ago; the crufty old hack is here.

I figured someone else would do a proper version, but haven't really come across anything besides David Flanagan's efforts, documented in his blog posts "CommonJS Modules implementation" and "A module loader with simple dependency management". I have to admit I don't watch the CommonJS stuff all that closely, I probably missed some other implementations.

David's code doesn't have any kind of a license associated with it, so I didn't really feel it was appropriate to use it for a real project. And there were some things I wanted to modify. So, at the very least David shamed me into sprucing up modjewel and I will admit to stealing bits of his design as well.

One thing I learned from David's blog post was that compliance tests exist. Loves having tests. They currently live here: http://github.com/kriskowal/commonjs. Note that David's blog post points to an older version of the tests.

Onto the goodies in the newly improved modjewel ...

nice ui for the compliance tests

As I mentioned above, there's a set of test cases for the require() function. Problem is, they're built for command-line invocation; remember, this CommonJS stuff is more server-side than client-side JavaScript. I was recently pawing through the source for the SunSpider JavaScript benchmarks, and realized I could use the same "run the tests serially in IFrames" technique here, so I lifted that design. I now have a single page that will load and run all the tests serially, and produce a green/red report card when done. It's not perfect, but it's very handy.

One weird thing to note was that I couldn't find any kind of license on the tests; I ended up importing them directly into my project, which is kind of silly. I should probably run them right out of the github repo.

support for module.setExports()

The existing facilities with modules make it easy to provide access to a number of objects within a particular module. However, for those times you only have a single object in a module, things get verbose. Here's an example: say you want to build and use a module called WebDav (so the file is named WebDav.js) which exports a single function, WebDav.

In the file WebDav.js, you would define the WebDav class as:

   exports.WebDav = function WebDav() {
       // the function is 'named' for debug purposes
   ...
   }

You can then use the WebDav class in another file as:

   var WebDav = require("WebDav").WebDav

All the RY of Java. The "WebDav" passed to require() specifies the module, and the next WebDav selects the function as a property of the exports object the module populated.

Alternatively, with the module.setExports() support you can define the class in WebDav.js as:

   module.setExports(function WebDav() {
   ...
   })

and then use it in another file as:

   var WebDav = require("WebDav")

Better! Very convenient if you have a 1-1 correspondence between classes and modules.

module.setExports() isn't official yet; see the wiki page for more information: http://wiki.commonjs.org/wiki/Modules/SetExports.

preloading modules via <script src="">

The way modjewel's version of require() works is like every other browser-based one: it use synchronous XHR. Which is ok for development-time, but completely unacceptable at deployment-time. So they say. Flanagan came up a preload story for his require2.js file, but the mechanism seemed a little crude.

Instead, I created a command-line utility which will convert a module into a file which can be embedded with <script src="">. So then the story would be that you run this utility as part of a build step, perhaps minize or whatever you kids do to your JavaScript afterwards. In the end, you have a "plain old" JavaScript file to "include". All the utility really does is wrap your code in a function. When this code is executed, it registers the module and the function, such that the first time require() is called to load the module, the function is executed and the module is loaded.

A nice aspect of this is that the order in which the <script src=""> are placed doesn't need to be in some kind of special pre-req order. Including the modules doesn't really load them, it just pre-loads them.

Anyway, it's one answer to the "you can't use synchronous XHR!" argument.

portability

I've run the tests successfully on Mac Chrome, Mac Safari, Mac Opera 10, the IPhone Simulator, Windows Chrome, and Windows FireFox 3.0. IE 7.0 was a no-go.

There is an issue with Chrome, where some tests fail when running against a file:// URL. See issue 1 for more info.

There are some jiggly issues with Windows - of course those being path separator issues (\ instead of /) and new-line issues (CR LF instead of LF). Should be easy to fix. Not sure what to do about IE 7, do I use FireBug Lite to debug that?

2 comments:

no said...

There is also another implementation:

http://github.com/Gozala/experiments/blob/master/commonjs/require.js

Patrick Mueller said...

Thanks for the link R-Fobic.

I also got a link from someone on RequireJS, located here: http://github.com/jrburke/requirejs