Porting the Library Detector to the Add-on SDK

Now it’s shipped the first stable release, one of the Jetpack project’s top priorities is to make it easier for people to port their add-ons to the Add-on SDK. I thought a small experiment would help me understand better what’s involved in porting to the SDK, and highlight some of the rough edges which make it harder than it needs to be. Louis-Rémi Babe suggested porting Paul Bakaus’s Library Detector, since it’s really simple, but still a useful tool.

What the Library Detector does

The Library Detector tells you which JavaScript frameworks the current web page is using. It does this by checking whether particular objects that those libraries add to the global window object are defined. For example, if window.jQuery is defined, then the page has loaded jQuery.

For each library that it finds, the library detector adds an icon representing that library to the status bar. It adds a tooltip to each icon, which contains the library name and version.

Library Detector script

The bulk of the script is an array of test objects, one for each library. Each test object contains a test attribute which is a function: if the function finds the library, it defines various additional properties for the test object, such as a version property containing the library version. Each test also contains a chrome:// URL pointing to the icon associated with its library.

The script listens to gBrowser’s DOMContentLoaded event. When this is triggered, the testLibraries function builds an array of libraries by iterating through the tests and adding an entry for each library which passes.

If a library is loaded into an iframe, then its objects will only be added to that iframe’s embedded window. The existing script will run for every window that generates the DOMContentLoaded event. It will look for libraries loaded into that window, and if it finds any, it will add the library information to that window’s topmost window, via window.top. It also avoids duplicates, to ensure that each library is only added once even if it is loaded into multiple iframes that share a topmost window.

Once the list is built, the switchLibraries function constructs a XUL statusbarpanel element for each library it found, populates it with the icon at the corresponding chrome:// URL, and adds it to the box.

Finally, it listen to gBrowser’s TabSelect event, to update the contents of the box for that window.

Porting

The widget module is a natural fit for this add-on’s UI. We’ll want to specify its content using HTML, so we can display an array of icons. The widget must be able to display different content for different windows, so we’ll use the new WidgetView object.

The test objects in the original script need access to the DOM window object, so we’ll package those in a content script. In fact, they need access to the un-proxied DOM window, so they can see the objects added by libraries, so we’ll need to use the experimental unsafeWindow object. We’ll use a page-mod to inject the content script into each page.

The content script is executed once for every window.onload event, so it will run multiple times when a single page containing multiple iframes is loaded. We just make a list of all the libraries we found in that window, and post the list to main.js using self.postMessage.

In main.js we handle the message by: fetching the tab corresponding to that worker using worker.tab, and adding the set of libraries to that tab’s libraries property, avoiding duplicates.

So to begin with we’ll have 2 scripts, a main.js and a content script which we’ll call “library-detector.js”.

main.js

First, main.js creates a page-mod. The page-mod matches all URLs and runs scripts at window.onload (i.e. setting contentScriptWhen: "end"). It responds to messages from each of the page-mod’s workers by updating a list of libraries which it will attach to the tab which corresponds to that worker.

Working around tooltips

So far the idea had been to make the interface identical to the existing version, in which the library information is shown as a tooltip. So the code that built the widget content inserted title attributes inside the img icon elements, and I thought this might be enough for tooltips to work. But it wasn’t – it isn’t possible to have tooltips associated with elements in a widget.

In the event listener for this message I initially tried to display the panel by calling panel.show: that preserves the original UI in which the library information is shown on mouseover. But if I do that, I can’t anchor the panel to the widget and it appears in the middle of the browser window. So I have to use the panel that belongs to the widget and is shown on click. So now, in the event listener I’ll just update the panel’s content.

Finally, I needed another content script to update the panel’s content with the library information. I found myself wishing for a way to set the panel’s content directly, in the same way I can with the widget. Then it would have been just a variable assignment.

Conclusions

It was mostly pretty smooth! Things I thought would have made it smoother:

I spent a bit of time confused about gBrowser, and that makes me feel that we should do a better job of explaining what’s available in a XUL script that isn’t available in content scripts (though that may just be my inexperience with XUL, and thus not very applicable to this audience).

Not being able to use tooltips was a bit of a pain. Actually, I like the new interface better, but specifying the interface declaratively using tooltips is nice, and it’s a shame that that is harder with the SDK.

Being able to anchor a panel to the widget would have given me more flexibility about the UI, although I think click-to-show is fine.

It seemed clunky to have to use a content script to update the panel’s content.

Pikadude No.1: if your ID is in email-style format, it looks as if you can set it by editing the “id” attribute in your “package.json”. At least, when I do that the expected ID shows up in the generated install.rdf, and Firefox’s update behavior is what I’d expect. cfx doesn’t support GUID-style IDs yet, but this is planned, I think.

Not a problem for me, as I’ve always felt that GUID-style IDs are ugly. I shall await the day when the SDK is either modernized to work on Python 3.x or rewritten to work on something else entirely (Node.js, perhaps?).