Skip to content

Instantly share code, notes, and snippets.

@empyrical
Created July 16, 2015 04:28
Show Gist options
  • Save empyrical/f1cca44da1f81cb09a26 to your computer and use it in GitHub Desktop.
Save empyrical/f1cca44da1f81cb09a26 to your computer and use it in GitHub Desktop.
Firefox addons: A monkey-patchable CommonJS environment

Firefox addons: A monkey-patchable CommonJS environment

This solution was written for JPM 1.0.0.

It is also a proof-of-concept, and has not been thoroughly tested yet. Use at your own risk!

The Firefox module loader, by default, has all modules live in their own isolated world and don't share any global object. You can't monkey-patch default methods/the global object in one module of your extension, and have it be affected in another module of your extension.

Mozilla's JPM tool handily lets you use packages straight from NPM in your Firefox add-on, but the globals in the JS environment in an extension's main process are pretty barebones until you require anything. Globals that tons of NPM modules take for granted as being available - like setTimeout, XMLHttpRequest, even variables pointing to the global object like global or window arent there.

And because of the sandboxing feature mentioned above, you can't monkey-patch your environment to be compatible with a module if you wanted to.

The loader system does, however, have an undocumented option called sharedGlobal which makes your modules all share the same globals. To take advantage of this, you'll need to start up a custom loader and then run your extension's main process through this custom loader.

In this gist, I have a file called _loader.js which was ripped from sdk/addon/bootstrap.js and patched to work as a stand-alone module in a Firefox addon. It acts as the entry point into this example extension, then starts up another loader system using index.js as its entry point

To make it use a file other than index.js as the entry point, edit the line that's changing metadata.main to a file of your choosing.

Important: This also results in SDK modules you require() sharing globals with your addon, and they were not written with this behavior in mind. Thankfully, you can maintain a blacklist (sharedGlobalBlacklist) of modules that you don't want to share globals with. Notably, method/core will not work at all and will stop your extension from starting altogether if you don't put it in this blacklist.

If you'd like to use the NPM Pouch DB in your Firefox addon, this blog post has figured out what you need to monkey-patch in to get it working right:

http://siphon9.net/loune/2015/02/pouchdb-for-firefox-addon-sdk/

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu } = require("chrome");
const { uri, id } = require('sdk/self');
const metadata = require('package.json');
const prefs = require("sdk/preferences/service");
const { Loader, Require, Module, Main } = require("toolkit/loader");
// Point this to your addon's entry point
metadata.main = 'index.js';
// load below now, so that it can be used by sdk/addon/runner
// see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1042239
const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {});
const UUID_PATTERN = /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
// Takes add-on ID and normalizes it to a domain name so that add-on
// can be mapped to resource://domain/
const readDomain = id =>
// If only `@` character is the first one, than just substract it,
// otherwise fallback to legacy normalization code path. Note: `.`
// is valid character for resource substitutaiton & we intend to
// make add-on URIs intuitive, so it's best to just stick to an
// add-on author typed input.
id.lastIndexOf("@") === 0 ? id.substr(1).toLowerCase() :
id.toLowerCase().
replace(/@/g, "-at-").
replace(/\./g, "-dot-").
replace(UUID_PATTERN, "$1");
const readPaths = id => {
const base = `extensions.modules.${id}.path.`;
const domain = readDomain(id);
return prefs.keys(base).reduce((paths, key) => {
const value = prefs.get(key);
const name = key.replace(base, "");
const path = name.split(".").join("/");
const prefix = path.length ? `${path}/` : path;
const uri = value.endsWith("/") ? value : `${value}/`;
const root = `extensions.modules.${domain}.commonjs.path.${name}`;
mount(root, uri);
paths[prefix] = `resource://${root}/`;
return paths;
}, {});
};
const domain = readDomain(id);
const baseURI = `resource://${domain}/`;
const command = prefs.get(`extensions.${id}.sdk.load.command`);
const loader = Loader({
id,
isNative: true,
checkCompatibility: true,
prefixURI: baseURI,
rootURI: baseURI,
name: metadata.name,
paths: Object.assign({
"": "resource://gre/modules/commonjs/",
"devtools/": "resource://gre/modules/devtools/",
"./": baseURI
}, readPaths(id)),
manifest: metadata,
metadata: metadata,
modules: {
"@test/options": {}
},
noQuit: prefs.get(`extensions.${id}.sdk.test.no-quit`, false),
sharedGlobal: true,
sharedGlobalBlacklist: [
"method/core",
"sdk/addon/window",
"sdk/indexed-db"
]
});
const module = Module("package.json", `${baseURI}package.json`);
const main = command === "test" ? "sdk/test/runner" : null;
const prefsURI = `${baseURI}defaults/preferences/prefs.js`;
const _require = Require(loader, module);
const { startup } = _require("sdk/addon/runner");
startup("startup", {loader, main, prefsURI});
'use strict';
var { setTimeout } = require('sdk/timers');
let _global = (1, eval)('this');
_global.global = _global;
global.setTimeout = setTimeout;
require('./test');
{
"title": "Monkeypatch Demo",
"name": "monkeypatch-demo",
"version": "0.0.1",
"description": "A basic add-on",
"main": "_loader.js",
"author": "",
"engines": {
"firefox": ">=38.0a1",
"fennec": ">=38.0a1"
},
"license": "MIT"
}
'use strict';
setTimeout(function() {
// Using console.error here so it'll still show up in the add-on console,
// even if you're not in debug mode
console.error('Hello, world!');
}, 500);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment