Skip to main content
 

Addonception: Addons extending addons.

8 min read

Copenhagen airport

I'd like to talk about addons again. This time about extendable addons. This is a natural consequence of go-faster.

I'm going to assume the shape of the manifest is largely like Chrome Extensions and WebExtensions.

Here, I'm proposing two new top-level properties of the manifest.json: extension-points and extenders.

Addons can contribute extenders to extension-points. Addons can consume contributed extenders via extension-points.

Extenders are fragments of the manifest.

The normalized forms of this would be:

{
  "name": "contributing-addon",
  …
  "extenders": {
    "${extension-point-id}": ARRAY_OF_EXTENDER_OBJECTS
  }
  …
}
{
  "name": "host-addon",
  …
  "extension-points": {
    "${extension-point-id}": CONFIG_OBJECT
  }
  …
}

The config object isn't strictly necessary, but may become useful to extension point tooling.

For the sake of Product type readers, the first bit is a clear cut use-case. For the second part, I use a README-driven-development style.

Here's a use case for us to talk about and around.

What

The example here: an addon to parse the content document for micro-formats.

Sidebar for the lay-reader: microformats are specially crafted generic HTML for structured data like addresses or contact details. There is a loose set of standards.

The bulk of the work is done in a microformatting core addon. This can detect and linkify the text, but what to do when the user interacts with it?

Why

We wish to be able to customize the browser based upon territory, or for partnership reasons.

Where the user is taken when she interacts with the microformat is customizable – by default mozilla and its internationalization and partnership choices.

The micro-formats team is part mozilla and part contributor based; they are likely proficient in any language but they already have good implementations of a detector in javascript, and are unlikely to want to start over. They are motivated to see their existing implementations running on iOS and will be able make an addon.

The micro-formats team would like to have a different release cadence to the Firefox iOS team.

The Firefox for iOS team have not the bandwidth to take on a microformat detector in Swift project especially for an addon so ripe for re-use from desktop.

Tapes

User choice

The choices of map, or contact resolver may be ultimately up to the user when they interact with a microformat, but the choices that the user selects from can be provided from several (once competing) places:

  • the very first run defaults. This is provided by the microformats team.
  • the i18n contributors. The most likely useful address finders in one country may be different to the next.
  • the partnership engineering team.

The browser only needs to glue the pieces together.

Concepts. Words.

In the eclipse world, the unit of distibution Firefox calls an addon is a 'plugin'.

In OSGi, it is a 'bundle'.

The other eclipse words would be 'extension' and 'extension point'. How the extensions were joined to the extension points was the job of the registry.

OSGi bundles have 'service provider' and 'service'. There was no explict Service Registry, but it described itself in terms of a 'whiteboad' pattern.

Analogues with Android intents are noted, and differs mainly in granularity and lifecycle control.

I prefer the Eclipse nomenclature simply because its more descriptive.

However, I am aware that other browsers already use 'extension'. Thus, I will now consistently use extender and extension point (or EP).

Back to business

For the rest of this post I'll assume a set of addons:

  • microformat-core
  • microformat-i18n-xx
  • microformat-commercial-xx

How these addons get there we can talk about later on.

The microformat-core addon advertises an extension point for microformat linkifiers. Linkifiers are templates for a given detected object that the core addon can use to send the user to a new tab.

Declaratively, we could define an address resolver like so:

{
  "name": "Open Street Map",
  "template": "http://www.openstreetmap.org/search?query={line1}%2C{line2}%2C{zipcode}%2C{country}"
}

Linkifiers are extenders provided by the i18n-xx and commercial-xx addons, as well as any other addon that the user could install. Importantly microformat-core addon can provide one, as a fallback when all other addons have been uninstalled.

The linkifier extension point is not part of the manifest spec, but for addon review purposes we'd like to be able to declare it in the manifest. But the syntax to declare extension-points and extenders should be in the spec, so more developers can add to the Firefox ecosystem without stepping on each others toes.

For similar review purposes, and convenience for small addon creators, the extenders should be declared in or near the manifest.json file.

