Move Add-on SDK source to toolkit/jetpack

This commit is contained in:
NTD 2018-02-09 06:46:43 -05:00 committed by Roy Tam
commit 23b67db438
993 changed files with 484 additions and 100663 deletions

View file

@ -0,0 +1,95 @@
/* 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";
module.metadata = {
"stability": "experimental"
};
const { Cu } = require("chrome");
const { Class } = require("../sdk/core/heritage");
const { MessagePort, MessageChannel } = require("../sdk/messaging");
const { require: devtoolsRequire } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const { DebuggerServer } = devtoolsRequire("devtools/server/main");
const outputs = new WeakMap();
const inputs = new WeakMap();
const targets = new WeakMap();
const transports = new WeakMap();
const inputFor = port => inputs.get(port);
const outputFor = port => outputs.get(port);
const transportFor = port => transports.get(port);
const fromTarget = target => {
const debuggee = new Debuggee();
const { port1, port2 } = new MessageChannel();
inputs.set(debuggee, port1);
outputs.set(debuggee, port2);
targets.set(debuggee, target);
return debuggee;
};
exports.fromTarget = fromTarget;
const Debuggee = Class({
extends: MessagePort.prototype,
close: function() {
const server = transportFor(this);
if (server) {
transports.delete(this);
server.close();
}
outputFor(this).close();
},
start: function() {
const target = targets.get(this);
if (target.isLocalTab) {
// Since a remote protocol connection will be made, let's start the
// DebuggerServer here, once and for all tools.
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
transports.set(this, DebuggerServer.connectPipe());
}
// TODO: Implement support for remote connections (See Bug 980421)
else {
throw Error("Remote targets are not yet supported");
}
// pipe messages send to the debuggee to an actual
// server via remote debugging protocol transport.
inputFor(this).addEventListener("message", ({data}) =>
transportFor(this).send(data));
// pipe messages received from the remote debugging
// server transport onto the this debuggee.
transportFor(this).hooks = {
onPacket: packet => inputFor(this).postMessage(packet),
onClosed: () => inputFor(this).close()
};
inputFor(this).start();
outputFor(this).start();
},
postMessage: function(data) {
return outputFor(this).postMessage(data);
},
get onmessage() {
return outputFor(this).onmessage;
},
set onmessage(onmessage) {
outputFor(this).onmessage = onmessage;
},
addEventListener: function(...args) {
return outputFor(this).addEventListener(...args);
},
removeEventListener: function(...args) {
return outputFor(this).removeEventListener(...args);
}
});
exports.Debuggee = Debuggee;

View file

@ -0,0 +1,120 @@
/* 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";
(function({content, sendSyncMessage, addMessageListener, sendAsyncMessage}) {
const Cc = Components.classes;
const Ci = Components.interfaces;
const observerService = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
const channels = new Map();
const handles = new WeakMap();
// Takes remote port handle and creates a local one.
// also set's up a messaging channel between them.
// This is temporary workaround until Bug 914974 is fixed
// and port can be transfered through message manager.
const demarshal = (handle) => {
if (handle.type === "MessagePort") {
if (!channels.has(handle.id)) {
const channel = new content.MessageChannel();
channels.set(handle.id, channel);
handles.set(channel.port1, handle);
channel.port1.onmessage = onOutPort;
}
return channels.get(handle.id).port2;
}
return null;
};
const onOutPort = event => {
const handle = handles.get(event.target);
sendAsyncMessage("sdk/port/message", {
port: handle,
message: event.data
});
};
const onInPort = ({data}) => {
const channel = channels.get(data.port.id);
if (channel)
channel.port1.postMessage(data.message);
};
const onOutEvent = event =>
sendSyncMessage("sdk/event/" + event.type,
{ type: event.type,
data: event.data });
const onInMessage = (message) => {
const {type, data, origin, bubbles, cancelable, ports} = message.data;
const event = new content.MessageEvent(type, {
bubbles: bubbles,
cancelable: cancelable,
data: data,
origin: origin,
target: content,
source: content,
ports: ports.map(demarshal)
});
content.dispatchEvent(event);
};
const onReady = event => {
channels.clear();
};
addMessageListener("sdk/event/message", onInMessage);
addMessageListener("sdk/port/message", onInPort);
const observer = {
handleEvent: ({target, type}) => {
observer.observe(target, type);
},
observe: (document, topic, data) => {
// When frame associated with message manager is removed from document `docShell`
// is set to `null` but observer is still kept alive. At this point accesing
// `content.document` throws "can't access dead object" exceptions. In order to
// avoid leaking observer and logged errors observer is going to be removed when
// `docShell` is set to `null`.
if (!docShell) {
observerService.removeObserver(observer, topic);
}
else if (document === content.document) {
if (topic.endsWith("-document-interactive")) {
sendAsyncMessage("sdk/event/ready", {
type: "ready",
readyState: document.readyState,
uri: document.documentURI
});
}
if (topic.endsWith("-document-loaded")) {
sendAsyncMessage("sdk/event/load", {
type: "load",
readyState: document.readyState,
uri: document.documentURI
});
}
if (topic === "unload") {
channels.clear();
sendAsyncMessage("sdk/event/unload", {
type: "unload",
readyState: "uninitialized",
uri: document.documentURI
});
}
}
}
};
observerService.addObserver(observer, "content-document-interactive", false);
observerService.addObserver(observer, "content-document-loaded", false);
observerService.addObserver(observer, "chrome-document-interactive", false);
observerService.addObserver(observer, "chrome-document-loaded", false);
addEventListener("unload", observer, false);
})(this);

View file

@ -0,0 +1,259 @@
/* 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";
module.metadata = {
"stability": "experimental"
};
const { Cu } = require("chrome");
const { Class } = require("../sdk/core/heritage");
const { curry } = require("../sdk/lang/functional");
const { EventTarget } = require("../sdk/event/target");
const { Disposable, setup, dispose } = require("../sdk/core/disposable");
const { emit, off, setListeners } = require("../sdk/event/core");
const { when } = require("../sdk/event/utils");
const { getFrameElement } = require("../sdk/window/utils");
const { contract, validate } = require("../sdk/util/contract");
const { data: { url: resolve }} = require("../sdk/self");
const { identify } = require("../sdk/ui/id");
const { isLocalURL, URL } = require("../sdk/url");
const { encode } = require("../sdk/base64");
const { marshal, demarshal } = require("./ports");
const { fromTarget } = require("./debuggee");
const { removed } = require("../sdk/dom/events");
const { id: addonID } = require("../sdk/self");
const { viewFor } = require("../sdk/view/core");
const { createView } = require("./panel/view");
const OUTER_FRAME_URI = module.uri.replace(/\.js$/, ".html");
const FRAME_SCRIPT = module.uri.replace("/panel.js", "/frame-script.js");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const HTML_NS = "http://www.w3.org/1999/xhtml";
const makeID = name =>
("dev-panel-" + addonID + "-" + name).
split("/").join("-").
split(".").join("-").
split(" ").join("-").
replace(/[^A-Za-z0-9_\-]/g, "");
// Weak mapping between `Panel` instances and their frame's
// `nsIMessageManager`.
const managers = new WeakMap();
// Return `nsIMessageManager` for the given `Panel` instance.
const managerFor = x => managers.get(x);
// Weak mappinging between iframe's and their owner
// `Panel` instances.
const panels = new WeakMap();
const panelFor = frame => panels.get(frame);
// Weak mapping between panels and debugees they're targeting.
const debuggees = new WeakMap();
const debuggeeFor = panel => debuggees.get(panel);
const frames = new WeakMap();
const frameFor = panel => frames.get(panel);
const setAttributes = (node, attributes) => {
for (var key in attributes)
node.setAttribute(key, attributes[key]);
};
const onStateChange = ({target, data}) => {
const panel = panelFor(target);
panel.readyState = data.readyState;
emit(panel, data.type, { target: panel, type: data.type });
};
// port event listener on the message manager that demarshalls
// and forwards to the actual receiver. This is a workaround
// until Bug 914974 is fixed.
const onPortMessage = ({data, target}) => {
const port = demarshal(target, data.port);
if (port)
port.postMessage(data.message);
};
// When frame is removed from the toolbox destroy panel
// associated with it to release all the resources.
const onFrameRemove = frame => {
panelFor(frame).destroy();
};
const onFrameInited = frame => {
frame.style.visibility = "visible";
}
const inited = frame => new Promise(resolve => {
const { messageManager } = frame.frameLoader;
const listener = message => {
messageManager.removeMessageListener("sdk/event/ready", listener);
resolve(frame);
};
messageManager.addMessageListener("sdk/event/ready", listener);
});
const getTarget = ({target}) => target;
const Panel = Class({
extends: Disposable,
implements: [EventTarget],
get id() {
return makeID(this.name || this.label);
},
readyState: "uninitialized",
ready: function() {
const { readyState } = this;
const isReady = readyState === "complete" ||
readyState === "interactive";
return isReady ? Promise.resolve(this) :
when(this, "ready").then(getTarget);
},
loaded: function() {
const { readyState } = this;
const isLoaded = readyState === "complete";
return isLoaded ? Promise.resolve(this) :
when(this, "load").then(getTarget);
},
unloaded: function() {
const { readyState } = this;
const isUninitialized = readyState === "uninitialized";
return isUninitialized ? Promise.resolve(this) :
when(this, "unload").then(getTarget);
},
postMessage: function(data, ports=[]) {
const manager = managerFor(this);
manager.sendAsyncMessage("sdk/event/message", {
type: "message",
bubbles: false,
cancelable: false,
data: data,
origin: this.url,
ports: ports.map(marshal(manager))
});
}
});
exports.Panel = Panel;
validate.define(Panel, contract({
label: {
is: ["string"],
msg: "The `option.label` must be a provided"
},
tooltip: {
is: ["string", "undefined"],
msg: "The `option.tooltip` must be a string"
},
icon: {
is: ["string"],
map: x => x && resolve(x),
ok: x => isLocalURL(x),
msg: "The `options.icon` must be a valid local URI."
},
url: {
map: x => resolve(x.toString()),
is: ["string"],
ok: x => isLocalURL(x),
msg: "The `options.url` must be a valid local URI."
},
invertIconForLightTheme: {
is: ["boolean", "undefined"],
msg: "The `options.invertIconForLightTheme` must be a boolean."
},
invertIconForDarkTheme: {
is: ["boolean", "undefined"],
msg: "The `options.invertIconForDarkTheme` must be a boolean."
}
}));
setup.define(Panel, (panel, {window, toolbox, url}) => {
// Hack: Given that iframe created by devtools API is no good for us,
// we obtain original iframe and replace it with the one that has
// desired configuration.
const original = getFrameElement(window);
const container = original.parentNode;
original.remove();
const frame = createView(panel, container.ownerDocument);
// Following modifications are a temporary workaround until Bug 1049188
// is fixed.
// Enforce certain iframe customizations regardless of users request.
setAttributes(frame, {
"id": original.id,
"src": url,
"flex": 1,
"forceOwnRefreshDriver": "",
"tooltip": "aHTMLTooltip"
});
frame.style.visibility = "hidden";
frame.classList.add("toolbox-panel-iframe");
// Inject iframe into designated node until add-on author decides
// to inject it elsewhere instead.
if (!frame.parentNode)
container.appendChild(frame);
// associate view with a panel
frames.set(panel, frame);
// associate panel model with a frame view.
panels.set(frame, panel);
const debuggee = fromTarget(toolbox.target);
// associate debuggee with a panel.
debuggees.set(panel, debuggee);
// Setup listeners for the frame message manager.
const { messageManager } = frame.frameLoader;
messageManager.addMessageListener("sdk/event/ready", onStateChange);
messageManager.addMessageListener("sdk/event/load", onStateChange);
messageManager.addMessageListener("sdk/event/unload", onStateChange);
messageManager.addMessageListener("sdk/port/message", onPortMessage);
messageManager.loadFrameScript(FRAME_SCRIPT, false);
managers.set(panel, messageManager);
// destroy panel if frame is removed.
removed(frame).then(onFrameRemove);
// show frame when it is initialized.
inited(frame).then(onFrameInited);
// set listeners if there are ones defined on the prototype.
setListeners(panel, Object.getPrototypeOf(panel));
panel.setup({ debuggee: debuggee });
});
createView.define(Panel, (panel, document) => {
const frame = document.createElement("iframe");
setAttributes(frame, {
"sandbox": "allow-scripts",
// We end up using chrome iframe with forced message manager
// as fixing a swapFrameLoader seemed like a giant task (see
// Bug 1075490).
"type": "chrome",
"forcemessagemanager": true,
"transparent": true,
"seamless": "seamless",
});
return frame;
});
dispose.define(Panel, function(panel) {
debuggeeFor(panel).close();
debuggees.delete(panel);
managers.delete(panel);
frames.delete(panel);
panel.readyState = "destroyed";
panel.dispose();
});
viewFor.define(Panel, frameFor);

View file

@ -0,0 +1,14 @@
/* 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";
module.metadata = {
"stability": "experimental"
};
const { method } = require("method/core");
const createView = method("dev/panel/view#createView");
exports.createView = createView;

View file

@ -0,0 +1,64 @@
/* 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";
module.metadata = {
"stability": "experimental"
};
// This module provides `marshal` and `demarshal` functions
// that can be used to send MessagePort's over `nsIFrameMessageManager`
// until Bug 914974 is fixed.
const { add, iterator } = require("../sdk/lang/weak-set");
const { curry } = require("../sdk/lang/functional");
var id = 0;
const ports = new WeakMap();
// Takes `nsIFrameMessageManager` and `MessagePort` instances
// and returns a handle representing given `port`. Messages
// received on given `port` will be forwarded to a message
// manager under `sdk/port/message` and messages like:
// { port: { type: "MessagePort", id: 2}, data: data }
// Where id is an identifier associated with a given `port`
// and `data` is an `event.data` received on port.
const marshal = curry((manager, port) => {
if (!ports.has(port)) {
id = id + 1;
const handle = {type: "MessagePort", id: id};
// Bind id to the given port
ports.set(port, handle);
// Obtain a weak reference to a port.
add(exports, port);
port.onmessage = event => {
manager.sendAsyncMessage("sdk/port/message", {
port: handle,
message: event.data
});
};
return handle;
}
return ports.get(port);
});
exports.marshal = marshal;
// Takes `nsIFrameMessageManager` instance and a handle returned
// `marshal(manager, port)` returning a `port` that was passed
// to it. Note that `port` may be GC-ed in which case returned
// value will be `null`.
const demarshal = curry((manager, {type, id}) => {
if (type === "MessagePort") {
for (let port of iterator(exports)) {
if (id === ports.get(port).id)
return port;
}
}
return null;
});
exports.demarshal = demarshal;

View file

@ -0,0 +1,135 @@
/* 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";
module.metadata = {
"stability": "experimental"
};
const { Class } = require("../sdk/core/heritage");
const { EventTarget } = require("../sdk/event/target");
const { Disposable, setup, dispose } = require("../sdk/core/disposable");
const { contract, validate } = require("../sdk/util/contract");
const { id: addonID } = require("../sdk/self");
const { onEnable, onDisable } = require("dev/theme/hooks");
const { isString, instanceOf, isFunction } = require("sdk/lang/type");
const { add } = require("sdk/util/array");
const { data } = require("../sdk/self");
const { isLocalURL } = require("../sdk/url");
const makeID = name =>
("dev-theme-" + addonID + (name ? "-" + name : "")).
split(/[ . /]/).join("-").
replace(/[^A-Za-z0-9_\-]/g, "");
const Theme = Class({
extends: Disposable,
implements: [EventTarget],
initialize: function(options) {
this.name = options.name;
this.label = options.label;
this.styles = options.styles;
// Event handlers
this.onEnable = options.onEnable;
this.onDisable = options.onDisable;
},
get id() {
return makeID(this.name || this.label);
},
setup: function() {
// Any initialization steps done at the registration time.
},
getStyles: function() {
if (!this.styles) {
return [];
}
if (isString(this.styles)) {
if (isLocalURL(this.styles)) {
return [data.url(this.styles)];
}
}
let result = [];
for (let style of this.styles) {
if (isString(style)) {
if (isLocalURL(style)) {
style = data.url(style);
}
add(result, style);
} else if (instanceOf(style, Theme)) {
result = result.concat(style.getStyles());
}
}
return result;
},
getClassList: function() {
let result = [];
for (let style of this.styles) {
if (instanceOf(style, Theme)) {
result = result.concat(style.getClassList());
}
}
if (this.name) {
add(result, this.name);
}
return result;
}
});
exports.Theme = Theme;
// Initialization & dispose
setup.define(Theme, (theme) => {
theme.classList = [];
theme.setup();
});
dispose.define(Theme, function(theme) {
theme.dispose();
});
// Validation
validate.define(Theme, contract({
label: {
is: ["string"],
msg: "The `option.label` must be a provided"
},
}));
// Support theme events: apply and unapply the theme.
onEnable.define(Theme, (theme, {window, oldTheme}) => {
if (isFunction(theme.onEnable)) {
theme.onEnable(window, oldTheme);
}
});
onDisable.define(Theme, (theme, {window, newTheme}) => {
if (isFunction(theme.onDisable)) {
theme.onDisable(window, newTheme);
}
});
// Support for built-in themes
const LightTheme = Theme({
name: "theme-light",
styles: "chrome://devtools/skin/light-theme.css",
});
const DarkTheme = Theme({
name: "theme-dark",
styles: "chrome://devtools/skin/dark-theme.css",
});
exports.LightTheme = LightTheme;
exports.DarkTheme = DarkTheme;

View file

@ -0,0 +1,17 @@
/* 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";
module.metadata = {
"stability": "experimental"
};
const { method } = require("method/core");
const onEnable = method("dev/theme/hooks#onEnable");
const onDisable = method("dev/theme/hooks#onDisable");
exports.onEnable = onEnable;
exports.onDisable = onDisable;

View file

@ -0,0 +1,107 @@
/* 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";
module.metadata = {
"stability": "experimental"
};
const { Cu, Cc, Ci } = require("chrome");
const { Class } = require("../sdk/core/heritage");
const { Disposable, setup } = require("../sdk/core/disposable");
const { contract, validate } = require("../sdk/util/contract");
const { each, pairs, values } = require("../sdk/util/sequence");
const { onEnable, onDisable } = require("../dev/theme/hooks");
const { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
// This is temporary workaround to allow loading of the developer tools client - volcan
// into a toolbox panel, this hack won't be necessary as soon as devtools patch will be
// shipped in nightly, after which it can be removed. Bug 1038517
const registerSDKURI = () => {
const ioService = Cc['@mozilla.org/network/io-service;1']
.getService(Ci.nsIIOService);
const resourceHandler = ioService.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler);
const uri = module.uri.replace("dev/toolbox.js", "");
resourceHandler.setSubstitution("sdk", ioService.newURI(uri, null, null));
};
registerSDKURI();
const Tool = Class({
extends: Disposable,
setup: function(params={}) {
const { panels } = validate(this, params);
const { themes } = validate(this, params);
this.panels = panels;
this.themes = themes;
each(([key, Panel]) => {
const { url, label, tooltip, icon, invertIconForLightTheme,
invertIconForDarkTheme } = validate(Panel.prototype);
const { id } = Panel.prototype;
gDevTools.registerTool({
id: id,
url: "about:blank",
label: label,
tooltip: tooltip,
icon: icon,
invertIconForLightTheme: invertIconForLightTheme,
invertIconForDarkTheme: invertIconForDarkTheme,
isTargetSupported: target => target.isLocalTab,
build: (window, toolbox) => {
const panel = new Panel();
setup(panel, { window: window,
toolbox: toolbox,
url: url });
return panel.ready();
}
});
}, pairs(panels));
each(([key, theme]) => {
validate(theme);
setup(theme);
gDevTools.registerTheme({
id: theme.id,
label: theme.label,
stylesheets: theme.getStyles(),
classList: theme.getClassList(),
onApply: (window, oldTheme) => {
onEnable(theme, { window: window,
oldTheme: oldTheme });
},
onUnapply: (window, newTheme) => {
onDisable(theme, { window: window,
newTheme: newTheme });
}
});
}, pairs(themes));
},
dispose: function() {
each(Panel => gDevTools.unregisterTool(Panel.prototype.id),
values(this.panels));
each(Theme => gDevTools.unregisterTheme(Theme.prototype.id),
values(this.themes));
}
});
validate.define(Tool, contract({
panels: {
is: ["object", "undefined"]
},
themes: {
is: ["object", "undefined"]
}
}));
exports.Tool = Tool;

View file

@ -0,0 +1,40 @@
/* 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 { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const { getActiveTab } = require("../sdk/tabs/utils");
const { getMostRecentBrowserWindow } = require("../sdk/window/utils");
const targetFor = target => {
target = target || getActiveTab(getMostRecentBrowserWindow());
return devtools.TargetFactory.forTab(target);
};
const getId = id => ((id.prototype && id.prototype.id) || id.id || id);
const getCurrentPanel = toolbox => toolbox.getCurrentPanel();
exports.getCurrentPanel = getCurrentPanel;
const openToolbox = (id, tab) => {
id = getId(id);
return gDevTools.showToolbox(targetFor(tab), id);
};
exports.openToolbox = openToolbox;
const closeToolbox = tab => gDevTools.closeToolbox(targetFor(tab));
exports.closeToolbox = closeToolbox;
const getToolbox = tab => gDevTools.getToolbox(targetFor(tab));
exports.getToolbox = getToolbox;
const openToolboxPanel = (id, tab) => {
id = getId(id);
return gDevTools.showToolbox(targetFor(tab), id).then(getCurrentPanel);
};
exports.openToolboxPanel = openToolboxPanel;

File diff suppressed because one or more lines are too long