Creating Reusable Modules

To follow this tutorial you'll need to have learned the basics of jpm.

With the SDK you don't have to keep all your add-on in a single "index.js" file. You can split your code into separate modules with clearly defined interfaces between them. You then import and use these modules from other parts of your add-on using the require() statement, in exactly that same way that you import core SDK modules like page-mod or panel.

It can often make sense to structure a larger or more complex add-on as a collection of modules. This makes the design of the add-on easier to understand and provides some encapsulation as each module will export only what it chooses to, so you can change the internals of the module without breaking its users.

Once you've done this, you can package the modules and distribute them independently of your add-on, making them available to other add-on developers and effectively extending the SDK itself.

In this tutorial we'll do exactly that with a module that calculates file hashes.

A hashing add-on

A hash function takes a string of bytes of any length, and produces a short, fixed length string of bytes as output. It's a useful way to create a "fingerprint" that can be used to identify a file. MD5 is a commonly used hash function: although it's no longer considered secure, it works fine outside a security context.

Here we'll write an add-on that lets the user select a file on disk and calculates its hash. For both these operations we'll use XPCOM interfaces.

File picker

To let the user select a file we'll use nsIFilePicker. The documentation for that interface includes an example which we can adapt like this:

var {Cc, Ci} = require("chrome");

function promptForFile() {
  const nsIFilePicker = Ci.nsIFilePicker;

  var fp = Cc["@mozilla.org/filepicker;1"]
           .createInstance(nsIFilePicker);

  var window = require("sdk/window/utils").getMostRecentBrowserWindow();
  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);

  var rv = fp.show();
  if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
    var file = fp.file;
    // Get the path as string. Note that you usually won't
    // need to work with the string paths.
    var path = fp.file.path;
    // work with returned nsILocalFile...
  }
  return path;
}

Hash function

Firefox has built-in support for hash functions, exposed via the nsICryptoHash XPCOM interface The documentation page for that interface includes an example of calculating an MD5 hash of a file's contents, given its path. We can adapt it like this:

var {Cc, Ci} = require("chrome");

// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
  return ("0" + charCode.toString(16)).slice(-2);
}

function md5File(path) {
  var f = Cc["@mozilla.org/file/local;1"]
          .createInstance(Ci.nsILocalFile);
  f.initWithPath(path);
  var istream = Cc["@mozilla.org/network/file-input-stream;1"]
                .createInstance(Ci.nsIFileInputStream);
  // open for reading
  istream.init(f, 0x01, 0444, 0);
  var ch = Cc["@mozilla.org/security/hash;1"]
           .createInstance(Ci.nsICryptoHash);
  // we want to use the MD5 algorithm
  ch.init(ch.MD5);
  // this tells updateFromStream to read the entire file
  const PR_UINT32_MAX = 0xffffffff;
  ch.updateFromStream(istream, PR_UINT32_MAX);
  // pass false here to get binary data back
  var hash = ch.finish(false);

  // convert the binary hash data to a hex string.
  var s = Array.from(hash, (c, i) =>
      toHexString(hash.charCodeAt(i))).join("");
  return s;
}

Putting it together

The complete add-on adds a button to Firefox: when the user clicks the button, we ask them to select a file, compute the hash, and log the hash to the console:

var {Cc, Ci} = require("chrome");

// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
  return ("0" + charCode.toString(16)).slice(-2);
}

function md5File(path) {
  var f = Cc["@mozilla.org/file/local;1"]
          .createInstance(Ci.nsILocalFile);
  f.initWithPath(path);
  var istream = Cc["@mozilla.org/network/file-input-stream;1"]
                .createInstance(Ci.nsIFileInputStream);
  // open for reading
  istream.init(f, 0x01, 0444, 0);
  var ch = Cc["@mozilla.org/security/hash;1"]
           .createInstance(Ci.nsICryptoHash);
  // we want to use the MD5 algorithm
  ch.init(ch.MD5);
  // this tells updateFromStream to read the entire file
  const PR_UINT32_MAX = 0xffffffff;
  ch.updateFromStream(istream, PR_UINT32_MAX);
  // pass false here to get binary data back
  var hash = ch.finish(false);

  // convert the binary hash data to a hex string.
  var s = Array.from(hash, (c, i) =>
      toHexString(hash.charCodeAt(i))).join("");
  return s;
}

