Bug 1287910 - move devtools stack-related APIs to per-platform require; r?jryans draft
authorTom Tromey <tom@tromey.com>
Fri, 05 Aug 2016 13:17:17 -0600
changeset 399983 a11038d6c87ba90a4ce2771f038b2fd0f2bd567a
parent 399322 4caf18e4aaca92756f79b2cba666073fe76eabb7
child 528121 789c29121dbae1f8c7b55b9b28bfdca5721f5aba
push id26056
push userbmo:ttromey@mozilla.com
push dateFri, 12 Aug 2016 14:28:55 +0000
reviewersjryans
bugs1287910
milestone51.0a1
Bug 1287910 - move devtools stack-related APIs to per-platform require; r?jryans MozReview-Commit-ID: CgT1VGJnJqB
.eslintignore
devtools/.eslintrc
devtools/client/inspector/.eslintrc
devtools/shared/DevToolsUtils.js
devtools/shared/Loader.jsm
devtools/shared/client/main.js
devtools/shared/event-emitter.js
devtools/shared/moz.build
devtools/shared/platform/README.md
devtools/shared/platform/chrome/moz.build
devtools/shared/platform/chrome/stack.js
devtools/shared/platform/content/moz.build
devtools/shared/platform/content/stack.js
devtools/shared/platform/content/test/.eslintrc
devtools/shared/platform/content/test/test_stack.js
devtools/shared/platform/content/test/xpcshell.ini
devtools/shared/platform/moz.build
devtools/shared/protocol.js
devtools/shared/worker/loader.js
--- a/.eslintignore
+++ b/.eslintignore
@@ -137,16 +137,17 @@ devtools/shared/apps/**
 devtools/shared/client/**
 devtools/shared/discovery/**
 devtools/shared/gcli/**
 !devtools/shared/gcli/templater.js
 devtools/shared/heapsnapshot/**
 devtools/shared/layout/**
 devtools/shared/locales/**
 devtools/shared/performance/**
+!devtools/shared/platform/**
 devtools/shared/qrcode/**
 devtools/shared/security/**
 devtools/shared/shims/**
 devtools/shared/tests/**
 !devtools/shared/tests/unit/test_csslexer.js
 devtools/shared/touch/**
 devtools/shared/transport/**
 !devtools/shared/transport/transport.js
--- a/devtools/.eslintrc
+++ b/devtools/.eslintrc
@@ -34,16 +34,20 @@
 
     // Rules from the mozilla plugin
     "mozilla/mark-test-function-used": 1,
     "mozilla/no-aArgs": 1,
     "mozilla/no-cpows-in-tests": 2,
     "mozilla/no-single-arg-cu-import": 2,
     // See bug 1224289.
     "mozilla/reject-importGlobalProperties": 2,
+    // devtools/shared/platform is special; see the README.md in that
+    // directory for details.  We reject requires using explicit
+    // subdirectories of this directory.
+    "mozilla/reject-some-requires": [2, "^devtools/shared/platform/(chome|content)/"],
     "mozilla/var-only-at-top-level": 1,
 
     // Rules from the React plugin
     "react/display-name": 2,
     "react/no-danger": 2,
     "react/no-did-mount-set-state": 2,
     "react/no-did-update-set-state": 2,
     "react/no-direct-mutation-state": 2,
--- a/devtools/client/inspector/.eslintrc
+++ b/devtools/client/inspector/.eslintrc
@@ -2,11 +2,11 @@
   // Extend from the devtools eslintrc.
   "extends": "../../.eslintrc",
 
   "rules": {
     // The inspector is being migrated to HTML and cleaned of
     // chrome-privileged code, so this rule disallows requiring chrome
     // code. Some files in the inspector disable this rule still. The
     // goal is to enable the rule globally on all files.
-    "mozilla/reject-some-requires": [2, "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm)$"],
+    "mozilla/reject-some-requires": [2, "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm|devtools/shared/platform/(chome|content)/.*)$"],
   },
 }
--- a/devtools/shared/DevToolsUtils.js
+++ b/devtools/shared/DevToolsUtils.js
@@ -6,16 +6,17 @@
 
 /* General utilities used throughout devtools. */
 
 var { Ci, Cu, Cc, components } = require("chrome");
 var Services = require("Services");
 var promise = require("promise");
 var defer = require("devtools/shared/defer");
 var flags = require("./flags");
