vaccine

CommonJS and AMD for libraries.

Vaccine

Vaccine is a temporary treatment (until Harmony) to JavaScript's modularity problem that works by injecting a small script into your code (like how a vaccine uses a virus). It is small enough to be used in libraries, so that library developers do not have to depend on the application using it. Vaccine includes support for Node/CommonJS modules.

The simplest installation is to copy vaccine.js into your project and concatenate all your source files with it to make your single app/lib file. Let's say you have all your source files (and only source files) under src/. Then from your project's root directory:

$ curl https://raw.github.com/jakesandlund/vaccine/master/vaccine.js > src/vaccine.js

Now to build your app's (or lib's) compiled javascript file, do something like this (or use the provided build script):

$ file=my_app.js          # Whatever you want your compiled file to be called 
echo '(function() {' > $file
$ cat src/* > $file       # Concatenate all source files and vaccine.js 
echo '}());'

Your compiled app now works with vaccine's module syntax. It's that easy!

You can also install vaccine with npm. This creates a binary that lets you configure vaccine scripts for your app. Calling vaccine with --app MyAppName will create a directory called vaccines with various scripts you can use. See vaccine --help to get a list of options.

$ npm install -g vaccine
$ vaccine --app MyAppName --main src/my/app/main/file.js
 
# Pick out scripts to use 
$ cp vaccines/vaccine.js src
$ cp vaccines/build .
$ cp vaccines/vaccine_loader.js .
$ cp vaccines/dev_server_standlone.js .
 
# Remove vaccines when you're done 
$ rm -r vaccines

In other places in this README I will make reference to copying a file. This can be done by curling github or by creating a vaccines folder. The root directory for vaccine is actually built using the vaccine binary.

If you are familiar with RequireJS or Asynchronus Module Definition (AMD), then you may find it easier to think of vaccine as the alternative CommonJS syntax with a required module id.

To make a module, simply wrap your module in a call to define with the module's name, and a function that accepts three arguments (require, exports, module):

  • require: Used to require other modules. A call to require with a module's string id will return that module's api object.
  • exports: Attach properties to this object, and they will be available as part of the module's public api. This object gets returned when this module is required (unless module.exports is overwritten).
  • module: Set module.exports to control exactly what gets returned from the require call.
define('util', function(requireexportsmodule) {
  exports.say = function(message) {
    console.log('You say: ' + message);
  };
});
 
define('message', function(requireexportsmodule) {
  var util = require('util');
  module.exports = function() {
    util.say('Hey, this is neat!');
  };
});
 
define('runner', function(requireexportsmodule) {
  var message = require('message');
  message();  // Outputs: "You say: Hey, this is neat!" 
});

The module name/id given to define is the full name, so be careful that no two modules use the same name. It is best to prefix your module names with the name of your app or lib, separated by a slash.

require can accept relative module "paths" to compute the module id based off of the current module being defined.

define('my_app/math/add', function(requireexportsmodule) {
  module.exports = function(xy) {
    return x + y;
  };
});
 
define('my_app/math/double', function(requireexportsmodule) {
  var add = require('./add');
  module.exports = function(x) {
    return add(x, x);
  };
});
 
define('my_app/examples/fun', function(requireexportsmodule) {
  var double = require('../math/double');
 
  // Outputs "Aren't pointless examples 2 times more fun?" 
  console.log("Aren't pointless examples " + double(1) + " times more fun?");
});
  • The define's can be in any order.
  • Circular dependencies are not supported. However, as long as you let the module's function return, you can call require at some later point to get it.

Vaccine supports many different development workflows. Take a look at the test/html directory for different html files reflecting some of the different ways to use vaccine.

One way to develop with vaccine is to develop with the built file that includes your sources and vaccine.js. This could be done manually (which would get annoying) with a separate build step before refreshing a page. However, vaccine comes with a simple development server (dev_server_standalone.js) that can build on the fly, so you can just refresh.

The one requirement with the various dev_server_*.js files, is that you must have a build file somewhere that will output (on stdout) the exact text of your compiled app when called. So you can't have the build file save directly into your file (as shown above). Instead, do this:

$ ./build > my_app.js

You can still develop with separate script tags for each file. Just make sure that vaccine.js is first in this case.

Vaccine comes with a loader version that automatically loads your modules. Just copy down vaccine_loader.js and edit some of the variables at the top of the file.

$ curl -O https://raw.github.com/jakesandlund/vaccine/master/vaccine_loader.js
$ vim vaccine_loader.js   # Or whatever your favorite editor is. 

Then use it in your html files like this:

<script src="/vaccine_loader.js"></script>
 
<!-- If you have scripts that depend on your app, add a short
     script below vaccine_loader. Like so: -->
<script>vaccine_load("/full/path/to/script.js");</script> 

If you want to have your modules be CommonJS compliant, there is a slightly more complicated (but still only ~20 lines of code) way to build your app/lib.

With this build script, simply leave out the define wrapper and use require, exports, and module as you would in any other CommonJS file. The module id is determined by your app name and each files path on your file system. Also make sure that the string passed to require does not end in ".js", and that it would work when you wrap the file in a define call.

dev_server_standalone_node.js and dev_server_express_node.js can be used to compile your app/lib on the fly. As an added bonus, it will even wrap the individual files with define so that you can use vaccine_loader.js or separate script tags.

Vaccine comes in different variations. Each one suits a different need. The full version (but without the loader) is only ~50 lines of code, and other versions are even less.

The minimal vaccine (17 lines of code) is just a global vaccine object with three methods:

  • get: function(id) ...: Pass it an id, and it will return the value associated with that id.
  • set: function(id, value) ...: Set's the value associated with the given id and calls all on functions registered to that id.
  • on: function(id, func) ...: Add's a callback tied to the id that gets called whenever the id's value gets set.

The other vaccine versions keep the minimal vaccine intact while varying the define and require functionality to suit different needs.

In AMD, where the app (generally) decides to use AMD, libraries must check if define and define.amd are true. With vaccine, if a library decides to support vaccine, they must at least use the minimal version. They provide their library with a line of code like so:

vaccine.set('my_lib', myLib);

If you are using libaries that do not have support for vaccine, you can easily add support by copying vaccine_minimal.js to the bottom of their lib, and adding a line like above.

Even easier, the vaccine binary has a way to do this with the --inject option:

$ vaccine -i lib_file.js     # You can also use the --app and --global options. 

To save a few bytes and split seconds to the run time, you can use this version if the modules are defined in order. In order means that every call to require must be to a module that has already been defined.

This version is vaccine.js but without support for relative requires. To use this, every module id must be typed out in full when calling require.

This is the combination of the above two versions. You must use full module ids for require, and define everything in order.

Lastly, vaccine is meant to be simple enough that you can easily make any changes you want. The two rules are don't break the global vaccine api (in vaccine_minimal.js), and don't add any other global functionality. Other than that, anything is game.

If you think you have some changes that other people might like, send a pull request, and I may support it if there is enough interest.

Vaccine aims to be as small as possible, so if you see a way to save a byte, let me know.

If you use the vaccine binary, you can figure out how much vaccine will add to the size of your library app by doing the following:

$ vaccine --size src        # the location of your source files 

Running this on DataZooka, a tool I am developing that uses vaccine, I get the following output:

$ vaccine --size src
                                   size types:  text  min gzip
 
                                   vaccine.js:  1534  605  367
                      (integrated) vaccine.js:  1507  579  241
 
                              already_ordered:  1282  519  323
                 (integrated) already_ordered:  1255  493  200
 
                             absolute_require:  1086  413  247
                (integrated) absolute_require:  1059  387  146
 
             absolute_require_already_ordered:   834  327  202
(integrated) absolute_require_already_ordered:   807  301  110
 
                                      minimal:   515  195  145
                         (integrated) minimal:   515  195   67

The integrated lines are the ones where it compares the size of your app with and without vaccine. As you can see, while vaccine is small to begin with, it gets even smaller when gzipped with your sources, due to the way compression works.