function promptForFile() {
  var window = require("sdk/window/utils").getMostRecentBrowserWindow();
  const nsIFilePicker = Ci.nsIFilePicker;

  var fp = Cc["@mozilla.org/filepicker;1"]
          .createInstance(nsIFilePicker);
  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);

  var rv = fp.show();
  if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
    var file = fp.file;
    // Get the path as string. Note that you usually won't
    // need to work with the string paths.
    var path = fp.file.path;
    // work with returned nsILocalFile...
  }
  return path;
}

require("sdk/ui/button/action").ActionButton({
  id: "show-panel",
  label: "Show Panel",
  icon: {
    "16": "./icon-16.png"
  },
  onClick: function() {
    console.log(md5File(promptForFile()));
  }
});

This works , but index.js is now getting longer and its logic is harder to understand. Let's factor the file picker and hashing code into separate modules.

Creating separate modules

filepicker.js

First create a new file in "lib" called "filepicker.js". Copy the file picker code into this new file, and add the following line at the end:

exports.promptForFile = promptForFile;

This defines the public interface of the new module.

So "filepicker.js" should look like this:

var {Cc, Ci} = require("chrome");

function promptForFile() {
  var window = require("sdk/window/utils").getMostRecentBrowserWindow();
  const nsIFilePicker = Ci.nsIFilePicker;

  var fp = Cc["@mozilla.org/filepicker;1"]
           .createInstance(nsIFilePicker);
  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);

  var rv = fp.show();
  if (rv == nsIFilePicker.returnOK ||
      rv == nsIFilePicker.returnReplace) {
    var file = fp.file;
    // Get the path as string. Note that you usually won't
    // need to work with the string paths.
    var path = fp.file.path;
    // work with returned nsILocalFile...
  }
  return path;
}

exports.promptForFile = promptForFile;

md5.js

Next, create another file in "lib", called "md5.js". Copy the hashing code there, and add this line at the end:

exports.hashFile = md5File;

The complete file looks like this:

var {Cc, Ci} = require("chrome");

// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
  return ("0" + charCode.toString(16)).slice(-2);
}

function md5File(path) {
  var f = Cc["@mozilla.org/file/local;1"]
          .createInstance(Ci.nsILocalFile);
  f.initWithPath(path);
  var istream = Cc["@mozilla.org/network/file-input-stream;1"]
                .createInstance(Ci.nsIFileInputStream);
  // open for reading
  istream.init(f, 0x01, 0444, 0);
  var ch = Cc["@mozilla.org/security/hash;1"]
           .createInstance(Ci.nsICryptoHash);
  // we want to use the MD5 algorithm
  ch.init(ch.MD5);
  // this tells updateFromStream to read the entire file
  const PR_UINT32_MAX = 0xffffffff;
  ch.updateFromStream(istream, PR_UINT32_MAX);
  // pass false here to get binary data back
  var hash = ch.finish(false);

  // convert the binary hash data to a hex string.
  var s = Array.from(hash, (c, i) =>
      toHexString(hash.charCodeAt(i))).join("");
  return s;
}

exports.hashFile = md5File;

index.js

Finally, update index.js to import these two new modules and use them:

var filepicker = require("./filepicker.js");
var md5 = require("./md5.js");

require("sdk/ui/button/action").ActionButton({
  id: "show-panel",
  label: "Show Panel",
  icon: {
    "16": "./icon-16.png"
  },
  onClick: function() {
    console.log(md5.hashFile(filepicker.promptForFile()));
  }
});

Distributing modules

With jpm, we use npm as the package manager for SDK modules that don't ship inside Firefox. Module developers can publish SDK modules to npm, and add-on developers can install them from npm and build them into their add-ons.

To learn how to use third-party modules in your own code, see the tutorial on adding menu items.