Bug 1287910 - move devtools stack-related APIs to per-platform require; r=jryans
authorTom Tromey <tom@tromey.com>
Fri, 05 Aug 2016 13:17:17 -0600
changeset 309121 782f590541b8173cd1b073a88732afa601f05715
parent 309120 a31f07fbdc2e249f76469bc897bcad548a892417
child 309122 4ad88cfd05b50399ab02a3814361c9cebacffefd
push id31307
push userttromey@mozilla.com
push dateFri, 12 Aug 2016 23:08:58 +0000
treeherderautoland@782f590541b8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1287910
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
@@ -37,16 +37,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 ⚠