summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/fronts')
-rw-r--r--devtools/client/fronts/accessibility.js592
-rw-r--r--devtools/client/fronts/addon/addons.js23
-rw-r--r--devtools/client/fronts/addon/moz.build10
-rw-r--r--devtools/client/fronts/addon/webextension-inspected-window.js97
-rw-r--r--devtools/client/fronts/animation.js213
-rw-r--r--devtools/client/fronts/array-buffer.js24
-rw-r--r--devtools/client/fronts/breakpoint-list.js15
-rw-r--r--devtools/client/fronts/changes.js33
-rw-r--r--devtools/client/fronts/compatibility.js16
-rw-r--r--devtools/client/fronts/content-viewer.js26
-rw-r--r--devtools/client/fronts/css-properties.js317
-rw-r--r--devtools/client/fronts/descriptors/moz.build12
-rw-r--r--devtools/client/fronts/descriptors/process.js112
-rw-r--r--devtools/client/fronts/descriptors/tab.js263
-rw-r--r--devtools/client/fronts/descriptors/webextension.js139
-rw-r--r--devtools/client/fronts/descriptors/worker.js142
-rw-r--r--devtools/client/fronts/device.js23
-rw-r--r--devtools/client/fronts/eventsource.js73
-rw-r--r--devtools/client/fronts/frame.js31
-rw-r--r--devtools/client/fronts/framerate.js26
-rw-r--r--devtools/client/fronts/highlighters.js49
-rw-r--r--devtools/client/fronts/inspector.js215
-rw-r--r--devtools/client/fronts/inspector/moz.build9
-rw-r--r--devtools/client/fronts/inspector/rule-rewriter.js750
-rw-r--r--devtools/client/fronts/layout.js169
-rw-r--r--devtools/client/fronts/manifest.js23
-rw-r--r--devtools/client/fronts/media-rule.js54
-rw-r--r--devtools/client/fronts/memory.js117
-rw-r--r--devtools/client/fronts/moz.build60
-rw-r--r--devtools/client/fronts/network-content.js22
-rw-r--r--devtools/client/fronts/network-parent.js25
-rw-r--r--devtools/client/fronts/node.js545
-rw-r--r--devtools/client/fronts/object.js441
-rw-r--r--devtools/client/fronts/page-style.js121
-rw-r--r--devtools/client/fronts/perf.js22
-rw-r--r--devtools/client/fronts/performance-recording.js171
-rw-r--r--devtools/client/fronts/performance.js167
-rw-r--r--devtools/client/fronts/preference.js31
-rw-r--r--devtools/client/fronts/property-iterator.js67
-rw-r--r--devtools/client/fronts/reflow.js31
-rw-r--r--devtools/client/fronts/responsive.js26
-rw-r--r--devtools/client/fronts/root.js325
-rw-r--r--devtools/client/fronts/screenshot.js29
-rw-r--r--devtools/client/fronts/source.js100
-rw-r--r--devtools/client/fronts/storage.js35
-rw-r--r--devtools/client/fronts/string.js60
-rw-r--r--devtools/client/fronts/style-rule.js335
-rw-r--r--devtools/client/fronts/style-sheet.js96
-rw-r--r--devtools/client/fronts/style-sheets.js35
-rw-r--r--devtools/client/fronts/symbol-iterator.js54
-rw-r--r--devtools/client/fronts/targets/browsing-context.js141
-rw-r--r--devtools/client/fronts/targets/content-process.js54
-rw-r--r--devtools/client/fronts/targets/moz.build12
-rw-r--r--devtools/client/fronts/targets/target-mixin.js750
-rw-r--r--devtools/client/fronts/targets/worker.js29
-rw-r--r--devtools/client/fronts/thread.js307
-rw-r--r--devtools/client/fronts/walker.js551
-rw-r--r--devtools/client/fronts/watcher.js113
-rw-r--r--devtools/client/fronts/webconsole.js507
-rw-r--r--devtools/client/fronts/websocket.js115
-rw-r--r--devtools/client/fronts/worker/moz.build11
-rw-r--r--devtools/client/fronts/worker/push-subscription.js30
-rw-r--r--devtools/client/fronts/worker/service-worker-registration.js79
-rw-r--r--devtools/client/fronts/worker/service-worker.js63
64 files changed, 9133 insertions, 0 deletions
diff --git a/devtools/client/fronts/accessibility.js b/devtools/client/fronts/accessibility.js
new file mode 100644
index 0000000000..fd7025919c
--- /dev/null
+++ b/devtools/client/fronts/accessibility.js
@@ -0,0 +1,592 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol.js");
+const {
+ accessibleSpec,
+ accessibleWalkerSpec,
+ accessibilitySpec,
+ parentAccessibilitySpec,
+ simulatorSpec,
+} = require("devtools/shared/specs/accessibility");
+const events = require("devtools/shared/event-emitter");
+const Services = require("Services");
+const BROWSER_TOOLBOX_FISSION_ENABLED = Services.prefs.getBoolPref(
+ "devtools.browsertoolbox.fission",
+ false
+);
+
+class AccessibleFront extends FrontClassWithSpec(accessibleSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.before("audited", this.audited.bind(this));
+ this.before("name-change", this.nameChange.bind(this));
+ this.before("value-change", this.valueChange.bind(this));
+ this.before("description-change", this.descriptionChange.bind(this));
+ this.before("shortcut-change", this.shortcutChange.bind(this));
+ this.before("reorder", this.reorder.bind(this));
+ this.before("text-change", this.textChange.bind(this));
+ this.before("index-in-parent-change", this.indexInParentChange.bind(this));
+ this.before("states-change", this.statesChange.bind(this));
+ this.before("actions-change", this.actionsChange.bind(this));
+ this.before("attributes-change", this.attributesChange.bind(this));
+ }
+
+ marshallPool() {
+ return this.getParent();
+ }
+
+ get remoteFrame() {
+ return BROWSER_TOOLBOX_FISSION_ENABLED && this._form.remoteFrame;
+ }
+
+ get role() {
+ return this._form.role;
+ }
+
+ get name() {
+ return this._form.name;
+ }
+
+ get value() {
+ return this._form.value;
+ }
+
+ get description() {
+ return this._form.description;
+ }
+
+ get keyboardShortcut() {
+ return this._form.keyboardShortcut;
+ }
+
+ get childCount() {
+ return this._form.childCount;
+ }
+
+ get domNodeType() {
+ return this._form.domNodeType;
+ }
+
+ get indexInParent() {
+ return this._form.indexInParent;
+ }
+
+ get states() {
+ return this._form.states;
+ }
+
+ get actions() {
+ return this._form.actions;
+ }
+
+ get attributes() {
+ return this._form.attributes;
+ }
+
+ get checks() {
+ return this._form.checks;
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = this._form || {};
+ Object.assign(this._form, form);
+ }
+
+ nameChange(name, parent) {
+ this._form.name = name;
+ // Name change event affects the tree rendering, we fire this event on
+ // accessibility walker as the point of interaction for UI.
+ const accessibilityWalkerFront = this.getParent();
+ if (accessibilityWalkerFront) {
+ events.emit(accessibilityWalkerFront, "name-change", this, parent);
+ }
+ }
+
+ valueChange(value) {
+ this._form.value = value;
+ }
+
+ descriptionChange(description) {
+ this._form.description = description;
+ }
+
+ shortcutChange(keyboardShortcut) {
+ this._form.keyboardShortcut = keyboardShortcut;
+ }
+
+ reorder(childCount) {
+ this._form.childCount = childCount;
+ // Reorder event affects the tree rendering, we fire this event on
+ // accessibility walker as the point of interaction for UI.
+ const accessibilityWalkerFront = this.getParent();
+ if (accessibilityWalkerFront) {
+ events.emit(accessibilityWalkerFront, "reorder", this);
+ }
+ }
+
+ textChange() {
+ // Text event affects the tree rendering, we fire this event on
+ // accessibility walker as the point of interaction for UI.
+ const accessibilityWalkerFront = this.getParent();
+ if (accessibilityWalkerFront) {
+ events.emit(accessibilityWalkerFront, "text-change", this);
+ }
+ }
+
+ indexInParentChange(indexInParent) {
+ this._form.indexInParent = indexInParent;
+ }
+
+ statesChange(states) {
+ this._form.states = states;
+ }
+
+ actionsChange(actions) {
+ this._form.actions = actions;
+ }
+
+ attributesChange(attributes) {
+ this._form.attributes = attributes;
+ }
+
+ audited(checks) {
+ this._form.checks = this._form.checks || {};
+ Object.assign(this._form.checks, checks);
+ }
+
+ hydrate() {
+ return super.hydrate().then(properties => {
+ Object.assign(this._form, properties);
+ });
+ }
+
+ async children() {
+ if (!this.remoteFrame) {
+ return super.children();
+ }
+
+ const { walker: domWalkerFront } = await this.targetFront.getFront(
+ "inspector"
+ );
+ const node = await domWalkerFront.getNodeFromActor(this.actorID, [
+ "rawAccessible",
+ "DOMNode",
+ ]);
+ // We are using DOM inspector/walker API here because we want to keep both
+ // the accessiblity tree and the DOM tree in sync. This is necessary for
+ // several features that the accessibility panel provides such as inspecting
+ // a corresponding DOM node or any other functionality that requires DOM
+ // node ancestries to be resolved all the way up to the top level document.
+ const {
+ nodes: [documentNodeFront],
+ } = await domWalkerFront.children(node);
+ const accessibilityFront = await documentNodeFront.targetFront.getFront(
+ "accessibility"
+ );
+
+ return accessibilityFront.accessibleWalkerFront.children();
+ }
+
+ /**
+ * Helper function that helps with building a complete snapshot of
+ * accessibility tree starting at the level of current accessible front. It
+ * accumulates subtrees from possible out of process frames that are children
+ * of the current accessible front.
+ * @param {JSON} snapshot
+ * Snapshot of the current accessible front or one of its in process
+ * children when recursing.
+ *
+ * @return {JSON}
+ * Complete snapshot of current accessible front.
+ */
+ async _accumulateSnapshot(snapshot) {
+ const { childCount, remoteFrame } = snapshot;
+ // No children, we are done.
+ if (childCount === 0) {
+ return snapshot;
+ }
+
+ // If current accessible is not a remote frame, continue accumulating inside
+ // its children.
+ if (!remoteFrame) {
+ const childSnapshots = [];
+ for (const childSnapshot of snapshot.children) {
+ childSnapshots.push(this._accumulateSnapshot(childSnapshot));
+ }
+ await Promise.all(childSnapshots);
+ return snapshot;
+ }
+
+ // When we have a remote frame, we need to obtain an accessible front for a
+ // remote frame document and retrieve its snapshot.
+ const inspectorFront = await this.targetFront.getFront("inspector");
+ const frameNodeFront = await inspectorFront.getNodeActorFromContentDomReference(
+ snapshot.contentDOMReference
+ );
+ // Remove contentDOMReference and remoteFrame properties.
+ delete snapshot.contentDOMReference;
+ delete snapshot.remoteFrame;
+ if (!frameNodeFront) {
+ return snapshot;
+ }
+
+ // Remote frame lives in the same process as the current accessible
+ // front we can retrieve the accessible front directly.
+ const frameAccessibleFront = await this.parentFront.getAccessibleFor(
+ frameNodeFront
+ );
+ if (!frameAccessibleFront) {
+ return snapshot;
+ }
+
+ const [docAccessibleFront] = await frameAccessibleFront.children();
+ const childSnapshot = await docAccessibleFront.snapshot();
+ snapshot.children.push(childSnapshot);
+
+ return snapshot;
+ }
+
+ /**
+ * Retrieves a complete JSON snapshot for an accessible subtree of a given
+ * accessible front (inclduing OOP frames).
+ */
+ async snapshot() {
+ const snapshot = await super.snapshot();
+ await this._accumulateSnapshot(snapshot);
+ return snapshot;
+ }
+}
+
+class AccessibleWalkerFront extends FrontClassWithSpec(accessibleWalkerSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.documentReady = this.documentReady.bind(this);
+ this.on("document-ready", this.documentReady);
+ }
+
+ destroy() {
+ this.off("document-ready", this.documentReady);
+ super.destroy();
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ }
+
+ documentReady() {
+ if (this.targetFront.isTopLevel) {
+ this.emit("top-level-document-ready");
+ }
+ }
+
+ pick(doFocus) {
+ if (doFocus) {
+ return this.pickAndFocus();
+ }
+
+ return super.pick();
+ }
+
+ /**
+ * Get the accessible object ancestry starting from the given accessible to
+ * the top level document. BROWSER_TOOLBOX_FISSION_ENABLED is false, the top
+ * level document is bound by current target's document. Otherwise, the top
+ * level document is in the top level content process.
+ * @param {Object} accessible
+ * Accessible front to determine the ancestry for.
+ *
+ * @return {Array} ancestry
+ * List of ancestry objects which consist of an accessible with its
+ * children.
+ */
+ async getAncestry(accessible) {
+ const ancestry = await super.getAncestry(accessible);
+ if (!BROWSER_TOOLBOX_FISSION_ENABLED) {
+ // Do not try to get the ancestry across the remote frame hierarchy.
+ return ancestry;
+ }
+
+ const parentTarget = await this.targetFront.getParentTarget();
+ if (!parentTarget) {
+ return ancestry;
+ }
+
+ // Get an accessible front for the parent frame. We go through the
+ // inspector's walker to keep both inspector and accessibility trees in
+ // sync.
+ const { walker: domWalkerFront } = await this.targetFront.getFront(
+ "inspector"
+ );
+ const frameNodeFront = (await domWalkerFront.getRootNode()).parentNode();
+ const accessibilityFront = await parentTarget.getFront("accessibility");
+ const { accessibleWalkerFront } = accessibilityFront;
+ const frameAccessibleFront = await accessibleWalkerFront.getAccessibleFor(
+ frameNodeFront
+ );
+
+ if (!frameAccessibleFront) {
+ // Most likely we are inside a hidden frame.
+ return Promise.reject(
+ `Can't get the ancestry for an accessible front ${accessible.actorID}. It is in the detached tree.`
+ );
+ }
+
+ // Compose the final ancestry out of ancestry for the given accessible in
+ // the current process and recursively get the ancestry for the frame
+ // accessible.
+ ancestry.push(
+ {
+ accessible: frameAccessibleFront,
+ children: await frameAccessibleFront.children(),
+ },
+ ...(await accessibleWalkerFront.getAncestry(frameAccessibleFront))
+ );
+
+ return ancestry;
+ }
+
+ /**
+ * Run an accessibility audit for a document that accessibility walker is
+ * responsible for (in process). In addition to plainly running an audit (in
+ * cases when the document is in the OOP frame), this method also updates
+ * relative ancestries of audited accessible objects all the way up to the top
+ * level document for the toolbox.
+ * @param {Object} options
+ * - {Array} types
+ * types of the accessibility issues to audit for
+ * - {Function} onProgress
+ * callback function for a progress audit-event
+ */
+ async audit({ types, onProgress }) {
+ const onAudit = new Promise(resolve => {
+ const auditEventHandler = ({ type, ancestries, progress }) => {
+ switch (type) {
+ case "error":
+ this.off("audit-event", auditEventHandler);
+ resolve({ error: true });
+ break;
+ case "completed":
+ this.off("audit-event", auditEventHandler);
+ resolve({ ancestries });
+ break;
+ case "progress":
+ onProgress(progress);
+ break;
+ default:
+ break;
+ }
+ };
+
+ this.on("audit-event", auditEventHandler);
+ super.startAudit({ types });
+ });
+
+ const audit = await onAudit;
+ // If audit resulted in an error or there's nothing to report, we are done
+ // (no need to check for ancestry across the remote frame hierarchy). See
+ // also https://bugzilla.mozilla.org/show_bug.cgi?id=1641551 why the rest of
+ // the code path is only supported when content toolbox fission is enabled.
+ if (audit.error || audit.ancestries.length === 0) {
+ return audit;
+ }
+
+ const parentTarget = await this.targetFront.getParentTarget();
+ // If there is no parent target, we do not need to update ancestries as we
+ // are in the top level document.
+ if (!parentTarget) {
+ return audit;
+ }
+
+ // Retrieve an ancestry (cross process) for a current root document and make
+ // audit report ancestries relative to it.
+ const [docAccessibleFront] = await this.children();
+ let docAccessibleAncestry;
+ try {
+ docAccessibleAncestry = await this.getAncestry(docAccessibleFront);
+ } catch (e) {
+ // We are in a detached subtree. We do not consider this an error, instead
+ // we need to ignore the audit for this frame and return an empty report.
+ return { ancestries: [] };
+ }
+ for (const ancestry of audit.ancestries) {
+ // Compose the final ancestries out of the ones in the audit report
+ // relative to this document and the ancestry of the document itself
+ // (cross process).
+ ancestry.push(...docAccessibleAncestry);
+ }
+
+ return audit;
+ }
+
+ /**
+ * A helper wrapper function to show tabbing order overlay for a given target.
+ * The only additional work done is resolving domnode front from a
+ * ContentDOMReference received from a remote target.
+ *
+ * @param {Object} startElm
+ * domnode front to be used as the starting point for generating the
+ * tabbing order.
+ * @param {Number} startIndex
+ * Starting index for the tabbing order.
+ */
+ async _showTabbingOrder(startElm, startIndex) {
+ const { contentDOMReference, index } = await super.showTabbingOrder(
+ startElm,
+ startIndex
+ );
+ let elm;
+ if (contentDOMReference) {
+ const inspectorFront = await this.targetFront.getFront("inspector");
+ elm = await inspectorFront.getNodeActorFromContentDomReference(
+ contentDOMReference
+ );
+ }
+
+ return { elm, index };
+ }
+
+ /**
+ * Show tabbing order overlay for a given target.
+ *
+ * @param {Object} startElm
+ * domnode front to be used as the starting point for generating the
+ * tabbing order.
+ * @param {Number} startIndex
+ * Starting index for the tabbing order.
+ *
+ * @return {JSON}
+ * Tabbing order information for the last element in the tabbing
+ * order. It includes a domnode front and a tabbing index. If we are
+ * at the end of the tabbing order for the top level content document,
+ * the domnode front will be null. If focus manager discovered a
+ * remote IFRAME, then the domnode front is for the IFRAME itself.
+ */
+ async showTabbingOrder(startElm, startIndex) {
+ let { elm: currentElm, index: currentIndex } = await this._showTabbingOrder(
+ startElm,
+ startIndex
+ );
+
+ // If no remote frames were found, currentElm will be null.
+ while (currentElm) {
+ // Safety check to ensure that the currentElm is a remote frame.
+ if (currentElm.remoteFrame) {
+ const {
+ walker: domWalkerFront,
+ } = await currentElm.targetFront.getFront("inspector");
+ const {
+ nodes: [childDocumentNodeFront],
+ } = await domWalkerFront.children(currentElm);
+ const {
+ accessibleWalkerFront,
+ } = await childDocumentNodeFront.targetFront.getFront("accessibility");
+ // Show tabbing order in the remote target, while updating the tabbing
+ // index.
+ ({ index: currentIndex } = await accessibleWalkerFront.showTabbingOrder(
+ childDocumentNodeFront,
+ currentIndex
+ ));
+ }
+
+ // Finished with the remote frame, continue in tabbing order, from the
+ // remote frame.
+ ({ elm: currentElm, index: currentIndex } = await this._showTabbingOrder(
+ currentElm,
+ currentIndex
+ ));
+ }
+
+ return { elm: currentElm, index: currentIndex };
+ }
+}
+
+class AccessibilityFront extends FrontClassWithSpec(accessibilitySpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.before("init", this.init.bind(this));
+ this.before("shutdown", this.shutdown.bind(this));
+
+ // Attribute name from which to retrieve the actorID out of the target
+ // actor's form
+ this.formAttributeName = "accessibilityActor";
+ }
+
+ async initialize() {
+ this.accessibleWalkerFront = await super.getWalker();
+ this.simulatorFront = await super.getSimulator();
+ const { enabled } = await super.bootstrap();
+ this.enabled = enabled;
+
+ try {
+ this._traits = await this.getTraits();
+ } catch (e) {
+ // @backward-compat { version 84 } getTraits isn't available on older server.
+ this._traits = {};
+ }
+ }
+
+ get traits() {
+ return this._traits;
+ }
+
+ init() {
+ this.enabled = true;
+ }
+
+ shutdown() {
+ this.enabled = false;
+ }
+}
+
+class ParentAccessibilityFront extends FrontClassWithSpec(
+ parentAccessibilitySpec
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.before("can-be-enabled-change", this.canBeEnabled.bind(this));
+ this.before("can-be-disabled-change", this.canBeDisabled.bind(this));
+
+ // Attribute name from which to retrieve the actorID out of the target
+ // actor's form
+ this.formAttributeName = "parentAccessibilityActor";
+ }
+
+ async initialize() {
+ ({
+ canBeEnabled: this.canBeEnabled,
+ canBeDisabled: this.canBeDisabled,
+ } = await super.bootstrap());
+ }
+
+ canBeEnabled(canBeEnabled) {
+ this.canBeEnabled = canBeEnabled;
+ }
+
+ canBeDisabled(canBeDisabled) {
+ this.canBeDisabled = canBeDisabled;
+ }
+}
+
+const SimulatorFront = FrontClassWithSpec(simulatorSpec);
+
+exports.AccessibleFront = AccessibleFront;
+registerFront(AccessibleFront);
+exports.AccessibleWalkerFront = AccessibleWalkerFront;
+registerFront(AccessibleWalkerFront);
+exports.AccessibilityFront = AccessibilityFront;
+registerFront(AccessibilityFront);
+exports.ParentAccessibilityFront = ParentAccessibilityFront;
+registerFront(ParentAccessibilityFront);
+exports.SimulatorFront = SimulatorFront;
+registerFront(SimulatorFront);
diff --git a/devtools/client/fronts/addon/addons.js b/devtools/client/fronts/addon/addons.js
new file mode 100644
index 0000000000..8722164646
--- /dev/null
+++ b/devtools/client/fronts/addon/addons.js
@@ -0,0 +1,23 @@
+/* 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 { addonsSpec } = require("devtools/shared/specs/addon/addons");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+class AddonsFront extends FrontClassWithSpec(addonsSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "addonsActor";
+ }
+}
+
+exports.AddonsFront = AddonsFront;
+registerFront(AddonsFront);
diff --git a/devtools/client/fronts/addon/moz.build b/devtools/client/fronts/addon/moz.build
new file mode 100644
index 0000000000..e382173641
--- /dev/null
+++ b/devtools/client/fronts/addon/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "addons.js",
+ "webextension-inspected-window.js",
+)
diff --git a/devtools/client/fronts/addon/webextension-inspected-window.js b/devtools/client/fronts/addon/webextension-inspected-window.js
new file mode 100644
index 0000000000..e8064d3691
--- /dev/null
+++ b/devtools/client/fronts/addon/webextension-inspected-window.js
@@ -0,0 +1,97 @@
+/* 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 {
+ webExtensionInspectedWindowSpec,
+} = require("devtools/shared/specs/addon/webextension-inspected-window");
+
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+const {
+ getAdHocFrontOrPrimitiveGrip,
+} = require("devtools/client/fronts/object");
+
+/**
+ * The corresponding Front object for the WebExtensionInspectedWindowActor.
+ */
+class WebExtensionInspectedWindowFront extends FrontClassWithSpec(
+ webExtensionInspectedWindowSpec
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "webExtensionInspectedWindowActor";
+ }
+
+ /**
+ * Evaluate the provided javascript code in a target window.
+ *
+ * @param {Object} webExtensionCallerInfo - The addonId and the url (the addon base url
+ * or the url of the actual caller filename and lineNumber) used to log useful
+ * debugging information in the produced error logs and eval stack trace.
+ * @param {String} expression - The expression to evaluate.
+ * @param {Object} options - An option object. Check the actor method definition to see
+ * what properties it can hold (minus the `consoleFront` property which is defined
+ * below).
+ * @param {WebConsoleFront} options.consoleFront - An optional webconsole front. When
+ * set, the result will be either a primitive, a LongStringFront or an
+ * ObjectFront, and the WebConsoleActor corresponding to the console front will
+ * be used to generate those, which is needed if we want to handle ObjectFronts
+ * on the client.
+ */
+ async eval(webExtensionCallerInfo, expression, options = {}) {
+ const { consoleFront } = options;
+
+ if (consoleFront) {
+ options.evalResultAsGrip = true;
+ options.toolboxConsoleActorID = consoleFront.actor;
+ delete options.consoleFront;
+ }
+
+ const response = await super.eval(
+ webExtensionCallerInfo,
+ expression,
+ options
+ );
+
+ // If no consoleFront was provided, we can directly return the response.
+ if (!consoleFront) {
+ return response;
+ }
+
+ if (
+ !response.hasOwnProperty("exceptionInfo") &&
+ !response.hasOwnProperty("valueGrip")
+ ) {
+ throw new Error(
+ "Response does not have `exceptionInfo` or `valueGrip` property"
+ );
+ }
+
+ if (response.exceptionInfo) {
+ console.error(
+ response.exceptionInfo.description,
+ ...(response.exceptionInfo.details || [])
+ );
+ return response;
+ }
+
+ // On the server, the valueGrip is created from the toolbox webconsole actor.
+ // If we want since the ObjectFront connection is inherited from the parent front, we
+ // need to set the console front as the parent front.
+ return getAdHocFrontOrPrimitiveGrip(
+ response.valueGrip,
+ consoleFront || this
+ );
+ }
+}
+
+exports.WebExtensionInspectedWindowFront = WebExtensionInspectedWindowFront;
+registerFront(WebExtensionInspectedWindowFront);
diff --git a/devtools/client/fronts/animation.js b/devtools/client/fronts/animation.js
new file mode 100644
index 0000000000..4bd452a1cb
--- /dev/null
+++ b/devtools/client/fronts/animation.js
@@ -0,0 +1,213 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const {
+ animationPlayerSpec,
+ animationsSpec,
+} = require("devtools/shared/specs/animation");
+
+class AnimationPlayerFront extends FrontClassWithSpec(animationPlayerSpec) {
+ constructor(conn, targetFront, parentFront) {
+ super(conn, targetFront, parentFront);
+
+ this.state = {};
+ this.before("changed", this.onChanged.bind(this));
+ }
+
+ form(form) {
+ this._form = form;
+ this.state = this.initialState;
+ }
+
+ /**
+ * If the AnimationsActor was given a reference to the WalkerActor previously
+ * then calling this getter will return the animation target NodeFront.
+ */
+ get animationTargetNodeFront() {
+ if (!this._form.animationTargetNodeActorID) {
+ return null;
+ }
+
+ return this.conn.getFrontByID(this._form.animationTargetNodeActorID);
+ }
+
+ /**
+ * Getter for the initial state of the player. Up to date states can be
+ * retrieved by calling the getCurrentState method.
+ */
+ get initialState() {
+ return {
+ type: this._form.type,
+ startTime: this._form.startTime,
+ currentTime: this._form.currentTime,
+ playState: this._form.playState,
+ playbackRate: this._form.playbackRate,
+ name: this._form.name,
+ duration: this._form.duration,
+ delay: this._form.delay,
+ endDelay: this._form.endDelay,
+ iterationCount: this._form.iterationCount,
+ iterationStart: this._form.iterationStart,
+ easing: this._form.easing,
+ fill: this._form.fill,
+ direction: this._form.direction,
+ animationTimingFunction: this._form.animationTimingFunction,
+ isRunningOnCompositor: this._form.isRunningOnCompositor,
+ propertyState: this._form.propertyState,
+ documentCurrentTime: this._form.documentCurrentTime,
+ createdTime: this._form.createdTime,
+ currentTimeAtCreated: this._form.currentTimeAtCreated,
+ absoluteValues: this.calculateAbsoluteValues(this._form),
+ };
+ }
+
+ /**
+ * Executed when the AnimationPlayerActor emits a "changed" event. Used to
+ * update the local knowledge of the state.
+ */
+ onChanged(partialState) {
+ const { state } = this.reconstructState(partialState);
+ this.state = state;
+ }
+
+ /**
+ * Refresh the current state of this animation on the client from information
+ * found on the server. Doesn't return anything, just stores the new state.
+ */
+ async refreshState() {
+ const data = await this.getCurrentState();
+ if (this.currentStateHasChanged) {
+ this.state = data;
+ }
+ }
+
+ /**
+ * getCurrentState interceptor re-constructs incomplete states since the actor
+ * only sends the values that have changed.
+ */
+ getCurrentState() {
+ this.currentStateHasChanged = false;
+ return super.getCurrentState().then(partialData => {
+ const { state, hasChanged } = this.reconstructState(partialData);
+ this.currentStateHasChanged = hasChanged;
+ return state;
+ });
+ }
+
+ reconstructState(data) {
+ let hasChanged = false;
+
+ for (const key in this.state) {
+ if (typeof data[key] === "undefined") {
+ data[key] = this.state[key];
+ } else if (data[key] !== this.state[key]) {
+ hasChanged = true;
+ }
+ }
+
+ data.absoluteValues = this.calculateAbsoluteValues(data);
+ return { state: data, hasChanged };
+ }
+
+ calculateAbsoluteValues(data) {
+ const {
+ createdTime,
+ currentTime,
+ currentTimeAtCreated,
+ delay,
+ duration,
+ endDelay = 0,
+ fill,
+ iterationCount,
+ playbackRate,
+ } = data;
+
+ const toRate = v => v / Math.abs(playbackRate);
+ const isPositivePlaybackRate = playbackRate > 0;
+ let absoluteDelay = 0;
+ let absoluteEndDelay = 0;
+ let isDelayFilled = false;
+ let isEndDelayFilled = false;
+
+ if (isPositivePlaybackRate) {
+ absoluteDelay = toRate(delay);
+ absoluteEndDelay = toRate(endDelay);
+ isDelayFilled = fill === "both" || fill === "backwards";
+ isEndDelayFilled = fill === "both" || fill === "forwards";
+ } else {
+ absoluteDelay = toRate(endDelay);
+ absoluteEndDelay = toRate(delay);
+ isDelayFilled = fill === "both" || fill === "forwards";
+ isEndDelayFilled = fill === "both" || fill === "backwards";
+ }
+
+ let endTime = 0;
+
+ if (duration === Infinity) {
+ // Set endTime so as to enable the scrubber with keeping the consinstency of UI
+ // even the duration was Infinity. In case of delay is longer than zero, handle
+ // the graph duration as double of the delay amount. In case of no delay, handle
+ // the duration as 1ms which is short enough so as to make the scrubber movable
+ // and the limited duration is prioritized.
+ endTime = absoluteDelay > 0 ? absoluteDelay * 2 : 1;
+ } else {
+ endTime =
+ absoluteDelay +
+ toRate(duration * (iterationCount || 1)) +
+ absoluteEndDelay;
+ }
+
+ const absoluteCreatedTime = isPositivePlaybackRate
+ ? createdTime
+ : createdTime - endTime;
+ const absoluteCurrentTimeAtCreated = isPositivePlaybackRate
+ ? currentTimeAtCreated
+ : endTime - currentTimeAtCreated;
+ const animationCurrentTime = isPositivePlaybackRate
+ ? currentTime
+ : endTime - currentTime;
+ const absoluteCurrentTime =
+ absoluteCreatedTime + toRate(animationCurrentTime);
+ const absoluteStartTime = absoluteCreatedTime + Math.min(absoluteDelay, 0);
+ const absoluteStartTimeAtCreated =
+ absoluteCreatedTime + absoluteCurrentTimeAtCreated;
+ // To show whole graph with endDelay, we add negative endDelay amount to endTime.
+ const endTimeWithNegativeEndDelay = endTime - Math.min(absoluteEndDelay, 0);
+ const absoluteEndTime = absoluteCreatedTime + endTimeWithNegativeEndDelay;
+
+ return {
+ createdTime: absoluteCreatedTime,
+ currentTime: absoluteCurrentTime,
+ currentTimeAtCreated: absoluteCurrentTimeAtCreated,
+ delay: absoluteDelay,
+ endDelay: absoluteEndDelay,
+ endTime: absoluteEndTime,
+ isDelayFilled,
+ isEndDelayFilled,
+ startTime: absoluteStartTime,
+ startTimeAtCreated: absoluteStartTimeAtCreated,
+ };
+ }
+}
+
+exports.AnimationPlayerFront = AnimationPlayerFront;
+registerFront(AnimationPlayerFront);
+
+class AnimationsFront extends FrontClassWithSpec(animationsSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "animationsActor";
+ }
+}
+
+exports.AnimationsFront = AnimationsFront;
+registerFront(AnimationsFront);
diff --git a/devtools/client/fronts/array-buffer.js b/devtools/client/fronts/array-buffer.js
new file mode 100644
index 0000000000..b3ef8b4195
--- /dev/null
+++ b/devtools/client/fronts/array-buffer.js
@@ -0,0 +1,24 @@
+/* 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 { arrayBufferSpec } = require("devtools/shared/specs/array-buffer");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+/**
+ * A ArrayBufferClient provides a way to access ArrayBuffer from the
+ * devtools server.
+ */
+class ArrayBufferFront extends FrontClassWithSpec(arrayBufferSpec) {
+ form(json) {
+ this.length = json.length;
+ }
+}
+
+exports.ArrayBufferFront = ArrayBufferFront;
+registerFront(ArrayBufferFront);
diff --git a/devtools/client/fronts/breakpoint-list.js b/devtools/client/fronts/breakpoint-list.js
new file mode 100644
index 0000000000..8832a401b7
--- /dev/null
+++ b/devtools/client/fronts/breakpoint-list.js
@@ -0,0 +1,15 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { breakpointListSpec } = require("devtools/shared/specs/breakpoint-list");
+
+class BreakpointListFront extends FrontClassWithSpec(breakpointListSpec) {}
+
+registerFront(BreakpointListFront);
diff --git a/devtools/client/fronts/changes.js b/devtools/client/fronts/changes.js
new file mode 100644
index 0000000000..272bd802cf
--- /dev/null
+++ b/devtools/client/fronts/changes.js
@@ -0,0 +1,33 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { changesSpec } = require("devtools/shared/specs/changes");
+
+/**
+ * ChangesFront, the front object for the ChangesActor
+ */
+class ChangesFront extends FrontClassWithSpec(changesSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "changesActor";
+ }
+
+ async initialize() {
+ // Ensure the corresponding ChangesActor is immediately available and ready to track
+ // changes by calling a method on it. Actors are lazy and won't be created until
+ // actually used.
+ await super.start();
+ }
+}
+
+exports.ChangesFront = ChangesFront;
+registerFront(ChangesFront);
diff --git a/devtools/client/fronts/compatibility.js b/devtools/client/fronts/compatibility.js
new file mode 100644
index 0000000000..84f7511317
--- /dev/null
+++ b/devtools/client/fronts/compatibility.js
@@ -0,0 +1,16 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { compatibilitySpec } = require("devtools/shared/specs/compatibility");
+
+class CompatibilityFront extends FrontClassWithSpec(compatibilitySpec) {}
+
+exports.CompatibilityFront = CompatibilityFront;
+registerFront(CompatibilityFront);
diff --git a/devtools/client/fronts/content-viewer.js b/devtools/client/fronts/content-viewer.js
new file mode 100644
index 0000000000..a92c3f27c0
--- /dev/null
+++ b/devtools/client/fronts/content-viewer.js
@@ -0,0 +1,26 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { contentViewerSpec } = require("devtools/shared/specs/content-viewer");
+
+/**
+ * The corresponding Front object for the ContentViewer actor.
+ */
+class ContentViewerFront extends FrontClassWithSpec(contentViewerSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "contentViewerActor";
+ }
+}
+
+exports.ContentViewerFront = ContentViewerFront;
+registerFront(ContentViewerFront);
diff --git a/devtools/client/fronts/css-properties.js b/devtools/client/fronts/css-properties.js
new file mode 100644
index 0000000000..3c229770c3
--- /dev/null
+++ b/devtools/client/fronts/css-properties.js
@@ -0,0 +1,317 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { cssPropertiesSpec } = require("devtools/shared/specs/css-properties");
+
+loader.lazyRequireGetter(
+ this,
+ "cssColors",
+ "devtools/shared/css/color-db",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "CSS_PROPERTIES_DB",
+ "devtools/shared/css/properties-db",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "CSS_TYPES",
+ "devtools/shared/css/constants",
+ true
+);
+
+/**
+ * Build up a regular expression that matches a CSS variable token. This is an
+ * ident token that starts with two dashes "--".
+ *
+ * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
+ */
+var NON_ASCII = "[^\\x00-\\x7F]";
+var ESCAPE = "\\\\[^\n\r]";
+var FIRST_CHAR = ["[_a-z]", NON_ASCII, ESCAPE].join("|");
+var TRAILING_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|");
+var IS_VARIABLE_TOKEN = new RegExp(
+ `^--(${FIRST_CHAR})(${TRAILING_CHAR})*$`,
+ "i"
+);
+
+/**
+ * The CssProperties front provides a mechanism to have a one-time asynchronous
+ * load of a CSS properties database. This is then fed into the CssProperties
+ * interface that provides synchronous methods for finding out what CSS
+ * properties the current server supports.
+ */
+class CssPropertiesFront extends FrontClassWithSpec(cssPropertiesSpec) {
+ constructor(client, targetFront) {
+ super(client, targetFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "cssPropertiesActor";
+ }
+
+ async initialize() {
+ const db = await super.getCSSDatabase();
+ this.cssProperties = new CssProperties(normalizeCssData(db));
+ }
+
+ destroy() {
+ this.cssProperties = null;
+ super.destroy();
+ }
+}
+
+/**
+ * Ask questions to a CSS database. This class does not care how the database
+ * gets loaded in, only the questions that you can ask to it.
+ * Prototype functions are bound to 'this' so they can be passed around as helper
+ * functions.
+ *
+ * @param {Object} db
+ * A database of CSS properties
+ * @param {Object} inheritedList
+ * The key is the property name, the value is whether or not
+ * that property is inherited.
+ */
+function CssProperties(db) {
+ this.properties = db.properties;
+ this.pseudoElements = db.pseudoElements;
+
+ // supported feature
+ this.cssColor4ColorFunction = hasFeature(
+ db.supportedFeature,
+ "css-color-4-color-function"
+ );
+
+ this.isKnown = this.isKnown.bind(this);
+ this.isInherited = this.isInherited.bind(this);
+ this.supportsType = this.supportsType.bind(this);
+ this.supportsCssColor4ColorFunction = this.supportsCssColor4ColorFunction.bind(
+ this
+ );
+}
+
+CssProperties.prototype = {
+ /**
+ * Checks to see if the property is known by the browser. This function has
+ * `this` already bound so that it can be passed around by reference.
+ *
+ * @param {String} property The property name to be checked.
+ * @return {Boolean}
+ */
+ isKnown(property) {
+ // Custom Property Names (aka CSS Variables) are case-sensitive; do not lowercase.
+ property = property.startsWith("--") ? property : property.toLowerCase();
+ return !!this.properties[property] || isCssVariable(property);
+ },
+
+ /**
+ * Checks to see if the property is an inherited one.
+ *
+ * @param {String} property The property name to be checked.
+ * @return {Boolean}
+ */
+ isInherited(property) {
+ return (
+ (this.properties[property] && this.properties[property].isInherited) ||
+ isCssVariable(property)
+ );
+ },
+
+ /**
+ * Checks if the property supports the given CSS type.
+ *
+ * @param {String} property The property to be checked.
+ * @param {String} type One of the values from InspectorPropertyType.
+ * @return {Boolean}
+ */
+ supportsType(property, type) {
+ const id = CSS_TYPES[type];
+ return (
+ this.properties[property] &&
+ (this.properties[property].supports.includes(type) ||
+ this.properties[property].supports.includes(id))
+ );
+ },
+
+ /**
+ * Gets the CSS values for a given property name.
+ *
+ * @param {String} property The property to use.
+ * @return {Array} An array of strings.
+ */
+ getValues(property) {
+ return this.properties[property] ? this.properties[property].values : [];
+ },
+
+ /**
+ * Gets the CSS property names.
+ *
+ * @return {Array} An array of strings.
+ */
+ getNames(property) {
+ return Object.keys(this.properties);
+ },
+
+ /**
+ * Return a list of subproperties for the given property. If |name|
+ * does not name a valid property, an empty array is returned. If
+ * the property is not a shorthand property, then array containing
+ * just the property itself is returned.
+ *
+ * @param {String} name The property to query
+ * @return {Array} An array of subproperty names.
+ */
+ getSubproperties(name) {
+ // Custom Property Names (aka CSS Variables) are case-sensitive; do not lowercase.
+ name = name.startsWith("--") ? name : name.toLowerCase();
+ if (this.isKnown(name)) {
+ if (this.properties[name] && this.properties[name].subproperties) {
+ return this.properties[name].subproperties;
+ }
+ return [name];
+ }
+ return [];
+ },
+
+ /**
+ * Checking for the css-color-4 color function support.
+ *
+ * @return {Boolean} Return true if the server supports css-color-4 color function.
+ */
+ supportsCssColor4ColorFunction() {
+ return this.cssColor4ColorFunction;
+ },
+};
+
+/**
+ * Check that this is a CSS variable.
+ *
+ * @param {String} input
+ * @return {Boolean}
+ */
+function isCssVariable(input) {
+ return !!input.match(IS_VARIABLE_TOKEN);
+}
+
+/**
+ * Query the feature supporting status in the featureSet.
+ *
+ * @param {Hashmap} featureSet the feature set hashmap
+ * @param {String} feature the feature name string
+ * @return {Boolean} has the feature or not
+ */
+function hasFeature(featureSet, feature) {
+ if (feature in featureSet) {
+ return featureSet[feature];
+ }
+ return false;
+}
+
+/**
+ * Get a client-side CssProperties. This is useful for dependencies in tests, or parts
+ * of the codebase that don't particularly need to match every known CSS property on
+ * the target.
+ * @return {CssProperties}
+ */
+function getClientCssProperties() {
+ return new CssProperties(normalizeCssData(CSS_PROPERTIES_DB));
+}
+
+/**
+ * Even if the target has the cssProperties actor, the returned data may not be in the
+ * same shape or have all of the data we need. This normalizes the data and fills in
+ * any missing information like color values.
+ *
+ * @return {Object} The normalized CSS database.
+ */
+function normalizeCssData(db) {
+ // If there is a `from` attributes, it means that it comes from RDP
+ // and it is not the client CSS_PROPERTIES_DB object.
+ // (prevent comparing to CSS_PROPERTIES_DB to avoid loading client database)
+ if (typeof db.from == "string") {
+ const missingSupports = !db.properties.color.supports;
+ const missingValues = !db.properties.color.values;
+ const missingSubproperties = !db.properties.background.subproperties;
+ const missingIsInherited = !db.properties.font.isInherited;
+
+ const missingSomething =
+ missingSupports ||
+ missingValues ||
+ missingSubproperties ||
+ missingIsInherited;
+
+ if (missingSomething) {
+ for (const name in db.properties) {
+ // Skip the current property if we can't find it in CSS_PROPERTIES_DB.
+ if (typeof CSS_PROPERTIES_DB.properties[name] !== "object") {
+ continue;
+ }
+
+ // Add "supports" information to the css properties if it's missing.
+ if (missingSupports) {
+ db.properties[name].supports =
+ CSS_PROPERTIES_DB.properties[name].supports;
+ }
+ // Add "values" information to the css properties if it's missing.
+ if (missingValues) {
+ db.properties[name].values =
+ CSS_PROPERTIES_DB.properties[name].values;
+ }
+ // Add "subproperties" information to the css properties if it's missing.
+ if (missingSubproperties) {
+ db.properties[name].subproperties =
+ CSS_PROPERTIES_DB.properties[name].subproperties;
+ }
+ // Add "isInherited" information to the css properties if it's missing.
+ if (missingIsInherited) {
+ db.properties[name].isInherited =
+ CSS_PROPERTIES_DB.properties[name].isInherited;
+ }
+ }
+ }
+ }
+
+ reattachCssColorValues(db);
+
+ // If there is no supportedFeature in db, create an empty one.
+ if (!db.supportedFeature) {
+ db.supportedFeature = {};
+ }
+
+ return db;
+}
+
+/**
+ * Color values are omitted to save on space. Add them back here.
+ * @param {Object} The CSS database.
+ */
+function reattachCssColorValues(db) {
+ if (db.properties.color.values[0] === "COLOR") {
+ const colors = Object.keys(cssColors);
+
+ for (const name in db.properties) {
+ const property = db.properties[name];
+ // "values" can be undefined if {name} was not found in CSS_PROPERTIES_DB.
+ if (property.values && property.values[0] === "COLOR") {
+ property.values.shift();
+ property.values = property.values.concat(colors).sort();
+ }
+ }
+ }
+}
+
+module.exports = {
+ CssPropertiesFront,
+ getClientCssProperties,
+ isCssVariable,
+};
+registerFront(CssPropertiesFront);
diff --git a/devtools/client/fronts/descriptors/moz.build b/devtools/client/fronts/descriptors/moz.build
new file mode 100644
index 0000000000..bf297b3dcb
--- /dev/null
+++ b/devtools/client/fronts/descriptors/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "process.js",
+ "tab.js",
+ "webextension.js",
+ "worker.js",
+)
diff --git a/devtools/client/fronts/descriptors/process.js b/devtools/client/fronts/descriptors/process.js
new file mode 100644
index 0000000000..0d0602920b
--- /dev/null
+++ b/devtools/client/fronts/descriptors/process.js
@@ -0,0 +1,112 @@
+/* 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 {
+ processDescriptorSpec,
+} = require("devtools/shared/specs/descriptors/process");
+const {
+ BrowsingContextTargetFront,
+} = require("devtools/client/fronts/targets/browsing-context");
+const {
+ ContentProcessTargetFront,
+} = require("devtools/client/fronts/targets/content-process");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+class ProcessDescriptorFront extends FrontClassWithSpec(processDescriptorSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.isParent = false;
+ this._processTargetFront = null;
+ this._targetFrontPromise = null;
+ this._client = client;
+ }
+
+ form(json) {
+ this.id = json.id;
+ this.isParent = json.isParent;
+ this.traits = json.traits || {};
+ }
+
+ async _createProcessTargetFront(form) {
+ let front = null;
+ // the request to getTarget may return a ContentProcessTargetActor or a
+ // ParentProcessTargetActor. In most cases getProcess(0) will return the
+ // main process target actor, which is a ParentProcessTargetActor, but
+ // not in xpcshell, which uses a ContentProcessTargetActor. So select
+ // the right front based on the actor ID.
+ if (form.actor.includes("parentProcessTarget")) {
+ // ParentProcessTargetActor doesn't have a specific front, instead it uses
+ // BrowsingContextTargetFront on the client side.
+ front = new BrowsingContextTargetFront(this._client, null, this);
+ } else {
+ front = new ContentProcessTargetFront(this._client, null, this);
+ }
+ // As these fronts aren't instantiated by protocol.js, we have to set their actor ID
+ // manually like that:
+ front.actorID = form.actor;
+ front.form(form);
+
+ // @backward-compat { version 84 } Older server don't send the processID in the form
+ if (!front.processID) {
+ front.processID = this.id;
+ }
+
+ this.manage(front);
+ return front;
+ }
+
+ getCachedTarget() {
+ return this._processTargetFront;
+ }
+
+ async getTarget() {
+ // Only return the cached Target if it is still alive.
+ if (this._processTargetFront && !this._processTargetFront.isDestroyed()) {
+ return this._processTargetFront;
+ }
+ // Otherwise, ensure that we don't try to spawn more than one Target by
+ // returning the pending promise
+ if (this._targetFrontPromise) {
+ return this._targetFrontPromise;
+ }
+ this._targetFrontPromise = (async () => {
+ let targetFront = null;
+ try {
+ const targetForm = await super.getTarget();
+ targetFront = await this._createProcessTargetFront(targetForm);
+ await targetFront.attach();
+ } catch (e) {
+ // This is likely to happen if we get a lot of events which drop previous
+ // processes.
+ console.log(
+ `Request to connect to ProcessDescriptor "${this.id}" failed: ${e}`
+ );
+ }
+ // Save the reference to the target only after the call to attach
+ // so that getTarget always returns the attached target in case of concurrent calls
+ this._processTargetFront = targetFront;
+ // clear the promise if we are finished so that we can re-connect if
+ // necessary
+ this._targetFrontPromise = null;
+ return targetFront;
+ })();
+ return this._targetFrontPromise;
+ }
+
+ destroy() {
+ if (this._processTargetFront) {
+ this._processTargetFront.destroy();
+ this._processTargetFront = null;
+ }
+ this._targetFrontPromise = null;
+ super.destroy();
+ }
+}
+
+exports.ProcessDescriptorFront = ProcessDescriptorFront;
+registerFront(ProcessDescriptorFront);
diff --git a/devtools/client/fronts/descriptors/tab.js b/devtools/client/fronts/descriptors/tab.js
new file mode 100644
index 0000000000..a3e649bfd2
--- /dev/null
+++ b/devtools/client/fronts/descriptors/tab.js
@@ -0,0 +1,263 @@
+/* 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 Services = require("Services");
+const { tabDescriptorSpec } = require("devtools/shared/specs/descriptors/tab");
+
+loader.lazyRequireGetter(
+ this,
+ "gDevTools",
+ "devtools/client/framework/devtools",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "BrowsingContextTargetFront",
+ "devtools/client/fronts/targets/browsing-context",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "TargetFactory",
+ "devtools/client/framework/target",
+ true
+);
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+/**
+ * DescriptorFront for tab targets.
+ *
+ * @fires remoteness-change
+ * Fired only for target switching, when the debugged tab is a local tab.
+ * TODO: This event could move to the server in order to support
+ * remoteness change for remote debugging.
+ */
+class TabDescriptorFront extends FrontClassWithSpec(tabDescriptorSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._client = client;
+
+ // The tab descriptor can be configured to create either local tab targets
+ // (eg, regular tab toolbox) or browsing context targets (eg tab remote
+ // debugging).
+ this._localTab = null;
+
+ // Flipped when creating dedicated tab targets for DevTools WebExtensions.
+ // See toolkit/components/extensions/ExtensionParent.jsm .
+ this.isDevToolsExtensionContext = false;
+
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+ this._handleTabEvent = this._handleTabEvent.bind(this);
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ this._form = json;
+ this.traits = json.traits || {};
+ }
+
+ destroy() {
+ if (this.isLocalTab) {
+ this._teardownLocalTabListeners();
+ }
+ super.destroy();
+ }
+
+ setLocalTab(localTab) {
+ this._localTab = localTab;
+ this._setupLocalTabListeners();
+ }
+
+ get isLocalTab() {
+ return !!this._localTab;
+ }
+
+ get localTab() {
+ return this._localTab;
+ }
+
+ _setupLocalTabListeners() {
+ this.localTab.addEventListener("TabClose", this._handleTabEvent);
+ this.localTab.addEventListener("TabRemotenessChange", this._handleTabEvent);
+ }
+
+ _teardownLocalTabListeners() {
+ this.localTab.removeEventListener("TabClose", this._handleTabEvent);
+ this.localTab.removeEventListener(
+ "TabRemotenessChange",
+ this._handleTabEvent
+ );
+ }
+
+ get isZombieTab() {
+ return this._form.isZombieTab;
+ }
+
+ get outerWindowID() {
+ return this._form.outerWindowID;
+ }
+
+ get selected() {
+ return this._form.selected;
+ }
+
+ get title() {
+ return this._form.title;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get favicon() {
+ // Note: the favicon is not part of the default form() payload, it will be
+ // added in `retrieveFavicon`.
+ return this._form.favicon;
+ }
+
+ _createTabTarget(form) {
+ const front = new BrowsingContextTargetFront(this._client, null, this);
+
+ if (this.isLocalTab) {
+ front.shouldCloseClient = true;
+ }
+
+ // As these fronts aren't instantiated by protocol.js, we have to set their actor ID
+ // manually like that:
+ front.actorID = form.actor;
+ front.form(form);
+ this.manage(front);
+ front.on("target-destroyed", this._onTargetDestroyed);
+ return front;
+ }
+
+ _onTargetDestroyed() {
+ // Clear the cached targetFront when the target is destroyed.
+ // Note that we are also checking that _targetFront has a valid actorID
+ // in getTarget, this acts as an additional security to avoid races.
+ this._targetFront = null;
+ }
+
+ /**
+ * Safely retrieves the favicon via getFavicon() and populates this._form.favicon.
+ *
+ * We could let callers explicitly retrieve the favicon instead of inserting it in the
+ * form dynamically.
+ */
+ async retrieveFavicon() {
+ try {
+ this._form.favicon = await this.getFavicon();
+ } catch (e) {
+ // We might request the data for a tab which is going to be destroyed.
+ // In this case the TargetFront will be destroyed. Otherwise log an error.
+ if (!this.isDestroyed()) {
+ console.error("Failed to retrieve the favicon for " + this.url, e);
+ }
+ }
+ }
+
+ async getTarget() {
+ if (this._targetFront && !this._targetFront.isDestroyed()) {
+ return this._targetFront;
+ }
+
+ if (this._targetFrontPromise) {
+ return this._targetFrontPromise;
+ }
+
+ this._targetFrontPromise = (async () => {
+ let targetFront = null;
+ try {
+ const targetForm = await super.getTarget();
+ targetFront = this._createTabTarget(targetForm);
+ await targetFront.attach();
+ } catch (e) {
+ console.log(
+ `Request to connect to TabDescriptor "${this.id}" failed: ${e}`
+ );
+ }
+ this._targetFront = targetFront;
+ this._targetFrontPromise = null;
+ return targetFront;
+ })();
+ return this._targetFrontPromise;
+ }
+
+ /**
+ * Handle tabs events.
+ */
+ async _handleTabEvent(event) {
+ switch (event.type) {
+ case "TabClose":
+ // Always destroy the toolbox opened for this local tab target.
+ // Toolboxes are no longer destroyed on target destruction.
+ // When the toolbox is in a Window Host, it won't be removed from the
+ // DOM when the tab is closed.
+ const toolbox = gDevTools.getToolbox(this._targetFront);
+ // A few tests are using TargetFactory.forTab, but aren't spawning any
+ // toolbox. In this case, the toobox won't destroy the target, so we
+ // do it from here. But ultimately, the target should destroy itself
+ // from the actor side anyway.
+ if (toolbox) {
+ // Toolbox.destroy will call target.destroy eventually.
+ await toolbox.destroy();
+ }
+ break;
+ case "TabRemotenessChange":
+ this._onRemotenessChange();
+ break;
+ }
+ }
+
+ /**
+ * Automatically respawn the toolbox when the tab changes between being
+ * loaded within the parent process and loaded from a content process.
+ * Process change can go in both ways.
+ */
+ async _onRemotenessChange() {
+ // Responsive design does a crazy dance around tabs and triggers
+ // remotenesschange events. But we should ignore them as at the end
+ // the content doesn't change its remoteness.
+ if (this.localTab.isResponsiveDesignMode) {
+ return;
+ }
+
+ // The front that was created for DevTools page extension does not have corresponding toolbox.
+ if (this.isDevToolsExtensionContext) {
+ return;
+ }
+
+ const toolbox = gDevTools.getToolbox(this._targetFront);
+
+ const targetSwitchingEnabled = Services.prefs.getBoolPref(
+ "devtools.target-switching.enabled",
+ false
+ );
+
+ // When target switching is enabled, everything is handled by the TargetList
+ // In a near future, this client side code should be replaced by actor code,
+ // notifying about new tab targets.
+ if (targetSwitchingEnabled) {
+ this.emit("remoteness-change", this._targetFront);
+ return;
+ }
+
+ // Otherwise, if we don't support target switching, ensure the toolbox is destroyed.
+ // We need to wait for the toolbox destruction because the TargetFactory memoized the targets,
+ // and only cleans up the cache after the target is destroyed via toolbox destruction.
+ await toolbox.destroy();
+
+ // Fetch the new target for this tab
+ const newTarget = await TargetFactory.forTab(this.localTab, null);
+
+ gDevTools.showToolbox(newTarget);
+ }
+}
+
+exports.TabDescriptorFront = TabDescriptorFront;
+registerFront(TabDescriptorFront);
diff --git a/devtools/client/fronts/descriptors/webextension.js b/devtools/client/fronts/descriptors/webextension.js
new file mode 100644
index 0000000000..4ed8cc53c5
--- /dev/null
+++ b/devtools/client/fronts/descriptors/webextension.js
@@ -0,0 +1,139 @@
+/* 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 {
+ webExtensionDescriptorSpec,
+} = require("devtools/shared/specs/descriptors/webextension");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+loader.lazyRequireGetter(
+ this,
+ "BrowsingContextTargetFront",
+ "devtools/client/fronts/targets/browsing-context",
+ true
+);
+
+class WebExtensionDescriptorFront extends FrontClassWithSpec(
+ webExtensionDescriptorSpec
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.client = client;
+ this.traits = {};
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this._form = json;
+ this.traits = json.traits || {};
+ }
+
+ get debuggable() {
+ return this._form.debuggable;
+ }
+
+ get hidden() {
+ return this._form.hidden;
+ }
+
+ get iconDataURL() {
+ return this._form.iconDataURL;
+ }
+
+ get iconURL() {
+ return this._form.iconURL;
+ }
+
+ get id() {
+ return this._form.id;
+ }
+
+ get isSystem() {
+ return this._form.isSystem;
+ }
+
+ get isWebExtension() {
+ return this._form.isWebExtension;
+ }
+
+ get manifestURL() {
+ return this._form.manifestURL;
+ }
+
+ get name() {
+ return this._form.name;
+ }
+
+ get temporarilyInstalled() {
+ return this._form.temporarilyInstalled;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get warnings() {
+ return this._form.warnings;
+ }
+
+ _createWebExtensionTarget(form) {
+ const front = new BrowsingContextTargetFront(this.conn, null, this);
+ front.form(form);
+ this.manage(front);
+ return front;
+ }
+
+ /**
+ * Retrieve the BrowsingContextTargetFront representing a
+ * WebExtensionTargetActor if this addon is a webextension.
+ *
+ * WebExtensionDescriptors will be created for any type of addon type
+ * (webextension, search plugin, themes). Only webextensions can be targets.
+ * This method will throw for other addon types.
+ *
+ * TODO: We should filter out non-webextension & non-debuggable addons on the
+ * server to avoid the isWebExtension check here. See Bug 1644355.
+ */
+ async getTarget() {
+ if (!this.isWebExtension) {
+ throw new Error(
+ "Tried to create a target for an addon which is not a webextension: " +
+ this.actorID
+ );
+ }
+
+ if (this._targetFront && !this._targetFront.isDestroyed()) {
+ return this._targetFront;
+ }
+
+ if (this._targetFrontPromise) {
+ return this._targetFrontPromise;
+ }
+
+ this._targetFrontPromise = (async () => {
+ let targetFront = null;
+ try {
+ const targetForm = await super.getTarget();
+ targetFront = this._createWebExtensionTarget(targetForm);
+ await targetFront.attach();
+ } catch (e) {
+ console.log(
+ `Request to connect to WebExtensionDescriptor "${this.id}" failed: ${e}`
+ );
+ }
+ this._targetFront = targetFront;
+ this._targetFrontPromise = null;
+ return targetFront;
+ })();
+ return this._targetFrontPromise;
+ }
+}
+
+exports.WebExtensionDescriptorFront = WebExtensionDescriptorFront;
+registerFront(WebExtensionDescriptorFront);
diff --git a/devtools/client/fronts/descriptors/worker.js b/devtools/client/fronts/descriptors/worker.js
new file mode 100644
index 0000000000..3e0ed5ed30
--- /dev/null
+++ b/devtools/client/fronts/descriptors/worker.js
@@ -0,0 +1,142 @@
+/* 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 { Ci } = require("chrome");
+const {
+ workerDescriptorSpec,
+} = require("devtools/shared/specs/descriptors/worker");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { TargetMixin } = require("devtools/client/fronts/targets/target-mixin");
+
+class WorkerDescriptorFront extends TargetMixin(
+ FrontClassWithSpec(workerDescriptorSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.traits = {};
+
+ // The actor sends a "close" event, which is translated to "worker-close" by
+ // the specification in order to not conflict with Target's "close" event.
+ // This event is similar to tabDetached and means that the worker is destroyed.
+ // So that we should destroy the target in order to significate that the target
+ // is no longer debuggable.
+ this.once("worker-close", this.destroy.bind(this));
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ this.id = json.id;
+
+ // Save the full form for Target class usage.
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this.targetForm = json;
+ this._url = json.url;
+ this.type = json.type;
+ this.scope = json.scope;
+ this.fetch = json.fetch;
+ }
+
+ get name() {
+ return this.url.split("/").pop();
+ }
+
+ get isDedicatedWorker() {
+ return this.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED;
+ }
+
+ get isSharedWorker() {
+ return this.type === Ci.nsIWorkerDebugger.TYPE_SHARED;
+ }
+
+ get isServiceWorker() {
+ return this.type === Ci.nsIWorkerDebugger.TYPE_SERVICE;
+ }
+
+ async attach() {
+ // temporary, will be moved once we have a target actor
+ return this.getTarget();
+ }
+
+ async getTarget() {
+ if (this._attach) {
+ return this._attach;
+ }
+
+ this._attach = (async () => {
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ const response = await super.attach();
+
+ if (this.isServiceWorker) {
+ this.registration = await this._getRegistrationIfActive();
+ if (this.registration) {
+ await this.registration.preventShutdown();
+ }
+ }
+
+ this._url = response.url;
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ const workerTargetForm = await super.getTarget();
+
+ // Set the console actor ID on the form to expose it to Target.attachConsole
+ // Set the ThreadActor on the target form so it is accessible by getFront
+ this.targetForm.consoleActor = workerTargetForm.consoleActor;
+ this.targetForm.threadActor = workerTargetForm.threadActor;
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ await this.attachConsole();
+ })();
+ return this._attach;
+ }
+
+ async detach() {
+ let response;
+ try {
+ response = await super.detach();
+
+ if (this.registration) {
+ // Bug 1644772 - Sometimes, the Browser Toolbox fails opening with a connection timeout
+ // with an exception related to this call to allowShutdown and its usage of detachDebugger API.
+ await this.registration.allowShutdown();
+ this.registration = null;
+ }
+ } catch (e) {
+ this.logDetachError(e, "worker");
+ }
+
+ return response;
+ }
+
+ async _getRegistrationIfActive() {
+ const {
+ registrations,
+ } = await this.client.mainRoot.listServiceWorkerRegistrations();
+ return registrations.find(({ activeWorker }) => {
+ return activeWorker && this.id === activeWorker.id;
+ });
+ }
+
+ reconfigure() {
+ // Toolbox and options panel are calling this method but Worker Target can't be
+ // reconfigured. So we ignore this call here.
+ return Promise.resolve();
+ }
+}
+
+exports.WorkerDescriptorFront = WorkerDescriptorFront;
+registerFront(exports.WorkerDescriptorFront);
diff --git a/devtools/client/fronts/device.js b/devtools/client/fronts/device.js
new file mode 100644
index 0000000000..b8101c10ad
--- /dev/null
+++ b/devtools/client/fronts/device.js
@@ -0,0 +1,23 @@
+/* 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 { deviceSpec } = require("devtools/shared/specs/device");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+class DeviceFront extends FrontClassWithSpec(deviceSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "deviceActor";
+ }
+}
+
+exports.DeviceFront = DeviceFront;
+registerFront(DeviceFront);
diff --git a/devtools/client/fronts/eventsource.js b/devtools/client/fronts/eventsource.js
new file mode 100644
index 0000000000..0325be1a99
--- /dev/null
+++ b/devtools/client/fronts/eventsource.js
@@ -0,0 +1,73 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { eventSourceSpec } = require("devtools/shared/specs/eventsource");
+
+/**
+ * A EventSourceFront is used as a front end for the EventSourceActor that is
+ * created on the server, hiding implementation details.
+ */
+class EventSourceFront extends FrontClassWithSpec(eventSourceSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._onEventSourceConnectionClosed = this._onEventSourceConnectionClosed.bind(
+ this
+ );
+ this._onEventReceived = this._onEventReceived.bind(this);
+
+ // Attribute name from which to retrieve the actorID
+ // out of the target actor's form
+ this.formAttributeName = "eventSourceActor";
+
+ this.on(
+ "serverEventSourceConnectionClosed",
+ this._onEventSourceConnectionClosed
+ );
+ this.on("serverEventReceived", this._onEventReceived);
+ }
+
+ /**
+ * Close the EventSourceFront.
+ */
+ destroy() {
+ this.off("serverEventSourceConnectionClosed");
+ this.off("serverEventReceived");
+ return super.destroy();
+ }
+
+ /**
+ * The "eventSourceConnectionClosed" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param number httpChannelId
+ */
+ async _onEventSourceConnectionClosed(httpChannelId) {
+ this.emit("eventSourceConnectionClosed", httpChannelId);
+ }
+
+ /**
+ * The "eventReceived" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string httpChannelId
+ * Channel ID of the eventSource connection.
+ * @param object data
+ * The data received from the server.
+ */
+ async _onEventReceived(httpChannelId, data) {
+ this.emit("eventReceived", httpChannelId, data);
+ }
+}
+
+exports.EventSourceFront = EventSourceFront;
+registerFront(EventSourceFront);
diff --git a/devtools/client/fronts/frame.js b/devtools/client/fronts/frame.js
new file mode 100644
index 0000000000..d194bbb2c3
--- /dev/null
+++ b/devtools/client/fronts/frame.js
@@ -0,0 +1,31 @@
+/* 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 { frameSpec } = require("devtools/shared/specs/frame");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+class FrameFront extends FrontClassWithSpec(frameSpec) {
+ form(json) {
+ this.displayName = json.displayName;
+ this.arguments = json.arguments;
+ this.type = json.type;
+ this.where = json.where;
+ this.this = json.this;
+ this.data = json;
+ this.asyncCause = json.asyncCause;
+ this.state = json.state;
+ }
+
+ getWebConsoleFront() {
+ return this.targetFront.getFront("console");
+ }
+}
+
+module.exports = FrameFront;
+registerFront(FrameFront);
diff --git a/devtools/client/fronts/framerate.js b/devtools/client/fronts/framerate.js
new file mode 100644
index 0000000000..385d0b3622
--- /dev/null
+++ b/devtools/client/fronts/framerate.js
@@ -0,0 +1,26 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { framerateSpec } = require("devtools/shared/specs/framerate");
+
+/**
+ * The corresponding Front object for the FramerateActor.
+ */
+class FramerateFront extends FrontClassWithSpec(framerateSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "framerateActor";
+ }
+}
+
+exports.FramerateFront = FramerateFront;
+registerFront(FramerateFront);
diff --git a/devtools/client/fronts/highlighters.js b/devtools/client/fronts/highlighters.js
new file mode 100644
index 0000000000..2f15d94128
--- /dev/null
+++ b/devtools/client/fronts/highlighters.js
@@ -0,0 +1,49 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { customHighlighterSpec } = require("devtools/shared/specs/highlighters");
+const { safeAsyncMethod } = require("devtools/shared/async-utils");
+
+class CustomHighlighterFront extends FrontClassWithSpec(customHighlighterSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // show/hide requests can be triggered while DevTools are closing.
+ this.show = safeAsyncMethod(this.show.bind(this), () => this.isDestroyed());
+ this.hide = safeAsyncMethod(this.hide.bind(this), () => this.isDestroyed());
+
+ this._isShown = false;
+ }
+
+ show(...args) {
+ this._isShown = true;
+ return super.show(...args);
+ }
+
+ hide() {
+ this._isShown = false;
+ return super.hide();
+ }
+
+ isShown() {
+ return this._isShown;
+ }
+
+ destroy() {
+ if (this.isDestroyed()) {
+ return;
+ }
+ super.finalize(); // oneway call, doesn't expect a response.
+ super.destroy();
+ }
+}
+
+exports.CustomHighlighterFront = CustomHighlighterFront;
+registerFront(CustomHighlighterFront);
diff --git a/devtools/client/fronts/inspector.js b/devtools/client/fronts/inspector.js
new file mode 100644
index 0000000000..d55f83bd67
--- /dev/null
+++ b/devtools/client/fronts/inspector.js
@@ -0,0 +1,215 @@
+/* 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 Services = require("Services");
+const Telemetry = require("devtools/client/shared/telemetry");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol.js");
+const { inspectorSpec } = require("devtools/shared/specs/inspector");
+
+const TELEMETRY_EYEDROPPER_OPENED = "DEVTOOLS_EYEDROPPER_OPENED_COUNT";
+const TELEMETRY_EYEDROPPER_OPENED_MENU =
+ "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT";
+const SHOW_ALL_ANONYMOUS_CONTENT_PREF =
+ "devtools.inspector.showAllAnonymousContent";
+
+const telemetry = new Telemetry();
+
+/**
+ * Client side of the inspector actor, which is used to create
+ * inspector-related actors, including the walker.
+ */
+class InspectorFront extends FrontClassWithSpec(inspectorSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._client = client;
+ this._highlighters = new Map();
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "inspectorActor";
+
+ // Map of highlighter types to unsettled promises to create a highlighter of that type
+ this._pendingGetHighlighterMap = new Map();
+ }
+
+ // async initialization
+ async initialize() {
+ if (this.initialized) {
+ return this.initialized;
+ }
+
+ this.initialized = await Promise.all([
+ this._getWalker(),
+ this._getPageStyle(),
+ ]);
+
+ return this.initialized;
+ }
+
+ async _getWalker() {
+ const showAllAnonymousContent = Services.prefs.getBoolPref(
+ SHOW_ALL_ANONYMOUS_CONTENT_PREF
+ );
+ this.walker = await this.getWalker({
+ showAllAnonymousContent,
+ });
+
+ // We need to reparent the RootNode of remote iframe Walkers
+ // so that their parent is the NodeFront of the <iframe>
+ // element, coming from another process/target/WalkerFront.
+ await this.walker.reparentRemoteFrame();
+ }
+
+ hasHighlighter(type) {
+ return this._highlighters.has(type);
+ }
+
+ async _getPageStyle() {
+ this.pageStyle = await super.getPageStyle();
+ }
+
+ async getCompatibilityFront() {
+ if (!this._compatibility) {
+ this._compatibility = await super.getCompatibility();
+ }
+
+ return this._compatibility;
+ }
+
+ destroy() {
+ this._compatibility = null;
+ // CustomHighlighter fronts are managed by InspectorFront and so will be
+ // automatically destroyed. But we have to clear the `_highlighters`
+ // Map as well as explicitly call `finalize` request on all of them.
+ this.destroyHighlighters();
+ super.destroy();
+ }
+
+ destroyHighlighters() {
+ for (const type of this._highlighters.keys()) {
+ if (this._highlighters.has(type)) {
+ const highlighter = this._highlighters.get(type);
+ if (!highlighter.isDestroyed()) {
+ highlighter.finalize();
+ }
+ this._highlighters.delete(type);
+ }
+ }
+ }
+
+ async getHighlighterByType(typeName) {
+ let highlighter = null;
+ try {
+ highlighter = await super.getHighlighterByType(typeName);
+ } catch (_) {
+ throw new Error(
+ "The target doesn't support " +
+ `creating highlighters by types or ${typeName} is unknown`
+ );
+ }
+ return highlighter;
+ }
+
+ getKnownHighlighter(type) {
+ return this._highlighters.get(type);
+ }
+
+ /**
+ * Return a highlighter instance of the given type.
+ * If an instance was previously created, return it. Else, create and return a new one.
+ *
+ * Store a promise for the request to create a new highlighter. If another request
+ * comes in before that promise is resolved, wait for it to resolve and return the
+ * highlighter instance it resolved with instead of creating a new request.
+ *
+ * @param {String} type
+ * Highlighter type
+ * @return {Promise}
+ * Promise which resolves with a highlighter instance of the given type
+ */
+ async getOrCreateHighlighterByType(type) {
+ let front = this._highlighters.get(type);
+ let pendingGetHighlighter = this._pendingGetHighlighterMap.get(type);
+
+ if (!front && !pendingGetHighlighter) {
+ pendingGetHighlighter = (async () => {
+ const highlighter = await this.getHighlighterByType(type);
+ this._highlighters.set(type, highlighter);
+ this._pendingGetHighlighterMap.delete(type);
+ return highlighter;
+ })();
+
+ this._pendingGetHighlighterMap.set(type, pendingGetHighlighter);
+ }
+
+ if (pendingGetHighlighter) {
+ front = await pendingGetHighlighter;
+ }
+
+ return front;
+ }
+
+ async pickColorFromPage(options) {
+ await super.pickColorFromPage(options);
+ if (options?.fromMenu) {
+ telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED_MENU).add(true);
+ } else {
+ telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED).add(true);
+ }
+ }
+
+ /**
+ * Given a node grip, return a NodeFront on the right context.
+ *
+ * @param {Object} grip: The node grip.
+ * @returns {Promise<NodeFront|null>} A promise that resolves with a NodeFront or null
+ * if the NodeFront couldn't be created/retrieved.
+ */
+ async getNodeFrontFromNodeGrip(grip) {
+ return this.getNodeActorFromContentDomReference(grip.contentDomReference);
+ }
+
+ async getNodeActorFromContentDomReference(contentDomReference) {
+ const { browsingContextId } = contentDomReference;
+ // If the contentDomReference lives in the same browsing context id than the
+ // current one, we can directly use the current walker.
+ if (this.targetFront.browsingContextID === browsingContextId) {
+ return this.walker.getNodeActorFromContentDomReference(
+ contentDomReference
+ );
+ }
+
+ // If the contentDomReference has a different browsing context than the current one,
+ // we are either in Fission or in the Multiprocess Browser Toolbox, so we need to
+ // retrieve the walker of the BrowsingContextTarget.
+ // Get the target for this remote frame element
+ const { descriptorFront } = this.targetFront;
+
+ // Tab and Process Descriptors expose a Watcher, which should be used to
+ // fetch the node's target.
+ let target;
+ if (descriptorFront && descriptorFront.traits.watcher) {
+ const watcherFront = await descriptorFront.getWatcher();
+ target = await watcherFront.getBrowsingContextTarget(browsingContextId);
+ } else {
+ // For descriptors which don't expose a watcher (e.g. WebExtension)
+ // we used to call RootActor::getBrowsingContextDescriptor, but it was
+ // removed in FF77.
+ // Support for watcher in WebExtension descriptors is Bug 1644341.
+ throw new Error(
+ `Unable to call getNodeActorFromContentDomReference for ${this.targetFront.actorID}`
+ );
+ }
+ const { walker } = await target.getFront("inspector");
+ return walker.getNodeActorFromContentDomReference(contentDomReference);
+ }
+}
+
+exports.InspectorFront = InspectorFront;
+registerFront(InspectorFront);
diff --git a/devtools/client/fronts/inspector/moz.build b/devtools/client/fronts/inspector/moz.build
new file mode 100644
index 0000000000..de635f5947
--- /dev/null
+++ b/devtools/client/fronts/inspector/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "rule-rewriter.js",
+)
diff --git a/devtools/client/fronts/inspector/rule-rewriter.js b/devtools/client/fronts/inspector/rule-rewriter.js
new file mode 100644
index 0000000000..14b96b7842
--- /dev/null
+++ b/devtools/client/fronts/inspector/rule-rewriter.js
@@ -0,0 +1,750 @@
+/* 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/. */
+
+// This file holds various CSS parsing and rewriting utilities.
+// Some entry points of note are:
+// parseDeclarations - parse a CSS rule into declarations
+// RuleRewriter - rewrite CSS rule text
+// parsePseudoClassesAndAttributes - parse selector and extract
+// pseudo-classes
+// parseSingleValue - parse a single CSS property value
+
+"use strict";
+
+const promise = require("promise");
+const { getCSSLexer } = require("devtools/shared/css/lexer");
+const {
+ COMMENT_PARSING_HEURISTIC_BYPASS_CHAR,
+ escapeCSSComment,
+ parseNamedDeclarations,
+ unescapeCSSComment,
+} = require("devtools/shared/css/parsing-utils");
+
+loader.lazyRequireGetter(
+ this,
+ ["getIndentationFromPrefs", "getIndentationFromString"],
+ "devtools/shared/indentation",
+ true
+);
+
+// Used to test whether a newline appears anywhere in some text.
+const NEWLINE_RX = /[\r\n]/;
+// Used to test whether a bit of text starts an empty comment, either
+// an "ordinary" /* ... */ comment, or a "heuristic bypass" comment
+// like /*! ... */.
+const EMPTY_COMMENT_START_RX = /^\/\*!?[ \r\n\t\f]*$/;
+// Used to test whether a bit of text ends an empty comment.
+const EMPTY_COMMENT_END_RX = /^[ \r\n\t\f]*\*\//;
+// Used to test whether a string starts with a blank line.
+const BLANK_LINE_RX = /^[ \t]*(?:\r\n|\n|\r|\f|$)/;
+
+/**
+ * Return an object that can be used to rewrite declarations in some
+ * source text. The source text and parsing are handled in the same
+ * way as @see parseNamedDeclarations, with |parseComments| being true.
+ * Rewriting is done by calling one of the modification functions like
+ * setPropertyEnabled. The returned object has the same interface
+ * as @see RuleModificationList.
+ *
+ * An example showing how to disable the 3rd property in a rule:
+ *
+ * let rewriter = new RuleRewriter(isCssPropertyKnown, ruleActor,
+ * ruleActor.authoredText);
+ * rewriter.setPropertyEnabled(3, "color", false);
+ * rewriter.apply().then(() => { ... the change is made ... });
+ *
+ * The exported rewriting methods are |renameProperty|, |setPropertyEnabled|,
+ * |createProperty|, |setProperty|, and |removeProperty|. The |apply|
+ * method can be used to send the edited text to the StyleRuleActor;
+ * |getDefaultIndentation| is useful for the methods requiring a
+ * default indentation value; and |getResult| is useful for testing.
+ *
+ * Additionally, editing will set the |changedDeclarations| property
+ * on this object. This property has the same form as the |changed|
+ * property of the object returned by |getResult|.
+ *
+ * @param {Function} isCssPropertyKnown
+ * A function to check if the CSS property is known. This is either an
+ * internal server function or from the CssPropertiesFront.
+ * that are supported by the server. Note that if Bug 1222047
+ * is completed then isCssPropertyKnown will not need to be passed in.
+ * The CssProperty front will be able to obtained directly from the
+ * RuleRewriter.
+ * @param {StyleRuleFront} rule The style rule to use. Note that this
+ * is only needed by the |apply| and |getDefaultIndentation| methods;
+ * and in particular for testing it can be |null|.
+ * @param {String} inputString The CSS source text to parse and modify.
+ * @return {Object} an object that can be used to rewrite the input text.
+ */
+function RuleRewriter(isCssPropertyKnown, rule, inputString) {
+ this.rule = rule;
+ this.isCssPropertyKnown = isCssPropertyKnown;
+ // The RuleRewriter sends CSS rules as text to the server, but with this modifications
+ // array, it also sends the list of changes so the server doesn't have to re-parse the
+ // rule if it needs to track what changed.
+ this.modifications = [];
+
+ // Keep track of which any declarations we had to rewrite while
+ // performing the requested action.
+ this.changedDeclarations = {};
+
+ // If not null, a promise that must be wait upon before |apply| can
+ // do its work.
+ this.editPromise = null;
+
+ // If the |defaultIndentation| property is set, then it is used;
+ // otherwise the RuleRewriter will try to compute the default
+ // indentation based on the style sheet's text. This override
+ // facility is for testing.
+ this.defaultIndentation = null;
+
+ this.startInitialization(inputString);
+}
+
+RuleRewriter.prototype = {
+ /**
+ * An internal function to initialize the rewriter with a given
+ * input string.
+ *
+ * @param {String} inputString the input to use
+ */
+ startInitialization: function(inputString) {
+ this.inputString = inputString;
+ // Whether there are any newlines in the input text.
+ this.hasNewLine = /[\r\n]/.test(this.inputString);
+ // The declarations.
+ this.declarations = parseNamedDeclarations(
+ this.isCssPropertyKnown,
+ this.inputString,
+ true
+ );
+ this.decl = null;
+ this.result = null;
+ },
+
+ /**
+ * An internal function to complete initialization and set some
+ * properties for further processing.
+ *
+ * @param {Number} index The index of the property to modify
+ */
+ completeInitialization: function(index) {
+ if (index < 0) {
+ throw new Error("Invalid index " + index + ". Expected positive integer");
+ }
+ // |decl| is the declaration to be rewritten, or null if there is no
+ // declaration corresponding to |index|.
+ // |result| is used to accumulate the result text.
+ if (index < this.declarations.length) {
+ this.decl = this.declarations[index];
+ this.result = this.inputString.substring(0, this.decl.offsets[0]);
+ } else {
+ this.decl = null;
+ this.result = this.inputString;
+ }
+ },
+
+ /**
+ * A helper function to compute the indentation of some text. This
+ * examines the rule's existing text to guess the indentation to use;
+ * unlike |getDefaultIndentation|, which examines the entire style
+ * sheet.
+ *
+ * @param {String} string the input text
+ * @param {Number} offset the offset at which to compute the indentation
+ * @return {String} the indentation at the indicated position
+ */
+ getIndentation: function(string, offset) {
+ let originalOffset = offset;
+ for (--offset; offset >= 0; --offset) {
+ const c = string[offset];
+ if (c === "\r" || c === "\n" || c === "\f") {
+ return string.substring(offset + 1, originalOffset);
+ }
+ if (c !== " " && c !== "\t") {
+ // Found some non-whitespace character before we found a newline
+ // -- let's reset the starting point and keep going, as we saw
+ // something on the line before the declaration.
+ originalOffset = offset;
+ }
+ }
+ // Ran off the end.
+ return "";
+ },
+
+ /**
+ * Modify a property value to ensure it is "lexically safe" for
+ * insertion into a style sheet. This function doesn't attempt to
+ * ensure that the resulting text is a valid value for the given
+ * property; but rather just that inserting the text into the style
+ * sheet will not cause unwanted changes to other rules or
+ * declarations.
+ *
+ * @param {String} text The input text. This should include the trailing ";".
+ * @return {Array} An array of the form [anySanitized, text], where
+ * |anySanitized| is a boolean that indicates
+ * whether anything substantive has changed; and
+ * where |text| is the text that has been rewritten
+ * to be "lexically safe".
+ */
+ sanitizePropertyValue: function(text) {
+ // Start by stripping any trailing ";". This is done here to
+ // avoid the case where the user types "url(" (which is turned
+ // into "url(;" by the rule view before coming here), being turned
+ // into "url(;)" by this code -- due to the way "url(...)" is
+ // parsed as a single token.
+ text = text.replace(/;$/, "");
+ const lexer = getCSSLexer(text);
+
+ let result = "";
+ let previousOffset = 0;
+ const parenStack = [];
+ let anySanitized = false;
+
+ // Push a closing paren on the stack.
+ const pushParen = (token, closer) => {
+ result =
+ result +
+ text.substring(previousOffset, token.startOffset) +
+ text.substring(token.startOffset, token.endOffset);
+ // We set the location of the paren in a funny way, to handle
+ // the case where we've seen a function token, where the paren
+ // appears at the end.
+ parenStack.push({ closer, offset: result.length - 1 });
+ previousOffset = token.endOffset;
+ };
+
+ // Pop a closing paren from the stack.
+ const popSomeParens = closer => {
+ while (parenStack.length > 0) {
+ const paren = parenStack.pop();
+
+ if (paren.closer === closer) {
+ return true;
+ }
+
+ // Found a non-matching closing paren, so quote it. Note that
+ // these are processed in reverse order.
+ result =
+ result.substring(0, paren.offset) +
+ "\\" +
+ result.substring(paren.offset);
+ anySanitized = true;
+ }
+ return false;
+ };
+
+ while (true) {
+ const token = lexer.nextToken();
+ if (!token) {
+ break;
+ }
+
+ if (token.tokenType === "symbol") {
+ switch (token.text) {
+ case ";":
+ // We simply drop the ";" here. This lets us cope with
+ // declarations that don't have a ";" and also other
+ // termination. The caller handles adding the ";" again.
+ result += text.substring(previousOffset, token.startOffset);
+ previousOffset = token.endOffset;
+ break;
+
+ case "{":
+ pushParen(token, "}");
+ break;
+
+ case "(":
+ pushParen(token, ")");
+ break;
+
+ case "[":
+ pushParen(token, "]");
+ break;
+
+ case "}":
+ case ")":
+ case "]":
+ // Did we find an unmatched close bracket?
+ if (!popSomeParens(token.text)) {
+ // Copy out text from |previousOffset|.
+ result += text.substring(previousOffset, token.startOffset);
+ // Quote the offending symbol.
+ result += "\\" + token.text;
+ previousOffset = token.endOffset;
+ anySanitized = true;
+ }
+ break;
+ }
+ } else if (token.tokenType === "function") {
+ pushParen(token, ")");
+ }
+ }
+
+ // Fix up any unmatched parens.
+ popSomeParens(null);
+
+ // Copy out any remaining text, then any needed terminators.
+ result += text.substring(previousOffset, text.length);
+ const eofFixup = lexer.performEOFFixup("", true);
+ if (eofFixup) {
+ anySanitized = true;
+ result += eofFixup;
+ }
+ return [anySanitized, result];
+ },
+
+ /**
+ * Start at |index| and skip whitespace
+ * backward in |string|. Return the index of the first
+ * non-whitespace character, or -1 if the entire string was
+ * whitespace.
+ * @param {String} string the input string
+ * @param {Number} index the index at which to start
+ * @return {Number} index of the first non-whitespace character, or -1
+ */
+ skipWhitespaceBackward: function(string, index) {
+ for (
+ --index;
+ index >= 0 && (string[index] === " " || string[index] === "\t");
+ --index
+ ) {
+ // Nothing.
+ }
+ return index;
+ },
+
+ /**
+ * Terminate a given declaration, if needed.
+ *
+ * @param {Number} index The index of the rule to possibly
+ * terminate. It might be invalid, so this
+ * function must check for that.
+ */
+ maybeTerminateDecl: function(index) {
+ if (
+ index < 0 ||
+ index >= this.declarations.length ||
+ // No need to rewrite declarations in comments.
+ "commentOffsets" in this.declarations[index]
+ ) {
+ return;
+ }
+
+ const termDecl = this.declarations[index];
+ let endIndex = termDecl.offsets[1];
+ // Due to an oddity of the lexer, we might have gotten a bit of
+ // extra whitespace in a trailing bad_url token -- so be sure to
+ // skip that as well.
+ endIndex = this.skipWhitespaceBackward(this.result, endIndex) + 1;
+
+ const trailingText = this.result.substring(endIndex);
+ if (termDecl.terminator) {
+ // Insert the terminator just at the end of the declaration,
+ // before any trailing whitespace.
+ this.result =
+ this.result.substring(0, endIndex) + termDecl.terminator + trailingText;
+ // In a couple of cases, we may have had to add something to
+ // terminate the declaration, but the termination did not
+ // actually affect the property's value -- and at this spot, we
+ // only care about reporting value changes. In particular, we
+ // might have added a plain ";", or we might have terminated a
+ // comment with "*/;". Neither of these affect the value.
+ if (termDecl.terminator !== ";" && termDecl.terminator !== "*/;") {
+ this.changedDeclarations[index] =
+ termDecl.value + termDecl.terminator.slice(0, -1);
+ }
+ }
+ // If the rule generally has newlines, but this particular
+ // declaration doesn't have a trailing newline, insert one now.
+ // Maybe this style is too weird to bother with.
+ if (this.hasNewLine && !NEWLINE_RX.test(trailingText)) {
+ this.result += "\n";
+ }
+ },
+
+ /**
+ * Sanitize the given property value and return the sanitized form.
+ * If the property is rewritten during sanitization, make a note in
+ * |changedDeclarations|.
+ *
+ * @param {String} text The property text.
+ * @param {Number} index The index of the property.
+ * @return {String} The sanitized text.
+ */
+ sanitizeText: function(text, index) {
+ const [anySanitized, sanitizedText] = this.sanitizePropertyValue(text);
+ if (anySanitized) {
+ this.changedDeclarations[index] = sanitizedText;
+ }
+ return sanitizedText;
+ },
+
+ /**
+ * Rename a declaration.
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name current name of the property
+ * @param {String} newName new name of the property
+ */
+ renameProperty: function(index, name, newName) {
+ this.completeInitialization(index);
+ this.result += CSS.escape(newName);
+ // We could conceivably compute the name offsets instead so we
+ // could preserve white space and comments on the LHS of the ":".
+ this.completeCopying(this.decl.colonOffsets[0]);
+ this.modifications.push({ type: "set", index, name, newName });
+ },
+
+ /**
+ * Enable or disable a declaration
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name current name of the property
+ * @param {Boolean} isEnabled true if the property should be enabled;
+ * false if it should be disabled
+ */
+ setPropertyEnabled: function(index, name, isEnabled) {
+ this.completeInitialization(index);
+ const decl = this.decl;
+ const priority = decl.priority;
+ let copyOffset = decl.offsets[1];
+ if (isEnabled) {
+ // Enable it. First see if the comment start can be deleted.
+ const commentStart = decl.commentOffsets[0];
+ if (EMPTY_COMMENT_START_RX.test(this.result.substring(commentStart))) {
+ this.result = this.result.substring(0, commentStart);
+ } else {
+ this.result += "*/ ";
+ }
+
+ // Insert the name and value separately, so we can report
+ // sanitization changes properly.
+ const commentNamePart = this.inputString.substring(
+ decl.offsets[0],
+ decl.colonOffsets[1]
+ );
+ this.result += unescapeCSSComment(commentNamePart);
+
+ // When uncommenting, we must be sure to sanitize the text, to
+ // avoid things like /* decl: }; */, which will be accepted as
+ // a property but which would break the entire style sheet.
+ let newText = this.inputString.substring(
+ decl.colonOffsets[1],
+ decl.offsets[1]
+ );
+ newText = cssTrimRight(unescapeCSSComment(newText));
+ this.result += this.sanitizeText(newText, index) + ";";
+
+ // See if the comment end can be deleted.
+ const trailingText = this.inputString.substring(decl.offsets[1]);
+ if (EMPTY_COMMENT_END_RX.test(trailingText)) {
+ copyOffset = decl.commentOffsets[1];
+ } else {
+ this.result += " /*";
+ }
+ } else {
+ // Disable it. Note that we use our special comment syntax
+ // here.
+ const declText = this.inputString.substring(
+ decl.offsets[0],
+ decl.offsets[1]
+ );
+ this.result +=
+ "/*" +
+ COMMENT_PARSING_HEURISTIC_BYPASS_CHAR +
+ " " +
+ escapeCSSComment(declText) +
+ " */";
+ }
+ this.completeCopying(copyOffset);
+
+ if (isEnabled) {
+ this.modifications.push({
+ type: "set",
+ index,
+ name,
+ value: decl.value,
+ priority,
+ });
+ } else {
+ this.modifications.push({ type: "disable", index, name });
+ }
+ },
+
+ /**
+ * Return a promise that will be resolved to the default indentation
+ * of the rule. This is a helper for internalCreateProperty.
+ *
+ * @return {Promise} a promise that will be resolved to a string
+ * that holds the default indentation that should be used
+ * for edits to the rule.
+ */
+ getDefaultIndentation: async function() {
+ if (!this.rule.parentStyleSheet) {
+ return null;
+ }
+
+ if (this.rule.parentStyleSheet.resourceId) {
+ const prefIndent = getIndentationFromPrefs();
+ if (prefIndent) {
+ const { indentUnit, indentWithTabs } = prefIndent;
+ return indentWithTabs ? "\t" : " ".repeat(indentUnit);
+ }
+
+ const styleSheetsFront = await this.rule.targetFront.getFront(
+ "stylesheets"
+ );
+ const { str: source } = await styleSheetsFront.getText(
+ this.rule.parentStyleSheet.resourceId
+ );
+ const { indentUnit, indentWithTabs } = getIndentationFromString(source);
+ return indentWithTabs ? "\t" : " ".repeat(indentUnit);
+ }
+
+ return this.rule.parentStyleSheet.guessIndentation();
+ },
+
+ /**
+ * An internal function to create a new declaration. This does all
+ * the work of |createProperty|.
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name name of the new property
+ * @param {String} value value of the new property
+ * @param {String} priority priority of the new property; either
+ * the empty string or "important"
+ * @param {Boolean} enabled True if the new property should be
+ * enabled, false if disabled
+ * @return {Promise} a promise that is resolved when the edit has
+ * completed
+ */
+ async internalCreateProperty(index, name, value, priority, enabled) {
+ this.completeInitialization(index);
+ let newIndentation = "";
+ if (this.hasNewLine) {
+ if (this.declarations.length > 0) {
+ newIndentation = this.getIndentation(
+ this.inputString,
+ this.declarations[0].offsets[0]
+ );
+ } else if (this.defaultIndentation) {
+ newIndentation = this.defaultIndentation;
+ } else {
+ newIndentation = await this.getDefaultIndentation();
+ }
+ }
+
+ this.maybeTerminateDecl(index - 1);
+
+ // If we generally have newlines, and if skipping whitespace
+ // backward stops at a newline, then insert our text before that
+ // whitespace. This ensures the indentation we computed is what
+ // is actually used.
+ let savedWhitespace = "";
+ if (this.hasNewLine) {
+ const wsOffset = this.skipWhitespaceBackward(
+ this.result,
+ this.result.length
+ );
+ if (this.result[wsOffset] === "\r" || this.result[wsOffset] === "\n") {
+ savedWhitespace = this.result.substring(wsOffset + 1);
+ this.result = this.result.substring(0, wsOffset + 1);
+ }
+ }
+
+ let newText = CSS.escape(name) + ": " + this.sanitizeText(value, index);
+ if (priority === "important") {
+ newText += " !important";
+ }
+ newText += ";";
+
+ if (!enabled) {
+ newText =
+ "/*" +
+ COMMENT_PARSING_HEURISTIC_BYPASS_CHAR +
+ " " +
+ escapeCSSComment(newText) +
+ " */";
+ }
+
+ this.result += newIndentation + newText;
+ if (this.hasNewLine) {
+ this.result += "\n";
+ }
+ this.result += savedWhitespace;
+
+ if (this.decl) {
+ // Still want to copy in the declaration previously at this
+ // index.
+ this.completeCopying(this.decl.offsets[0]);
+ }
+ },
+
+ /**
+ * Create a new declaration.
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name name of the new property
+ * @param {String} value value of the new property
+ * @param {String} priority priority of the new property; either
+ * the empty string or "important"
+ * @param {Boolean} enabled True if the new property should be
+ * enabled, false if disabled
+ */
+ createProperty: function(index, name, value, priority, enabled) {
+ this.editPromise = this.internalCreateProperty(
+ index,
+ name,
+ value,
+ priority,
+ enabled
+ );
+ // Log the modification only if the created property is enabled.
+ if (enabled) {
+ this.modifications.push({ type: "set", index, name, value, priority });
+ }
+ },
+
+ /**
+ * Set a declaration's value.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name the property's name
+ * @param {String} value the property's value
+ * @param {String} priority the property's priority, either the empty
+ * string or "important"
+ */
+ setProperty: function(index, name, value, priority) {
+ this.completeInitialization(index);
+ // We might see a "set" on a previously non-existent property; in
+ // that case, act like "create".
+ if (!this.decl) {
+ this.createProperty(index, name, value, priority, true);
+ return;
+ }
+
+ // Note that this assumes that "set" never operates on disabled
+ // properties.
+ this.result +=
+ this.inputString.substring(
+ this.decl.offsets[0],
+ this.decl.colonOffsets[1]
+ ) + this.sanitizeText(value, index);
+
+ if (priority === "important") {
+ this.result += " !important";
+ }
+ this.result += ";";
+ this.completeCopying(this.decl.offsets[1]);
+ this.modifications.push({ type: "set", index, name, value, priority });
+ },
+
+ /**
+ * Remove a declaration.
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name the name of the property to remove
+ */
+ removeProperty: function(index, name) {
+ this.completeInitialization(index);
+
+ // If asked to remove a property that does not exist, bail out.
+ if (!this.decl) {
+ return;
+ }
+
+ // If the property is disabled, then first enable it, and then
+ // delete it. We take this approach because we want to remove the
+ // entire comment if possible; but the logic for dealing with
+ // comments is hairy and already implemented in
+ // setPropertyEnabled.
+ if (this.decl.commentOffsets) {
+ this.setPropertyEnabled(index, name, true);
+ this.startInitialization(this.result);
+ this.completeInitialization(index);
+ }
+
+ let copyOffset = this.decl.offsets[1];
+ // Maybe removing this rule left us with a completely blank
+ // line. In this case, we'll delete the whole thing. We only
+ // bother with this if we're looking at sources that already
+ // have a newline somewhere.
+ if (this.hasNewLine) {
+ const nlOffset = this.skipWhitespaceBackward(
+ this.result,
+ this.decl.offsets[0]
+ );
+ if (
+ nlOffset < 0 ||
+ this.result[nlOffset] === "\r" ||
+ this.result[nlOffset] === "\n"
+ ) {
+ const trailingText = this.inputString.substring(copyOffset);
+ const match = BLANK_LINE_RX.exec(trailingText);
+ if (match) {
+ this.result = this.result.substring(0, nlOffset + 1);
+ copyOffset += match[0].length;
+ }
+ }
+ }
+ this.completeCopying(copyOffset);
+ this.modifications.push({ type: "remove", index, name });
+ },
+
+ /**
+ * An internal function to copy any trailing text to the output
+ * string.
+ *
+ * @param {Number} copyOffset Offset into |inputString| of the
+ * final text to copy to the output string.
+ */
+ completeCopying: function(copyOffset) {
+ // Add the trailing text.
+ this.result += this.inputString.substring(copyOffset);
+ },
+
+ /**
+ * Apply the modifications in this object to the associated rule.
+ *
+ * @return {Promise} A promise which will be resolved when the modifications
+ * are complete.
+ */
+ apply: function() {
+ return promise.resolve(this.editPromise).then(() => {
+ return this.rule.setRuleText(this.result, this.modifications);
+ });
+ },
+
+ /**
+ * Get the result of the rewriting. This is used for testing.
+ *
+ * @return {object} an object of the form {changed: object, text: string}
+ * |changed| is an object where each key is
+ * the index of a property whose value had to be
+ * rewritten during the sanitization process, and
+ * whose value is the new text of the property.
+ * |text| is the rewritten text of the rule.
+ */
+ getResult: function() {
+ return { changed: this.changedDeclarations, text: this.result };
+ },
+};
+
+/**
+ * Like trimRight, but only trims CSS-allowed whitespace.
+ */
+function cssTrimRight(str) {
+ const match = /^(.*?)[ \t\r\n\f]*$/.exec(str);
+ if (match) {
+ return match[1];
+ }
+ return str;
+}
+
+module.exports = RuleRewriter;
diff --git a/devtools/client/fronts/layout.js b/devtools/client/fronts/layout.js
new file mode 100644
index 0000000000..ca1cddb400
--- /dev/null
+++ b/devtools/client/fronts/layout.js
@@ -0,0 +1,169 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const {
+ flexboxSpec,
+ flexItemSpec,
+ gridSpec,
+ layoutSpec,
+} = require("devtools/shared/specs/layout");
+
+class FlexboxFront extends FrontClassWithSpec(flexboxSpec) {
+ form(form) {
+ this._form = form;
+ }
+
+ /**
+ * In some cases, the FlexboxActor already knows the NodeActor ID of the node where the
+ * flexbox is located. In such cases, this getter returns the NodeFront for it.
+ */
+ get containerNodeFront() {
+ if (!this._form.containerNodeActorID) {
+ return null;
+ }
+
+ return this.conn.getFrontByID(this._form.containerNodeActorID);
+ }
+
+ /**
+ * Get the WalkerFront instance that owns this FlexboxFront.
+ */
+ get walkerFront() {
+ return this.parentFront.walkerFront;
+ }
+
+ /**
+ * Get the computed style properties for the flex container.
+ */
+ get properties() {
+ return this._form.properties;
+ }
+}
+
+class FlexItemFront extends FrontClassWithSpec(flexItemSpec) {
+ form(form) {
+ this._form = form;
+ }
+
+ /**
+ * Get the flex item sizing data.
+ */
+ get flexItemSizing() {
+ return this._form.flexItemSizing;
+ }
+
+ /**
+ * In some cases, the FlexItemActor already knows the NodeActor ID of the node where the
+ * flex item is located. In such cases, this getter returns the NodeFront for it.
+ */
+ get nodeFront() {
+ if (!this._form.nodeActorID) {
+ return null;
+ }
+
+ return this.conn.getFrontByID(this._form.nodeActorID);
+ }
+
+ /**
+ * Get the WalkerFront instance that owns this FlexItemFront.
+ */
+ get walkerFront() {
+ return this.parentFront.walkerFront;
+ }
+
+ /**
+ * Get the computed style properties for the flex item.
+ */
+ get computedStyle() {
+ return this._form.computedStyle;
+ }
+
+ /**
+ * Get the style properties for the flex item.
+ */
+ get properties() {
+ return this._form.properties;
+ }
+}
+
+class GridFront extends FrontClassWithSpec(gridSpec) {
+ form(form) {
+ this._form = form;
+ }
+
+ /**
+ * In some cases, the GridActor already knows the NodeActor ID of the node where the
+ * grid is located. In such cases, this getter returns the NodeFront for it.
+ */
+ get containerNodeFront() {
+ if (!this._form.containerNodeActorID) {
+ return null;
+ }
+
+ return this.conn.getFrontByID(this._form.containerNodeActorID);
+ }
+
+ /**
+ * Get the WalkerFront instance that owns this GridFront.
+ */
+ get walkerFront() {
+ return this.parentFront.walkerFront;
+ }
+
+ /**
+ * Get the text direction of the grid container.
+ */
+ get direction() {
+ return this._form.direction;
+ }
+
+ /**
+ * Getter for the grid fragments data.
+ */
+ get gridFragments() {
+ return this._form.gridFragments;
+ }
+
+ /**
+ * Get whether or not the grid is a subgrid.
+ */
+ get isSubgrid() {
+ return !!this._form.isSubgrid;
+ }
+
+ /**
+ * Get the writing mode of the grid container.
+ */
+ get writingMode() {
+ return this._form.writingMode;
+ }
+}
+
+class LayoutFront extends FrontClassWithSpec(layoutSpec) {
+ /**
+ * Get the WalkerFront instance that owns this LayoutFront.
+ */
+ get walkerFront() {
+ return this.parentFront;
+ }
+
+ getAllGrids() {
+ return this.getGrids(this.walkerFront.rootNode);
+ }
+}
+
+exports.FlexboxFront = FlexboxFront;
+registerFront(FlexboxFront);
+exports.FlexItemFront = FlexItemFront;
+registerFront(FlexItemFront);
+exports.GridFront = GridFront;
+registerFront(GridFront);
+exports.LayoutFront = LayoutFront;
+registerFront(LayoutFront);
diff --git a/devtools/client/fronts/manifest.js b/devtools/client/fronts/manifest.js
new file mode 100644
index 0000000000..9c643ffb4e
--- /dev/null
+++ b/devtools/client/fronts/manifest.js
@@ -0,0 +1,23 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { manifestSpec } = require("devtools/shared/specs/manifest");
+
+class ManifestFront extends FrontClassWithSpec(manifestSpec) {
+ constructor(client) {
+ super(client);
+
+ // attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "manifestActor";
+ }
+}
+
+exports.ManifestFront = ManifestFront;
+registerFront(ManifestFront);
diff --git a/devtools/client/fronts/media-rule.js b/devtools/client/fronts/media-rule.js
new file mode 100644
index 0000000000..48f23aa61e
--- /dev/null
+++ b/devtools/client/fronts/media-rule.js
@@ -0,0 +1,54 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { mediaRuleSpec } = require("devtools/shared/specs/media-rule");
+
+/**
+ * Corresponding client-side front for a MediaRuleActor.
+ */
+class MediaRuleFront extends FrontClassWithSpec(mediaRuleSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._onMatchesChange = this._onMatchesChange.bind(this);
+ this.on("matches-change", this._onMatchesChange);
+ }
+
+ _onMatchesChange(matches) {
+ this._form.matches = matches;
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ }
+
+ get mediaText() {
+ return this._form.mediaText;
+ }
+ get conditionText() {
+ return this._form.conditionText;
+ }
+ get matches() {
+ return this._form.matches;
+ }
+ get line() {
+ return this._form.line || -1;
+ }
+ get column() {
+ return this._form.column || -1;
+ }
+ get parentStyleSheet() {
+ return this.conn.getFrontByID(this._form.parentStyleSheet);
+ }
+}
+
+exports.MediaRuleFront = MediaRuleFront;
+registerFront(MediaRuleFront);
diff --git a/devtools/client/fronts/memory.js b/devtools/client/fronts/memory.js
new file mode 100644
index 0000000000..aa2bda6ec9
--- /dev/null
+++ b/devtools/client/fronts/memory.js
@@ -0,0 +1,117 @@
+/* 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 { memorySpec } = require("devtools/shared/specs/memory");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+loader.lazyRequireGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "HeapSnapshotFileUtils",
+ "devtools/shared/heapsnapshot/HeapSnapshotFileUtils"
+);
+
+class MemoryFront extends FrontClassWithSpec(memorySpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._client = client;
+ this.heapSnapshotFileActorID = null;
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "memoryActor";
+ }
+
+ /**
+ * Save a heap snapshot, transfer it from the server to the client if the
+ * server and client do not share a file system, and return the local file
+ * path to the heap snapshot.
+ *
+ * Note that this is safe to call for actors inside sandoxed child processes,
+ * as we jump through the correct IPDL hoops.
+ *
+ * @params Boolean options.forceCopy
+ * Always force a bulk data copy of the saved heap snapshot, even when
+ * the server and client share a file system.
+ *
+ * @params {Object|undefined} options.boundaries
+ * The boundaries for the heap snapshot. See
+ * ChromeUtils.webidl for more details.
+ *
+ * @returns Promise<String>
+ */
+ async saveHeapSnapshot(options = {}) {
+ const snapshotId = await super.saveHeapSnapshot(options.boundaries);
+
+ if (
+ !options.forceCopy &&
+ (await HeapSnapshotFileUtils.haveHeapSnapshotTempFile(snapshotId))
+ ) {
+ return HeapSnapshotFileUtils.getHeapSnapshotTempFilePath(snapshotId);
+ }
+
+ return this.transferHeapSnapshot(snapshotId);
+ }
+
+ /**
+ * Given that we have taken a heap snapshot with the given id, transfer the
+ * heap snapshot file to the client. The path to the client's local file is
+ * returned.
+ *
+ * @param {String} snapshotId
+ *
+ * @returns Promise<String>
+ */
+ async transferHeapSnapshot(snapshotId) {
+ if (!this.heapSnapshotFileActorID) {
+ const form = await this._client.mainRoot.rootForm;
+ this.heapSnapshotFileActorID = form.heapSnapshotFileActor;
+ }
+
+ try {
+ const request = this._client.request({
+ to: this.heapSnapshotFileActorID,
+ type: "transferHeapSnapshot",
+ snapshotId,
+ });
+
+ const outFilePath = HeapSnapshotFileUtils.getNewUniqueHeapSnapshotTempFilePath();
+ const outFile = new FileUtils.File(outFilePath);
+ const outFileStream = FileUtils.openSafeFileOutputStream(outFile);
+
+ // This request is a bulk request. That's why the result of the request is
+ // an object with the `copyTo` function that can transfer the data to
+ // another stream.
+ // See devtools/shared/transport/transport.js to know more about this mode.
+ const { copyTo } = await request;
+ await copyTo(outFileStream);
+
+ FileUtils.closeSafeFileOutputStream(outFileStream);
+ return outFilePath;
+ } catch (e) {
+ if (e.error) {
+ // This isn't a real error, rather this is a message coming from the
+ // server. So let's throw a real error instead.
+ throw new Error(
+ `The server's actor threw an error: (${e.error}) ${e.message}`
+ );
+ }
+
+ // Otherwise, rethrow the error
+ throw e;
+ }
+ }
+}
+
+exports.MemoryFront = MemoryFront;
+registerFront(MemoryFront);
diff --git a/devtools/client/fronts/moz.build b/devtools/client/fronts/moz.build
new file mode 100644
index 0000000000..1f808c21d7
--- /dev/null
+++ b/devtools/client/fronts/moz.build
@@ -0,0 +1,60 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "addon",
+ "descriptors",
+ "inspector",
+ "targets",
+ "worker",
+]
+
+DevToolsModules(
+ "accessibility.js",
+ "animation.js",
+ "array-buffer.js",
+ "breakpoint-list.js",
+ "changes.js",
+ "compatibility.js",
+ "content-viewer.js",
+ "css-properties.js",
+ "device.js",
+ "eventsource.js",
+ "frame.js",
+ "framerate.js",
+ "highlighters.js",
+ "inspector.js",
+ "layout.js",
+ "manifest.js",
+ "media-rule.js",
+ "memory.js",
+ "network-content.js",
+ "network-parent.js",
+ "node.js",
+ "object.js",
+ "page-style.js",
+ "perf.js",
+ "performance-recording.js",
+ "performance.js",
+ "preference.js",
+ "property-iterator.js",
+ "reflow.js",
+ "responsive.js",
+ "root.js",
+ "screenshot.js",
+ "source.js",
+ "storage.js",
+ "string.js",
+ "style-rule.js",
+ "style-sheet.js",
+ "style-sheets.js",
+ "symbol-iterator.js",
+ "thread.js",
+ "walker.js",
+ "watcher.js",
+ "webconsole.js",
+ "websocket.js",
+)
diff --git a/devtools/client/fronts/network-content.js b/devtools/client/fronts/network-content.js
new file mode 100644
index 0000000000..fee47c7429
--- /dev/null
+++ b/devtools/client/fronts/network-content.js
@@ -0,0 +1,22 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { networkContentSpec } = require("devtools/shared/specs/network-content");
+
+class NetworkContentFront extends FrontClassWithSpec(networkContentSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "networkContentActor";
+ }
+}
+
+exports.NetworkContentFront = NetworkContentFront;
+registerFront(NetworkContentFront);
diff --git a/devtools/client/fronts/network-parent.js b/devtools/client/fronts/network-parent.js
new file mode 100644
index 0000000000..7b918e5509
--- /dev/null
+++ b/devtools/client/fronts/network-parent.js
@@ -0,0 +1,25 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { networkParentSpec } = require("devtools/shared/specs/network-parent");
+
+/**
+ * Client side for the network actor, used for managing network requests.
+ */
+
+class NetworkParentFront extends FrontClassWithSpec(networkParentSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "networkParentActor";
+ }
+}
+
+registerFront(NetworkParentFront);
diff --git a/devtools/client/fronts/node.js b/devtools/client/fronts/node.js
new file mode 100644
index 0000000000..261e2aaf9c
--- /dev/null
+++ b/devtools/client/fronts/node.js
@@ -0,0 +1,545 @@
+/* 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 promise = require("promise");
+const {
+ FrontClassWithSpec,
+ types,
+ registerFront,
+} = require("devtools/shared/protocol.js");
+const { nodeSpec, nodeListSpec } = require("devtools/shared/specs/node");
+const { SimpleStringFront } = require("devtools/client/fronts/string");
+const Services = require("Services");
+
+loader.lazyRequireGetter(
+ this,
+ "nodeConstants",
+ "devtools/shared/dom-node-constants"
+);
+
+const BROWSER_TOOLBOX_FISSION_ENABLED = Services.prefs.getBoolPref(
+ "devtools.browsertoolbox.fission",
+ false
+);
+
+const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
+
+/**
+ * Client side of a node list as returned by querySelectorAll()
+ */
+class NodeListFront extends FrontClassWithSpec(nodeListSpec) {
+ marshallPool() {
+ return this.getParent();
+ }
+
+ // Update the object given a form representation off the wire.
+ form(json) {
+ this.length = json.length;
+ }
+
+ item(index) {
+ return super.item(index).then(response => {
+ return response.node;
+ });
+ }
+
+ items(start, end) {
+ return super.items(start, end).then(response => {
+ return response.nodes;
+ });
+ }
+}
+
+exports.NodeListFront = NodeListFront;
+registerFront(NodeListFront);
+
+/**
+ * Convenience API for building a list of attribute modifications
+ * for the `modifyAttributes` request.
+ */
+class AttributeModificationList {
+ constructor(node) {
+ this.node = node;
+ this.modifications = [];
+ }
+
+ apply() {
+ const ret = this.node.modifyAttributes(this.modifications);
+ return ret;
+ }
+
+ destroy() {
+ this.node = null;
+ this.modification = null;
+ }
+
+ setAttributeNS(ns, name, value) {
+ this.modifications.push({
+ attributeNamespace: ns,
+ attributeName: name,
+ newValue: value,
+ });
+ }
+
+ setAttribute(name, value) {
+ this.setAttributeNS(undefined, name, value);
+ }
+
+ removeAttributeNS(ns, name) {
+ this.setAttributeNS(ns, name, undefined);
+ }
+
+ removeAttribute(name) {
+ this.setAttributeNS(undefined, name, undefined);
+ }
+}
+
+/**
+ * Client side of the node actor.
+ *
+ * Node fronts are strored in a tree that mirrors the DOM tree on the
+ * server, but with a few key differences:
+ * - Not all children will be necessary loaded for each node.
+ * - The order of children isn't guaranteed to be the same as the DOM.
+ * Children are stored in a doubly-linked list, to make addition/removal
+ * and traversal quick.
+ *
+ * Due to the order/incompleteness of the child list, it is safe to use
+ * the parent node from clients, but the `children` request should be used
+ * to traverse children.
+ */
+class NodeFront extends FrontClassWithSpec(nodeSpec) {
+ constructor(conn, targetFront, parentFront) {
+ super(conn, targetFront, parentFront);
+ // The parent node
+ this._parent = null;
+ // The first child of this node.
+ this._child = null;
+ // The next sibling of this node.
+ this._next = null;
+ // The previous sibling of this node.
+ this._prev = null;
+ }
+
+ /**
+ * Destroy a node front. The node must have been removed from the
+ * ownership tree before this is called, unless the whole walker front
+ * is being destroyed.
+ */
+ destroy() {
+ super.destroy();
+ }
+
+ // Update the object given a form representation off the wire.
+ form(form, ctx) {
+ // backward-compatibility: shortValue indicates we are connected to old server
+ if (form.shortValue) {
+ // If the value is not complete, set nodeValue to null, it will be fetched
+ // when calling getNodeValue()
+ form.nodeValue = form.incompleteValue ? null : form.shortValue;
+ }
+
+ this.traits = form.traits || {};
+
+ // Shallow copy of the form. We could just store a reference, but
+ // eventually we'll want to update some of the data.
+ this._form = Object.assign({}, form);
+ this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
+
+ if (form.parent) {
+ // Get the owner actor for this actor (the walker), and find the
+ // parent node of this actor from it, creating a standin node if
+ // necessary.
+ const parentNodeFront = ctx
+ .marshallPool()
+ .ensureDOMNodeFront(form.parent);
+ this.reparent(parentNodeFront);
+ }
+
+ if (form.host) {
+ this.host = ctx.marshallPool().ensureDOMNodeFront(form.host);
+ }
+
+ if (form.inlineTextChild) {
+ this.inlineTextChild = types
+ .getType("domnode")
+ .read(form.inlineTextChild, ctx);
+ } else {
+ this.inlineTextChild = undefined;
+ }
+ }
+
+ /**
+ * Returns the parent NodeFront for this NodeFront.
+ */
+ parentNode() {
+ return this._parent;
+ }
+
+ /**
+ * Returns the NodeFront corresponding to the parentNode of this NodeFront, or the
+ * NodeFront corresponding to the host element for shadowRoot elements.
+ */
+ parentOrHost() {
+ return this.isShadowRoot ? this.host : this._parent;
+ }
+
+ /**
+ * Process a mutation entry as returned from the walker's `getMutations`
+ * request. Only tries to handle changes of the node's contents
+ * themselves (character data and attribute changes), the walker itself
+ * will keep the ownership tree up to date.
+ */
+ updateMutation(change) {
+ if (change.type === "attributes") {
+ // We'll need to lazily reparse the attributes after this change.
+ this._attrMap = undefined;
+
+ // Update any already-existing attributes.
+ let found = false;
+ for (let i = 0; i < this.attributes.length; i++) {
+ const attr = this.attributes[i];
+ if (
+ attr.name == change.attributeName &&
+ attr.namespace == change.attributeNamespace
+ ) {
+ if (change.newValue !== null) {
+ attr.value = change.newValue;
+ } else {
+ this.attributes.splice(i, 1);
+ }
+ found = true;
+ break;
+ }
+ }
+ // This is a new attribute. The null check is because of Bug 1192270,
+ // in the case of a newly added then removed attribute
+ if (!found && change.newValue !== null) {
+ this.attributes.push({
+ name: change.attributeName,
+ namespace: change.attributeNamespace,
+ value: change.newValue,
+ });
+ }
+ } else if (change.type === "characterData") {
+ this._form.nodeValue = change.newValue;
+ } else if (change.type === "pseudoClassLock") {
+ this._form.pseudoClassLocks = change.pseudoClassLocks;
+ } else if (change.type === "events") {
+ this._form.hasEventListeners = change.hasEventListeners;
+ } else if (change.type === "mutationBreakpoint") {
+ this._form.mutationBreakpoints = change.mutationBreakpoints;
+ }
+ }
+
+ // Some accessors to make NodeFront feel more like a Node
+
+ get id() {
+ return this.getAttribute("id");
+ }
+
+ get nodeType() {
+ return this._form.nodeType;
+ }
+ get namespaceURI() {
+ return this._form.namespaceURI;
+ }
+ get nodeName() {
+ return this._form.nodeName;
+ }
+ get displayName() {
+ const { displayName, nodeName } = this._form;
+
+ // Keep `nodeName.toLowerCase()` for backward compatibility
+ return displayName || nodeName.toLowerCase();
+ }
+ get doctypeString() {
+ return (
+ "<!DOCTYPE " +
+ this._form.name +
+ (this._form.publicId ? ' PUBLIC "' + this._form.publicId + '"' : "") +
+ (this._form.systemId ? ' "' + this._form.systemId + '"' : "") +
+ ">"
+ );
+ }
+
+ get baseURI() {
+ return this._form.baseURI;
+ }
+
+ get className() {
+ return this.getAttribute("class") || "";
+ }
+
+ get hasChildren() {
+ return this._form.numChildren > 0;
+ }
+ get numChildren() {
+ return this._form.numChildren;
+ }
+ get remoteFrame() {
+ return BROWSER_TOOLBOX_FISSION_ENABLED && this._form.remoteFrame;
+ }
+ get hasEventListeners() {
+ return this._form.hasEventListeners;
+ }
+
+ get isMarkerPseudoElement() {
+ return this._form.isMarkerPseudoElement;
+ }
+ get isBeforePseudoElement() {
+ return this._form.isBeforePseudoElement;
+ }
+ get isAfterPseudoElement() {
+ return this._form.isAfterPseudoElement;
+ }
+ get isPseudoElement() {
+ return (
+ this.isBeforePseudoElement ||
+ this.isAfterPseudoElement ||
+ this.isMarkerPseudoElement
+ );
+ }
+ get isAnonymous() {
+ return this._form.isAnonymous;
+ }
+ get isInHTMLDocument() {
+ return this._form.isInHTMLDocument;
+ }
+ get tagName() {
+ return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null;
+ }
+
+ get isDocumentElement() {
+ return !!this._form.isDocumentElement;
+ }
+
+ get isTopLevelDocument() {
+ return this._form.isTopLevelDocument;
+ }
+
+ get isShadowRoot() {
+ return this._form.isShadowRoot;
+ }
+
+ get shadowRootMode() {
+ return this._form.shadowRootMode;
+ }
+
+ get isShadowHost() {
+ return this._form.isShadowHost;
+ }
+
+ get customElementLocation() {
+ return this._form.customElementLocation;
+ }
+
+ get isDirectShadowHostChild() {
+ return this._form.isDirectShadowHostChild;
+ }
+
+ // doctype properties
+ get name() {
+ return this._form.name;
+ }
+ get publicId() {
+ return this._form.publicId;
+ }
+ get systemId() {
+ return this._form.systemId;
+ }
+
+ getAttribute(name) {
+ const attr = this._getAttribute(name);
+ return attr ? attr.value : null;
+ }
+ hasAttribute(name) {
+ this._cacheAttributes();
+ return name in this._attrMap;
+ }
+
+ get hidden() {
+ const cls = this.getAttribute("class");
+ return cls && cls.indexOf(HIDDEN_CLASS) > -1;
+ }
+
+ get attributes() {
+ return this._form.attrs;
+ }
+
+ get mutationBreakpoints() {
+ return this._form.mutationBreakpoints;
+ }
+
+ get pseudoClassLocks() {
+ return this._form.pseudoClassLocks || [];
+ }
+ hasPseudoClassLock(pseudo) {
+ return this.pseudoClassLocks.some(locked => locked === pseudo);
+ }
+
+ get displayType() {
+ return this._form.displayType;
+ }
+
+ get isDisplayed() {
+ return this._form.isDisplayed;
+ }
+
+ get isScrollable() {
+ return this._form.isScrollable;
+ }
+
+ get causesOverflow() {
+ return this._form.causesOverflow;
+ }
+
+ get isTreeDisplayed() {
+ let parent = this;
+ while (parent) {
+ if (!parent.isDisplayed) {
+ return false;
+ }
+ parent = parent.parentNode();
+ }
+ return true;
+ }
+
+ get inspectorFront() {
+ return this.parentFront.parentFront;
+ }
+
+ get walkerFront() {
+ return this.parentFront;
+ }
+
+ getNodeValue() {
+ // backward-compatibility: if nodevalue is null and shortValue is defined, the actual
+ // value of the node needs to be fetched on the server.
+ if (this._form.nodeValue === null && this._form.shortValue) {
+ return super.getNodeValue();
+ }
+
+ const str = this._form.nodeValue || "";
+ return promise.resolve(new SimpleStringFront(str));
+ }
+
+ /**
+ * Return a new AttributeModificationList for this node.
+ */
+ startModifyingAttributes() {
+ return new AttributeModificationList(this);
+ }
+
+ _cacheAttributes() {
+ if (typeof this._attrMap != "undefined") {
+ return;
+ }
+ this._attrMap = {};
+ for (const attr of this.attributes) {
+ this._attrMap[attr.name] = attr;
+ }
+ }
+
+ _getAttribute(name) {
+ this._cacheAttributes();
+ return this._attrMap[name] || undefined;
+ }
+
+ /**
+ * Set this node's parent. Note that the children saved in
+ * this tree are unordered and incomplete, so shouldn't be used
+ * instead of a `children` request.
+ */
+ reparent(parent) {
+ if (this._parent === parent) {
+ return;
+ }
+
+ if (this._parent && this._parent._child === this) {
+ this._parent._child = this._next;
+ }
+ if (this._prev) {
+ this._prev._next = this._next;
+ }
+ if (this._next) {
+ this._next._prev = this._prev;
+ }
+ this._next = null;
+ this._prev = null;
+ this._parent = parent;
+ if (!parent) {
+ // Subtree is disconnected, we're done
+ return;
+ }
+ this._next = parent._child;
+ if (this._next) {
+ this._next._prev = this;
+ }
+ parent._child = this;
+ }
+
+ /**
+ * Return all the known children of this node.
+ */
+ treeChildren() {
+ const ret = [];
+ for (let child = this._child; child != null; child = child._next) {
+ ret.push(child);
+ }
+ return ret;
+ }
+
+ /**
+ * Do we use a local target?
+ * Useful to know if a rawNode is available or not.
+ *
+ * This will, one day, be removed. External code should
+ * not need to know if the target is remote or not.
+ */
+ isLocalToBeDeprecated() {
+ return !!this.conn._transport._serverConnection;
+ }
+
+ /**
+ * Get a Node for the given node front. This only works locally,
+ * and is only intended as a stopgap during the transition to the remote
+ * protocol. If you depend on this you're likely to break soon.
+ */
+ rawNode(rawNode) {
+ if (!this.isLocalToBeDeprecated()) {
+ console.warn("Tried to use rawNode on a remote connection.");
+ return null;
+ }
+ const { DevToolsServer } = require("devtools/server/devtools-server");
+ const actor = DevToolsServer.searchAllConnectionsForActor(this.actorID);
+ if (!actor) {
+ // Can happen if we try to get the raw node for an already-expired
+ // actor.
+ return null;
+ }
+ return actor.rawNode;
+ }
+
+ async connectToRemoteFrame() {
+ if (!this.remoteFrame) {
+ console.warn("Tried to open remote connection to an invalid frame.");
+ return null;
+ }
+ if (this._remoteFrameTarget && !this._remoteFrameTarget.isDestroyed()) {
+ return this._remoteFrameTarget;
+ }
+
+ // Get the target for this remote frame element
+ this._remoteFrameTarget = await this.targetFront.getBrowsingContextTarget(
+ this._form.browsingContextID
+ );
+ return this._remoteFrameTarget;
+ }
+}
+
+exports.NodeFront = NodeFront;
+registerFront(NodeFront);
diff --git a/devtools/client/fronts/object.js b/devtools/client/fronts/object.js
new file mode 100644
index 0000000000..774538ec85
--- /dev/null
+++ b/devtools/client/fronts/object.js
@@ -0,0 +1,441 @@
+/* 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 { objectSpec } = require("devtools/shared/specs/object");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { LongStringFront } = require("devtools/client/fronts/string");
+
+/**
+ * A ObjectFront is used as a front end for the ObjectActor that is
+ * created on the server, hiding implementation details.
+ */
+class ObjectFront extends FrontClassWithSpec(objectSpec) {
+ constructor(conn = null, targetFront = null, parentFront = null, data) {
+ if (!parentFront) {
+ throw new Error("ObjectFront require a parent front");
+ }
+
+ super(conn, targetFront, parentFront);
+
+ this._grip = data;
+ this.actorID = this._grip.actor;
+ this.valid = true;
+
+ parentFront.manage(this);
+ }
+
+ skipDestroy() {
+ // Object fronts are simple fronts, they don't need to be cleaned up on
+ // toolbox destroy. `conn` is a DebuggerClient instance, check the
+ // `isToolboxDestroy` flag to skip the destroy.
+ return this.conn && this.conn.isToolboxDestroy;
+ }
+
+ getGrip() {
+ return this._grip;
+ }
+
+ get isFrozen() {
+ return this._grip.frozen;
+ }
+
+ get isSealed() {
+ return this._grip.sealed;
+ }
+
+ get isExtensible() {
+ return this._grip.extensible;
+ }
+
+ /**
+ * Request the names of a function's formal parameters.
+ */
+ getParameterNames() {
+ if (this._grip.class !== "Function") {
+ console.error("getParameterNames is only valid for function grips.");
+ return null;
+ }
+ return super.parameterNames();
+ }
+
+ /**
+ * Request the names of the properties defined on the object and not its
+ * prototype.
+ */
+ getOwnPropertyNames() {
+ return super.ownPropertyNames();
+ }
+
+ /**
+ * Request the prototype and own properties of the object.
+ */
+ async getPrototypeAndProperties() {
+ const result = await super.prototypeAndProperties();
+
+ if (result.prototype) {
+ result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this);
+ }
+
+ // The result packet can have multiple properties that hold grips which we may need
+ // to turn into fronts.
+ const gripKeys = ["value", "getterValue", "get", "set"];
+
+ if (result.ownProperties) {
+ Object.entries(result.ownProperties).forEach(([key, descriptor]) => {
+ if (descriptor) {
+ for (const gripKey of gripKeys) {
+ if (descriptor.hasOwnProperty(gripKey)) {
+ result.ownProperties[key][gripKey] = getAdHocFrontOrPrimitiveGrip(
+ descriptor[gripKey],
+ this
+ );
+ }
+ }
+ }
+ });
+ }
+
+ if (result.safeGetterValues) {
+ Object.entries(result.safeGetterValues).forEach(([key, descriptor]) => {
+ if (descriptor) {
+ for (const gripKey of gripKeys) {
+ if (descriptor.hasOwnProperty(gripKey)) {
+ result.safeGetterValues[key][
+ gripKey
+ ] = getAdHocFrontOrPrimitiveGrip(descriptor[gripKey], this);
+ }
+ }
+ }
+ });
+ }
+
+ if (result.ownSymbols) {
+ result.ownSymbols.forEach((descriptor, i, arr) => {
+ if (descriptor) {
+ for (const gripKey of gripKeys) {
+ if (descriptor.hasOwnProperty(gripKey)) {
+ arr[i][gripKey] = getAdHocFrontOrPrimitiveGrip(
+ descriptor[gripKey],
+ this
+ );
+ }
+ }
+ }
+ });
+ }
+
+ return result;
+ }
+
+ /**
+ * Request a PropertyIteratorFront instance to ease listing
+ * properties for this object.
+ *
+ * @param options Object
+ * A dictionary object with various boolean attributes:
+ * - ignoreIndexedProperties Boolean
+ * If true, filters out Array items.
+ * e.g. properties names between `0` and `object.length`.
+ * - ignoreNonIndexedProperties Boolean
+ * If true, filters out items that aren't array items
+ * e.g. properties names that are not a number between `0`
+ * and `object.length`.
+ * - sort Boolean
+ * If true, the iterator will sort the properties by name
+ * before dispatching them.
+ */
+ enumProperties(options) {
+ return super.enumProperties(options);
+ }
+
+ /**
+ * Request a PropertyIteratorFront instance to enumerate entries in a
+ * Map/Set-like object.
+ */
+ enumEntries() {
+ if (
+ !["Map", "WeakMap", "Set", "WeakSet", "Storage"].includes(
+ this._grip.class
+ )
+ ) {
+ console.error(
+ "enumEntries is only valid for Map/Set/Storage-like grips."
+ );
+ return null;
+ }
+ return super.enumEntries();
+ }
+
+ /**
+ * Request a SymbolIteratorFront instance to enumerate symbols in an object.
+ */
+ enumSymbols() {
+ if (this._grip.type !== "object") {
+ console.error("enumSymbols is only valid for objects grips.");
+ return null;
+ }
+ return super.enumSymbols();
+ }
+
+ /**
+ * Request the property descriptor of the object's specified property.
+ *
+ * @param name string The name of the requested property.
+ */
+ getProperty(name) {
+ return super.property(name);
+ }
+
+ /**
+ * Request the value of the object's specified property.
+ *
+ * @param name string The name of the requested property.
+ * @param receiverId string|null The actorId of the receiver to be used for getters.
+ */
+ async getPropertyValue(name, receiverId) {
+ const response = await super.propertyValue(name, receiverId);
+
+ if (response.value) {
+ const { value } = response;
+ if (value.return) {
+ response.value.return = getAdHocFrontOrPrimitiveGrip(
+ value.return,
+ this
+ );
+ }
+
+ if (value.throw) {
+ response.value.throw = getAdHocFrontOrPrimitiveGrip(value.throw, this);
+ }
+ }
+ return response;
+ }
+
+ /**
+ * Request the prototype of the object.
+ */
+ async getPrototype() {
+ const result = await super.prototype();
+
+ if (!result.prototype) {
+ return result;
+ }
+
+ result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this);
+
+ return result;
+ }
+
+ /**
+ * Request the display string of the object.
+ */
+ getDisplayString() {
+ return super.displayString();
+ }
+
+ /**
+ * Request the state of a promise.
+ */
+ async getPromiseState() {
+ if (this._grip.class !== "Promise") {
+ console.error("getPromiseState is only valid for promise grips.");
+ return null;
+ }
+
+ let response, promiseState;
+ try {
+ response = await super.promiseState();
+ promiseState = response.promiseState;
+ } catch (error) {
+ // @backward-compat { version 85 } On older server, the promiseState request didn't
+ // didn't exist (bug 1552648). The promise state was directly included in the grip.
+ if (error.message.includes("unrecognizedPacketType")) {
+ promiseState = this._grip.promiseState;
+ response = { promiseState };
+ } else {
+ throw error;
+ }
+ }
+
+ const { value, reason } = promiseState;
+
+ if (value) {
+ promiseState.value = getAdHocFrontOrPrimitiveGrip(value, this);
+ }
+
+ if (reason) {
+ promiseState.reason = getAdHocFrontOrPrimitiveGrip(reason, this);
+ }
+
+ return response;
+ }
+
+ /**
+ * Request the target and handler internal slots of a proxy.
+ */
+ async getProxySlots() {
+ if (this._grip.class !== "Proxy") {
+ console.error("getProxySlots is only valid for proxy grips.");
+ return null;
+ }
+
+ const response = await super.proxySlots();
+ const { proxyHandler, proxyTarget } = response;
+
+ if (proxyHandler) {
+ response.proxyHandler = getAdHocFrontOrPrimitiveGrip(proxyHandler, this);
+ }
+
+ if (proxyTarget) {
+ response.proxyTarget = getAdHocFrontOrPrimitiveGrip(proxyTarget, this);
+ }
+
+ return response;
+ }
+
+ get isSyntaxError() {
+ return this._grip.preview && this._grip.preview.name == "SyntaxError";
+ }
+}
+
+/**
+ * When we are asking the server for the value of a given variable, we might get different
+ * type of objects:
+ * - a primitive (string, number, null, false, boolean)
+ * - a long string
+ * - an "object" (i.e. not primitive nor long string)
+ *
+ * Each of those type need a different front, or none:
+ * - a primitive does not allow further interaction with the server, so we don't need
+ * to have a dedicated front.
+ * - a long string needs a longStringFront to be able to retrieve the full string.
+ * - an object need an objectFront to retrieve properties, symbols and prototype.
+ *
+ * In the case an ObjectFront is created, we also check if the object has properties
+ * that should be turned into fronts as well.
+ *
+ * @param {String|Number|Object} options: The packet returned by the server.
+ * @param {Front} parentFront
+ *
+ * @returns {Number|String|Object|LongStringFront|ObjectFront}
+ */
+function getAdHocFrontOrPrimitiveGrip(packet, parentFront) {
+ // We only want to try to create a front when it makes sense, i.e when it has an
+ // actorID, unless:
+ // - it's a Symbol (See Bug 1600299)
+ // - it's a mapEntry (the preview.key and preview.value properties can hold actors)
+ // - or it is already a front (happens when we are using the legacy listeners in the ResourceWatcher)
+ const isPacketAnObject = packet && typeof packet === "object";
+ const isFront = !!packet.typeName;
+ if (
+ !isPacketAnObject ||
+ packet.type == "symbol" ||
+ (packet.type !== "mapEntry" && !packet.actor) ||
+ isFront
+ ) {
+ return packet;
+ }
+
+ const { conn } = parentFront;
+ // If the parent front is a target, consider it as the target to use for all objects
+ const targetFront = parentFront.isTargetFront
+ ? parentFront
+ : parentFront.targetFront;
+
+ // We may have already created a front for this object actor since some actor (e.g. the
+ // thread actor) cache the object actors they create.
+ const existingFront = conn.getFrontByID(packet.actor);
+ if (existingFront) {
+ return existingFront;
+ }
+
+ const { type } = packet;
+
+ if (type === "longString") {
+ const longStringFront = new LongStringFront(conn, targetFront, parentFront);
+ longStringFront.form(packet);
+ parentFront.manage(longStringFront);
+ return longStringFront;
+ }
+
+ if (type === "mapEntry" && packet.preview) {
+ const { key, value } = packet.preview;
+ packet.preview.key = getAdHocFrontOrPrimitiveGrip(
+ key,
+ parentFront,
+ targetFront
+ );
+ packet.preview.value = getAdHocFrontOrPrimitiveGrip(
+ value,
+ parentFront,
+ targetFront
+ );
+ return packet;
+ }
+
+ const objectFront = new ObjectFront(conn, targetFront, parentFront, packet);
+ createChildFronts(objectFront, packet);
+ return objectFront;
+}
+
+/**
+ * Create child fronts of the passed object front given a packet. Those child fronts are
+ * usually mapping actors of the packet sub-properties (preview items, promise fullfilled
+ * values, …).
+ *
+ * @param {ObjectFront} objectFront
+ * @param {String|Number|Object} packet: The packet returned by the server
+ */
+function createChildFronts(objectFront, packet) {
+ if (packet.preview) {
+ const { message, entries } = packet.preview;
+
+ // The message could be a longString.
+ if (packet.preview.message) {
+ packet.preview.message = getAdHocFrontOrPrimitiveGrip(
+ message,
+ objectFront
+ );
+ }
+
+ // Handle Map/WeakMap preview entries (the preview might be directly used if has all the
+ // items needed, i.e. if the Map has less than 10 items).
+ if (entries && Array.isArray(entries)) {
+ packet.preview.entries = entries.map(([key, value]) => [
+ getAdHocFrontOrPrimitiveGrip(key, objectFront),
+ getAdHocFrontOrPrimitiveGrip(value, objectFront),
+ ]);
+ }
+ }
+
+ if (packet && typeof packet.ownProperties === "object") {
+ for (const [name, descriptor] of Object.entries(packet.ownProperties)) {
+ // The descriptor can have multiple properties that hold grips which we may need
+ // to turn into fronts.
+ const gripKeys = ["value", "getterValue", "get", "set"];
+ for (const key of gripKeys) {
+ if (
+ descriptor &&
+ typeof descriptor === "object" &&
+ descriptor.hasOwnProperty(key)
+ ) {
+ packet.ownProperties[name][key] = getAdHocFrontOrPrimitiveGrip(
+ descriptor[key],
+ objectFront
+ );
+ }
+ }
+ }
+ }
+}
+
+registerFront(ObjectFront);
+
+exports.ObjectFront = ObjectFront;
+exports.getAdHocFrontOrPrimitiveGrip = getAdHocFrontOrPrimitiveGrip;
diff --git a/devtools/client/fronts/page-style.js b/devtools/client/fronts/page-style.js
new file mode 100644
index 0000000000..7157065452
--- /dev/null
+++ b/devtools/client/fronts/page-style.js
@@ -0,0 +1,121 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { pageStyleSpec } = require("devtools/shared/specs/page-style");
+
+/**
+ * PageStyleFront, the front object for the PageStyleActor
+ */
+class PageStyleFront extends FrontClassWithSpec(pageStyleSpec) {
+ _attributesCache = new Map();
+
+ constructor(conn, targetFront, parentFront) {
+ super(conn, targetFront, parentFront);
+ this.inspector = this.getParent();
+
+ this._clearAttributesCache = this._clearAttributesCache.bind(this);
+ this.on("stylesheet-updated", this._clearAttributesCache);
+ this.walker.on("new-mutations", this._clearAttributesCache);
+ }
+
+ form(form) {
+ this._form = form;
+ }
+
+ get walker() {
+ return this.inspector.walker;
+ }
+
+ get supportsFontStretchLevel4() {
+ return this._form.traits && this._form.traits.fontStretchLevel4;
+ }
+
+ get supportsFontStyleLevel4() {
+ return this._form.traits && this._form.traits.fontStyleLevel4;
+ }
+
+ get supportsFontVariations() {
+ return this._form.traits && this._form.traits.fontVariations;
+ }
+
+ get supportsFontWeightLevel4() {
+ return this._form.traits && this._form.traits.fontWeightLevel4;
+ }
+
+ getMatchedSelectors(node, property, options) {
+ return super.getMatchedSelectors(node, property, options).then(ret => {
+ return ret.matched;
+ });
+ }
+
+ async getApplied(node, options = {}) {
+ const ret = await super.getApplied(node, options);
+ return ret.entries;
+ }
+
+ addNewRule(node, pseudoClasses) {
+ return super.addNewRule(node, pseudoClasses).then(ret => {
+ return ret.entries[0];
+ });
+ }
+
+ /**
+ * Get an array of existing attribute values in a node document, given an attribute type.
+ *
+ * @param {String} search: A string to filter attribute value on.
+ * @param {String} attributeType: The type of attribute we want to retrieve the values.
+ * @param {Element} node: The element we want to get possible attributes for. This will
+ * be used to get the document where the search is happening.
+ * @returns {Array<String>} An array of strings
+ */
+ async getAttributesInOwnerDocument(search, attributeType, node) {
+ if (!attributeType) {
+ throw new Error("`type` should not be empty");
+ }
+
+ if (!search) {
+ return [];
+ }
+
+ const lcFilter = search.toLowerCase();
+
+ // If the new filter includes the string that was used on our last trip to the server,
+ // we can filter the cached results instead of calling the server again.
+ if (
+ this._attributesCache &&
+ this._attributesCache.has(attributeType) &&
+ search.startsWith(this._attributesCache.get(attributeType).search)
+ ) {
+ const cachedResults = this._attributesCache
+ .get(attributeType)
+ .results.filter(item => item.toLowerCase().startsWith(lcFilter));
+ this.emitForTests(
+ "getAttributesInOwnerDocument-cache-hit",
+ cachedResults
+ );
+ return cachedResults;
+ }
+
+ const results = await super.getAttributesInOwnerDocument(
+ search,
+ attributeType,
+ node
+ );
+ this._attributesCache.set(attributeType, { search, results });
+ return results;
+ }
+
+ _clearAttributesCache() {
+ this._attributesCache.clear();
+ }
+}
+
+exports.PageStyleFront = PageStyleFront;
+registerFront(PageStyleFront);
diff --git a/devtools/client/fronts/perf.js b/devtools/client/fronts/perf.js
new file mode 100644
index 0000000000..1a9b3ae9f6
--- /dev/null
+++ b/devtools/client/fronts/perf.js
@@ -0,0 +1,22 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { perfSpec } = require("devtools/shared/specs/perf");
+
+class PerfFront extends FrontClassWithSpec(perfSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "perfActor";
+ }
+}
+
+registerFront(PerfFront);
diff --git a/devtools/client/fronts/performance-recording.js b/devtools/client/fronts/performance-recording.js
new file mode 100644
index 0000000000..93015e8df1
--- /dev/null
+++ b/devtools/client/fronts/performance-recording.js
@@ -0,0 +1,171 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const {
+ performanceRecordingSpec,
+} = require("devtools/shared/specs/performance-recording");
+
+loader.lazyRequireGetter(
+ this,
+ "PerformanceIO",
+ "devtools/client/performance/modules/io"
+);
+loader.lazyRequireGetter(
+ this,
+ "PerformanceRecordingCommon",
+ "devtools/shared/performance/recording-common",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "RecordingUtils",
+ "devtools/shared/performance/recording-utils"
+);
+
+/**
+ * This can be used on older Profiler implementations, but the methods cannot
+ * be changed -- you must introduce a new method, and detect the server.
+ */
+class PerformanceRecordingFront extends FrontClassWithSpec(
+ performanceRecordingSpec
+) {
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ this._configuration = form.configuration;
+ this._startingBufferStatus = form.startingBufferStatus;
+ this._console = form.console;
+ this._label = form.label;
+ this._startTime = form.startTime;
+ this._localStartTime = form.localStartTime;
+ this._recording = form.recording;
+ this._completed = form.completed;
+ this._duration = form.duration;
+
+ if (form.finalizedData) {
+ this._profile = form.profile;
+ this._systemHost = form.systemHost;
+ this._systemClient = form.systemClient;
+ }
+
+ // Sort again on the client side if we're using realtime markers and the recording
+ // just finished. This is because GC/Compositing markers can come into the array out
+ // of order with the other markers, leading to strange collapsing in waterfall view.
+ if (this._completed && !this._markersSorted) {
+ this._markers = this._markers.sort((a, b) => a.start > b.start);
+ this._markersSorted = true;
+ }
+ }
+
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._markers = [];
+ this._frames = [];
+ this._memory = [];
+ this._ticks = [];
+ this._allocations = { sites: [], timestamps: [], frames: [], sizes: [] };
+ }
+
+ /**
+ * Saves the current recording to a file.
+ *
+ * @param nsIFile file
+ * The file to stream the data into.
+ */
+ exportRecording(file) {
+ const recordingData = this.getAllData();
+ return PerformanceIO.saveRecordingToFile(recordingData, file);
+ }
+
+ /**
+ * Fired whenever the PerformanceFront emits markers, memory or ticks.
+ */
+ _addTimelineData(eventName, data) {
+ const config = this.getConfiguration();
+
+ switch (eventName) {
+ // Accumulate timeline markers into an array. Furthermore, the timestamps
+ // do not have a zero epoch, so offset all of them by the start time.
+ case "markers": {
+ if (!config.withMarkers) {
+ break;
+ }
+ const { markers } = data;
+ RecordingUtils.offsetMarkerTimes(markers, this._startTime);
+ RecordingUtils.pushAll(this._markers, markers);
+ break;
+ }
+ // Accumulate stack frames into an array.
+ case "frames": {
+ if (!config.withMarkers) {
+ break;
+ }
+ const { frames } = data;
+ RecordingUtils.pushAll(this._frames, frames);
+ break;
+ }
+ // Accumulate memory measurements into an array. Furthermore, the timestamp
+ // does not have a zero epoch, so offset it by the actor's start time.
+ case "memory": {
+ if (!config.withMemory) {
+ break;
+ }
+ const { delta, measurement } = data;
+ this._memory.push({
+ delta: delta - this._startTime,
+ value: measurement.total / 1024 / 1024,
+ });
+ break;
+ }
+ // Save the accumulated refresh driver ticks.
+ case "ticks": {
+ if (!config.withTicks) {
+ break;
+ }
+ const { timestamps } = data;
+ this._ticks = timestamps;
+ break;
+ }
+ // Accumulate allocation sites into an array.
+ case "allocations": {
+ if (!config.withAllocations) {
+ break;
+ }
+ const {
+ allocations: sites,
+ allocationsTimestamps: timestamps,
+ allocationSizes: sizes,
+ frames,
+ } = data;
+
+ RecordingUtils.offsetAndScaleTimestamps(timestamps, this._startTime);
+ RecordingUtils.pushAll(this._allocations.sites, sites);
+ RecordingUtils.pushAll(this._allocations.timestamps, timestamps);
+ RecordingUtils.pushAll(this._allocations.frames, frames);
+ RecordingUtils.pushAll(this._allocations.sizes, sizes);
+ break;
+ }
+ }
+ }
+
+ toString() {
+ return "[object PerformanceRecordingFront]";
+ }
+}
+
+// PerformanceRecordingFront also needs to inherit from PerformanceRecordingCommon
+// but as ES classes don't support multiple inheritance, we are overriding the
+// prototype with PerformanceRecordingCommon methods.
+Object.defineProperties(
+ PerformanceRecordingFront.prototype,
+ Object.getOwnPropertyDescriptors(PerformanceRecordingCommon)
+);
+
+exports.PerformanceRecordingFront = PerformanceRecordingFront;
+registerFront(PerformanceRecordingFront);
diff --git a/devtools/client/fronts/performance.js b/devtools/client/fronts/performance.js
new file mode 100644
index 0000000000..4233efa52e
--- /dev/null
+++ b/devtools/client/fronts/performance.js
@@ -0,0 +1,167 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const {
+ PerformanceRecordingFront,
+} = require("devtools/client/fronts/performance-recording");
+const { performanceSpec } = require("devtools/shared/specs/performance");
+
+loader.lazyRequireGetter(
+ this,
+ "PerformanceIO",
+ "devtools/client/performance/modules/io"
+);
+loader.lazyRequireGetter(this, "getSystemInfo", "devtools/shared/system", true);
+
+class PerformanceFront extends FrontClassWithSpec(performanceSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._queuedRecordings = [];
+ this._onRecordingStartedEvent = this._onRecordingStartedEvent.bind(this);
+ this.flushQueuedRecordings = this.flushQueuedRecordings.bind(this);
+
+ this.before("profiler-status", this._onProfilerStatus.bind(this));
+ this.before("timeline-data", this._onTimelineEvent.bind(this));
+ this.on("recording-started", this._onRecordingStartedEvent);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "performanceActor";
+ }
+
+ async initialize() {
+ await this.connect();
+ }
+
+ /**
+ * Conenct to the server, and handle once-off tasks like storing traits
+ * or system info.
+ */
+ async connect() {
+ const systemClient = await getSystemInfo();
+ const { traits } = await super.connect({ systemClient });
+ this._traits = traits;
+
+ return this._traits;
+ }
+
+ /**
+ * Called when the "recording-started" event comes from the PerformanceFront.
+ * this is only used to queue up observed recordings before the performance tool can
+ * handle them, which will only occur when `console.profile()` recordings are started
+ * before the tool loads.
+ */
+ async _onRecordingStartedEvent(recording) {
+ this._queuedRecordings.push(recording);
+ }
+
+ flushQueuedRecordings() {
+ this.off("recording-started", this._onPerformanceFrontEvent);
+ const recordings = this._queuedRecordings;
+ this._queuedRecordings = [];
+ return recordings;
+ }
+
+ get traits() {
+ if (!this._traits) {
+ Cu.reportError(
+ "Cannot access traits of PerformanceFront before " +
+ "calling `connect()`."
+ );
+ }
+ return this._traits;
+ }
+
+ /**
+ * Pass in a PerformanceRecording and get a normalized value from 0 to 1 of how much
+ * of this recording's lifetime remains without being overwritten.
+ *
+ * @param {PerformanceRecording} recording
+ * @return {number?}
+ */
+ getBufferUsageForRecording(recording) {
+ if (!recording.isRecording()) {
+ return void 0;
+ }
+ const {
+ position: currentPosition,
+ totalSize,
+ generation: currentGeneration,
+ } = this._currentBufferStatus;
+ const {
+ position: origPosition,
+ generation: origGeneration,
+ } = recording.getStartingBufferStatus();
+
+ const normalizedCurrent =
+ totalSize * (currentGeneration - origGeneration) + currentPosition;
+ const percent = (normalizedCurrent - origPosition) / totalSize;
+
+ // Clamp between 0 and 1; can get negative percentage values when a new
+ // recording starts and the currentBufferStatus has not yet been updated. Rather
+ // than fetching another status update, just clamp to 0, and this will be updated
+ // on the next profiler-status event.
+ if (percent < 0) {
+ return 0;
+ } else if (percent > 1) {
+ return 1;
+ }
+
+ return percent;
+ }
+
+ /**
+ * Loads a recording from a file.
+ *
+ * @param {nsIFile} file
+ * The file to import the data from.
+ * @return {Promise<PerformanceRecordingFront>}
+ */
+ importRecording(file) {
+ return PerformanceIO.loadRecordingFromFile(file).then(recordingData => {
+ const model = new PerformanceRecordingFront();
+ model._imported = true;
+ model._label = recordingData.label || "";
+ model._duration = recordingData.duration;
+ model._markers = recordingData.markers;
+ model._frames = recordingData.frames;
+ model._memory = recordingData.memory;
+ model._ticks = recordingData.ticks;
+ model._allocations = recordingData.allocations;
+ model._profile = recordingData.profile;
+ model._configuration = recordingData.configuration || {};
+ model._systemHost = recordingData.systemHost;
+ model._systemClient = recordingData.systemClient;
+ return model;
+ });
+ }
+
+ /**
+ * Store profiler status when the position has been update so we can
+ * calculate recording's buffer percentage usage after emitting the event.
+ */
+ _onProfilerStatus(data) {
+ this._currentBufferStatus = data;
+ }
+
+ /**
+ * For all PerformanceRecordings that are recording, and needing realtime markers,
+ * apply the timeline data to the front PerformanceRecording (so we only have one event
+ * for each timeline data chunk as they could be shared amongst several recordings).
+ */
+ _onTimelineEvent(type, data, recordings) {
+ for (const recording of recordings) {
+ recording._addTimelineData(type, data);
+ }
+ }
+}
+
+exports.PerformanceFront = PerformanceFront;
+registerFront(PerformanceFront);
diff --git a/devtools/client/fronts/preference.js b/devtools/client/fronts/preference.js
new file mode 100644
index 0000000000..ce77d0b248
--- /dev/null
+++ b/devtools/client/fronts/preference.js
@@ -0,0 +1,31 @@
+/* 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 { preferenceSpec } = require("devtools/shared/specs/preference");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+class PreferenceFront extends FrontClassWithSpec(preferenceSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "preferenceActor";
+ }
+
+ async getTraits() {
+ if (!this._traits) {
+ this._traits = await super.getTraits();
+ }
+
+ return this._traits;
+ }
+}
+
+exports.PreferenceFront = PreferenceFront;
+registerFront(PreferenceFront);
diff --git a/devtools/client/fronts/property-iterator.js b/devtools/client/fronts/property-iterator.js
new file mode 100644
index 0000000000..dcbdf3e22e
--- /dev/null
+++ b/devtools/client/fronts/property-iterator.js
@@ -0,0 +1,67 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const {
+ propertyIteratorSpec,
+} = require("devtools/shared/specs/property-iterator");
+const {
+ getAdHocFrontOrPrimitiveGrip,
+} = require("devtools/client/fronts/object");
+
+/**
+ * A PropertyIteratorFront provides a way to access to property names and
+ * values of an object efficiently, slice by slice.
+ * Note that the properties can be sorted in the backend,
+ * this is controled while creating the PropertyIteratorFront
+ * from ObjectFront.enumProperties.
+ */
+class PropertyIteratorFront extends FrontClassWithSpec(propertyIteratorSpec) {
+ form(data) {
+ this.actorID = data.actor;
+ this.count = data.count;
+ }
+
+ async slice(start, count) {
+ const result = await super.slice({ start, count });
+ return this._onResult(result);
+ }
+
+ async all() {
+ const result = await super.all();
+ return this._onResult(result);
+ }
+
+ _onResult(result) {
+ if (!result.ownProperties) {
+ return result;
+ }
+
+ // The result packet can have multiple properties that hold grips which we may need
+ // to turn into fronts.
+ const gripKeys = ["value", "getterValue", "get", "set"];
+
+ Object.entries(result.ownProperties).forEach(([key, descriptor]) => {
+ if (descriptor) {
+ for (const gripKey of gripKeys) {
+ if (descriptor.hasOwnProperty(gripKey)) {
+ result.ownProperties[key][gripKey] = getAdHocFrontOrPrimitiveGrip(
+ descriptor[gripKey],
+ this
+ );
+ }
+ }
+ }
+ });
+ return result;
+ }
+}
+
+exports.PropertyIteratorFront = PropertyIteratorFront;
+registerFront(PropertyIteratorFront);
diff --git a/devtools/client/fronts/reflow.js b/devtools/client/fronts/reflow.js
new file mode 100644
index 0000000000..1104e2f9ca
--- /dev/null
+++ b/devtools/client/fronts/reflow.js
@@ -0,0 +1,31 @@
+/* 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 { reflowSpec } = require("devtools/shared/specs/reflow");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+/**
+ * Usage example of the reflow front:
+ *
+ * let front = await target.getFront("reflow");
+ * front.on("reflows", this._onReflows);
+ * front.start();
+ * // now wait for events to come
+ */
+class ReflowFront extends FrontClassWithSpec(reflowSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "reflowActor";
+ }
+}
+
+exports.ReflowFront = ReflowFront;
+registerFront(ReflowFront);
diff --git a/devtools/client/fronts/responsive.js b/devtools/client/fronts/responsive.js
new file mode 100644
index 0000000000..3eb9a625ef
--- /dev/null
+++ b/devtools/client/fronts/responsive.js
@@ -0,0 +1,26 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { responsiveSpec } = require("devtools/shared/specs/responsive");
+
+/**
+ * The corresponding Front object for the Responsive actor.
+ */
+class ResponsiveFront extends FrontClassWithSpec(responsiveSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "responsiveActor";
+ }
+}
+
+exports.ResponsiveFront = ResponsiveFront;
+registerFront(ResponsiveFront);
diff --git a/devtools/client/fronts/root.js b/devtools/client/fronts/root.js
new file mode 100644
index 0000000000..4d04941333
--- /dev/null
+++ b/devtools/client/fronts/root.js
@@ -0,0 +1,325 @@
+/* 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 { Ci } = require("chrome");
+const { rootSpec } = require("devtools/shared/specs/root");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+loader.lazyRequireGetter(this, "getFront", "devtools/shared/protocol", true);
+
+class RootFront extends FrontClassWithSpec(rootSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Cache root form as this will always be the same value.
+ Object.defineProperty(this, "rootForm", {
+ get() {
+ delete this.rootForm;
+ this.rootForm = this.getRoot();
+ return this.rootForm;
+ },
+ configurable: true,
+ });
+
+ // Cache of already created global scoped fronts
+ // [typeName:string => Front instance]
+ this.fronts = new Map();
+
+ this._client = client;
+ }
+
+ form(form) {
+ // Root Front is a special Front. It is the only one to set its actor ID manually
+ // out of the form object returned by RootActor.sayHello which is called when calling
+ // DevToolsClient.connect().
+ this.actorID = form.from;
+
+ this.applicationType = form.applicationType;
+ this.traits = form.traits;
+ }
+ /**
+ * Retrieve all service worker registrations with their corresponding workers.
+ * @param {Array} [workerTargets] (optional)
+ * Array containing the result of a call to `listAllWorkerTargets`.
+ * (this exists to avoid duplication of calls to that method)
+ * @return {Object[]} result - An Array of Objects with the following format
+ * - {result[].registration} - The registration front
+ * - {result[].workers} Array of form-like objects for service workers
+ */
+ async listAllServiceWorkers(workerTargets) {
+ const result = [];
+ const { registrations } = await this.listServiceWorkerRegistrations();
+ const allWorkers = workerTargets
+ ? workerTargets
+ : await this.listAllWorkerTargets();
+
+ for (const registrationFront of registrations) {
+ // workers from the registration, ordered from most recent to older
+ const workers = [
+ registrationFront.activeWorker,
+ registrationFront.waitingWorker,
+ registrationFront.installingWorker,
+ registrationFront.evaluatingWorker,
+ ]
+ // filter out non-existing workers
+ .filter(w => !!w)
+ // build a worker object with its WorkerDescriptorFront
+ .map(workerFront => {
+ const workerDescriptorFront = allWorkers.find(
+ targetFront => targetFront.id === workerFront.id
+ );
+
+ return {
+ id: workerFront.id,
+ name: workerFront.url,
+ state: workerFront.state,
+ stateText: workerFront.stateText,
+ url: workerFront.url,
+ workerDescriptorFront,
+ };
+ });
+
+ // TODO: return only the worker targets. See Bug 1620605
+ result.push({
+ registration: registrationFront,
+ workers,
+ });
+ }
+
+ return result;
+ }
+
+ /**
+ * Retrieve all service worker registrations as well as workers from the parent and
+ * content processes. Listing service workers involves merging information coming from
+ * registrations and workers, this method will combine this information to present a
+ * unified array of serviceWorkers. If you are only interested in other workers, use
+ * listWorkers.
+ *
+ * @return {Object}
+ * - {Array} service
+ * array of form-like objects for serviceworkers
+ * - {Array} shared
+ * Array of WorkerTargetActor forms, containing shared workers.
+ * - {Array} other
+ * Array of WorkerTargetActor forms, containing other workers.
+ */
+ async listAllWorkers() {
+ const allWorkers = await this.listAllWorkerTargets();
+ const serviceWorkers = await this.listAllServiceWorkers(allWorkers);
+
+ // NOTE: listAllServiceWorkers() now returns all the workers belonging to
+ // a registration. To preserve the usual behavior at about:debugging,
+ // in which we show only the most recent one, we grab the first
+ // worker in the array only.
+ const result = {
+ service: serviceWorkers
+ .map(({ registration, workers }) => {
+ return workers.slice(0, 1).map(worker => {
+ return Object.assign(worker, {
+ registrationFront: registration,
+ fetch: registration.fetch,
+ });
+ });
+ })
+ .flat(),
+ shared: [],
+ other: [],
+ };
+
+ allWorkers.forEach(front => {
+ const worker = {
+ id: front.id,
+ url: front.url,
+ name: front.url,
+ workerDescriptorFront: front,
+ };
+
+ switch (front.type) {
+ case Ci.nsIWorkerDebugger.TYPE_SERVICE:
+ // do nothing, since we already fetched them in `serviceWorkers`
+ break;
+ case Ci.nsIWorkerDebugger.TYPE_SHARED:
+ result.shared.push(worker);
+ break;
+ default:
+ result.other.push(worker);
+ }
+ });
+
+ return result;
+ }
+
+ /** Get the target fronts for all worker threads running in any process. */
+ async listAllWorkerTargets() {
+ const listParentWorkers = async () => {
+ const { workers } = await this.listWorkers();
+ return workers;
+ };
+ const listChildWorkers = async () => {
+ const processes = await this.listProcesses();
+ const processWorkers = await Promise.all(
+ processes.map(async processDescriptorFront => {
+ // Ignore parent process
+ if (processDescriptorFront.isParent) {
+ return [];
+ }
+ const front = await processDescriptorFront.getTarget();
+ if (!front) {
+ return [];
+ }
+ const response = await front.listWorkers();
+ return response.workers;
+ })
+ );
+
+ return processWorkers.flat();
+ };
+
+ const [parentWorkers, childWorkers] = await Promise.all([
+ listParentWorkers(),
+ listChildWorkers(),
+ ]);
+
+ return parentWorkers.concat(childWorkers);
+ }
+
+ /**
+ * Fetch the ProcessDescriptorFront for the main process.
+ *
+ * `getProcess` requests allows to fetch the descriptor for any process and
+ * the main process is having the process ID zero.
+ */
+ getMainProcess() {
+ return this.getProcess(0);
+ }
+
+ /**
+ * Fetch the tab descriptor for the currently selected tab, or for a specific
+ * tab given as first parameter.
+ *
+ * @param [optional] object filter
+ * A dictionary object with following optional attributes:
+ * - outerWindowID: used to match tabs in parent process
+ * - tabId: used to match tabs in child processes
+ * - tab: a reference to xul:tab element
+ * If nothing is specified, returns the actor for the currently
+ * selected tab.
+ */
+ async getTab(filter) {
+ const packet = {};
+ if (filter) {
+ if (typeof filter.outerWindowID == "number") {
+ packet.outerWindowID = filter.outerWindowID;
+ } else if (typeof filter.tabId == "number") {
+ packet.tabId = filter.tabId;
+ } else if ("tab" in filter) {
+ const browser = filter.tab.linkedBrowser;
+ if (browser.frameLoader.remoteTab) {
+ // Tabs in child process
+ packet.tabId = browser.frameLoader.remoteTab.tabId;
+ } else {
+ // <xul:browser> or <iframe mozbrowser> tabs in parent process
+ packet.outerWindowID =
+ browser.browsingContext.currentWindowGlobal.outerWindowId;
+ }
+ } else {
+ // Throw if a filter object have been passed but without
+ // any clearly idenfified filter.
+ throw new Error("Unsupported argument given to getTab request");
+ }
+ }
+
+ const descriptorFront = await super.getTab(packet);
+
+ // If the tab is a local tab, forward it to the descriptor.
+ if (filter?.tab?.tagName == "tab") {
+ // Ignore the fake `tab` object we receive, where there is only a
+ // `linkedBrowser` attribute, but this isn't a real <tab> element.
+ // devtools/client/framework/test/browser_toolbox_target.js is passing such
+ // a fake tab.
+ descriptorFront.setLocalTab(filter.tab);
+ }
+
+ return descriptorFront;
+ }
+
+ /**
+ * Fetch the target front for a given add-on.
+ * This is just an helper on top of `listAddons` request.
+ *
+ * @param object filter
+ * A dictionary object with following attribute:
+ * - id: used to match the add-on to connect to.
+ */
+ async getAddon({ id }) {
+ const addons = await this.listAddons();
+ const webextensionDescriptorFront = addons.find(addon => addon.id === id);
+ return webextensionDescriptorFront;
+ }
+
+ /**
+ * Fetch the target front for a given worker.
+ * This is just an helper on top of `listAllWorkers` request.
+ *
+ * @param id
+ */
+ async getWorker(id) {
+ const { service, shared, other } = await this.listAllWorkers();
+ const worker = [...service, ...shared, ...other].find(w => w.id === id);
+ if (!worker) {
+ return null;
+ }
+ return worker.workerDescriptorFront || worker.registrationFront;
+ }
+
+ /**
+ * Test request that returns the object passed as first argument.
+ *
+ * `echo` is special as all the property of the given object have to be passed
+ * on the packet object. That's not something that can be achieve by requester helper.
+ */
+
+ echo(packet) {
+ packet.type = "echo";
+ return this.request(packet);
+ }
+
+ /*
+ * This function returns a protocol.js Front for any root actor.
+ * i.e. the one directly served from RootActor.listTabs or getRoot.
+ *
+ * @param String typeName
+ * The type name used in protocol.js's spec for this actor.
+ */
+ async getFront(typeName) {
+ let front = this.fronts.get(typeName);
+ if (front) {
+ return front;
+ }
+ const rootForm = await this.rootForm;
+ front = getFront(this._client, typeName, rootForm);
+ this.fronts.set(typeName, front);
+ return front;
+ }
+
+ /*
+ * This function returns true if the root actor has a registered global actor
+ * with a given name.
+ * @param {String} actorName
+ * The name of a global actor.
+ *
+ * @return {Boolean}
+ */
+ async hasActor(actorName) {
+ const rootForm = await this.rootForm;
+ return !!rootForm[actorName + "Actor"];
+ }
+}
+exports.RootFront = RootFront;
+registerFront(RootFront);
diff --git a/devtools/client/fronts/screenshot.js b/devtools/client/fronts/screenshot.js
new file mode 100644
index 0000000000..55fa9e4ef3
--- /dev/null
+++ b/devtools/client/fronts/screenshot.js
@@ -0,0 +1,29 @@
+/* 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 { screenshotSpec } = require("devtools/shared/specs/screenshot");
+const saveScreenshot = require("devtools/client/shared/save-screenshot");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+class ScreenshotFront extends FrontClassWithSpec(screenshotSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "screenshotActor";
+ }
+
+ async captureAndSave(window, args) {
+ const screenshot = await this.capture(args);
+ return saveScreenshot(window, args, screenshot);
+ }
+}
+
+exports.ScreenshotFront = ScreenshotFront;
+registerFront(ScreenshotFront);
diff --git a/devtools/client/fronts/source.js b/devtools/client/fronts/source.js
new file mode 100644
index 0000000000..c433f95f97
--- /dev/null
+++ b/devtools/client/fronts/source.js
@@ -0,0 +1,100 @@
+/* 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 { sourceSpec } = require("devtools/shared/specs/source");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { ArrayBufferFront } = require("devtools/client/fronts/array-buffer");
+
+/**
+ * A SourceFront provides a way to access the source text of a script.
+ *
+ * @param client DevToolsClient
+ * The DevTools Client instance.
+ * @param form Object
+ * The form sent across the remote debugging protocol.
+ */
+class SourceFront extends FrontClassWithSpec(sourceSpec) {
+ constructor(client, form) {
+ super(client);
+ if (form) {
+ this._url = form.url;
+ // this is here for the time being, until the source front is managed
+ // via protocol.js marshalling
+ this.actorID = form.actor;
+ }
+ }
+
+ form(json) {
+ this._url = json.url;
+ }
+
+ get actor() {
+ return this.actorID;
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ // Alias for source.blackbox to avoid changing protocol.js packets
+ blackBox(range) {
+ return this.blackbox(range);
+ }
+
+ // Alias for source.unblackbox to avoid changing protocol.js packets
+ unblackBox() {
+ return this.unblackbox();
+ }
+
+ /**
+ * Get a Front for either an ArrayBuffer or LongString
+ * for this SourceFront's source.
+ */
+ async source() {
+ const response = await super.source();
+ return this._onSourceResponse(response);
+ }
+
+ _onSourceResponse(response) {
+ const { contentType, source } = response;
+ if (source instanceof ArrayBufferFront) {
+ return source.slice(0, source.length).then(function(resp) {
+ if (resp.error) {
+ return resp;
+ }
+ // Keeping str as a string, ArrayBuffer/Uint8Array will not survive
+ // setIn/mergeIn operations.
+ const str = atob(resp.encoded);
+ const newResponse = {
+ source: {
+ binary: str,
+ toString: () => "[wasm]",
+ },
+ contentType,
+ };
+ return newResponse;
+ });
+ }
+
+ return source.substring(0, source.length).then(function(resp) {
+ if (resp.error) {
+ return resp;
+ }
+
+ const newResponse = {
+ source: resp,
+ contentType: contentType,
+ };
+ return newResponse;
+ });
+ }
+}
+
+exports.SourceFront = SourceFront;
+registerFront(SourceFront);
diff --git a/devtools/client/fronts/storage.js b/devtools/client/fronts/storage.js
new file mode 100644
index 0000000000..390682e2a2
--- /dev/null
+++ b/devtools/client/fronts/storage.js
@@ -0,0 +1,35 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { childSpecs, storageSpec } = require("devtools/shared/specs/storage");
+
+for (const childSpec of Object.values(childSpecs)) {
+ class ChildStorageFront extends FrontClassWithSpec(childSpec) {
+ form(form) {
+ this.actorID = form.actor;
+ this.hosts = form.hosts;
+ this.traits = form.traits || {};
+ return null;
+ }
+ }
+ registerFront(ChildStorageFront);
+}
+
+class StorageFront extends FrontClassWithSpec(storageSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "storageActor";
+ }
+}
+
+exports.StorageFront = StorageFront;
+registerFront(StorageFront);
diff --git a/devtools/client/fronts/string.js b/devtools/client/fronts/string.js
new file mode 100644
index 0000000000..f9b42e260c
--- /dev/null
+++ b/devtools/client/fronts/string.js
@@ -0,0 +1,60 @@
+/* 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 { DevToolsServer } = require("devtools/server/devtools-server");
+const {
+ longStringSpec,
+ SimpleStringFront,
+} = require("devtools/shared/specs/string");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+class LongStringFront extends FrontClassWithSpec(longStringSpec) {
+ destroy() {
+ this.initial = null;
+ this.length = null;
+ this.strPromise = null;
+ super.destroy();
+ }
+
+ form(data) {
+ this.actorID = data.actor;
+ this.initial = data.initial;
+ this.length = data.length;
+
+ this._grip = data;
+ }
+
+ // We expose the grip so consumers (e.g. ObjectInspector) that handle webconsole
+ // evaluations (which can return primitive, object fronts or longString front),
+ // can directly call this without further check.
+ getGrip() {
+ return this._grip;
+ }
+
+ string() {
+ if (!this.strPromise) {
+ const promiseRest = thusFar => {
+ if (thusFar.length === this.length) {
+ return Promise.resolve(thusFar);
+ }
+ return this.substring(
+ thusFar.length,
+ thusFar.length + DevToolsServer.LONG_STRING_READ_LENGTH
+ ).then(next => promiseRest(thusFar + next));
+ };
+
+ this.strPromise = promiseRest(this.initial);
+ }
+ return this.strPromise;
+ }
+}
+
+exports.LongStringFront = LongStringFront;
+registerFront(LongStringFront);
+exports.SimpleStringFront = SimpleStringFront;
+registerFront(SimpleStringFront);
diff --git a/devtools/client/fronts/style-rule.js b/devtools/client/fronts/style-rule.js
new file mode 100644
index 0000000000..fa4a03841c
--- /dev/null
+++ b/devtools/client/fronts/style-rule.js
@@ -0,0 +1,335 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { styleRuleSpec } = require("devtools/shared/specs/style-rule");
+const promise = require("promise");
+
+loader.lazyRequireGetter(
+ this,
+ "RuleRewriter",
+ "devtools/client/fronts/inspector/rule-rewriter"
+);
+
+/**
+ * StyleRuleFront, the front for the StyleRule actor.
+ */
+class StyleRuleFront extends FrontClassWithSpec(styleRuleSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.before("location-changed", this._locationChangedPre.bind(this));
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ this.traits = form.traits || {};
+ if (this._mediaText) {
+ this._mediaText = null;
+ }
+ }
+
+ /**
+ * Ensure _form is updated when location-changed is emitted.
+ */
+ _locationChangedPre(line, column) {
+ this._clearOriginalLocation();
+ this._form.line = line;
+ this._form.column = column;
+ }
+
+ /**
+ * Return a new RuleModificationList or RuleRewriter for this node.
+ * A RuleRewriter will be returned when the rule's canSetRuleText
+ * trait is true; otherwise a RuleModificationList will be
+ * returned.
+ *
+ * @param {CssPropertiesFront} cssProperties
+ * This is needed by the RuleRewriter.
+ * @return {RuleModificationList}
+ */
+ startModifyingProperties(cssProperties) {
+ if (this.canSetRuleText) {
+ return new RuleRewriter(cssProperties.isKnown, this, this.authoredText);
+ }
+ return new RuleModificationList(this);
+ }
+
+ get type() {
+ return this._form.type;
+ }
+ get line() {
+ return this._form.line || -1;
+ }
+ get column() {
+ return this._form.column || -1;
+ }
+ get cssText() {
+ return this._form.cssText;
+ }
+ get authoredText() {
+ return typeof this._form.authoredText === "string"
+ ? this._form.authoredText
+ : this._form.cssText;
+ }
+ get declarations() {
+ return this._form.declarations || [];
+ }
+ get keyText() {
+ return this._form.keyText;
+ }
+ get name() {
+ return this._form.name;
+ }
+ get selectors() {
+ return this._form.selectors;
+ }
+ get media() {
+ return this._form.media;
+ }
+ get mediaText() {
+ if (!this._form.media) {
+ return null;
+ }
+ if (this._mediaText) {
+ return this._mediaText;
+ }
+ this._mediaText = this.media.join(", ");
+ return this._mediaText;
+ }
+
+ get parentRule() {
+ return this.conn.getFrontByID(this._form.parentRule);
+ }
+
+ get parentStyleSheet() {
+ const resourceWatcher = this.parentFront.resourceWatcher;
+ if (resourceWatcher) {
+ return resourceWatcher.getResourceById(
+ resourceWatcher.TYPES.STYLESHEET,
+ this._form.parentStyleSheet
+ );
+ }
+
+ return this.conn.getFrontByID(this._form.parentStyleSheet);
+ }
+
+ get element() {
+ return this.conn.getFrontByID(this._form.element);
+ }
+
+ get href() {
+ if (this._form.href) {
+ return this._form.href;
+ }
+ const sheet = this.parentStyleSheet;
+ return sheet ? sheet.href : "";
+ }
+
+ get nodeHref() {
+ const sheet = this.parentStyleSheet;
+ return sheet ? sheet.nodeHref : "";
+ }
+
+ get canSetRuleText() {
+ return this._form.traits && this._form.traits.canSetRuleText;
+ }
+
+ get location() {
+ return {
+ source: this.parentStyleSheet,
+ href: this.href,
+ line: this.line,
+ column: this.column,
+ };
+ }
+
+ _clearOriginalLocation() {
+ this._originalLocation = null;
+ }
+
+ getOriginalLocation() {
+ if (this._originalLocation) {
+ return promise.resolve(this._originalLocation);
+ }
+ const parentSheet = this.parentStyleSheet;
+ if (!parentSheet) {
+ // This rule doesn't belong to a stylesheet so it is an inline style.
+ // Inline styles do not have any mediaText so we can return early.
+ return promise.resolve(this.location);
+ }
+ return parentSheet
+ .getOriginalLocation(this.line, this.column)
+ .then(({ fromSourceMap, source, line, column }) => {
+ const location = {
+ href: source,
+ line: line,
+ column: column,
+ mediaText: this.mediaText,
+ };
+ if (fromSourceMap === false) {
+ location.source = this.parentStyleSheet;
+ }
+ if (!source) {
+ location.href = this.href;
+ }
+ this._originalLocation = location;
+ return location;
+ });
+ }
+
+ async modifySelector(node, value) {
+ const response = await super.modifySelector(
+ node,
+ value,
+ this.canSetRuleText
+ );
+
+ if (response.ruleProps) {
+ response.ruleProps = response.ruleProps.entries[0];
+ }
+ return response;
+ }
+
+ setRuleText(newText, modifications) {
+ this._form.authoredText = newText;
+ return super.setRuleText(newText, modifications);
+ }
+}
+
+exports.StyleRuleFront = StyleRuleFront;
+registerFront(StyleRuleFront);
+
+/**
+ * Convenience API for building a list of attribute modifications
+ * for the `modifyProperties` request. A RuleModificationList holds a
+ * list of modifications that will be applied to a StyleRuleActor.
+ * The modifications are processed in the order in which they are
+ * added to the RuleModificationList.
+ *
+ * Objects of this type expose the same API as @see RuleRewriter.
+ * This lets the inspector use (mostly) the same code, regardless of
+ * whether the server implements setRuleText.
+ */
+class RuleModificationList {
+ /**
+ * Initialize a RuleModificationList.
+ * @param {StyleRuleFront} rule the associated rule
+ */
+ constructor(rule) {
+ this.rule = rule;
+ this.modifications = [];
+ }
+
+ /**
+ * Apply the modifications in this object to the associated rule.
+ *
+ * @return {Promise} A promise which will be resolved when the modifications
+ * are complete; @see StyleRuleActor.modifyProperties.
+ */
+ apply() {
+ return this.rule.modifyProperties(this.modifications);
+ }
+
+ /**
+ * Add a "set" entry to the modification list.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name the property's name
+ * @param {String} value the property's value
+ * @param {String} priority the property's priority, either the empty
+ * string or "important"
+ */
+ setProperty(index, name, value, priority) {
+ this.modifications.push({ type: "set", index, name, value, priority });
+ }
+
+ /**
+ * Add a "remove" entry to the modification list.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name the name of the property to remove
+ */
+ removeProperty(index, name) {
+ this.modifications.push({ type: "remove", index, name });
+ }
+
+ /**
+ * Rename a property. This implementation acts like
+ * |removeProperty|, because |setRuleText| is not available.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name current name of the property
+ *
+ * This parameter is also passed, but as it is not used in this
+ * implementation, it is omitted. It is documented here as this
+ * code also defined the interface implemented by @see RuleRewriter.
+ * @param {String} newName new name of the property
+ */
+ renameProperty(index, name) {
+ this.removeProperty(index, name);
+ }
+
+ /**
+ * Enable or disable a property. This implementation acts like
+ * a no-op when enabling, because |setRuleText| is not available.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name current name of the property
+ * @param {Boolean} isEnabled true if the property should be enabled;
+ * false if it should be disabled
+ */
+ setPropertyEnabled(index, name, isEnabled) {
+ if (!isEnabled) {
+ this.modifications.push({ type: "disable", index, name });
+ }
+ }
+
+ /**
+ * Create a new property. This implementation does nothing, because
+ * |setRuleText| is not available.
+ *
+ * These parameters are passed, but as they are not used in this
+ * implementation, they are omitted. They are documented here as
+ * this code also defined the interface implemented by @see
+ * RuleRewriter.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name name of the new property
+ * @param {String} value value of the new property
+ * @param {String} priority priority of the new property; either
+ * the empty string or "important"
+ * @param {Boolean} enabled True if the new property should be
+ * enabled, false if disabled
+ */
+ createProperty() {
+ // Nothing.
+ }
+}
diff --git a/devtools/client/fronts/style-sheet.js b/devtools/client/fronts/style-sheet.js
new file mode 100644
index 0000000000..7f95e09ddb
--- /dev/null
+++ b/devtools/client/fronts/style-sheet.js
@@ -0,0 +1,96 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { styleSheetSpec } = require("devtools/shared/specs/style-sheet");
+const promise = require("promise");
+
+loader.lazyRequireGetter(
+ this,
+ ["getIndentationFromPrefs", "getIndentationFromString"],
+ "devtools/shared/indentation",
+ true
+);
+
+/**
+ * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
+ */
+class StyleSheetFront extends FrontClassWithSpec(styleSheetSpec) {
+ constructor(conn, targetFront, parentFront) {
+ super(conn, targetFront, parentFront);
+
+ this._onPropertyChange = this._onPropertyChange.bind(this);
+ this.on("property-change", this._onPropertyChange);
+ }
+
+ destroy() {
+ this.off("property-change", this._onPropertyChange);
+ super.destroy();
+ }
+
+ _onPropertyChange(property, value) {
+ this[property] = value;
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this.href = form.href;
+ this.nodeHref = form.nodeHref;
+ this.disabled = form.disabled;
+ this.title = form.title;
+ this.system = form.system;
+ this.styleSheetIndex = form.styleSheetIndex;
+ this.ruleCount = form.ruleCount;
+ this.sourceMapURL = form.sourceMapURL;
+ this._sourceMapBaseURL = form.sourceMapBaseURL;
+ }
+
+ get isSystem() {
+ return this.system;
+ }
+
+ get sourceMapBaseURL() {
+ // Handle backward-compat for servers that don't return sourceMapBaseURL.
+ if (this._sourceMapBaseURL === undefined) {
+ return this.href || this.nodeHref;
+ }
+
+ return this._sourceMapBaseURL;
+ }
+
+ set sourceMapBaseURL(sourceMapBaseURL) {
+ this._sourceMapBaseURL = sourceMapBaseURL;
+ }
+
+ /**
+ * Get the indentation to use for edits to this style sheet.
+ *
+ * @return {Promise} A promise that will resolve to a string that
+ * should be used to indent a block in this style sheet.
+ */
+ guessIndentation() {
+ const prefIndent = getIndentationFromPrefs();
+ if (prefIndent) {
+ const { indentUnit, indentWithTabs } = prefIndent;
+ return promise.resolve(indentWithTabs ? "\t" : " ".repeat(indentUnit));
+ }
+
+ return async function() {
+ const longStr = await this.getText();
+ const source = await longStr.string();
+
+ const { indentUnit, indentWithTabs } = getIndentationFromString(source);
+
+ return indentWithTabs ? "\t" : " ".repeat(indentUnit);
+ }.bind(this)();
+ }
+}
+
+exports.StyleSheetFront = StyleSheetFront;
+registerFront(StyleSheetFront);
diff --git a/devtools/client/fronts/style-sheets.js b/devtools/client/fronts/style-sheets.js
new file mode 100644
index 0000000000..bf8d3a9db5
--- /dev/null
+++ b/devtools/client/fronts/style-sheets.js
@@ -0,0 +1,35 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { styleSheetsSpec } = require("devtools/shared/specs/style-sheets");
+
+/**
+ * The corresponding Front object for the StyleSheetsActor.
+ */
+class StyleSheetsFront extends FrontClassWithSpec(styleSheetsSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "styleSheetsActor";
+ }
+
+ async getTraits() {
+ if (this._traits) {
+ return this._traits;
+ }
+ const { traits } = await super.getTraits();
+ this._traits = traits;
+ return this._traits;
+ }
+}
+
+exports.StyleSheetsFront = StyleSheetsFront;
+registerFront(StyleSheetsFront);
diff --git a/devtools/client/fronts/symbol-iterator.js b/devtools/client/fronts/symbol-iterator.js
new file mode 100644
index 0000000000..749deca7d5
--- /dev/null
+++ b/devtools/client/fronts/symbol-iterator.js
@@ -0,0 +1,54 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { symbolIteratorSpec } = require("devtools/shared/specs/symbol-iterator");
+const {
+ getAdHocFrontOrPrimitiveGrip,
+} = require("devtools/client/fronts/object");
+
+/**
+ * A SymbolIteratorFront is used as a front end for the SymbolIterator that is
+ * created on the server, hiding implementation details.
+ */
+class SymbolIteratorFront extends FrontClassWithSpec(symbolIteratorSpec) {
+ form(data) {
+ this.actorID = data.actor;
+ this.count = data.count;
+ }
+
+ async slice(start, count) {
+ const result = await super.slice({ start, count });
+ return this._onResult(result);
+ }
+
+ async all() {
+ const result = await super.all();
+ return this._onResult(result);
+ }
+
+ _onResult(result) {
+ if (!result.ownSymbols) {
+ return result;
+ }
+
+ result.ownSymbols.forEach((item, i) => {
+ if (item?.descriptor) {
+ result.ownSymbols[i].descriptor.value = getAdHocFrontOrPrimitiveGrip(
+ item.descriptor.value,
+ this
+ );
+ }
+ });
+ return result;
+ }
+}
+
+exports.SymbolIteratorFront = SymbolIteratorFront;
+registerFront(SymbolIteratorFront);
diff --git a/devtools/client/fronts/targets/browsing-context.js b/devtools/client/fronts/targets/browsing-context.js
new file mode 100644
index 0000000000..117ed076a1
--- /dev/null
+++ b/devtools/client/fronts/targets/browsing-context.js
@@ -0,0 +1,141 @@
+/* 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 {
+ browsingContextTargetSpec,
+} = require("devtools/shared/specs/targets/browsing-context");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { TargetMixin } = require("devtools/client/fronts/targets/target-mixin");
+
+class BrowsingContextTargetFront extends TargetMixin(
+ FrontClassWithSpec(browsingContextTargetSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Cache the value of some target properties that are being returned by `attach`
+ // request and then keep them up-to-date in `reconfigure` request.
+ this.configureOptions = {
+ javascriptEnabled: null,
+ };
+
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onFrameUpdate = this._onFrameUpdate.bind(this);
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ this.browsingContextID = json.browsingContextID;
+
+ // Save the full form for Target class usage.
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this.targetForm = json;
+
+ this.outerWindowID = json.outerWindowID;
+ this.favicon = json.favicon;
+ this._title = json.title;
+ this._url = json.url;
+ }
+
+ /**
+ * Event listener for `frameUpdate` event.
+ */
+ _onFrameUpdate(packet) {
+ this.emit("frame-update", packet);
+ }
+
+ /**
+ * Event listener for `tabNavigated` event.
+ */
+ _onTabNavigated(packet) {
+ const event = Object.create(null);
+ event.url = packet.url;
+ event.title = packet.title;
+ event.nativeConsoleAPI = packet.nativeConsoleAPI;
+ event.isFrameSwitching = packet.isFrameSwitching;
+
+ // Keep the title unmodified when a developer toolbox switches frame
+ // for a tab (Bug 1261687), but always update the title when the target
+ // is a WebExtension (where the addon name is always included in the title
+ // and the url is supposed to be updated every time the selected frame changes).
+ if (!packet.isFrameSwitching || this.isWebExtension) {
+ this._url = packet.url;
+ this._title = packet.title;
+ }
+
+ // Send any stored event payload (DOMWindow or nsIRequest) for backwards
+ // compatibility with non-remotable tools.
+ if (packet.state == "start") {
+ this.emit("will-navigate", event);
+ } else {
+ this.emit("navigate", event);
+ }
+ }
+
+ async attach() {
+ if (this._attach) {
+ return this._attach;
+ }
+ this._attach = (async () => {
+ // All Browsing context inherited target emit a few event that are being
+ // translated on the target class. Listen for them before attaching as they
+ // can start firing on attach call.
+ this.on("tabNavigated", this._onTabNavigated);
+ this.on("frameUpdate", this._onFrameUpdate);
+
+ const response = await super.attach();
+
+ this.targetForm.threadActor = response.threadActor;
+ this.configureOptions.javascriptEnabled = response.javascriptEnabled;
+ this.traits = response.traits || {};
+
+ // xpcshell tests from devtools/server/tests/xpcshell/ are implementing
+ // fake BrowsingContextTargetActor which do not expose any console actor.
+ if (this.targetForm.consoleActor) {
+ await this.attachConsole();
+ }
+ })();
+ return this._attach;
+ }
+
+ async reconfigure({ options }) {
+ const response = await super.reconfigure({ options });
+
+ if (typeof options.javascriptEnabled != "undefined") {
+ this.configureOptions.javascriptEnabled = options.javascriptEnabled;
+ }
+
+ return response;
+ }
+
+ async detach() {
+ try {
+ await super.detach();
+ } catch (e) {
+ this.logDetachError(e, "browsing context");
+ }
+
+ // Remove listeners set in attach
+ this.off("tabNavigated", this._onTabNavigated);
+ this.off("frameUpdate", this._onFrameUpdate);
+ }
+
+ destroy() {
+ const promise = super.destroy();
+
+ // As detach isn't necessarily called on target's destroy
+ // (it isn't for local tabs), ensure removing listeners set in attach.
+ this.off("tabNavigated", this._onTabNavigated);
+ this.off("frameUpdate", this._onFrameUpdate);
+
+ return promise;
+ }
+}
+
+exports.BrowsingContextTargetFront = BrowsingContextTargetFront;
+registerFront(exports.BrowsingContextTargetFront);
diff --git a/devtools/client/fronts/targets/content-process.js b/devtools/client/fronts/targets/content-process.js
new file mode 100644
index 0000000000..65b9eafe91
--- /dev/null
+++ b/devtools/client/fronts/targets/content-process.js
@@ -0,0 +1,54 @@
+/* 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 {
+ contentProcessTargetSpec,
+} = require("devtools/shared/specs/targets/content-process");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { TargetMixin } = require("devtools/client/fronts/targets/target-mixin");
+
+class ContentProcessTargetFront extends TargetMixin(
+ FrontClassWithSpec(contentProcessTargetSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.traits = {};
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ this.processID = json.processID;
+
+ // Save the full form for Target class usage.
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this.targetForm = json;
+ }
+
+ get name() {
+ return `Content Process (pid ${this.processID})`;
+ }
+
+ attach() {
+ // All target actors have a console actor to attach.
+ // All but xpcshell test actors... which is using a ContentProcessTargetActor
+ if (this.targetForm.consoleActor) {
+ return this.attachConsole();
+ }
+ return Promise.resolve();
+ }
+
+ reconfigure() {
+ // Toolbox and options panel are calling this method but Worker Target can't be
+ // reconfigured. So we ignore this call here.
+ return Promise.resolve();
+ }
+}
+
+exports.ContentProcessTargetFront = ContentProcessTargetFront;
+registerFront(exports.ContentProcessTargetFront);
diff --git a/devtools/client/fronts/targets/moz.build b/devtools/client/fronts/targets/moz.build
new file mode 100644
index 0000000000..420b99e433
--- /dev/null
+++ b/devtools/client/fronts/targets/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "browsing-context.js",
+ "content-process.js",
+ "target-mixin.js",
+ "worker.js",
+)
diff --git a/devtools/client/fronts/targets/target-mixin.js b/devtools/client/fronts/targets/target-mixin.js
new file mode 100644
index 0000000000..937eabdbd1
--- /dev/null
+++ b/devtools/client/fronts/targets/target-mixin.js
@@ -0,0 +1,750 @@
+/* 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";
+
+loader.lazyRequireGetter(this, "getFront", "devtools/shared/protocol", true);
+loader.lazyRequireGetter(
+ this,
+ "getThreadOptions",
+ "devtools/client/shared/thread-utils",
+ true
+);
+
+/**
+ * A Target represents a debuggable context. It can be a browser tab, a tab on
+ * a remote device, like a tab on Firefox for Android. But it can also be an add-on,
+ * as well as firefox parent process, or just one of its content process.
+ * A Target is related to a given TargetActor, for which we derive this class.
+ *
+ * Providing a generalized abstraction of a web-page or web-browser (available
+ * either locally or remotely) is beyond the scope of this class (and maybe
+ * also beyond the scope of this universe) However Target does attempt to
+ * abstract some common events and read-only properties common to many Tools.
+ *
+ * Supported read-only properties:
+ * - name, url
+ *
+ * Target extends EventEmitter and provides support for the following events:
+ * - close: The target window has been closed. All tools attached to this
+ * target should close. This event is not currently cancelable.
+ *
+ * Optional events only dispatched by BrowsingContextTarget:
+ * - will-navigate: The target window will navigate to a different URL
+ * - navigate: The target window has navigated to a different URL
+ */
+function TargetMixin(parentClass) {
+ class Target extends parentClass {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._forceChrome = false;
+
+ this.destroy = this.destroy.bind(this);
+
+ this.threadFront = null;
+
+ // This flag will be set to true from:
+ // - TabDescriptorFront getTarget(), for local tab targets
+ // - targetFromURL(), for local targets (about:debugging)
+ // - initToolbox(), for some test-only targets
+ this.shouldCloseClient = false;
+
+ this._client = client;
+
+ // Cache of already created targed-scoped fronts
+ // [typeName:string => Front instance]
+ this.fronts = new Map();
+
+ // `resource-available-form` events can be emitted by target actors before the
+ // ResourceWatcher could add event listeners. The target front will cache those
+ // events until the ResourceWatcher has added the listeners.
+ this._resourceCache = [];
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ // In order to avoid destroying the `_resourceCache`, we need to call `super.on()`
+ // instead of `this.on()`.
+ super.on("resource-available-form", this._onResourceAvailable);
+
+ this._addListeners();
+ }
+
+ on(eventName, listener) {
+ if (eventName === "resource-available-form" && this._resourceCache) {
+ this.off("resource-available-form", this._onResourceAvailable);
+ for (const cache of this._resourceCache) {
+ listener(cache);
+ }
+ this._resourceCache = null;
+ }
+
+ super.on(eventName, listener);
+ }
+
+ /**
+ * Boolean flag to help distinguish Target Fronts from other Fronts.
+ * As we are using a Mixin, we can't easily distinguish these fronts via instanceof().
+ */
+ get isTargetFront() {
+ return true;
+ }
+
+ /**
+ * Get the descriptor front for this target.
+ *
+ * TODO: Should be removed. This is misleading as only the top level target should have a descriptor.
+ * This will return null for targets created by the Watcher actor and will still be defined
+ * by targets created by RootActor methods (listSomething methods).
+ */
+ get descriptorFront() {
+ if (this.isDestroyed()) {
+ // If the target was already destroyed, parentFront will be null.
+ return null;
+ }
+
+ if (this.parentFront.typeName.endsWith("Descriptor")) {
+ return this.parentFront;
+ }
+ return null;
+ }
+
+ get targetType() {
+ return this._targetType;
+ }
+
+ get isTopLevel() {
+ return this._isTopLevel;
+ }
+
+ setTargetType(type) {
+ this._targetType = type;
+ }
+
+ setIsTopLevel(isTopLevel) {
+ this._isTopLevel = isTopLevel;
+ }
+
+ /**
+ * Get the top level WatcherFront for this target.
+ *
+ * The targets should all ultimately be managed by a unique Watcher actor,
+ * created from the unique Descriptor actor which is passed to the Toolbox.
+ * For now, the top level target is still created by the top level Descriptor,
+ * but it is also meant to be created by the Watcher.
+ *
+ * @return {TargetMixin} the parent target.
+ */
+ getWatcherFront() {
+ // All additional frame targets are spawn by the WatcherActor and are managed by it.
+ if (this.parentFront.typeName == "watcher") {
+ return this.parentFront;
+ }
+
+ // Otherwise, for top level targets, the parent front is a Descriptor, from which we can retrieve the Watcher.
+ // TODO: top level target should also be exposed by the Watcher actor, like any target.
+ if (
+ this.parentFront.typeName.endsWith("Descriptor") &&
+ this.parentFront.traits &&
+ this.parentFront.traits.watcher
+ ) {
+ return this.parentFront.getWatcher();
+ }
+
+ // For WebExtension, the descriptor doesn't expose a watcher yet (See Bug 1675456).
+ return null;
+ }
+
+ /**
+ * Get the immediate parent target for this target.
+ *
+ * @return {TargetMixin} the parent target.
+ */
+ async getParentTarget() {
+ // We now support frames watching via watchTargets for Tab and Process descriptors.
+ const watcherFront = await this.getWatcherFront();
+ if (watcherFront) {
+ // Safety check, in theory all watcher should support frames. We should be able
+ // to remove this as part of Bug 1680280.
+ if (watcherFront.traits.frame) {
+ // Retrieve the Watcher, which manage all the targets and should already have a reference to
+ // to the parent target.
+ return watcherFront.getParentBrowsingContextTarget(
+ this.browsingContextID
+ );
+ }
+ return null;
+ }
+
+ if (this.parentFront.getParentTarget) {
+ return this.parentFront.getParentTarget();
+ }
+
+ // Other targets, like WebExtensions, don't have a Watcher yet, nor do expose `getParentTarget`.
+ // We can't fetch parent target yet for these targets.
+ return null;
+ }
+
+ /**
+ * Get the target for the given Browsing Context ID.
+ *
+ * @return {TargetMixin} the requested target.
+ */
+ async getBrowsingContextTarget(browsingContextID) {
+ // Tab and Process Descriptors expose a Watcher, which is creating the
+ // targets and should be used to fetch any.
+ const watcherFront = await this.getWatcherFront();
+ if (watcherFront) {
+ // Safety check, in theory all watcher should support frames.
+ if (watcherFront.traits.frame) {
+ return watcherFront.getBrowsingContextTarget(browsingContextID);
+ }
+ return null;
+ }
+
+ // For descriptors which don't expose a watcher (e.g. WebExtension)
+ // we used to call RootActor::getBrowsingContextDescriptor, but it was
+ // removed in FF77.
+ // Support for watcher in WebExtension descriptors is Bug 1644341.
+ throw new Error(
+ `Unable to call getBrowsingContextTarget for ${this.actorID}`
+ );
+ }
+
+ /**
+ * Returns a boolean indicating whether or not the specific actor
+ * type exists.
+ *
+ * @param {String} actorName
+ * @return {Boolean}
+ */
+ hasActor(actorName) {
+ if (this.targetForm) {
+ return !!this.targetForm[actorName + "Actor"];
+ }
+ return false;
+ }
+
+ /**
+ * Returns a trait from the root actor.
+ *
+ * @param {String} traitName
+ * @return {Mixed}
+ */
+ getTrait(traitName) {
+ // If the targeted actor exposes traits and has a defined value for this
+ // traits, override the root actor traits
+ if (this.targetForm.traits && traitName in this.targetForm.traits) {
+ return this.targetForm.traits[traitName];
+ }
+
+ return this.client.traits[traitName];
+ }
+
+ get isLocalTab() {
+ return !!this.descriptorFront?.isLocalTab;
+ }
+
+ get localTab() {
+ return this.descriptorFront?.localTab || null;
+ }
+
+ // Get a promise of the RootActor's form
+ get root() {
+ return this.client.mainRoot.rootForm;
+ }
+
+ // Get a Front for a target-scoped actor.
+ // i.e. an actor served by RootActor.listTabs or RootActorActor.getTab requests
+ async getFront(typeName) {
+ let front = this.fronts.get(typeName);
+ if (front) {
+ // XXX: This is typically the kind of spot where switching to
+ // `isDestroyed()` is complicated, because `front` is not necessarily a
+ // Front...
+ const isFrontInitializing = typeof front.then === "function";
+ const isFrontAlive = !isFrontInitializing && !front.isDestroyed();
+ if (isFrontInitializing || isFrontAlive) {
+ return front;
+ }
+ }
+
+ front = getFront(this.client, typeName, this.targetForm, this);
+ this.fronts.set(typeName, front);
+ // replace the placeholder with the instance of the front once it has loaded
+ front = await front;
+ this.fronts.set(typeName, front);
+ return front;
+ }
+
+ getCachedFront(typeName) {
+ // do not wait for async fronts;
+ const front = this.fronts.get(typeName);
+ // ensure that the front is a front, and not async front
+ if (front?.actorID) {
+ return front;
+ }
+ return null;
+ }
+
+ get client() {
+ return this._client;
+ }
+
+ // Tells us if we are debugging content document
+ // or if we are debugging chrome stuff.
+ // Allows to controls which features are available against
+ // a chrome or a content document.
+ get chrome() {
+ return (
+ this.isAddon ||
+ this.isContentProcess ||
+ this.isParentProcess ||
+ this.isWindowTarget ||
+ this._forceChrome
+ );
+ }
+
+ forceChrome() {
+ this._forceChrome = true;
+ }
+
+ // Tells us if the related actor implements BrowsingContextTargetActor
+ // interface and requires to call `attach` request before being used and
+ // `detach` during cleanup.
+ get isBrowsingContext() {
+ return this.typeName === "browsingContextTarget";
+ }
+
+ get name() {
+ if (this.isAddon || this.isContentProcess) {
+ return this.targetForm.name;
+ }
+ return this.title;
+ }
+
+ get title() {
+ return this._title || this.url;
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ get isAddon() {
+ return this.isLegacyAddon || this.isWebExtension;
+ }
+
+ get isWorkerTarget() {
+ // XXX Remove the check on `workerDescriptor` as part of Bug 1667404.
+ return (
+ this.typeName === "workerTarget" || this.typeName === "workerDescriptor"
+ );
+ }
+
+ get isLegacyAddon() {
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ this.targetForm.actor.match(/conn\d+\.addon(Target)?\d+/)
+ );
+ }
+
+ get isWebExtension() {
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ (this.targetForm.actor.match(/conn\d+\.webExtension(Target)?\d+/) ||
+ this.targetForm.actor.match(/child\d+\/webExtension(Target)?\d+/))
+ );
+ }
+
+ get isContentProcess() {
+ // browser content toolbox's form will be of the form:
+ // server0.conn0.content-process0/contentProcessTarget7
+ // while xpcshell debugging will be:
+ // server1.conn0.contentProcessTarget7
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ this.targetForm.actor.match(
+ /conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/
+ )
+ );
+ }
+
+ get isParentProcess() {
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ this.targetForm.actor.match(/conn\d+\.parentProcessTarget\d+/)
+ );
+ }
+
+ get isWindowTarget() {
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ this.targetForm.actor.match(/conn\d+\.chromeWindowTarget\d+/)
+ );
+ }
+
+ get isMultiProcess() {
+ return !this.window;
+ }
+
+ getExtensionPathName(url) {
+ // Return the url if the target is not a webextension.
+ if (!this.isWebExtension) {
+ throw new Error("Target is not a WebExtension");
+ }
+
+ try {
+ const parsedURL = new URL(url);
+ // Only moz-extension URL should be shortened into the URL pathname.
+ if (parsedURL.protocol !== "moz-extension:") {
+ return url;
+ }
+ return parsedURL.pathname;
+ } catch (e) {
+ // Return the url if unable to resolve the pathname.
+ return url;
+ }
+ }
+
+ // Attach the console actor
+ async attachConsole() {
+ const consoleFront = await this.getFront("console");
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ // Calling startListeners will populate the traits as it's the first request we
+ // make to the front.
+ await consoleFront.startListeners([]);
+
+ this._onInspectObject = packet => this.emit("inspect-object", packet);
+ this.removeOnInspectObjectListener = consoleFront.on(
+ "inspectObject",
+ this._onInspectObject
+ );
+ }
+
+ /**
+ * This method attaches the target and then attaches its related thread, sending it
+ * the options it needs (e.g. breakpoints, pause on exception setting, …).
+ * This function can be called multiple times, it will only perform the actual
+ * initialization process once; on subsequent call the original promise (_onThreadInitialized)
+ * will be returned.
+ *
+ * @param {TargetList} targetList
+ * @returns {Promise} A promise that resolves once the thread is attached and resumed.
+ */
+ attachAndInitThread(targetList) {
+ if (this._onThreadInitialized) {
+ return this._onThreadInitialized;
+ }
+
+ this._onThreadInitialized = this._attachAndInitThread(targetList);
+ return this._onThreadInitialized;
+ }
+
+ /**
+ * This method attach the target and then attach its related thread, sending it the
+ * options it needs (e.g. breakpoints, pause on exception setting, …)
+ *
+ * @private
+ * @param {TargetList} targetList
+ * @returns {Promise} A promise that resolves once the thread is attached and resumed.
+ */
+ async _attachAndInitThread(targetList) {
+ // If the target is destroyed or soon will be, don't go further
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ // WorkerTargetFront don't have an attach function as the related console and thread
+ // actors are created right away (from devtools/server/startup/worker.js)
+ if (this.attach) {
+ await this.attach();
+ }
+
+ const isBrowserToolbox = targetList.targetFront.isParentProcess;
+ const isNonTopLevelFrameTarget =
+ !this.isTopLevel && this.targetType === targetList.TYPES.FRAME;
+
+ if (isBrowserToolbox && isNonTopLevelFrameTarget) {
+ // In the BrowserToolbox, non-top-level frame targets are already
+ // debugged via content-process targets.
+ // Do not attach the thread here, as it was already done by the
+ // corresponding content-process target.
+ return;
+ }
+
+ const options = await getThreadOptions();
+ // If the target is destroyed or soon will be, don't go further
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+ const threadFront = await this.attachThread(options);
+
+ // @backward-compat { version 86 } ThreadActor.attach no longer pause the thread,
+ // so that we no longer have to resume.
+ // Once 86 is in release, we can remove the rest of this method.
+ if (this.getTrait("noPauseOnThreadActorAttach")) {
+ return;
+ }
+ try {
+ if (this.isDestroyedOrBeingDestroyed() || threadFront.isDestroyed()) {
+ return;
+ }
+ await threadFront.resume();
+ } catch (ex) {
+ if (ex.error === "wrongOrder") {
+ targetList.emit("target-thread-wrong-order-on-resume");
+ } else {
+ throw ex;
+ }
+ }
+ }
+
+ async attachThread(options = {}) {
+ if (!this.targetForm || !this.targetForm.threadActor) {
+ throw new Error(
+ "TargetMixin sub class should set targetForm.threadActor before calling " +
+ "attachThread"
+ );
+ }
+ this.threadFront = await this.getFront("thread");
+ if (
+ this.isDestroyedOrBeingDestroyed() ||
+ this.threadFront.isDestroyed()
+ ) {
+ return this.threadFront;
+ }
+
+ await this.threadFront.attach(options);
+
+ return this.threadFront;
+ }
+
+ /**
+ * Setup listeners.
+ */
+ _addListeners() {
+ this.client.on("closed", this.destroy);
+
+ // `tabDetached` is sent by all target targets types: frame, process and workers.
+ // This is sent when the target is destroyed:
+ // * the target context destroys itself (the tab closes for ex, or the worker shuts down)
+ // in this case, it may be the connector that send this event in the name of the target actor
+ // * the target actor is destroyed, but the target context stays up and running (for ex, when we call Watcher.unwatchTargets)
+ // * the DevToolsServerConnection closes (client closes the connection)
+ this.on("tabDetached", this.destroy);
+ }
+
+ /**
+ * Teardown listeners.
+ */
+ _removeListeners() {
+ // Remove listeners set in _addListeners
+ if (this.client) {
+ this.client.off("closed", this.destroy);
+ }
+ this.off("tabDetached", this.destroy);
+
+ // Remove listeners set in attachConsole
+ if (this.removeOnInspectObjectListener) {
+ this.removeOnInspectObjectListener();
+ this.removeOnInspectObjectListener = null;
+ }
+ }
+
+ isDestroyedOrBeingDestroyed() {
+ return this.isDestroyed() || this._destroyer;
+ }
+
+ /**
+ * Target is not alive anymore.
+ */
+ destroy() {
+ // If several things call destroy then we give them all the same
+ // destruction promise so we're sure to destroy only once
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ // This pattern allows to immediately return the destroyer promise.
+ // See Bug 1602727 for more details.
+ let destroyerResolve;
+ this._destroyer = new Promise(r => (destroyerResolve = r));
+ this._destroyTarget().then(destroyerResolve);
+
+ return this._destroyer;
+ }
+
+ async _destroyTarget() {
+ // Before taking any action, notify listeners that destruction is imminent.
+ this.emit("close");
+
+ // If the target is being attached, try to wait until it's done, to prevent having
+ // pending connection to the server when the toolbox is destroyed.
+ if (this._onThreadInitialized) {
+ try {
+ await this._onThreadInitialized;
+ } catch (e) {
+ // We might still get into cases where attaching fails (e.g. the worker we're
+ // trying to attach to is already closed). Since the target is being destroyed,
+ // we don't need to do anything special here.
+ }
+ }
+
+ for (let [name, front] of this.fronts) {
+ try {
+ // If a Front with an async initialize method is still being instantiated,
+ // we should wait for completion before trying to destroy it.
+ if (front instanceof Promise) {
+ front = await front;
+ }
+ front.destroy();
+ } catch (e) {
+ console.warn("Error while destroying front:", name, e);
+ }
+ }
+
+ this._removeListeners();
+
+ this.threadFront = null;
+
+ if (this.shouldCloseClient) {
+ try {
+ await this._client.close();
+ } catch (e) {
+ // Ignore any errors while closing, since there is not much that can be done
+ // at this point.
+ console.warn("Error while closing client:", e);
+ }
+
+ // Not all targets supports attach/detach. For example content process doesn't.
+ // Also ensure that the front is still active before trying to do the request.
+ } else if (this.detach && !this.isDestroyed()) {
+ // The client was handed to us, so we are not responsible for closing
+ // it. We just need to detach from the tab, if already attached.
+ // |detach| may fail if the connection is already dead, so proceed with
+ // cleanup directly after this.
+ try {
+ await this.detach();
+ } catch (e) {
+ this.logDetachError(e);
+ }
+ }
+
+ // This event should be emitted before calling super.destroy(), because
+ // super.destroy() will remove all event listeners attached to this front.
+ this.emit("target-destroyed");
+
+ // Do that very last in order to let a chance to dispatch `detach` requests.
+ super.destroy();
+
+ this._cleanup();
+ }
+
+ /**
+ * Detach can fail under regular circumstances, if the target was already
+ * destroyed on the server side. All target fronts should handle detach
+ * error logging in similar ways so this might be used by subclasses
+ * with custom detach() implementations.
+ *
+ * @param {Error} e
+ * The real error object.
+ * @param {String} targetType
+ * The type of the target front ("worker", "browsing-context", ...)
+ */
+ logDetachError(e, targetType) {
+ const ignoredError =
+ e?.message.includes("noSuchActor") ||
+ e?.message.includes("Connection closed");
+
+ // Silence exceptions for already destroyed actors and fronts:
+ // - "noSuchActor" errors from the server
+ // - "Connection closed" errors from the client, when purging requests
+ if (ignoredError) {
+ return;
+ }
+
+ // Properly log any other error.
+ const message = targetType
+ ? `Error while detaching the ${targetType} target:`
+ : "Error while detaching target:";
+ console.warn(message, e);
+ }
+
+ /**
+ * Clean up references to what this target points to.
+ */
+ _cleanup() {
+ this.threadFront = null;
+ this._client = null;
+
+ // All target front subclasses set this variable in their `attach` method.
+ // None of them overload destroy, so clean this up from here.
+ this._attach = null;
+
+ this._title = null;
+ this._url = null;
+ }
+
+ _onResourceAvailable(resources) {
+ if (this._resourceCache) {
+ this._resourceCache.push(resources);
+ }
+ }
+
+ toString() {
+ const id = this.targetForm ? this.targetForm.actor : null;
+ return `Target:${id}`;
+ }
+
+ dumpPools() {
+ // NOTE: dumpPools is defined in the Thread actor to avoid
+ // adding it to multiple target specs and actors.
+ return this.threadFront.dumpPools();
+ }
+
+ /**
+ * Log an error of some kind to the tab's console.
+ *
+ * @param {String} text
+ * The text to log.
+ * @param {String} category
+ * The category of the message. @see nsIScriptError.
+ * @returns {Promise}
+ */
+ logErrorInPage(text, category) {
+ if (this.traits.logInPage) {
+ const errorFlag = 0;
+ return this.logInPage({ text, category, flags: errorFlag });
+ }
+ return Promise.resolve();
+ }
+
+ /**
+ * Log a warning of some kind to the tab's console.
+ *
+ * @param {String} text
+ * The text to log.
+ * @param {String} category
+ * The category of the message. @see nsIScriptError.
+ * @returns {Promise}
+ */
+ logWarningInPage(text, category) {
+ if (this.traits.logInPage) {
+ const warningFlag = 1;
+ return this.logInPage({ text, category, flags: warningFlag });
+ }
+ return Promise.resolve();
+ }
+ }
+ return Target;
+}
+exports.TargetMixin = TargetMixin;
diff --git a/devtools/client/fronts/targets/worker.js b/devtools/client/fronts/targets/worker.js
new file mode 100644
index 0000000000..80c875fa5c
--- /dev/null
+++ b/devtools/client/fronts/targets/worker.js
@@ -0,0 +1,29 @@
+/* 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 { workerTargetSpec } = require("devtools/shared/specs/targets/worker");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { TargetMixin } = require("devtools/client/fronts/targets/target-mixin");
+
+class WorkerTargetFront extends TargetMixin(
+ FrontClassWithSpec(workerTargetSpec)
+) {
+ form(json) {
+ this.actorID = json.actor;
+
+ // Save the full form for Target class usage.
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this.targetForm = json;
+
+ this._title = json.title;
+ this._url = json.url;
+ }
+}
+
+exports.WorkerTargetFront = WorkerTargetFront;
+registerFront(exports.WorkerTargetFront);
diff --git a/devtools/client/fronts/thread.js b/devtools/client/fronts/thread.js
new file mode 100644
index 0000000000..a597cd9b5a
--- /dev/null
+++ b/devtools/client/fronts/thread.js
@@ -0,0 +1,307 @@
+/* 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 { ThreadStateTypes } = require("devtools/client/constants");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+const { threadSpec } = require("devtools/shared/specs/thread");
+
+loader.lazyRequireGetter(
+ this,
+ "ObjectFront",
+ "devtools/client/fronts/object",
+ true
+);
+loader.lazyRequireGetter(this, "FrameFront", "devtools/client/fronts/frame");
+loader.lazyRequireGetter(
+ this,
+ "SourceFront",
+ "devtools/client/fronts/source",
+ true
+);
+
+/**
+ * Creates a thread front for the remote debugging protocol server. This client
+ * is a front to the thread actor created in the server side, hiding the
+ * protocol details in a traditional JavaScript API.
+ *
+ * @param client DevToolsClient
+ * @param actor string
+ * The actor ID for this thread.
+ */
+class ThreadFront extends FrontClassWithSpec(threadSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.client = client;
+ this._pauseGrips = {};
+ this._threadGrips = {};
+ // Note that this isn't matching ThreadActor state field.
+ // ThreadFront is only using two values: paused or attached.
+ // @backward-compat { version 86 } ThreadActor.attach no longer pauses the thread,
+ // so that the default state is "attached" by default.
+ if (this.targetFront.getTrait("noPauseOnThreadActorAttach")) {
+ this._state = "attached";
+ } else {
+ this._state = "paused";
+ }
+ this._beforePaused = this._beforePaused.bind(this);
+ this._beforeResumed = this._beforeResumed.bind(this);
+ this.before("paused", this._beforePaused);
+ this.before("resumed", this._beforeResumed);
+ this.targetFront.on("will-navigate", this._onWillNavigate.bind(this));
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "threadActor";
+ }
+
+ get state() {
+ return this._state;
+ }
+
+ get paused() {
+ return this._state === "paused";
+ }
+
+ get actor() {
+ return this.actorID;
+ }
+
+ getWebconsoleFront() {
+ return this.targetFront.getFront("console");
+ }
+
+ _assertPaused(command) {
+ if (!this.paused) {
+ throw Error(
+ command + " command sent while not paused. Currently " + this._state
+ );
+ }
+ }
+
+ getFrames(start, count) {
+ return super.frames(start, count);
+ }
+
+ /**
+ * Resume a paused thread. If the optional limit parameter is present, then
+ * the thread will also pause when that limit is reached.
+ *
+ * @param [optional] object limit
+ * An object with a type property set to the appropriate limit (next,
+ * step, or finish) per the remote debugging protocol specification.
+ * Use null to specify no limit.
+ */
+ async _doResume(resumeLimit, frameActorID) {
+ this._assertPaused("resume");
+
+ // Put the client in a tentative "resuming" state so we can prevent
+ // further requests that should only be sent in the paused state.
+ this._previousState = this._state;
+ this._state = "resuming";
+ try {
+ await super.resume(resumeLimit, frameActorID);
+ } catch (e) {
+ if (this._state == "resuming") {
+ // There was an error resuming, update the state to the new one
+ // reported by the server, if given (only on wrongState), otherwise
+ // reset back to the previous state.
+ if (e.state) {
+ this._state = ThreadStateTypes[e.state];
+ } else {
+ this._state = this._previousState;
+ }
+ }
+ }
+
+ delete this._previousState;
+ }
+
+ /**
+ * Resume a paused thread.
+ */
+ resume() {
+ return this._doResume(null);
+ }
+
+ /**
+ * Resume then pause without stepping.
+ *
+ */
+ resumeThenPause() {
+ return this._doResume({ type: "break" });
+ }
+
+ /**
+ * Step over a function call.
+ */
+ stepOver(frameActorID) {
+ return this._doResume({ type: "next" }, frameActorID);
+ }
+
+ /**
+ * Step into a function call.
+ */
+ stepIn(frameActorID) {
+ return this._doResume({ type: "step" }, frameActorID);
+ }
+
+ /**
+ * Step out of a function call.
+ */
+ stepOut(frameActorID) {
+ return this._doResume({ type: "finish" }, frameActorID);
+ }
+
+ /**
+ * Restart selected frame.
+ */
+ restart(frameActorID) {
+ return this._doResume({ type: "restart" }, frameActorID);
+ }
+
+ /**
+ * Immediately interrupt a running thread.
+ */
+ interrupt() {
+ return this._doInterrupt(null);
+ }
+
+ /**
+ * Pause execution right before the next JavaScript bytecode is executed.
+ */
+ breakOnNext() {
+ return this._doInterrupt("onNext");
+ }
+
+ /**
+ * Interrupt a running thread.
+ */
+ _doInterrupt(when) {
+ return super.interrupt(when);
+ }
+
+ /**
+ * Request the loaded sources for the current thread.
+ */
+ async getSources() {
+ let sources = [];
+ try {
+ sources = await super.sources();
+ } catch (e) {
+ // we may have closed the connection
+ console.log(`getSources failed. Connection may have closed: ${e}`);
+ }
+ return { sources };
+ }
+
+ /**
+ * attach to the thread actor.
+ */
+ async attach(options) {
+ const noPauseOnThreadActorAttach = this.targetFront.getTrait(
+ "noPauseOnThreadActorAttach"
+ );
+ const onPaused = noPauseOnThreadActorAttach ? null : this.once("paused");
+ await super.attach(options);
+ // @backward-compat { version 86 } ThreadActor.attach no longer pause the thread,
+ // so that we shouldn't wait for the paused event,
+ // since it won't be emitted anymore.
+ if (!noPauseOnThreadActorAttach) {
+ await onPaused;
+ }
+ }
+
+ /**
+ * Return a ObjectFront object for the given object grip.
+ *
+ * @param grip object
+ * A pause-lifetime object grip returned by the protocol.
+ */
+ pauseGrip(grip) {
+ if (grip.actor in this._pauseGrips) {
+ return this._pauseGrips[grip.actor];
+ }
+
+ const objectFront = new ObjectFront(
+ this.conn,
+ this.targetFront,
+ this,
+ grip
+ );
+ this._pauseGrips[grip.actor] = objectFront;
+ return objectFront;
+ }
+
+ /**
+ * Clear and invalidate all the grip fronts from the given cache.
+ *
+ * @param gripCacheName
+ * The property name of the grip cache we want to clear.
+ */
+ _clearObjectFronts(gripCacheName) {
+ for (const id in this[gripCacheName]) {
+ this[gripCacheName][id].valid = false;
+ }
+ this[gripCacheName] = {};
+ }
+
+ /**
+ * Invalidate pause-lifetime grip clients and clear the list of current grip
+ * clients.
+ */
+ _clearPauseGrips() {
+ this._clearObjectFronts("_pauseGrips");
+ }
+
+ _beforePaused(packet) {
+ this._state = "paused";
+ this._onThreadState(packet);
+ }
+
+ _beforeResumed() {
+ this._state = "attached";
+ this._onThreadState(null);
+ this.unmanageChildren(FrameFront);
+ }
+
+ _onWillNavigate() {
+ this.unmanageChildren(SourceFront);
+ }
+
+ /**
+ * Handle thread state change by doing necessary cleanup
+ */
+ _onThreadState(packet) {
+ // The debugger UI may not be initialized yet so we want to keep
+ // the packet around so it knows what to pause state to display
+ // when it's initialized
+ this._lastPausePacket = packet;
+ this._clearPauseGrips();
+ }
+
+ getLastPausePacket() {
+ return this._lastPausePacket;
+ }
+
+ /**
+ * Return an instance of SourceFront for the given source actor form.
+ */
+ source(form) {
+ if (form.actor in this._threadGrips) {
+ return this._threadGrips[form.actor];
+ }
+
+ const sourceFront = new SourceFront(this.client, form);
+ this.manage(sourceFront);
+ this._threadGrips[form.actor] = sourceFront;
+ return sourceFront;
+ }
+}
+
+exports.ThreadFront = ThreadFront;
+registerFront(ThreadFront);
diff --git a/devtools/client/fronts/walker.js b/devtools/client/fronts/walker.js
new file mode 100644
index 0000000000..a0f6d71041
--- /dev/null
+++ b/devtools/client/fronts/walker.js
@@ -0,0 +1,551 @@
+/* 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 {
+ FrontClassWithSpec,
+ types,
+ registerFront,
+} = require("devtools/shared/protocol.js");
+const { walkerSpec } = require("devtools/shared/specs/walker");
+const { safeAsyncMethod } = require("devtools/shared/async-utils");
+
+/**
+ * Client side of the DOM walker.
+ */
+class WalkerFront extends FrontClassWithSpec(walkerSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._isPicking = false;
+ this._orphaned = new Set();
+ this._retainedOrphans = new Set();
+
+ // Set to true if cleanup should be requested after every mutation list.
+ this.autoCleanup = true;
+
+ this._rootNodePromise = new Promise(
+ r => (this._rootNodePromiseResolve = r)
+ );
+
+ this._onRootNodeAvailable = this._onRootNodeAvailable.bind(this);
+ this._onRootNodeDestroyed = this._onRootNodeDestroyed.bind(this);
+
+ // pick/cancelPick requests can be triggered while the Walker is being destroyed.
+ this.pick = safeAsyncMethod(this.pick.bind(this), () => this.isDestroyed());
+ this.cancelPick = safeAsyncMethod(this.cancelPick.bind(this), () =>
+ this.isDestroyed()
+ );
+
+ this.before("new-mutations", this.onMutations.bind(this));
+
+ // Those events will be emitted if watchRootNode was called on the
+ // corresponding WalkerActor, which should be handled by the ResourceWatcher
+ // as long as a consumer is watching for root-node resources.
+ // This should be fixed by using watchResources directly from the walker
+ // front, either with the ROOT_NODE resource, or with the DOCUMENT_EVENT
+ // resource. See Bug 1663973.
+ this.on("root-available", this._onRootNodeAvailable);
+ this.on("root-destroyed", this._onRootNodeDestroyed);
+ }
+
+ // Update the object given a form representation off the wire.
+ form(json) {
+ this.actorID = json.actor;
+
+ // The rootNode property should usually be provided via watchRootNode.
+ // However tests are currently using the walker front without explicitly
+ // calling watchRootNode, so we keep this assignment as a fallback.
+ this.rootNode = types.getType("domnode").read(json.root, this);
+
+ this.traits = json.traits;
+ }
+
+ /**
+ * Clients can use walker.rootNode to get the current root node of the
+ * walker, but during a reload the root node might be null. This
+ * method returns a promise that will resolve to the root node when it is
+ * set.
+ */
+ async getRootNode() {
+ let rootNode = this.rootNode;
+ if (!rootNode) {
+ rootNode = await this._rootNodePromise;
+ }
+
+ return rootNode;
+ }
+
+ /**
+ * When reading an actor form off the wire, we want to hook it up to its
+ * parent or host front. The protocol guarantees that the parent will
+ * be seen by the client in either a previous or the current request.
+ * So if we've already seen this parent return it, otherwise create
+ * a bare-bones stand-in node. The stand-in node will be updated
+ * with a real form by the end of the deserialization.
+ */
+ ensureDOMNodeFront(id) {
+ const front = this.getActorByID(id);
+ if (front) {
+ return front;
+ }
+
+ return types.getType("domnode").read({ actor: id }, this, "standin");
+ }
+
+ /**
+ * See the documentation for WalkerActor.prototype.retainNode for
+ * information on retained nodes.
+ *
+ * From the client's perspective, `retainNode` can fail if the node in
+ * question is removed from the ownership tree before the `retainNode`
+ * request reaches the server. This can only happen if the client has
+ * asked the server to release nodes but hasn't gotten a response
+ * yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
+ * set is outstanding.
+ *
+ * If either of those requests is outstanding AND releases the retained
+ * node, this request will fail with noSuchActor, but the ownership tree
+ * will stay in a consistent state.
+ *
+ * Because the protocol guarantees that requests will be processed and
+ * responses received in the order they were sent, we get the right
+ * semantics by setting our local retained flag on the node only AFTER
+ * a SUCCESSFUL retainNode call.
+ */
+ async retainNode(node) {
+ await super.retainNode(node);
+ node.retained = true;
+ }
+
+ async unretainNode(node) {
+ await super.unretainNode(node);
+ node.retained = false;
+ if (this._retainedOrphans.has(node)) {
+ this._retainedOrphans.delete(node);
+ this._releaseFront(node);
+ }
+ }
+
+ releaseNode(node, options = {}) {
+ // NodeFront.destroy will destroy children in the ownership tree too,
+ // mimicking what the server will do here.
+ const actorID = node.actorID;
+ this._releaseFront(node, !!options.force);
+ return super.releaseNode({ actorID: actorID });
+ }
+
+ async findInspectingNode() {
+ const response = await super.findInspectingNode();
+ return response.node;
+ }
+
+ async querySelector(queryNode, selector) {
+ const response = await super.querySelector(queryNode, selector);
+ return response.node;
+ }
+
+ async getNodeActorFromWindowID(windowID) {
+ const response = await super.getNodeActorFromWindowID(windowID);
+ return response ? response.node : null;
+ }
+
+ async getNodeActorFromContentDomReference(contentDomReference) {
+ const response = await super.getNodeActorFromContentDomReference(
+ contentDomReference
+ );
+ return response ? response.node : null;
+ }
+
+ async getStyleSheetOwnerNode(styleSheetActorID) {
+ const response = await super.getStyleSheetOwnerNode(styleSheetActorID);
+ return response ? response.node : null;
+ }
+
+ async getNodeFromActor(actorID, path) {
+ const response = await super.getNodeFromActor(actorID, path);
+ return response ? response.node : null;
+ }
+
+ /*
+ * Incrementally search the document for a given string.
+ * For modern servers, results will be searched with using the WalkerActor
+ * `search` function (includes tag names, attributes, and text contents).
+ * Only 1 result is sent back, and calling the method again with the same
+ * query will send the next result. When there are no more results to be sent
+ * back, null is sent.
+ * @param {String} query
+ * @param {Object} options
+ * - "reverse": search backwards
+ */
+ async search(query, options = {}) {
+ const searchData = (this.searchData = this.searchData || {});
+ const result = await super.search(query, options);
+ const nodeList = result.list;
+
+ // If this is a new search, start at the beginning.
+ if (searchData.query !== query) {
+ searchData.query = query;
+ searchData.index = -1;
+ }
+
+ if (!nodeList.length) {
+ return null;
+ }
+
+ // Move search result cursor and cycle if necessary.
+ searchData.index = options.reverse
+ ? searchData.index - 1
+ : searchData.index + 1;
+ if (searchData.index >= nodeList.length) {
+ searchData.index = 0;
+ }
+ if (searchData.index < 0) {
+ searchData.index = nodeList.length - 1;
+ }
+
+ // Send back the single node, along with any relevant search data
+ const node = await nodeList.item(searchData.index);
+ return {
+ type: "search",
+ node: node,
+ resultsLength: nodeList.length,
+ resultsIndex: searchData.index,
+ };
+ }
+
+ _releaseFront(node, force) {
+ if (node.retained && !force) {
+ node.reparent(null);
+ this._retainedOrphans.add(node);
+ return;
+ }
+
+ if (node.retained) {
+ // Forcing a removal.
+ this._retainedOrphans.delete(node);
+ }
+
+ // Release any children
+ for (const child of node.treeChildren()) {
+ this._releaseFront(child, force);
+ }
+
+ // All children will have been removed from the node by this point.
+ node.reparent(null);
+ node.destroy();
+ }
+
+ /**
+ * Get any unprocessed mutation records and process them.
+ */
+ // eslint-disable-next-line complexity
+ async getMutations(options = {}) {
+ const mutations = await super.getMutations(options);
+ const emitMutations = [];
+ for (const change of mutations) {
+ // The target is only an actorID, get the associated front.
+ const targetID = change.target;
+ const targetFront = this.getActorByID(targetID);
+
+ if (!targetFront) {
+ console.warn(
+ "Got a mutation for an unexpected actor: " +
+ targetID +
+ ", please file a bug on bugzilla.mozilla.org!"
+ );
+ console.trace();
+ continue;
+ }
+
+ const emittedMutation = Object.assign(change, { target: targetFront });
+
+ if (
+ change.type === "childList" ||
+ change.type === "nativeAnonymousChildList"
+ ) {
+ // Update the ownership tree according to the mutation record.
+ const addedFronts = [];
+ const removedFronts = [];
+ for (const removed of change.removed) {
+ const removedFront = this.getActorByID(removed);
+ if (!removedFront) {
+ console.error(
+ "Got a removal of an actor we didn't know about: " + removed
+ );
+ continue;
+ }
+ // Remove from the ownership tree
+ removedFront.reparent(null);
+
+ // This node is orphaned unless we get it in the 'added' list
+ // eventually.
+ this._orphaned.add(removedFront);
+ removedFronts.push(removedFront);
+ }
+ for (const added of change.added) {
+ const addedFront = this.getActorByID(added);
+ if (!addedFront) {
+ console.error(
+ "Got an addition of an actor we didn't know " + "about: " + added
+ );
+ continue;
+ }
+ addedFront.reparent(targetFront);
+
+ // The actor is reconnected to the ownership tree, unorphan
+ // it.
+ this._orphaned.delete(addedFront);
+ addedFronts.push(addedFront);
+ }
+
+ // Before passing to users, replace the added and removed actor
+ // ids with front in the mutation record.
+ emittedMutation.added = addedFronts;
+ emittedMutation.removed = removedFronts;
+
+ // If this is coming from a DOM mutation, the actor's numChildren
+ // was passed in. Otherwise, it is simulated from a frame load or
+ // unload, so don't change the front's form.
+ if ("numChildren" in change) {
+ targetFront._form.numChildren = change.numChildren;
+ }
+ } else if (change.type === "shadowRootAttached") {
+ targetFront._form.isShadowHost = true;
+ } else if (change.type === "customElementDefined") {
+ targetFront._form.customElementLocation = change.customElementLocation;
+ } else if (change.type === "unretained") {
+ // Retained orphans were force-released without the intervention of
+ // client (probably a navigated frame).
+ for (const released of change.nodes) {
+ const releasedFront = this.getActorByID(released);
+ this._retainedOrphans.delete(released);
+ this._releaseFront(releasedFront, true);
+ }
+ } else {
+ targetFront.updateMutation(change);
+ }
+
+ // Update the inlineTextChild property of the target for a selected list of
+ // mutation types.
+ if (
+ change.type === "inlineTextChild" ||
+ change.type === "childList" ||
+ change.type === "shadowRootAttached" ||
+ change.type === "nativeAnonymousChildList"
+ ) {
+ if (change.inlineTextChild) {
+ targetFront.inlineTextChild = types
+ .getType("domnode")
+ .read(change.inlineTextChild, this);
+ } else {
+ targetFront.inlineTextChild = undefined;
+ }
+ }
+
+ emitMutations.push(emittedMutation);
+ }
+
+ if (options.cleanup) {
+ for (const node of this._orphaned) {
+ // This will move retained nodes to this._retainedOrphans.
+ this._releaseFront(node);
+ }
+ this._orphaned = new Set();
+ }
+
+ this.emit("mutations", emitMutations);
+ }
+
+ /**
+ * Handle the `new-mutations` notification by fetching the
+ * available mutation records.
+ */
+ onMutations() {
+ // Fetch and process the mutations.
+ this.getMutations({ cleanup: this.autoCleanup }).catch(() => {});
+ }
+
+ isLocal() {
+ return !!this.conn._transport._serverConnection;
+ }
+
+ async removeNode(node) {
+ const previousSibling = await this.previousSibling(node);
+ const nextSibling = await super.removeNode(node);
+ return {
+ previousSibling: previousSibling,
+ nextSibling: nextSibling,
+ };
+ }
+
+ async children(node, options) {
+ if (!node.remoteFrame) {
+ return super.children(node, options);
+ }
+ const remoteTarget = await node.connectToRemoteFrame();
+ const walker = (await remoteTarget.getFront("inspector")).walker;
+
+ // Finally retrieve the NodeFront of the remote frame's document
+ const documentNode = await walker.getRootNode();
+
+ // Force reparenting through the remote frame boundary.
+ documentNode.reparent(node);
+
+ // And return the same kind of response `walker.children` returns
+ return {
+ nodes: [documentNode],
+ hasFirst: true,
+ hasLast: true,
+ };
+ }
+
+ /**
+ * Ensure that the RootNode of this Walker has the right parent NodeFront.
+ *
+ * This method does nothing if we are on the top level target's WalkerFront,
+ * as the RootNode won't have any parent.
+ *
+ * Otherwise, if we are in an iframe's WalkerFront, we would expect the parent
+ * of the RootNode (i.e. the NodeFront for the document loaded within the iframe)
+ * to be the <iframe>'s NodeFront. Because of fission, the two NodeFront may refer
+ * to DOM Element running in distinct processes and so the NodeFront comes from
+ * two distinct Targets and two distinct WalkerFront.
+ * This is why we need this manual "reparent" code to do the glue between the
+ * two documents.
+ */
+ async reparentRemoteFrame() {
+ const parentTarget = await this.targetFront.getParentTarget();
+ if (!parentTarget) {
+ return;
+ }
+ // Don't reparent if we are on the top target
+ if (parentTarget == this.targetFront) {
+ return;
+ }
+ // Get the NodeFront for the embedder element
+ // i.e. the <iframe> element which is hosting the document that
+ const parentWalker = (await parentTarget.getFront("inspector")).walker;
+ // As this <iframe> most likely runs in another process, we have to get it through the parent
+ // target's WalkerFront.
+ const parentNode = (
+ await parentWalker.getEmbedderElement(this.targetFront.browsingContextID)
+ ).node;
+
+ // Finally, set this embedder element's node front as the
+ const documentNode = await this.getRootNode();
+ documentNode.reparent(parentNode);
+ }
+
+ /**
+ * Evaluate the cross iframes query selectors for the current walker front.
+ *
+ * @param {Array} selectors
+ * An array of CSS selectors to find the target accessible object.
+ * Several selectors can be needed if the element is nested in frames
+ * and not directly in the root document.
+ * @return {Promise} a promise that resolves when the node front is found for
+ * selection using inspector tools.
+ */
+ async findNodeFront(nodeSelectors) {
+ const querySelectors = async nodeFront => {
+ const selector = nodeSelectors.shift();
+ if (!selector) {
+ return nodeFront;
+ }
+ nodeFront = await this.querySelector(nodeFront, selector);
+
+ // It's possible the containing iframe isn't available by the time
+ // this.querySelector is called, which causes the re-selected node to be
+ // unavailable. There also isn't a way for us to know when all iframes on the page
+ // have been created after a reload. Because of this, we should should bail here.
+ if (!nodeFront) {
+ return null;
+ }
+
+ if (nodeSelectors.length > 0) {
+ await nodeFront.waitForFrameLoad();
+
+ const { nodes } = await this.children(nodeFront);
+
+ // If there are remaining selectors to process, they will target a document or a
+ // document-fragment under the current node. Whether the element is a frame or
+ // a web component, it can only contain one document/document-fragment, so just
+ // select the first one available.
+ nodeFront = nodes.find(node => {
+ const { nodeType } = node;
+ return (
+ nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
+ nodeType === Node.DOCUMENT_NODE
+ );
+ });
+ }
+ return querySelectors(nodeFront) || nodeFront;
+ };
+ const nodeFront = await this.getRootNode();
+
+ // If rootSelectors are [frameSelector1, ..., frameSelectorN, rootSelector]
+ // we expect that [frameSelector1, ..., frameSelectorN] will also be in
+ // nodeSelectors.
+ // Otherwise it means the nodeSelectors target a node outside of this walker
+ // and we should return null.
+ const rootFrontSelectors = await nodeFront.getAllSelectors();
+ for (let i = 0; i < rootFrontSelectors.length - 1; i++) {
+ if (rootFrontSelectors[i] !== nodeSelectors[i]) {
+ return null;
+ }
+ }
+
+ // The query will start from the walker's rootNode, remove all the
+ // "frameSelectors".
+ nodeSelectors.splice(0, rootFrontSelectors.length - 1);
+
+ return querySelectors(nodeFront);
+ }
+
+ _onRootNodeAvailable(rootNode) {
+ if (rootNode.isTopLevelDocument) {
+ this.rootNode = rootNode;
+ this._rootNodePromiseResolve(this.rootNode);
+ }
+ }
+
+ _onRootNodeDestroyed(rootNode) {
+ if (rootNode.isTopLevelDocument) {
+ this._rootNodePromise = new Promise(
+ r => (this._rootNodePromiseResolve = r)
+ );
+ this.rootNode = null;
+ }
+ }
+
+ /**
+ * Start the element picker on the debuggee target.
+ * @param {Boolean} doFocus - Optionally focus the content area once the picker is
+ * activated.
+ */
+ pick(doFocus) {
+ if (this._isPicking) {
+ return Promise.resolve();
+ }
+
+ this._isPicking = true;
+ return super.pick(doFocus);
+ }
+
+ /**
+ * Stop the element picker.
+ */
+ cancelPick() {
+ if (!this._isPicking) {
+ return Promise.resolve();
+ }
+
+ this._isPicking = false;
+ return super.cancelPick();
+ }
+}
+
+exports.WalkerFront = WalkerFront;
+registerFront(WalkerFront);
diff --git a/devtools/client/fronts/watcher.js b/devtools/client/fronts/watcher.js
new file mode 100644
index 0000000000..239d3047a0
--- /dev/null
+++ b/devtools/client/fronts/watcher.js
@@ -0,0 +1,113 @@
+/* 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 { watcherSpec } = require("devtools/shared/specs/watcher");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+loader.lazyRequireGetter(
+ this,
+ "BrowsingContextTargetFront",
+ "devtools/client/fronts/targets/browsing-context",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ContentProcessTargetFront",
+ "devtools/client/fronts/targets/content-process",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "WorkerTargetFront",
+ "devtools/client/fronts/targets/worker",
+ true
+);
+
+class WatcherFront extends FrontClassWithSpec(watcherSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._onTargetAvailable = this._onTargetAvailable.bind(this);
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+
+ // Convert form, which is just JSON object to Fronts for these two events
+ this.on("target-available-form", this._onTargetAvailable);
+ this.on("target-destroyed-form", this._onTargetDestroyed);
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ this.traits = json.traits;
+ }
+
+ _onTargetAvailable(form) {
+ let front;
+ if (form.actor.includes("/contentProcessTarget")) {
+ front = new ContentProcessTargetFront(this.conn, null, this);
+ } else if (form.actor.includes("/workerTarget")) {
+ front = new WorkerTargetFront(this.conn, null, this);
+ } else {
+ front = new BrowsingContextTargetFront(this.conn, null, this);
+ }
+ front.actorID = form.actor;
+ front.form(form);
+ this.manage(front);
+ this.emit("target-available", front);
+ }
+
+ _onTargetDestroyed(form) {
+ const front = this.getActorByID(form.actor);
+ this.emit("target-destroyed", front);
+ }
+
+ /**
+ * Retrieve the already existing BrowsingContextTargetFront for the parent
+ * BrowsingContext of the given BrowsingContext ID.
+ */
+ async getParentBrowsingContextTarget(browsingContextID) {
+ const id = await this.getParentBrowsingContextID(browsingContextID);
+ if (!id) {
+ return null;
+ }
+ return this.getBrowsingContextTarget(id);
+ }
+
+ /**
+ * For a given BrowsingContext ID, return the already existing BrowsingContextTargetFront
+ */
+ async getBrowsingContextTarget(id) {
+ // First scan the watcher children as the watcher manages all the targets
+ for (const front of this.poolChildren()) {
+ if (front.browsingContextID == id) {
+ return front;
+ }
+ }
+ // But the top level target will be created by the Descriptor.getTarget() method
+ // and so be hosted in the Descriptor's pool.
+ // The parent front of the WatcherActor happens to be the Descriptor Actor.
+ // This code could go away or be simplified if the Descriptor starts fetch all
+ // the targets, including the top level one via the Watcher. i.e. drop Descriptor.getTarget().
+ const topLevelTarget = await this.parentFront.getTarget();
+ if (topLevelTarget.browsingContextID == id) {
+ return topLevelTarget;
+ }
+
+ // If we could not find a browsing context target for the provided id, the
+ // browsing context might not be the topmost browsing context of a given
+ // process. For now we only create targets for the top browsing context of
+ // each process, so we recursively check the parent browsing context ids
+ // until we find a valid target.
+ const parentBrowsingContextID = await this.getParentBrowsingContextID(id);
+ if (parentBrowsingContextID && parentBrowsingContextID !== id) {
+ return this.getBrowsingContextTarget(parentBrowsingContextID);
+ }
+
+ return null;
+ }
+}
+registerFront(WatcherFront);
diff --git a/devtools/client/fronts/webconsole.js b/devtools/client/fronts/webconsole.js
new file mode 100644
index 0000000000..6b0e705810
--- /dev/null
+++ b/devtools/client/fronts/webconsole.js
@@ -0,0 +1,507 @@
+/* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { LongStringFront } = require("devtools/client/fronts/string");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { webconsoleSpec } = require("devtools/shared/specs/webconsole");
+const {
+ getAdHocFrontOrPrimitiveGrip,
+} = require("devtools/client/fronts/object");
+
+/**
+ * A WebConsoleFront is used as a front end for the WebConsoleActor that is
+ * created on the server, hiding implementation details.
+ *
+ * @param object client
+ * The DevToolsClient instance we live for.
+ */
+class WebConsoleFront extends FrontClassWithSpec(webconsoleSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._client = client;
+ this.traits = {};
+ this._longStrings = {};
+ this.events = [];
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "consoleActor";
+
+ this.pendingEvaluationResults = new Map();
+ this.onEvaluationResult = this.onEvaluationResult.bind(this);
+ this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
+
+ this.on("evaluationResult", this.onEvaluationResult);
+ this.before("consoleAPICall", this.beforeConsoleAPICall);
+ this.before("pageError", this.beforePageError);
+ this.before("serverNetworkEvent", this.beforeServerNetworkEvent);
+
+ this._client.on("networkEventUpdate", this._onNetworkEventUpdate);
+ }
+
+ get actor() {
+ return this.actorID;
+ }
+
+ /**
+ * The "networkEventUpdate" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param object packet
+ * The message received from the server.
+ */
+ _onNetworkEventUpdate(packet) {
+ this.emit("serverNetworkUpdateEvent", packet);
+ }
+
+ beforeServerNetworkEvent(packet) {
+ // The stacktrace info needs to be sent before
+ // the network event.
+ this.emit("serverNetworkStackTrace", packet);
+ }
+
+ /**
+ * Evaluate a JavaScript expression asynchronously.
+ *
+ * @param {String} string: The code you want to evaluate.
+ * @param {Object} opts: Options for evaluation:
+ *
+ * - {String} frameActor: a FrameActor ID. The FA holds a reference to
+ * a Debugger.Frame. This option allows you to evaluate the string in
+ * the frame of the given FA.
+ *
+ * - {String} url: the url to evaluate the script as. Defaults to
+ * "debugger eval code".
+ *
+ * - {String} selectedNodeActor: the NodeActor ID of the current
+ * selection in the Inspector, if such a selection
+ * exists. This is used by helper functions that can
+ * reference the currently selected node in the Inspector, like $0.
+ *
+ * - {String} selectedObjectActor: the actorID of a given objectActor.
+ * This is used by context menu entries to get a reference to an object, in order
+ * to perform some operation on it (copy it, store it as a global variable, …).
+ *
+ * - {Integer} innerWindowID: An optional window id to be used for the evaluation,
+ * instead of the regular webConsoleActor.evalWindow.
+ * This is used by functions that may want to evaluate in a different window (for
+ * example a non-remote iframe), like getting the elements of a given document.
+ *
+ * @return {Promise}: A promise that resolves with the response.
+ */
+ async evaluateJSAsync(string, opts = {}) {
+ const options = {
+ text: string,
+ frameActor: opts.frameActor,
+ url: opts.url,
+ selectedNodeActor: opts.selectedNodeActor,
+ selectedObjectActor: opts.selectedObjectActor,
+ innerWindowID: opts.innerWindowID,
+ mapped: opts.mapped,
+ eager: opts.eager,
+ };
+
+ this._pendingAsyncEvaluation = super.evaluateJSAsync(options);
+ const { resultID } = await this._pendingAsyncEvaluation;
+ this._pendingAsyncEvaluation = null;
+
+ return new Promise((resolve, reject) => {
+ // Null check this in case the client has been detached while sending
+ // the one way request
+ if (this.pendingEvaluationResults) {
+ this.pendingEvaluationResults.set(resultID, resp => {
+ if (resp.error) {
+ reject(resp);
+ } else {
+ if (resp.result) {
+ resp.result = getAdHocFrontOrPrimitiveGrip(resp.result, this);
+ }
+
+ if (resp.helperResult?.object) {
+ resp.helperResult.object = getAdHocFrontOrPrimitiveGrip(
+ resp.helperResult.object,
+ this
+ );
+ }
+
+ if (resp.exception) {
+ resp.exception = getAdHocFrontOrPrimitiveGrip(
+ resp.exception,
+ this
+ );
+ }
+
+ if (resp.exceptionMessage) {
+ resp.exceptionMessage = getAdHocFrontOrPrimitiveGrip(
+ resp.exceptionMessage,
+ this
+ );
+ }
+
+ resolve(resp);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Handler for the actors's unsolicited evaluationResult packet.
+ */
+ async onEvaluationResult(packet) {
+ // In some cases, the evaluationResult event can be received before the initial call
+ // to evaluationJSAsync completes. So make sure to wait for the corresponding promise
+ // before handling the event.
+ await this._pendingAsyncEvaluation;
+
+ // Find the associated callback based on this ID, and fire it.
+ // In a sync evaluation, this would have already been called in
+ // direct response to the client.request function.
+ const onResponse = this.pendingEvaluationResults.get(packet.resultID);
+ if (onResponse) {
+ onResponse(packet);
+ this.pendingEvaluationResults.delete(packet.resultID);
+ } else {
+ DevToolsUtils.reportException(
+ "onEvaluationResult",
+ "No response handler for an evaluateJSAsync result (resultID: " +
+ packet.resultID +
+ ")"
+ );
+ }
+ }
+
+ beforeConsoleAPICall(packet) {
+ if (packet.message && Array.isArray(packet.message.arguments)) {
+ // We might need to create fronts for each of the message arguments.
+ packet.message.arguments = packet.message.arguments.map(arg =>
+ getAdHocFrontOrPrimitiveGrip(arg, this)
+ );
+ }
+ return packet;
+ }
+
+ beforePageError(packet) {
+ if (packet?.pageError?.errorMessage) {
+ packet.pageError.errorMessage = getAdHocFrontOrPrimitiveGrip(
+ packet.pageError.errorMessage,
+ this
+ );
+ }
+
+ if (packet?.pageError?.exception) {
+ packet.pageError.exception = getAdHocFrontOrPrimitiveGrip(
+ packet.pageError.exception,
+ this
+ );
+ }
+ return packet;
+ }
+
+ async getCachedMessages(messageTypes) {
+ const response = await super.getCachedMessages(messageTypes);
+ if (Array.isArray(response.messages)) {
+ response.messages = response.messages.map(packet => {
+ if (Array.isArray(packet?.message?.arguments)) {
+ // We might need to create fronts for each of the message arguments.
+ packet.message.arguments = packet.message.arguments.map(arg =>
+ getAdHocFrontOrPrimitiveGrip(arg, this)
+ );
+ }
+
+ if (packet.pageError?.exception) {
+ packet.pageError.exception = getAdHocFrontOrPrimitiveGrip(
+ packet.pageError.exception,
+ this
+ );
+ }
+
+ return packet;
+ });
+ }
+ return response;
+ }
+
+ /**
+ * Retrieve the request headers from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getRequestHeaders(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getRequestHeaders",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the request cookies from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getRequestCookies(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getRequestCookies",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the request post data from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getRequestPostData(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getRequestPostData",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the response headers from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getResponseHeaders(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getResponseHeaders",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the response cookies from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getResponseCookies(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getResponseCookies",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the response content from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getResponseContent(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getResponseContent",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the response cache from the given NetworkEventActor
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces.
+ */
+ getResponseCache(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getResponseCache",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the timing information for the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getEventTimings(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getEventTimings",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the security information for the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getSecurityInfo(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getSecurityInfo",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the stack-trace information for the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the stack-trace is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getStackTrace(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getStackTrace",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Start the given Web Console listeners.
+ * TODO: remove once the front is retrieved via getFront, and we use form()
+ *
+ * @see this.LISTENERS
+ * @param array listeners
+ * Array of listeners you want to start. See this.LISTENERS for
+ * known listeners.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ async startListeners(listeners) {
+ const response = await super.startListeners(listeners);
+ this.hasNativeConsoleAPI = response.nativeConsoleAPI;
+ this.traits = response.traits;
+ return response;
+ }
+
+ /**
+ * Return an instance of LongStringFront for the given long string grip.
+ *
+ * @param object grip
+ * The long string grip returned by the protocol.
+ * @return {LongStringFront} the front for the given long string grip.
+ */
+ longString(grip) {
+ if (grip.actor in this._longStrings) {
+ return this._longStrings[grip.actor];
+ }
+
+ const front = new LongStringFront(this._client, this.targetFront, this);
+ front.form(grip);
+ this.manage(front);
+ this._longStrings[grip.actor] = front;
+ return front;
+ }
+
+ /**
+ * Fetches the full text of a LongString.
+ *
+ * @param object | string stringGrip
+ * The long string grip containing the corresponding actor.
+ * If you pass in a plain string (by accident or because you're lazy),
+ * then a promise of the same string is simply returned.
+ * @return object Promise
+ * A promise that is resolved when the full string contents
+ * are available, or rejected if something goes wrong.
+ */
+ async getString(stringGrip) {
+ // Make sure this is a long string.
+ if (typeof stringGrip !== "object" || stringGrip.type !== "longString") {
+ // Go home string, you're drunk.
+ return stringGrip;
+ }
+
+ // Fetch the long string only once.
+ if (stringGrip._fullText) {
+ return stringGrip._fullText;
+ }
+
+ const { initial, length } = stringGrip;
+ const longStringFront = this.longString(stringGrip);
+
+ try {
+ const response = await longStringFront.substring(initial.length, length);
+ return initial + response;
+ } catch (e) {
+ DevToolsUtils.reportException("getString", e.message);
+ throw e;
+ }
+ }
+
+ /**
+ * Close the WebConsoleFront.
+ *
+ */
+ destroy() {
+ if (!this._client) {
+ return null;
+ }
+
+ this._client.off("networkEventUpdate", this._onNetworkEventUpdate);
+ // This will make future calls to this function harmless because of the early return
+ // at the top of the function.
+ this._client = null;
+
+ this.off("evaluationResult", this.onEvaluationResult);
+ this._longStrings = null;
+ this.pendingEvaluationResults.clear();
+ this.pendingEvaluationResults = null;
+ return super.destroy();
+ }
+}
+
+exports.WebConsoleFront = WebConsoleFront;
+registerFront(WebConsoleFront);
diff --git a/devtools/client/fronts/websocket.js b/devtools/client/fronts/websocket.js
new file mode 100644
index 0000000000..8aa60e9b38
--- /dev/null
+++ b/devtools/client/fronts/websocket.js
@@ -0,0 +1,115 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { webSocketSpec } = require("devtools/shared/specs/websocket");
+
+/**
+ * A WebSocketFront is used as a front end for the WebSocketActor that is
+ * created on the server, hiding implementation details.
+ */
+class WebSocketFront extends FrontClassWithSpec(webSocketSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._onWebSocketOpened = this._onWebSocketOpened.bind(this);
+ this._onWebSocketClosed = this._onWebSocketClosed.bind(this);
+ this._onFrameSent = this._onFrameSent.bind(this);
+ this._onFrameReceived = this._onFrameReceived.bind(this);
+
+ // Attribute name from which to retrieve the actorID
+ // out of the target actor's form
+ this.formAttributeName = "webSocketActor";
+
+ this.on("serverWebSocketOpened", this._onWebSocketOpened);
+ this.on("serverWebSocketClosed", this._onWebSocketClosed);
+ this.on("serverFrameSent", this._onFrameSent);
+ this.on("serverFrameReceived", this._onFrameReceived);
+ }
+
+ /**
+ * Close the WebSocketFront.
+ *
+ */
+ destroy() {
+ this.off("serverWebSocketOpened");
+ this.off("serverWebSocketClosed");
+ this.off("serverFrameSent");
+ this.off("serverFrameReceived");
+ return super.destroy();
+ }
+
+ /**
+ * The "webSocketOpened" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param number httpChannelId
+ * Channel ID of the websocket connection.
+ * @param string effectiveURI
+ * URI of the page.
+ * @param string protocols
+ * WebSocket procotols.
+ * @param string extensions
+ */
+ async _onWebSocketOpened(httpChannelId, effectiveURI, protocols, extensions) {
+ this.emit(
+ "webSocketOpened",
+ httpChannelId,
+ effectiveURI,
+ protocols,
+ extensions
+ );
+ }
+
+ /**
+ * The "webSocketClosed" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param number httpChannelId
+ * @param boolean wasClean
+ * @param number code
+ * @param string reason
+ */
+ async _onWebSocketClosed(httpChannelId, wasClean, code, reason) {
+ this.emit("webSocketClosed", httpChannelId, wasClean, code, reason);
+ }
+
+ /**
+ * The "frameReceived" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string httpChannelId
+ * Channel ID of the websocket connection.
+ * @param object data
+ * The data received from the server.
+ */
+ async _onFrameReceived(httpChannelId, data) {
+ this.emit("frameReceived", httpChannelId, data);
+ }
+
+ /**
+ * The "frameSent" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param string httpChannelId
+ * Channel ID of the websocket connection.
+ * @param object data
+ * The data received from the server.
+ */
+ async _onFrameSent(httpChannelId, data) {
+ this.emit("frameSent", httpChannelId, data);
+ }
+}
+
+exports.WebSocketFront = WebSocketFront;
+registerFront(WebSocketFront);
diff --git a/devtools/client/fronts/worker/moz.build b/devtools/client/fronts/worker/moz.build
new file mode 100644
index 0000000000..dae0e2d606
--- /dev/null
+++ b/devtools/client/fronts/worker/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "push-subscription.js",
+ "service-worker-registration.js",
+ "service-worker.js",
+)
diff --git a/devtools/client/fronts/worker/push-subscription.js b/devtools/client/fronts/worker/push-subscription.js
new file mode 100644
index 0000000000..5c71065867
--- /dev/null
+++ b/devtools/client/fronts/worker/push-subscription.js
@@ -0,0 +1,30 @@
+/* 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 {
+ pushSubscriptionSpec,
+} = require("devtools/shared/specs/worker/push-subscription");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+
+class PushSubscriptionFront extends FrontClassWithSpec(pushSubscriptionSpec) {
+ get endpoint() {
+ return this._form.endpoint;
+ }
+
+ get quota() {
+ return this._form.quota;
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ }
+}
+
+exports.PushSubscriptionFront = PushSubscriptionFront;
+registerFront(PushSubscriptionFront);
diff --git a/devtools/client/fronts/worker/service-worker-registration.js b/devtools/client/fronts/worker/service-worker-registration.js
new file mode 100644
index 0000000000..6dbe836962
--- /dev/null
+++ b/devtools/client/fronts/worker/service-worker-registration.js
@@ -0,0 +1,79 @@
+/* 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 {
+ serviceWorkerRegistrationSpec,
+} = require("devtools/shared/specs/worker/service-worker-registration");
+const {
+ FrontClassWithSpec,
+ registerFront,
+ types,
+} = require("devtools/shared/protocol");
+
+class ServiceWorkerRegistrationFront extends FrontClassWithSpec(
+ serviceWorkerRegistrationSpec
+) {
+ get active() {
+ return this._form.active;
+ }
+
+ get fetch() {
+ return this._form.fetch;
+ }
+
+ get id() {
+ return this.url;
+ }
+
+ get lastUpdateTime() {
+ return this._form.lastUpdateTime;
+ }
+
+ get scope() {
+ return this._form.scope;
+ }
+
+ get type() {
+ return this._form.type;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get evaluatingWorker() {
+ return this._getServiceWorker("evaluatingWorker");
+ }
+
+ get activeWorker() {
+ return this._getServiceWorker("activeWorker");
+ }
+
+ get installingWorker() {
+ return this._getServiceWorker("installingWorker");
+ }
+
+ get waitingWorker() {
+ return this._getServiceWorker("waitingWorker");
+ }
+
+ _getServiceWorker(type) {
+ const workerForm = this._form[type];
+ if (!workerForm) {
+ return null;
+ }
+ return types.getType("serviceWorker").read(workerForm, this);
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ // @backward-compat { version 70 } ServiceWorkerRegistration actor now exposes traits
+ this.traits = form.traits || {};
+ }
+}
+
+exports.ServiceWorkerRegistrationFront = ServiceWorkerRegistrationFront;
+registerFront(ServiceWorkerRegistrationFront);
diff --git a/devtools/client/fronts/worker/service-worker.js b/devtools/client/fronts/worker/service-worker.js
new file mode 100644
index 0000000000..39480f95a3
--- /dev/null
+++ b/devtools/client/fronts/worker/service-worker.js
@@ -0,0 +1,63 @@
+/* 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 {
+ serviceWorkerSpec,
+} = require("devtools/shared/specs/worker/service-worker");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("devtools/shared/protocol");
+const { Ci } = require("chrome");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/debugger.properties"
+);
+
+class ServiceWorkerFront extends FrontClassWithSpec(serviceWorkerSpec) {
+ get fetch() {
+ return this._form.fetch;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get state() {
+ return this._form.state;
+ }
+
+ get stateText() {
+ switch (this.state) {
+ case Ci.nsIServiceWorkerInfo.STATE_PARSED:
+ return L10N.getStr("serviceWorkerInfo.parsed");
+ case Ci.nsIServiceWorkerInfo.STATE_INSTALLING:
+ return L10N.getStr("serviceWorkerInfo.installing");
+ case Ci.nsIServiceWorkerInfo.STATE_INSTALLED:
+ return L10N.getStr("serviceWorkerInfo.installed");
+ case Ci.nsIServiceWorkerInfo.STATE_ACTIVATING:
+ return L10N.getStr("serviceWorkerInfo.activating");
+ case Ci.nsIServiceWorkerInfo.STATE_ACTIVATED:
+ return L10N.getStr("serviceWorkerInfo.activated");
+ case Ci.nsIServiceWorkerInfo.STATE_REDUNDANT:
+ return L10N.getStr("serviceWorkerInfo.redundant");
+ default:
+ return L10N.getStr("serviceWorkerInfo.unknown");
+ }
+ }
+
+ get id() {
+ return this._form.id;
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ }
+}
+
+exports.ServiceWorkerFront = ServiceWorkerFront;
+registerFront(ServiceWorkerFront);