+var {getStack, callFunctionWithAsyncStack} = require("devtools/shared/platform/stack");
 
 loader.lazyRequireGetter(this, "FileUtils",
                          "resource://gre/modules/FileUtils.jsm", true);
 
 // Re-export the thread-safe utils.
 const ThreadSafeDevToolsUtils = require("./ThreadSafeDevToolsUtils.js");
 for (let key of Object.keys(ThreadSafeDevToolsUtils)) {
   exports[key] = ThreadSafeDevToolsUtils[key];
@@ -27,19 +28,19 @@ for (let key of Object.keys(ThreadSafeDe
 exports.executeSoon = function executeSoon(aFn) {
   if (isWorker) {
     setImmediate(aFn);
   } else {
     let executor;
     // Only enable async stack reporting when DEBUG_JS_MODULES is set
     // (customized local builds) to avoid a performance penalty.
     if (AppConstants.DEBUG_JS_MODULES || flags.testing) {
-      let stack = components.stack;
+      let stack = getStack();
       executor = () => {
-        Cu.callFunctionWithAsyncStack(aFn, stack, "DevToolsUtils.executeSoon");
+        callFunctionWithAsyncStack(aFn, stack, "DevToolsUtils.executeSoon");
       };
     } else {
       executor = aFn;
     }
     Services.tm.mainThread.dispatch({
       run: exports.makeInfallible(executor)
     }, Ci.nsIThread.DISPATCH_NORMAL);
   }
--- a/devtools/shared/Loader.jsm
+++ b/devtools/shared/Loader.jsm
@@ -28,16 +28,23 @@ var sharedGlobalBlocklist = ["sdk/indexe
  */
 function BuiltinProvider() {}
 BuiltinProvider.prototype = {
   load: function () {
     const paths = {
       // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
       "": "resource://gre/modules/commonjs/",
       // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+      // Modules here are intended to have one implementation for
+      // chrome, and a separate implementation for content.  Here we
+      // map the directory to the chrome subdirectory, but the content
+      // loader will map to the content subdirectory.  See the
+      // README.md in devtools/shared/platform.
+      "devtools/shared/platform": "resource://devtools/shared/platform/chrome",
+      // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
       "devtools": "resource://devtools",
       // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
       "gcli": "resource://devtools/shared/gcli/source/lib/gcli",
       // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
       "acorn": "resource://devtools/acorn",
       // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
       "acorn/util/walk": "resource://devtools/acorn/walk.js",
       // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -1,19 +1,20 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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, Cu, components } = require("chrome");
+const { Ci, Cu } = require("chrome");
 const Services = require("Services");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { getStack, callFunctionWithAsyncStack } = require("devtools/shared/platform/stack");
 
 const promise = Cu.import("resource://devtools/shared/deprecated-sync-thenables.js", {}).Promise;
 
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 loader.lazyRequireGetter(this, "WebConsoleClient", "devtools/shared/webconsole/client", true);
 loader.lazyRequireGetter(this, "DebuggerSocket", "devtools/shared/security/socket", true);
 loader.lazyRequireGetter(this, "Authentication", "devtools/shared/security/auth");
 
@@ -698,17 +699,17 @@ DebuggerClient.prototype = {
       if (aOnResponse) {
         aOnResponse(resp);
       }
       return promise.reject(resp);
     }
 
     let request = new Request(aRequest);
     request.format = "json";
-    request.stack = components.stack;
+    request.stack = getStack();
     if (aOnResponse) {
       request.on("json-reply", aOnResponse);
     }
 
     this._sendOrQueueRequest(request);
 
     // Implement a Promise like API on the returned object
     // that resolves/rejects on request response
@@ -1004,18 +1005,18 @@ DebuggerClient.prototype = {
     // that lack a packet type.
     if (aPacket.type) {
       this.emit(aPacket.type, aPacket);
     }
 
     if (activeRequest) {
       let emitReply = () => activeRequest.emit("json-reply", aPacket);
       if (activeRequest.stack) {
-        Cu.callFunctionWithAsyncStack(emitReply, activeRequest.stack,
-                                      "DevTools RDP");
+        callFunctionWithAsyncStack(emitReply, activeRequest.stack,
+                                   "DevTools RDP");
       } else {
         emitReply();
       }
     }
   },
 
   /**
    * Called by the DebuggerTransport to dispatch incoming bulk packets as
--- a/devtools/shared/event-emitter.js
+++ b/devtools/shared/event-emitter.js
@@ -40,38 +40,38 @@
     // but it doesn't depends on any real module. We can save a few cycles
     // and bytes by not loading Loader.jsm.
     let require = function (module) {
       switch (module) {
         case "devtools/shared/defer":
           return Cu.import("resource://gre/modules/Promise.jsm", {}).Promise.defer;
         case "Services":
           return Cu.import("resource://gre/modules/Services.jsm", {}).Services;
-        case "chrome":
-          return {
-            Cu,
-            components: Components
-          };
+        case "devtools/shared/platform/stack": {
+          let obj = {};
+          Cu.import("resource://devtools/shared/platform/chrome/stack.js", obj);
+          return obj;
+        }
       }
       return null;
     };
     factory.call(this, require, this, { exports: this }, console);
     this.EXPORTED_SYMBOLS = ["EventEmitter"];
   }
 }).call(this, function (require, exports, module, console) {
   // ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
   // After this point the code may not use Cu.import, and should only
   // require() modules that are "clean-for-content".
   let EventEmitter = this.EventEmitter = function () {};
   module.exports = EventEmitter;
 
   // See comment in JSM module boilerplate when adding a new dependency.
-  const { components } = require("chrome");
   const Services = require("Services");
   const defer = require("devtools/shared/defer");
+  const { describeNthCaller } = require("devtools/shared/platform/stack");
   let loggingEnabled = true;
 
   if (!isWorker) {
     loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
     Services.prefs.addObserver("devtools.dump.emit", {
       observe: () => {
         loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
       }
@@ -199,26 +199,17 @@
       }
     },
 
     logEvent(event, args) {
       if (!loggingEnabled) {
         return;
       }
 
-      let caller, func, path;
-      if (!isWorker) {
-        caller = components.stack.caller.caller;
-        func = caller.name;
-        let file = caller.filename;
-        if (file.includes(" -> ")) {
-          file = caller.filename.split(/ -> /)[1];
-        }
-        path = file + ":" + caller.lineNumber;
-      }
+      let description = describeNthCaller(2);
 
       let argOut = "(";
       if (args.length === 1) {
         argOut += event;
       }
 
       let out = "EMITTING: ";
 
@@ -246,14 +237,14 @@
           }
         }
       } catch (e) {
         // Object is dead so the toolbox is most likely shutting down,
         // do nothing.
       }
 
       argOut += ")";
-      out += "emit" + argOut + " from " + func + "() -> " + path + "\n";
+      out += "emit" + argOut + " from " + description + "\n";
 
       dump(out);
     },
   };
 });
--- a/devtools/shared/moz.build
+++ b/devtools/shared/moz.build
@@ -14,16 +14,17 @@ DIRS += [
     'fronts',
     'gcli',
     'heapsnapshot',
     'inspector',
     'jsbeautify',
     'layout',
     'locales',
     'performance',
+    'platform',
     'pretty-fast',
     'qrcode',
     'security',
     'sourcemap',
     'shims',
     'specs',
     'touch',
     'transport',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/README.md
@@ -0,0 +1,13 @@
+This directory is treated specially by the loaders.
+
+In particular, when running in chrome, a resource like
+"devtools/shared/platform/mumble" will be found in the chrome
+subdirectory; and when running in content, it will be found in the
+content subdirectory.
+
+Outside of tests, it's not ok to require a specific version of a file;
+and there is an eslint test to check for that.  That is,
+require("devtools/shared/platform/client/mumble") is an error.
+
+When adding a new file, you must add two copies, one to chrome and one
+to content.  Otherwise, one case or the other will fail to work.
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/chrome/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(
+    'stack.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/chrome/stack.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+// A few wrappers for stack-manipulation.  This version of the module
+// is used in chrome code.
+
+"use strict";
+
+(function (factory) {
+  // This file might be require()d, but might also be loaded via
+  // Cu.import.  Account for the differences here.
+  if (this.module && module.id.indexOf("stack") >= 0) {
+    // require.
+    const {components, Cu} = require("chrome");
+    factory.call(this, components, Cu, exports);
+  } else {
+    // Cu.import.
+    this.isWorker = false;
+    factory.call(this, Components, Components.utils, this);
+    this.EXPORTED_SYMBOLS = ["callFunctionWithAsyncStack", "describeNthCaller",
+                             "getStack"];
+  }
+}).call(this, function (components, Cu, exports) {
+  /**
+   * Return a description of the Nth caller, suitable for logging.
+   *
+   * @param {Number} n the caller to describe
+   * @return {String} a description of the nth caller.
+   */
+  function describeNthCaller(n) {
+    if (isWorker) {
+      return "";
+    }
+
+    let caller = components.stack;
+    // Do one extra iteration to skip this function.
+    while (n >= 0) {
+      --n;
+      caller = caller.caller;
+    }
+
+    let func = caller.name;
+    let file = caller.filename;
+    if (file.includes(" -> ")) {
+      file = caller.filename.split(/ -> /)[1];
+    }
+    let path = file + ":" + caller.lineNumber;
+
+    return func + "() -> " + path;
+  }
+
+  /**
+   * Return a stack object that can be serialized and, when
+   * deserialized, passed to callFunctionWithAsyncStack.
+   */
+  function getStack() {
+    return components.stack.caller;
+  }
+
+  /**
+   * Like Cu.callFunctionWithAsyncStack but handles the isWorker case
+   * -- |Cu| isn't defined in workers.
+   */
+  function callFunctionWithAsyncStack(callee, stack, id) {
+    if (isWorker) {
+      return callee();
+    }
+    return Cu.callFunctionWithAsyncStack(callee, stack, id);
+  }
+
+  exports.callFunctionWithAsyncStack = callFunctionWithAsyncStack;
+  exports.describeNthCaller = describeNthCaller;
+  exports.getStack = getStack;
+});
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/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(
+    'stack.js',
+)
+
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/stack.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/. */
+
+// A few wrappers for stack-manipulation.  This version of the module
+// is used in content code.  Note that this particular copy of the
+// file can only be loaded via require(), because Cu.import doesn't
+// exist in the content case.  So, we don't need the code to handle
+// both require and import here.
+
+"use strict";
+
+/**
+ * Looks like Cu.callFunctionWithAsyncStack, but just calls the callee.
+ */
+function callFunctionWithAsyncStack(callee, stack, id) {
+  return callee();
+}
+
+/**
+ * Return a description of the Nth caller, suitable for logging.
+ *
+ * @param {Number} n the caller to describe
+ * @return {String} a description of the nth caller.
+ */
+function describeNthCaller(n) {
+  if (isWorker) {
+    return "";
+  }
+
+  let stack = new Error().stack.split("\n");
+  // Add one here to skip this function.
+  return stack[n + 1];
+}
+
+/**
+ * Return a stack object that can be serialized and, when
+ * deserialized, passed to callFunctionWithAsyncStack.
+ */
+function getStack() {
+  // There's no reason for this to do anything fancy, since it's only
+  // used to pass back into callFunctionWithAsyncStack, which we can't
+  // implement.
+  return null;
+}
+
+exports.callFunctionWithAsyncStack = callFunctionWithAsyncStack;
+exports.describeNthCaller = describeNthCaller;
+exports.getStack = getStack;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/test/.eslintrc
@@ -0,0 +1,4 @@
+{
+  // Extend from the common devtools xpcshell eslintrc config.
+  "extends": "../../../../.eslintrc.xpcshell"
+}
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/test/test_stack.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// There isn't really very much about the content stack.js that we can
+// test, but we'll do what we can.
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+// Make sure to explicitly require the content version of this module.
+// We have to use the ".." trick due to the way the loader remaps
+// devtools/shared/platform.
+const {
+  callFunctionWithAsyncStack,
+  getStack,
+  describeNthCaller
+} = require("devtools/shared/platform/../content/stack");
+
+function f3() {
+  return describeNthCaller(2);
+}
+
+function f2() {
+  return f3();
+}
+
+function f1() {
+  return f2();
+}
+
+function run_test() {
+  let value = 7;
+
+  const changeValue = () => {
+    value = 9;
+  };
+
+  callFunctionWithAsyncStack(changeValue, getStack(), "test_stack");
+  equal(value, 9, "callFunctionWithAsyncStack worked");
+
+  let stack = getStack();
+  equal(JSON.parse(JSON.stringify(stack)), stack, "stack is serializable");
+
+  let desc = f1();
+  ok(desc.includes("f1"), "stack description includes f1");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/content/test/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+firefox-appdir = browser
+
+[test_stack.js]
new file mode 100644
--- /dev/null
+++ b/devtools/shared/platform/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/.
+
+DIRS += [
+    'chrome',
+    'content',
+]
--- a/devtools/shared/protocol.js
+++ b/devtools/shared/protocol.js
@@ -1,22 +1,21 @@
 /* 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";
 
-var { Cu, components } = require("chrome");
-var Services = require("Services");
 var promise = require("promise");
 var defer = require("devtools/shared/defer");
 var {Class} = require("sdk/core/heritage");
 var {EventTarget} = require("sdk/event/target");
 var events = require("sdk/event/core");
 var object = require("sdk/util/object");
+var {getStack, callFunctionWithAsyncStack} = require("devtools/shared/platform/stack");
 
 exports.emit = events.emit;
 
 /**
  * Types: named marshallers/demarshallers.
  *
  * Types provide a 'write' function that takes a js representation and
  * returns a protocol representation, and a "read" function that
@@ -1201,17 +1200,17 @@ var Front = Class({
   request: function (packet) {
     let deferred = defer();
     // Save packet basics for debugging
     let { to, type } = packet;
     this._requests.push({
       deferred,
       to: to || this.actorID,
       type,
-      stack: components.stack,
+      stack: getStack(),
     });
     this.send(packet);
     return deferred.promise;
   },
 
   /**
    * Handler for incoming packets from the client's actor.
    */
@@ -1247,17 +1246,17 @@ var Front = Class({
     if (this._requests.length === 0) {
       let msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
       let err = Error(msg);
       console.error(err);
       throw err;
     }
 
     let { deferred, stack } = this._requests.shift();
-    Cu.callFunctionWithAsyncStack(() => {
+    callFunctionWithAsyncStack(() => {
       if (packet.error) {
         // "Protocol error" is here to avoid TBPL heuristics. See also
         // https://mxr.mozilla.org/webtools-central/source/tbpl/php/inc/GeneralErrorFilter.php
         let message;
         if (packet.error && packet.message) {
           message = "Protocol error (" + packet.error + "): " + packet.message;
         } else {
           message = packet.error;
--- a/devtools/shared/worker/loader.js
+++ b/devtools/shared/worker/loader.js
@@ -493,16 +493,23 @@ this.worker = new WorkerDebuggerLoader({
     "Services": Object.create(null),
     "chrome": chrome,
     "xpcInspector": xpcInspector
   },
   paths: {
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
     "": "resource://gre/modules/commonjs/",
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+    // Modules here are intended to have one implementation for
+    // chrome, and a separate implementation for content.  Here we
+    // map the directory to the chrome subdirectory, but the content
+    // loader will map to the content subdirectory.  See the
+    // README.md in devtools/shared/platform.
+    "devtools/shared/platform": "resource://devtools/shared/platform/chrome",
+    // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
     "devtools": "resource://devtools",
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
     "promise": "resource://gre/modules/Promise-backend.js",
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
     "source-map": "resource://devtools/shared/sourcemap/source-map.js",
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
     "xpcshell-test": "resource://test"
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