tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ext-devtools-panels.js (23136B)


      1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set sts=2 sw=2 et tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 "use strict";
      8 
      9 var { ExtensionParent } = ChromeUtils.importESModule(
     10  "resource://gre/modules/ExtensionParent.sys.mjs"
     11 );
     12 
     13 ChromeUtils.defineESModuleGetters(this, {
     14  BroadcastConduit: "resource://gre/modules/ConduitsParent.sys.mjs",
     15 });
     16 
     17 var { watchExtensionProxyContextLoad } = ExtensionParent;
     18 
     19 const WEBEXT_PANELS_URL = "chrome://browser/content/webext-panels.xhtml";
     20 
     21 class BaseDevToolsPanel {
     22  constructor(context, panelOptions) {
     23    const toolbox = context.devToolsToolbox;
     24    if (!toolbox) {
     25      // This should never happen when this constructor is called with a valid
     26      // devtools extension context.
     27      throw Error("Missing mandatory toolbox");
     28    }
     29 
     30    this.context = context;
     31    this.extension = context.extension;
     32    this.toolbox = toolbox;
     33    this.viewType = "devtools_panel";
     34    this.panelOptions = panelOptions;
     35    this.id = panelOptions.id;
     36 
     37    this.unwatchExtensionProxyContextLoad = null;
     38 
     39    // References to the panel browser XUL element and the toolbox window global which
     40    // contains the devtools panel UI.
     41    this.browser = null;
     42    this.browserContainerWindow = null;
     43  }
     44 
     45  async createBrowserElement(window) {
     46    const { toolbox } = this;
     47    const { extension } = this.context;
     48    const { url } = this.panelOptions || { url: "about:blank" };
     49 
     50    this.browser = await window.getBrowser({
     51      extension,
     52      extensionUrl: url,
     53      browserStyle: false,
     54      viewType: "devtools_panel",
     55      browserInsertedData: {
     56        devtoolsToolboxInfo: {
     57          toolboxPanelId: this.id,
     58          inspectedWindowTabId: getTargetTabIdForToolbox(toolbox),
     59        },
     60      },
     61    });
     62 
     63    let hasTopLevelContext = false;
     64 
     65    // Listening to new proxy contexts.
     66    this.unwatchExtensionProxyContextLoad = watchExtensionProxyContextLoad(
     67      this,
     68      context => {
     69        // Keep track of the toolbox and target associated to the context, which is
     70        // needed by the API methods implementation.
     71        context.devToolsToolbox = toolbox;
     72 
     73        if (!hasTopLevelContext) {
     74          hasTopLevelContext = true;
     75 
     76          // Resolve the promise when the root devtools_panel context has been created.
     77          if (this._resolveTopLevelContext) {
     78            this._resolveTopLevelContext(context);
     79          }
     80        }
     81      }
     82    );
     83 
     84    this.syncToolboxZoom();
     85    this.toolbox.win.browsingContext.embedderElement.addEventListener(
     86      "FullZoomChange",
     87      this
     88    );
     89 
     90    this.browser.fixupAndLoadURIString(url, {
     91      triggeringPrincipal: this.context.principal,
     92    });
     93  }
     94 
     95  handleEvent(event) {
     96    switch (event.type) {
     97      case "FullZoomChange": {
     98        this.syncToolboxZoom();
     99        break;
    100      }
    101    }
    102  }
    103 
    104  /**
    105   * The remote `<browser>` that loads the panel content does not inherit
    106   * the zoom level of the `<browser>` it's nested inside of.
    107   *
    108   *   about:devtools-toolbox <browser> (zoom level applied here)
    109   *     - ...
    110   *     - webext-panels.xhtml <iframe> (inherits zoom)
    111   *       - <browser remote="true"> (doesn't inherit zoom)
    112   *
    113   * To work around this, we manually synchronize the zoom levels.
    114   */
    115  syncToolboxZoom() {
    116    if (!this.browser) {
    117      return;
    118    }
    119 
    120    this.browser.fullZoom = this.toolbox.win.browsingContext.fullZoom;
    121  }
    122 
    123  destroyBrowserElement() {
    124    const { browser, unwatchExtensionProxyContextLoad } = this;
    125    if (unwatchExtensionProxyContextLoad) {
    126      this.unwatchExtensionProxyContextLoad = null;
    127      unwatchExtensionProxyContextLoad();
    128    }
    129 
    130    if (this.toolbox) {
    131      this.toolbox.win.browsingContext.embedderElement.removeEventListener(
    132        "FullZoomChange",
    133        this
    134      );
    135    }
    136 
    137    if (browser) {
    138      browser.remove();
    139      this.browser = null;
    140    }
    141  }
    142 }
    143 
    144 /**
    145 * Represents an addon devtools panel in the main process.
    146 */
    147 class ParentDevToolsPanel extends BaseDevToolsPanel {
    148  /**
    149   * @param {DevToolsExtensionPageContextParent} context
    150   *        A devtools extension proxy context running in a main process.
    151   * @param {object} panelOptions
    152   * @param {string} panelOptions.id
    153   *        The id of the addon devtools panel.
    154   * @param {string} panelOptions.icon
    155   *        The icon of the addon devtools panel.
    156   * @param {string} panelOptions.title
    157   *        The title of the addon devtools panel.
    158   * @param {string} panelOptions.url
    159   *        The url of the addon devtools panel, relative to the extension base URL.
    160   */
    161  constructor(context, panelOptions) {
    162    super(context, panelOptions);
    163 
    164    this.visible = false;
    165    this.destroyed = false;
    166 
    167    this.context.callOnClose(this);
    168 
    169    this.conduit = new BroadcastConduit(this, {
    170      id: `${this.id}-parent`,
    171      send: ["PanelHidden", "PanelShown"],
    172    });
    173 
    174    this.onToolboxPanelSelect = this.onToolboxPanelSelect.bind(this);
    175    this.onToolboxHostWillChange = this.onToolboxHostWillChange.bind(this);
    176    this.onToolboxHostChanged = this.onToolboxHostChanged.bind(this);
    177 
    178    this.waitTopLevelContext = new Promise(resolve => {
    179      this._resolveTopLevelContext = resolve;
    180    });
    181 
    182    this.panelAdded = false;
    183    this.addPanel();
    184  }
    185 
    186  addPanel() {
    187    const { icon, title } = this.panelOptions;
    188    const extensionName = this.context.extension.name;
    189 
    190    this.toolbox.addAdditionalTool({
    191      id: this.id,
    192      extensionId: this.context.extension.id,
    193      url: WEBEXT_PANELS_URL,
    194      icon: icon,
    195      label: title,
    196      // panelLabel is used to set the aria-label attribute (See Bug 1570645).
    197      panelLabel: title,
    198      tooltip: `DevTools Panel added by "${extensionName}" add-on.`,
    199      isToolSupported: toolbox => toolbox.commands.descriptorFront.isLocalTab,
    200      build: (window, toolbox) => {
    201        if (toolbox !== this.toolbox) {
    202          throw new Error(
    203            "Unexpected toolbox received on addAdditionalTool build property"
    204          );
    205        }
    206 
    207        const destroy = this.buildPanel(window);
    208 
    209        return { toolbox, destroy };
    210      },
    211    });
    212 
    213    this.panelAdded = true;
    214  }
    215 
    216  buildPanel(window) {
    217    const { toolbox } = this;
    218 
    219    this.createBrowserElement(window);
    220 
    221    // Store the last panel's container element (used to restore it when the toolbox
    222    // host is switched between docked and undocked).
    223    this.browserContainerWindow = window;
    224 
    225    toolbox.on("select", this.onToolboxPanelSelect);
    226    toolbox.on("host-will-change", this.onToolboxHostWillChange);
    227    toolbox.on("host-changed", this.onToolboxHostChanged);
    228 
    229    // Return a cleanup method that is when the panel is destroyed, e.g.
    230    // - when addon devtool panel has been disabled by the user from the toolbox preferences,
    231    //   its ParentDevToolsPanel instance is still valid, but the built devtools panel is removed from
    232    //   the toolbox (and re-built again if the user re-enables it from the toolbox preferences panel)
    233    // - when the creator context has been destroyed, the ParentDevToolsPanel close method is called,
    234    //   it removes the tool definition from the toolbox, which will call this destroy method.
    235    return () => {
    236      this.destroyBrowserElement();
    237      this.browserContainerWindow = null;
    238      toolbox.off("select", this.onToolboxPanelSelect);
    239      toolbox.off("host-will-change", this.onToolboxHostWillChange);
    240      toolbox.off("host-changed", this.onToolboxHostChanged);
    241    };
    242  }
    243 
    244  onToolboxHostWillChange() {
    245    // NOTE: Using a content iframe here breaks the devtools panel
    246    // switching between docked and undocked mode,
    247    // because of a swapFrameLoader exception (see bug 1075490),
    248    // destroy the browser and recreate it after the toolbox host has been
    249    // switched is a reasonable workaround to fix the issue on release and beta
    250    // Firefox versions (at least until the underlying bug can be fixed).
    251    if (this.browser) {
    252      // Fires a panel.onHidden event before destroying the browser element because
    253      // the toolbox hosts is changing.
    254      if (this.visible) {
    255        this.conduit.sendPanelHidden(this.id);
    256      }
    257 
    258      this.destroyBrowserElement();
    259    }
    260  }
    261 
    262  async onToolboxHostChanged() {
    263    if (this.browserContainerWindow) {
    264      this.createBrowserElement(this.browserContainerWindow);
    265 
    266      // Fires a panel.onShown event once the browser element has been recreated
    267      // after the toolbox hosts has been changed (needed to provide the new window
    268      // object to the extension page that has created the devtools panel).
    269      if (this.visible) {
    270        await this.waitTopLevelContext;
    271        this.conduit.sendPanelShown(this.id);
    272      }
    273    }
    274  }
    275 
    276  async onToolboxPanelSelect(id) {
    277    if (!this.waitTopLevelContext || !this.panelAdded) {
    278      return;
    279    }
    280 
    281    // Wait that the panel is fully loaded and emit show.
    282    await this.waitTopLevelContext;
    283 
    284    if (!this.visible && id === this.id) {
    285      this.visible = true;
    286      this.conduit.sendPanelShown(this.id);
    287    } else if (this.visible && id !== this.id) {
    288      this.visible = false;
    289      this.conduit.sendPanelHidden(this.id);
    290    }
    291  }
    292 
    293  close() {
    294    const { toolbox } = this;
    295 
    296    if (!toolbox) {
    297      throw new Error("Unable to destroy a closed devtools panel");
    298    }
    299 
    300    this.conduit.close();
    301 
    302    // Explicitly remove the panel if it is registered and the toolbox is not
    303    // closing itself.
    304    if (this.panelAdded && toolbox.isToolRegistered(this.id)) {
    305      this.destroyBrowserElement();
    306      toolbox.removeAdditionalTool(this.id);
    307    }
    308 
    309    this.waitTopLevelContext = null;
    310    this._resolveTopLevelContext = null;
    311    this.context = null;
    312    this.toolbox = null;
    313    this.browser = null;
    314    this.browserContainerWindow = null;
    315  }
    316 
    317  destroyBrowserElement() {
    318    super.destroyBrowserElement();
    319 
    320    // If the panel has been removed or disabled (e.g. from the toolbox preferences
    321    // or during the toolbox switching between docked and undocked),
    322    // we need to re-initialize the waitTopLevelContext Promise.
    323    this.waitTopLevelContext = new Promise(resolve => {
    324      this._resolveTopLevelContext = resolve;
    325    });
    326  }
    327 }
    328 
    329 class DevToolsSelectionObserver extends EventEmitter {
    330  constructor(context) {
    331    if (!context.devToolsToolbox) {
    332      // This should never happen when this constructor is called with a valid
    333      // devtools extension context.
    334      throw Error("Missing mandatory toolbox");
    335    }
    336 
    337    super();
    338    context.callOnClose(this);
    339 
    340    this.toolbox = context.devToolsToolbox;
    341    this.onSelected = this.onSelected.bind(this);
    342    this.initialized = false;
    343  }
    344 
    345  on(...args) {
    346    this.lazyInit();
    347    super.on.apply(this, args);
    348  }
    349 
    350  once(...args) {
    351    this.lazyInit();
    352    super.once.apply(this, args);
    353  }
    354 
    355  async lazyInit() {
    356    if (!this.initialized) {
    357      this.initialized = true;
    358      this.toolbox.on("selection-changed", this.onSelected);
    359    }
    360  }
    361 
    362  close() {
    363    if (this.destroyed) {
    364      throw new Error("Unable to close a destroyed DevToolsSelectionObserver");
    365    }
    366 
    367    if (this.initialized) {
    368      this.toolbox.off("selection-changed", this.onSelected);
    369    }
    370 
    371    this.toolbox = null;
    372    this.destroyed = true;
    373  }
    374 
    375  onSelected() {
    376    this.emit("selectionChanged");
    377  }
    378 }
    379 
    380 /**
    381 * Represents an addon devtools inspector sidebar in the main process.
    382 */
    383 class ParentDevToolsInspectorSidebar extends BaseDevToolsPanel {
    384  /**
    385   * @param {DevToolsExtensionPageContextParent} context
    386   *        A devtools extension proxy context running in a main process.
    387   * @param {object} panelOptions
    388   * @param {string} panelOptions.id
    389   *        The id of the addon devtools sidebar.
    390   * @param {string} panelOptions.title
    391   *        The title of the addon devtools sidebar.
    392   */
    393  constructor(context, panelOptions) {
    394    super(context, panelOptions);
    395 
    396    this.visible = false;
    397    this.destroyed = false;
    398 
    399    this.context.callOnClose(this);
    400 
    401    this.conduit = new BroadcastConduit(this, {
    402      id: `${this.id}-parent`,
    403      send: ["InspectorSidebarHidden", "InspectorSidebarShown"],
    404    });
    405 
    406    this.onSidebarSelect = this.onSidebarSelect.bind(this);
    407    this.onSidebarCreated = this.onSidebarCreated.bind(this);
    408    this.onExtensionPageMount = this.onExtensionPageMount.bind(this);
    409    this.onExtensionPageUnmount = this.onExtensionPageUnmount.bind(this);
    410    this.onToolboxHostWillChange = this.onToolboxHostWillChange.bind(this);
    411    this.onToolboxHostChanged = this.onToolboxHostChanged.bind(this);
    412 
    413    this.toolbox.once(
    414      `extension-sidebar-created-${this.id}`,
    415      this.onSidebarCreated
    416    );
    417    this.toolbox.on("inspector-sidebar-select", this.onSidebarSelect);
    418    this.toolbox.on("host-will-change", this.onToolboxHostWillChange);
    419    this.toolbox.on("host-changed", this.onToolboxHostChanged);
    420 
    421    // Set by setObject if the sidebar has not been created yet.
    422    this._initializeSidebar = null;
    423 
    424    // Set by _updateLastExpressionResult to keep track of the last
    425    // object value grip (to release the previous selected actor
    426    // on the remote debugging server when the actor changes).
    427    this._lastExpressionResult = null;
    428 
    429    this.toolbox.registerInspectorExtensionSidebar(this.id, {
    430      title: panelOptions.title,
    431    });
    432  }
    433 
    434  close() {
    435    if (this.destroyed) {
    436      throw new Error("Unable to close a destroyed DevToolsSelectionObserver");
    437    }
    438 
    439    this.conduit.close();
    440 
    441    if (this.extensionSidebar) {
    442      this.extensionSidebar.off(
    443        "extension-page-mount",
    444        this.onExtensionPageMount
    445      );
    446      this.extensionSidebar.off(
    447        "extension-page-unmount",
    448        this.onExtensionPageUnmount
    449      );
    450    }
    451 
    452    if (this.browser) {
    453      this.destroyBrowserElement();
    454      this.browser = null;
    455      this.containerEl = null;
    456    }
    457 
    458    this.toolbox.off(
    459      `extension-sidebar-created-${this.id}`,
    460      this.onSidebarCreated
    461    );
    462    this.toolbox.off("inspector-sidebar-select", this.onSidebarSelect);
    463    this.toolbox.off("host-changed", this.onToolboxHostChanged);
    464    this.toolbox.off("host-will-change", this.onToolboxHostWillChange);
    465 
    466    this.toolbox.unregisterInspectorExtensionSidebar(this.id);
    467    this.extensionSidebar = null;
    468    this._lazySidebarInit = null;
    469 
    470    this.destroyed = true;
    471  }
    472 
    473  onToolboxHostWillChange() {
    474    if (this.browser) {
    475      this.destroyBrowserElement();
    476    }
    477  }
    478 
    479  onToolboxHostChanged() {
    480    if (this.containerEl && this.panelOptions.url) {
    481      this.createBrowserElement(this.containerEl.contentWindow);
    482    }
    483  }
    484 
    485  onExtensionPageMount(containerEl) {
    486    this.containerEl = containerEl;
    487 
    488    // Wait the webext-panel.xhtml page to have been loaded in the
    489    // inspector sidebar panel.
    490    const onLoaded = () => {
    491      this.createBrowserElement(containerEl.contentWindow);
    492    };
    493    // ExtensionUtils.promiseDocumentLoaded would attach a load listener to the
    494    // container window, which will be replaced during the load (Bug 1955324).
    495    const doc = containerEl.contentDocument;
    496    if (doc.readyState == "complete" && doc.location.href != "about:blank") {
    497      onLoaded();
    498    } else {
    499      containerEl.addEventListener("load", onLoaded, { once: true });
    500    }
    501  }
    502 
    503  onExtensionPageUnmount() {
    504    this.containerEl = null;
    505    this.destroyBrowserElement();
    506  }
    507 
    508  onSidebarCreated(sidebar) {
    509    this.extensionSidebar = sidebar;
    510 
    511    sidebar.on("extension-page-mount", this.onExtensionPageMount);
    512    sidebar.on("extension-page-unmount", this.onExtensionPageUnmount);
    513 
    514    const { _lazySidebarInit } = this;
    515    this._lazySidebarInit = null;
    516 
    517    if (typeof _lazySidebarInit === "function") {
    518      _lazySidebarInit();
    519    }
    520  }
    521 
    522  onSidebarSelect(id) {
    523    if (!this.extensionSidebar) {
    524      return;
    525    }
    526 
    527    if (!this.visible && id === this.id) {
    528      this.visible = true;
    529      this.conduit.sendInspectorSidebarShown(this.id);
    530    } else if (this.visible && id !== this.id) {
    531      this.visible = false;
    532      this.conduit.sendInspectorSidebarHidden(this.id);
    533    }
    534  }
    535 
    536  setPage(extensionPageURL) {
    537    this.panelOptions.url = extensionPageURL;
    538 
    539    if (this.extensionSidebar) {
    540      if (this.browser) {
    541        // Just load the new extension page url in the existing browser, if
    542        // it already exists.
    543        this.browser.fixupAndLoadURIString(this.panelOptions.url, {
    544          triggeringPrincipal: this.context.extension.principal,
    545        });
    546      } else {
    547        // The browser element doesn't exist yet, but the sidebar has been
    548        // already created (e.g. because the inspector was already selected
    549        // in a open toolbox and the extension has been installed/reloaded/updated).
    550        this.extensionSidebar.setExtensionPage(WEBEXT_PANELS_URL);
    551      }
    552    } else {
    553      // Defer the sidebar.setExtensionPage call.
    554      this._setLazySidebarInit(() =>
    555        this.extensionSidebar.setExtensionPage(WEBEXT_PANELS_URL)
    556      );
    557    }
    558  }
    559 
    560  setObject(object, rootTitle) {
    561    delete this.panelOptions.url;
    562 
    563    this._updateLastExpressionResult(null);
    564 
    565    // Nest the object inside an object, as the value of the `rootTitle` property.
    566    if (rootTitle) {
    567      object = { [rootTitle]: object };
    568    }
    569 
    570    if (this.extensionSidebar) {
    571      this.extensionSidebar.setObject(object);
    572    } else {
    573      // Defer the sidebar.setObject call.
    574      this._setLazySidebarInit(() => this.extensionSidebar.setObject(object));
    575    }
    576  }
    577 
    578  _setLazySidebarInit(cb) {
    579    this._lazySidebarInit = cb;
    580  }
    581 
    582  setExpressionResult(expressionResult, rootTitle) {
    583    delete this.panelOptions.url;
    584 
    585    this._updateLastExpressionResult(expressionResult);
    586 
    587    if (this.extensionSidebar) {
    588      this.extensionSidebar.setExpressionResult(expressionResult, rootTitle);
    589    } else {
    590      // Defer the sidebar.setExpressionResult call.
    591      this._setLazySidebarInit(() => {
    592        this.extensionSidebar.setExpressionResult(expressionResult, rootTitle);
    593      });
    594    }
    595  }
    596 
    597  _updateLastExpressionResult(newExpressionResult = null) {
    598    const { _lastExpressionResult } = this;
    599 
    600    this._lastExpressionResult = newExpressionResult;
    601 
    602    const oldActor = _lastExpressionResult && _lastExpressionResult.actorID;
    603    const newActor = newExpressionResult && newExpressionResult.actorID;
    604 
    605    // Release the previously active actor on the remote debugging server.
    606    if (
    607      oldActor &&
    608      oldActor !== newActor &&
    609      typeof _lastExpressionResult.release === "function"
    610    ) {
    611      _lastExpressionResult.release();
    612    }
    613  }
    614 }
    615 
    616 const sidebarsById = new Map();
    617 
    618 this.devtools_panels = class extends ExtensionAPI {
    619  getAPI(context) {
    620    // TODO - Bug 1448878: retrieve a more detailed callerInfo object,
    621    // like the filename and lineNumber of the actual extension called
    622    // in the child process.
    623    const callerInfo = {
    624      addonId: context.extension.id,
    625      url: context.extension.baseURI.spec,
    626    };
    627 
    628    // An incremental "per context" id used in the generated devtools panel id.
    629    let nextPanelId = 0;
    630 
    631    const toolboxSelectionObserver = new DevToolsSelectionObserver(context);
    632 
    633    function newBasePanelId() {
    634      return `${context.extension.id}-${context.contextId}-${nextPanelId++}`;
    635    }
    636 
    637    return {
    638      devtools: {
    639        panels: {
    640          elements: {
    641            onSelectionChanged: new EventManager({
    642              context,
    643              name: "devtools.panels.elements.onSelectionChanged",
    644              register: fire => {
    645                const listener = () => {
    646                  fire.async();
    647                };
    648                toolboxSelectionObserver.on("selectionChanged", listener);
    649                return () => {
    650                  toolboxSelectionObserver.off("selectionChanged", listener);
    651                };
    652              },
    653            }).api(),
    654            createSidebarPane(title) {
    655              const id = `devtools-inspector-sidebar-${makeWidgetId(
    656                newBasePanelId()
    657              )}`;
    658 
    659              const parentSidebar = new ParentDevToolsInspectorSidebar(
    660                context,
    661                { title, id }
    662              );
    663              sidebarsById.set(id, parentSidebar);
    664 
    665              context.callOnClose({
    666                close() {
    667                  sidebarsById.delete(id);
    668                },
    669              });
    670 
    671              // Resolved to the devtools sidebar id into the child addon process,
    672              // where it will be used to identify the messages related
    673              // to the panel API onShown/onHidden events.
    674              return Promise.resolve(id);
    675            },
    676            // The following methods are used internally to allow the sidebar API
    677            // piece that is running in the child process to asks the parent process
    678            // to execute the sidebar methods.
    679            Sidebar: {
    680              setPage(sidebarId, extensionPageURL) {
    681                const sidebar = sidebarsById.get(sidebarId);
    682                return sidebar.setPage(extensionPageURL);
    683              },
    684              setObject(sidebarId, jsonObject, rootTitle) {
    685                const sidebar = sidebarsById.get(sidebarId);
    686                return sidebar.setObject(jsonObject, rootTitle);
    687              },
    688              async setExpression(sidebarId, evalExpression, rootTitle) {
    689                const sidebar = sidebarsById.get(sidebarId);
    690 
    691                const toolboxEvalOptions = await getToolboxEvalOptions(context);
    692 
    693                const commands = await context.getDevToolsCommands();
    694                const target = commands.targetCommand.targetFront;
    695                const consoleFront = await target.getFront("console");
    696                toolboxEvalOptions.consoleFront = consoleFront;
    697 
    698                const evalResult = await commands.inspectedWindowCommand.eval(
    699                  callerInfo,
    700                  evalExpression,
    701                  toolboxEvalOptions
    702                );
    703 
    704                let jsonObject;
    705 
    706                if (evalResult.exceptionInfo) {
    707                  jsonObject = evalResult.exceptionInfo;
    708 
    709                  return sidebar.setObject(jsonObject, rootTitle);
    710                }
    711 
    712                return sidebar.setExpressionResult(evalResult, rootTitle);
    713              },
    714            },
    715          },
    716          create(title, icon, url) {
    717            // Get a fallback icon from the manifest data.
    718            if (icon === "") {
    719              icon = context.extension.getPreferredIcon(128);
    720            }
    721 
    722            icon = context.extension.baseURI.resolve(icon);
    723            url = context.extension.baseURI.resolve(url);
    724 
    725            const id = `webext-devtools-panel-${makeWidgetId(
    726              newBasePanelId()
    727            )}`;
    728 
    729            new ParentDevToolsPanel(context, { title, icon, url, id });
    730 
    731            // Resolved to the devtools panel id into the child addon process,
    732            // where it will be used to identify the messages related
    733            // to the panel API onShown/onHidden events.
    734            return Promise.resolve(id);
    735          },
    736        },
    737      },
    738    };
    739  }
    740 };