Add-ons using the techniques described in this document are considered a legacy technology in Firefox. Don't use these techniques to develop new add-ons. Use WebExtensions instead. If you maintain an add-on which uses the techniques described here, consider migrating it to use WebExtensions.
From Firefox 53 onwards, no new legacy add-ons will be accepted on addons.mozilla.org (AMO).
From Firefox 57 onwards, WebExtensions will be the only supported extension type, and Firefox will not load other types.
Even before Firefox 57, changes coming up in the Firefox platform will break many legacy extensions. These changes include multiprocess Firefox (e10s), sandboxing, and multiple content processes. Legacy extensions that are affected by these changes should migrate to WebExtensions if they can. See the "Compatibility Milestones" document for more.
A wiki page containing resources, migration paths, office hours, and more, is available to help developers transition to the new technologies.
This module is new in Firefox 34.
Note that at the moment you can't debug remote targets (for example, Firefox OS, the Firefox OS Simulator, or Firefox for Android) using tools developed with this API. We're working on removing this restriction.
Enables you to extend the Firefox Developer Tools. Most of the Firefox Developer Tools are hosted in a UI component called the Toolbox. Individual built-in tools, such as the JavaScript Debugger or the Web Console, occupy "panels" in the toolbox. With the dev/panel module, you can create your own panels in the Toolbox:
The panel gets a tab in the Toolbox toolbar which enables the user to open it:
You specify the panel's content and behavior using HTML, CSS, and JavaScript. When the panel's created, the framework passes it a debuggee
: this is a MessagePort
object that you can use to exchange JSON messages with the browser that the developer tools are currently debugging. The messages follow the remote debugging protocol.
For a simple walkthrough of using the dev/panel API to add a new tool, see Adding a panel to the toolbox.
Basic usage
Defining the panel constructor
To add a new tool you first need to define a constructor that inherits from the Panel
class, and in that constructor you need to supply values for various properties .
You can set the constructor up manually if you like, or you can use the Add-on SDK core/heritage module to simplify the mechanics of inheriting from Panel
. You can use the Class
utility function:
const { Panel } = require("dev/panel"); const { Class } = require("sdk/core/heritage"); const MyPanel = Class({ extends: Panel, label: "My Panel", tooltip: "My new devtool", icon: "./my-devtool.png", url: "./my-devtool.html", setup: function(options) { // my setup goes here }, dispose: function() { // my teardown goes here }, onReady: function() { // I can send messages to // the panel document here } });
Alternatively, you can use the extend
function:
const { extend } = require("sdk/core/heritage"); function MyPanel() {}; MyPanel.prototype = extend(Panel.prototype, { label: "My Panel", tooltip: "...", .... });
In the constructor definition there are a number of mandatory and optional parameters for you to supply.
Name | Type | Description | |
---|---|---|---|
label |
String | The string to display in the Toolbox toolbar. | Mandatory |
icon |
String |
The icon to display in the Toolbox toolbar, specified as a resource:// URL pointing to an icon file, typically in your add-on's "data" directory. You can use the notation |
Mandatory |
url |
String |
A resource:// URL pointing to an HTML file, typically in your add-on's "data" directory. This file contains the specification of the panel's user interface. You can use the notation You can't directly manipulate the panel's content from main.js, but you can exchange messages with scripts running in the panel. |
Mandatory |
tooltip |
String | A string that will be used as a tooltip in the Toolbox toolbar. | Optional |
setup |
Function |
A function that will be called when the panel is created. It's passed an
|
Optional |
dispose |
Function | A function that will be called when the panel is about to be destroyed. You can use it to do any cleanup. | Optional |
onReady |
Function | An event handler that will be called when the document in the panel becomes interactive. It's equivalent to document.readyState === "interactive" . At this point you can send the panel document messages. |
Optional |
onLoad |
Function | An event handler that will be called after the document in the panel is fully loaded. It's equivalent to document.readyState === "complete" . |
Optional |
Once you've defined the panel's constructor you have to export it so it can be called by the framework.
Creating a Tool
Next, you need to create a new Tool using the dev/toolbox
module, initializing it with the newly defined constructor.
Example
Here's a complete main.js:
// main.js // require the SDK modules const { Panel } = require("dev/panel"); const { Tool } = require("dev/toolbox"); const { Class } = require("sdk/core/heritage"); // define the panel constructor const MyPanel = Class({ extends: Panel, label: "My Panel", tooltip: "My new devtool's panel", icon: "./my-icon.png", url: "./my-panel.html", // when the panel is created, // take a reference to the debuggee setup: function(options) { this.debuggee = options.debuggee; }, dispose: function() { this.debuggee = null; }, onReady: function() { // in this function you can communicate // with the panel document } }); // export the constructor exports.MyPanel = MyPanel; // create a new tool, initialized // with the new constructor const myTool = new Tool({ panels: { myPanel: MyPanel } });
Panel document environment
The panel document loaded from the url
property can of course include CSS and JavaScript just like a normal web page:
<html> <head> <meta charset="utf-8"> <link href="./my-panel.css"rel="stylesheet"></link> <script src="resource://sdk/dev/volcan.js"></script> </head> <body> <div id="content"></div> </body> <script src="./my-panel.js"></script> </html>
It doesn't have access to any privileged APIs, including the Add-on SDK APIs. However, it can receive messages from the add-on that created it, and the add-on can pass it the debuggee so it can communicate with the debugger server the developer tools are targeting.
Communicating with the panel document
The main add-on code can't directly access the panel document or any scripts loaded by the panel document. However it can send messages to the panel document using the postMessage
method of Panel
. This is closely modeled on window.postMessage
. You can only send the panel document message after you've received the ready
event.
In the main add-on code:
// main.js // require the SDK modules const { Panel } = require("dev/panel"); const { Tool } = require("dev/toolbox"); const { Class } = require("sdk/core/heritage"); const MyPanel = Class({ extends: Panel, label: "My Panel", tooltip: "My new devtool's panel", icon: "./my-icon.png", url: "./my-panel.html", onReady: function() { this.postMessage("Message from the add-on"); } }); // export the constructor exports.MyPanel = MyPanel; // create a new tool, initialized // with the new constructor const myTool = new Tool({ panels: { myPanel: MyPanel } });
In the panel document script:
// my-panel.js window.addEventListener("message", function(event) { var content = document.getElementById("content"); content.textContent = event.data; });
Note that at the moment you have to pass an array of ports into postMessage
, even if you don't need to use them:
// main.js // require the SDK modules const { Panel } = require("dev/panel"); const { Tool } = require("dev/toolbox"); const { Class } = require("sdk/core/heritage"); const MyPanel = Class({ extends: Panel, label: "My Panel", tooltip: "My new devtool's panel", icon: "./my-icon.png", url: "./my-panel.html", setup: function(options) { this.debuggee = options.debuggee; }, dispose: function() { this.debuggee = null; }, onReady: function() { this.postMessage("Message from the add-on", [this.debuggee]); } }); // export the constructor exports.MyPanel = MyPanel; // create a new tool, initialized // with the new constructor const myTool = new Tool({ panels: { myPanel: MyPanel } });
This is being tracked as bug 1079540.
Two-way messaging
The panel document does not have an equivalent postMessage
method, so if you want your panel document to communicate back to the add-on, you can use channel messaging.
In the add-on side:
- require the
sdk/messaging
module - create a
MessageChannel
- keep one
MessagePort
in the add-on side for receiving messages from the panel document - pass the other
MessagePort
to the panel document in theports
argument to postMessage
// main.js // require the SDK modules const { Panel } = require("dev/panel"); const { Tool } = require("dev/toolbox"); const { Class } = require("sdk/core/heritage"); const { MessageChannel } = require("sdk/messaging"); const channel = new MessageChannel(); const addonSide = channel.port1; const panelSide = channel.port2; // messages from the panel arrive here addonSide.onmessage = function(event) { console.log(event.data); } const MyPanel = Class({ extends: Panel, label: "My Panel", tooltip: "My new devtool's panel", icon: "./my-icon.png", url: "./my-panel.html", onReady: function() { // send a port to the panel document this.postMessage("Message from the add-on", [panelSide]); } }); // export the constructor exports.MyPanel = MyPanel; // create a new tool, initialized // with the new constructor const myTool = new Tool({ panels: { myPanel: MyPanel } });
In the panel document:
- retrieve the
MessagePort
from the event, and use it to send messages to the add-on
// my-panel.js window.addEventListener("message", function(event) { // retrieve the port var toAddon = event.ports[0]; toAddon.start(); toAddon.postMessage("Message from the panel document"); console.log(toAddon); });
Communicating with the debugger server
The Firefox developer tools use a client/server protocol: a tool, such as a JavaScript debugger or style editor, is the client, and the program being debugged, such as Firefox, is the server. Clients connect to the server and send it messages to examine and modify the state of the program being debugged. The Remote Debugging Protocol page describes the protocol in detail.
When the user opens a panel and the panel is created, the framework calls the panel's setup
method. The setup
method will be passed a debuggee
object, which is a MessagePort
that the add-on can use to exchange messages with the debugger server.
For example, here's a main.js which sends the listTabs
message to the debugger server, and logs the response:
// main.js // require the SDK modules const { Panel } = require("dev/panel"); const { Tool } = require("dev/toolbox"); const { Class } = require("sdk/core/heritage"); const MyPanel = Class({ extends: Panel, label: "My Panel", tooltip: "My new devtool's panel", icon: "./my-icon.png", url: "./my-panel.html", setup: function(options) { this.debuggee = options.debuggee; this.debuggee.start(); this.debuggee.onmessage = function(event) { console.log(event.data); } this.debuggee.postMessage({ "to":"root", "type":"listTabs" }); } }); // export the constructor exports.MyPanel = MyPanel; // create a new tool, initialized // with the new constructor const myTool = new Tool({ panels: { myPanel: MyPanel } });
More usefully, you can pass debuggee
from the main add-on to the panel document using the ports
argument to postMessage
. Then the panel document can communicate with the debugger server.
In main.js:
// main.js // require the SDK modules const { Panel } = require("dev/panel"); const { Tool } = require("dev/toolbox"); const { Class } = require("sdk/core/heritage"); const MyPanel = Class({ extends: Panel, label: "My Panel", tooltip: "My new devtool's panel", icon: "./my-icon.png", url: "./my-panel.html", setup: function(options) { this.debuggee = options.debuggee; }, dispose: function() { this.debuggee = null; }, onReady: function() { this.debuggee.start(); this.postMessage("port", [this.debuggee]); } }); // export the constructor exports.MyPanel = MyPanel; // create a new tool, initialized // with the new constructor const myTool = new Tool({ panels: { myPanel: MyPanel } });
In my-panel.js:
// my-panel.js var content = document.getElementById("content"); window.addEventListener("message", function(event) { var debuggee = event.ports[0]; console.log(debuggee); debuggee.onmessage = function(event) { content.textContent = JSON.stringify(event.data); } debuggee.postMessage({ "to":"root", "type":"listTabs" }); });
If you do this, don't forget to call start()
on the port before passing it over to the panel document.
Volcan.js: a JavaScript API for the debugging protocol
Communicating with the debugger server by exchanging JSON messages can be cumbersome. So we've also provided a library called volcan.js which gives you a JavaScript API to the remote debugging protocol.
Note that at the moment volcan.js does not support the complete remote debugging protocol. We're still working on documenting exactly which messages are supported by volcan.js.
To use volcan.js, you can just include it from your panel's HTML like this:
<html> <head> <meta charset="utf-8"> <link href="./my-panel.css"rel="stylesheet"></link> <script src="resource://sdk/dev/volcan.js"></script> </head> <body> <div id = "content"></div> </body> <script src="./my-panel.js"></script> </html>
Here's a script that uses volcan.js to get the selected tab and display its URL:
// my-panel.js var content = document.getElementById("content"); window.addEventListener("message", function(event) { var debuggee = event.ports[0]; volcan.connect(debuggee). then(listTabs). then(writeTabList); }); function listTabs(root) { return root.listTabs(); } function writeTabList(tabList) { content.textContent = tabList.tabs[tabList.selected].url; }
We don't have detailed documentation for volcan.js, but it's coming soon. In the meantime, here's a quick guide.
Connecting
Volcan.js provides a global connect()
function that takes a MessagePort
connected to the debugger server, and returns a promise which is fulfilled with an object representing the root actor:
volcan.connect(debuggee).then(gotRoot); function gotRoot(root) { // can use root actor here }
Actors
Actors in the Remote Debugging Protocol are volcan.js objects. The messages actors can accept are methods of those objects. The message type becomes the method name. For example, the root actor object has a method listTabs
and the stylesheet actor object has a method getStyleSheets
. Any additional parameters sent in the message become arguments to the method.
If a message is expected to get a reply, then the method returns a promise which is fulfilled with the reply. If the message reply in the remote debugging protocol would contain an actor ID, then in volcan.js the object that fulfills the promise contains that actor instance. If the message reply contains some JSON object, then in volcan.js the object that fulfills the promise includes a corresponding JSON object.
If an actor can send notifications, as in the request/reply/notify pattern, then the corresponding volcan.js object emits an event, which you can listen for using addEventListener
. Any data sent with the notification is available to the event listener in the event.data
property.