Sunday, March 28, 2010

Jetpack/Addon SDK and XUL extensions

Update [2011-10]: New links to the fork of the Add-on SDK with support for XUL extensions and the documentation.

Update [2011-03]: See the fork of the Add-on SDK with support for XUL extensions and the updated documentation. I'm hoping to get the changes into the main Add-on SDK repository soon.

Update [2011-01]: These steps no longer work with the newer SDK versions. I'm working on making the SDK work with XUL extensions again. It's not finished, but you're welcome to try it out.

Update: These instructions were included in the Jetpack SDK documentation. Refer to the "Using the SDK with XUL extensions" section of the documentation for the up to date instructions for your SDK version.

I played around with the recently released Jetpack SDK. Currently at version 0.2, the SDK doesn't introduce many new APIs, focusing instead on the infrastructure: implementation of the CommonJS Modules spec and command-line tools to run, unit-test, and package Jetpacks as XPIs.

I find this functionality interesting for XUL-based extensions as well, so I wanted to try using it with my existing "old-style" extension (InfoLister).

Getting started

It turned out to be easy:

  1. Get the SDK source with
    hg clone http://hg.mozilla.org/labs/jetpack-sdk/
  2. cd jetpack-sdk
    . bin/activate
    mkdir packages/my-extension
  3. Copy the extension template the SDK uses to run jetpacks to your own folder:
    cp -R python-lib/cuddlefish/app-extension packages/my-extension/extension
    There's only one interesting file (as of SDK 0.2) in the template extension - the harness.js component that provides the CommonJS module loader (i.e. the require() implementation) and bootstraps the Jetpack (i.e. starts its main program or runs tests).
  4. Copy your other extension files to packages/my-extension/extension (components, chrome.manifest and chrome files, etc.)
  5. Create packages/my-extension/package.json (described in the introductory tutorial and the detailed Package Specification):
    {
      "id": "infolister@nickolay.ponomarev",
      "description": "Lists installed extensions and themes",
      "author": "Nickolay Ponomarev (asqueella@gmail.com)",
      "version": "0.11",
      "license": "MPL/GPL/LGPL"
    }
    The specified id is important, as it becomes the packaged extension's ID and is used to obtain the module loader below.
  6. Create directories for your CommonJS modules and tests, and the main module for cfx run:
    mkdir packages/my-extension/lib
    mkdir packages/my-extension/tests
    // packages/my-extension/lib/main.js
    exports.main = function(options, callbacks) {
    //  callbacks.quit();
    }
    Since we're just trying to get an old style-extension launched, we don't do anything in main() yet.
  7. Now we can run Firefox with a clean profile and our extension installed using:
    cfx run -a firefox -t extension
    -t extension tells cfx to use the directory we created earlier to install the extension.
  8. Similarly, to build XPI of the extension we can use
    cfx xpi -t extension

(cfx is a python script that uses mozrunner to create the test profile and launch the specified Mozilla application. It would be interesting to teach it to set up an advanced development profile (and re-use it instead of recreating it each time).)

Using CommonJS modules

Now that we have our extension running along with Jetpack core, we can start using modules in our code.

CommonJS modules are a neat idea: it's a standard way to write pieces of JS code that:

  • are isolated from each other (each module gets its own global object);
  • have explicitly defined "exported" properties (via exports) and imports (via require());
  • can be shared and reused by other developers, theoretically even across platforms (e.g. there can be modules used by both Mozilla-based jetpacks/extensions, website code, and even other non-browser platforms).

Jetpack SDK also provides a standard way to unit-test modules and a nice documentation browser for them, which will likely get even more useful with time.

Since the SDK tutorial has enough details about testing and documentation of modules, let's see how the modules can be accessed from a regular extension. We have already created a main module earlier, so how do we access it from the extension?

The answer is to use the loader from the harness service, which we mentioned earlier:

function loadJetpackModule(module) {
  return Components.classes["@mozilla.org/harness-service;1?id=infolister@nickolay.ponomarev"].
    getService().wrappedJSObject.loader.require(module);
}
alert(typeof loadJetpackModule("main").main); // ==> function