{
  "name": "Microformatter"
  …
  "extension-points": {
    "mf-linkifier-address": {
      "selector": "pref"
    },
  },
  "permissions": [
    "extenders"
  ]

Somewhere, in a manifest not far away.

  "extenders": {
    "mf-linkifier-address": {
      "name": "Open Street Map",
      "template": "http://www.openstreetmap.org/search?query={line1}%2C{line2}%2C{zipcode}%2C{country}"
    },
  }
  // no explicit permission is needed to declare extenders in the manifest.
}

The extension point collects all the extenders up from different addons and presents them as a list.

In the microformats-core addon, the extension point is consumed in an event based manner:

function findAddress(detectedObject) {
  var ep = chrome.extenders.get('mf-linkifier-address');
  var linkifiers = ep.all();
  var linkifier = linkifiers[0]; // naïve

  var url = makeURL(linkifier.template, detectedObject);
  return url;
}
// 

It is possible to limit the access to extension points via the manifest.

Thus, we have parts of other addon manifests squirted into an addon that invites it, and the many-to-many relationship is mediated by an addon 'registry'.

Further work

Dinosaurs

Bootstrapping addons

The underlying technology is not particularly difficult: a dictionary of extension-point ids mapping onto a list of extenders, with some events and ordering independence thrown in.

The same objects can be used to implement other parts of the addon manifest interfaces (e.g. content-scripts, background-pages, pageActions).

It starts to become magical when extension points are notified when extenders appear and disappear.

var ep = chrome.extenders.get('firefox.addon-manifests');
ep.onAdd(function (addonManifest) {
  var allExtenders = addonManifest.extenders || {};
  …
  for (extensionPointID in allExtenders) {
      // …
      for (var extender in allExtenders[extensionPointID]) {
        var extender = … // iterating through the manifest
        chrome.extenders.add(extensionPointID, extender);
        // …
      }
  }
});

ep.onRemove(function (manifest) {
  …
  chrome.extenders.remove(extensionPointID, extender);
  …
});

Thus, we have restart-less install.

Developer tools to ease extender

The mf-linkifier-address extension point above declares a "selector".

  var linkifier = ep.choose(); // select based upon a preference page choice.

Swift consumers of extension points

  let ep = registry.get("browserAction")
  let buttonMetadata = ep.choose(); // select based upon a preference page choice.

The integration between javascript and Swift is just nice enough to get things done, but pretty ugly syntax wise.

However, the registry can be kept in Swift or Javascript, and be shared relatively painlessly between Swift consumers of extension points.

Going further, Swift knows enough about the extension point and the contributed extenders that it can generate a preference page for the extension point.

Extenders with js functions

JSContexts can share functions quite comfortably executing functions in the context they were declared in. JSContexts seem to behave almost exactly like a module-pattern module would.

Thus, programatic access to add functions as or part of an extender would be really interesting.

The extender registry provides a simple API to add and remove extenders – simple enough for a manifest parser to do it in an automated way. Providing access through JS may not be advisable for addon-review purposes, so some manifest driven access

Concerns with JS adding extenders

  • Content-scripts access to the registry is needed (WebExtensions needs getManifest()) anyway, but sharing functions across this boundary may be unneccessary or difficult.
  • Contributing addons may expose user-data to other addons.
  • A good fences rule will be needed to ensure one addon doesn't crash another.

Security

  • Addons can only consume the extension-points they declare in their manifest.
  • Addons contribute only the data they want to – extenders do not have access to the extension-points.
  • Extension points starting with 'firefox.' are not accessible from any addon.
  • A policy where multiple addons consume the same extension-point.

Ghetto Gecko Back to our example

So we have an microformats-core addon and a microformats-i18n, microformats-commercial addons.

Now we also have API for home tiles. This would also be a likely candidate for the same pattern.

However, the units of extension are extenders, which are can be distributed as a group in single addon.

  • feature-a
  • feature-b
  • feature-c
  • i18n-xx
  • commercial-xx

The feature addons are shipped with the app.

The locale specific configuration for feature addons can either be distributed from a remote server – and appropriately signed, or distributed with the app and chosen at runtime.

Additional conifg for a, b and c can of course come from other addons the user installs.

This, plus an addon update mechanism gives us great scope to ship code faster, prototype new additions to the spec.

Summary

I've described an extender registry: a fairly simple data structure which can be the enabling technology for addon management and implementation, but also enables a secure and stable extension mechanism for third party addons.