View on GitHub

Joules

Node.js-style module loading in the browser

Download this project as a .zip file Download this project as a tar.gz file

Joules

Joules is a library that provides Node.js-style module loading* on the browser. Unlike other similar libraries, it doesn't have a build step in the development environment, allowing for rapid iteration. It also builds into a bundle for faster loading in a deployment environment.

Check out a live demo using Joules.

* Node.js is the most popular implementation of CommonJS module loading, but it has a few differences from the CommonJS spec, such as exporting a single function with module.exports = myFunction and looking for node_modules. Joules follows Node.js's conventions in those cases instead of strictly following the CommonJS spec.

Installation

Using NPM, pass the -g flag to make the joules command available on the command line

$ npm install joules -g

Install it in your project's repository to make deployment builds and hinting available on the command line

$ npm install joules

or clone the repository with Git

$ git clone git://github.com/treygriffith/joules.git node_modules/express-render/

Usage

Development

Building the Development Script

Joules comes with a development script, joules.js, in the root directory, but it can be re-built at any time with the joules-dev command:

$ joules-dev public/js
/Users/username/project/public/js/joules.js written

Using in your project

When using in development, you just need to add the development script to every page that you want to have module loading support, before any scripts that use modules.

<html>
    <head>
        <title>My Cool Webpage</title>
    </head>
    <body>
        <p>This is my cool webpage. Welcome!</p>
        <script src="/js/joules.js"></script>
        <script>
            require.ready(function() {
                var $ = require('./js/jquery');
                $("p").text("Go Away!");
            });
        </script>
    </body>
</html>

You'll notice that when using a script inline on the page, it has to be wrapped in a function that is a callback for require.ready, which functions similarly to $(document).ready for jQuery, but waits for dependencies to load as opposed to waiting for domReady.

Inline Dependency Declaration Limitations

You should note that every dependency must be declared within the function body of the callback for require.ready. For example, this won't work:

...
<script>
    // DON'T DO THIS
    var ready = function() {
        var $ = require('jquery');
    }
    require.ready(function() {
        // no dependencies are declared within the callback function body
        ready();
    });
</script>
...

Main Entrypoint

You can also define a main script, which is the primary entry-point for the javascript on the page. Scripts defined externally do not need the require.ready callback - they won't be invoked until all dependencies are loaded. For larger web apps, this is usually desirable when compared to inline scripts. This is similar to RequireJS's behavior:

index.html:

...
<script src="/js/joules.js" data-main="/js/main.js"></script>
....

main.js:

var $ = require('jquery');
var content = require('./content');

$("p").text(content);

Multiple Scripts

Joules supports multiple data-main scripts (in different script tags) as well as multiple inline script. Each inline script and data-main script will have dependencies loaded independently.

Module Lookup

Joules loads modules using the same lookup pattern as Node. In addition, it also looks for HTML files (as index files, and also as exact filenames) that it can scan for inline javascript. The HTML behavior is recommended only when targeting as the base module, not as a way to reference dependencies.

Hinting

Due to the sometimes lengthy lookup process for modules, using HTTP requests can be burdensome, even in a development environment. To help alleviate the problem, Joules provides a way to generate a hinting file for your public directory to reduce HTTP requests.

If you're using Node.js, the hinting file is generated as follows:

// public is the directory from which our static assets are served
joules.hint('./public', function(err) {
    if(err) throw err;
    console.log("hinting file created.");
});

This command automatically watches the ./public directory for any changes, and will update the hinting file appropriately, allowing you to keep the same rapid development environment without tons of HTTP requests.

Deployment

To serve your modules as a compiled bundle that is optimized for deployment, you can use the command line builder, or the programmatic version of the same tool for use in a larger build process.

To build a script, you simply have to define a target. The target can be an HTML file, which will be scanned for inline javascript, or the main entrypoint to your program, like main.js in the development section.

Command Line Build

$ joules ./js/main.js
main.joules.js written.

By default, Joules outputs a file called target.joules.js where target is the target module or filename, in the same directory as the target module or file. This can be modified by passing the out flag.

$ joules ./js/main.js --out ./js/bundle.js
bundle.js written.

Programmatic Build

For using Joules as part of a larger build process, require the Joules module:

var joules = require('joules');
joules.build('./js/main.js', function(err, script) {
    fs.writeFileSync('./js/my-bundle.js', script, 'utf8');
});

Using in your project

Once you've built your bundled file, you can include it like any regular script on the page. To borrow examples from the deployment section:

Targeting an external script, main.js:

<html>
    <head>
        <title>My Cool Webpage</title>
    </head>
    <body>
        <p>This is my cool webpage. Welcome!</p>
        <script src="/js/main.joules.js"></script>
    </body>
</html>

Targeting the inline script for this page, index.html:

<html>
    <head>
        <title>My Cool Webpage</title>
    </head>
    <body>
        <p>This is my cool webpage. Welcome!</p>
        <script src="/js/index.joules.js"></script>
    </body>
</html>

Why Modules?

If you've ever developed anything significant in front-end Javascript, you know that dependency handling is a pain. Add in to that the inherent problems with Javascript's global namespacing defaults and any complex project will quickly get bogged down.

Modules are a way to separate Javascript code into logical chunks that can be depended upon by other modules. If you've ever programmed in another paradigm (including SSJS), modules are a clearly lacking feature of client-side Javascript.

Why not use RequireJS?

(or another AMD module loader)

The CommonJS Module loading standard (or rather, Node's "interpretation" of the standard) is a more comfortable way to include dependencies, and has tons of traction on the server side. From a module perspective, it is by far the largest ecosystem out there, so it makes sense to maximize browser modules with Node-style modules.

AMD works well for what it's for, which is asynchronous module-loading on the web. However the javascript ecosystem is much larger than that, and I think that by limiting use to the browser only, it's limiting its usefulness to developers.

AMD requires that module developers include a bunch of boilerplate in their projects, which doesn't make sense for maximum compatibility.

In addition to the clunky syntax it forces on you, RequireJS in particular also forces you to have a certain project hierarchy for scripts. While practical in terms of reducing I/O, I think a module loader should be unopinionated in terms of project structure.

Why not use Browserify?

Browserify is a great project and one of the big inspirations for this project. However, one of the things that I think RequireJS got right was the development environment - devs want to be able to rapidly change code and see results. Browserify requires a build step, and while a build step is important for a production environment to maximize end-user speed and minimize server load, for development a build step is detrimental to developer speed.

That said, the work that Browserify has done on translating Node's core modules for the web is excellent, and I hope to incorporate those into this project as well as extend them where possible.

Why Joules?

My hope is that by taking advantage of the primary strengths of the two leading Javascript browser module loaders, RequireJS and Browserify, I can produce an experience that is ultimately better for developers.

It takes the strength of the CommonJS/Node.js module system and community (like Browserify), and puts it into an browser environment that is suitable for rapid development as well as robust deployment (like RequireJS).

When CommonJS was first discussed, the browser was supposed to have stop-gap measures until module loading was natively supported. Those stop-gap measures were: 1) Either use a server to translate CJS modules to something usable in the browser. 2) Or use XMLHttpRequest (XHR) to load the text of modules and do text transforms/parsing in browser.

This implementation does both, using (1) for deployment builds, and (2) for development.

Features

Issues

There are several issues with Joules that do not come up or are better supported by other browser module loaders.

License

MIT - View Full License