Bug 1342928 - Keep the commands / buttons state in sync. r=jwalker, a=gchang
authorMatteo Ferretti <mferretti@mozilla.com>
Mon, 20 Mar 2017 14:54:03 +0100
changeset 395666 9cd943b99c3e1ae619ea9b6a34f2ead015023fff
parent 395665 1ed88ff1fc58ddb9c3f5231c28d61b63f28859b1
child 395667 66f1093c8d591087ada25bb353546408f4d200d1
push id1468
push userasasaki@mozilla.com
push dateMon, 05 Jun 2017 19:31:07 +0000
treeherdermozilla-release@0641fc6ee9d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker, gchang
bugs1342928, 1320149
milestone54.0a2
Bug 1342928 - Keep the commands / buttons state in sync. r=jwalker, a=gchang This was a regression given by bug 1320149; in order to keep the performance gain I created a lightweight object (`CommandState`) that is required from both gcli's command and toolbox so that the last one doesn't need to be strong coupled with the first one. MozReview-Commit-ID: 3NcTt6i4ezx
devtools/client/commandline/test/browser_cmd_paintflashing.js
devtools/client/definitions.js
devtools/client/framework/toolbox.js
devtools/client/shared/test/browser_telemetry_button_paintflashing.js
devtools/shared/event-emitter.js
devtools/shared/gcli/command-state.js
devtools/shared/gcli/commands/measure.js
devtools/shared/gcli/commands/paintflashing.js
devtools/shared/gcli/commands/rulers.js
devtools/shared/gcli/moz.build
--- a/devtools/client/commandline/test/browser_cmd_paintflashing.js
+++ b/devtools/client/commandline/test/browser_cmd_paintflashing.js
@@ -11,17 +11,17 @@ const TEST_URI = "http://example.com/bro
 function test() {
   return Task.spawn(testTask).then(finish, helpers.handleError);
 }
 
 var tests = {
   testInput: function (options) {
     let toggleCommand = options.requisition.system.commands.get("paintflashing toggle");
 
-    let _tab = options.tab;
+    let { tab } = options;
 
     let actions = [
       {
         command: "paintflashing on",
         isChecked: true,
         label: "checked after on"
       },
       {
@@ -39,17 +39,17 @@ var tests = {
         isChecked: false,
         label: "unchecked after toggle"
       }
     ];
 
     return helpers.audit(options, actions.map(spec => ({
       setup: spec.command,
       exec: {},
-      post: () => is(toggleCommand.state.isChecked({_tab}), spec.isChecked, spec.label)
+      post: () => is(toggleCommand.state.isChecked({tab}), spec.isChecked, spec.label)
     })));
   },
 };
 
 function* testTask() {
   let options = yield helpers.openTab(TEST_URI);
   yield helpers.openToolbar(options);
 
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -21,16 +21,17 @@ loader.lazyGetter(this, "MemoryPanel", (
 loader.lazyGetter(this, "PerformancePanel", () => require("devtools/client/performance/panel").PerformancePanel);
 loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/client/netmonitor/panel").NetMonitorPanel);
 loader.lazyGetter(this, "StoragePanel", () => require("devtools/client/storage/panel").StoragePanel);
 loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/client/scratchpad/scratchpad-panel").ScratchpadPanel);
 loader.lazyGetter(this, "DomPanel", () => require("devtools/client/dom/dom-panel").DomPanel);
 
 // Other dependencies
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
+loader.lazyRequireGetter(this, "CommandState", "devtools/shared/gcli/command-state", true);
 loader.lazyImporter(this, "ResponsiveUIManager", "resource://devtools/client/responsivedesign/responsivedesign.jsm");
 loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/startup.properties");
 
 var Tools = {};
 exports.Tools = Tools;
@@ -492,17 +493,25 @@ exports.ToolboxButtons = [
     }
   },
   { id: "command-button-paintflashing",
     description: l10n("toolbox.buttons.paintflashing"),
     isTargetSupported: target => target.isLocalTab,
     onClick(event, toolbox) {
       CommandUtils.executeOnTarget(toolbox.target, "paintflashing toggle");
     },
-    autoToggle: true
+    isChecked(toolbox) {
+      return CommandState.isEnabledForTarget(toolbox.target, "paintflashing");
+    },
+    setup(toolbox, onChange) {
+      CommandState.on("changed", onChange);
+    },
+    teardown(toolbox, onChange) {
+      CommandState.off("changed", onChange);
+    }
   },
   { id: "command-button-scratchpad",
     description: l10n("toolbox.buttons.scratchpad"),
     isTargetSupported: target => target.isLocalTab,
     onClick(event, toolbox) {
       ScratchpadManager.openScratchpad();
     }
   },
@@ -546,25 +555,41 @@ exports.ToolboxButtons = [
     }
   },
   { id: "command-button-rulers",
     description: l10n("toolbox.buttons.rulers"),
     isTargetSupported: target => target.isLocalTab,
     onClick(event, toolbox) {
       CommandUtils.executeOnTarget(toolbox.target, "rulers");
     },
-    autoToggle: true
+    isChecked(toolbox) {
+      return CommandState.isEnabledForTarget(toolbox.target, "rulers");
+    },
+    setup(toolbox, onChange) {
+      CommandState.on("changed", onChange);
+    },
+    teardown(toolbox, onChange) {
+      CommandState.off("changed", onChange);
+    }
   },
   { id: "command-button-measure",
     description: l10n("toolbox.buttons.measure"),
     isTargetSupported: target => target.isLocalTab,
     onClick(event, toolbox) {
       CommandUtils.executeOnTarget(toolbox.target, "measure");
     },
-    autoToggle: true
+    isChecked(toolbox) {
+      return CommandState.isEnabledForTarget(toolbox.target, "measure");
+    },
+    setup(toolbox, onChange) {
+      CommandState.on("changed", onChange);
+    },
+    teardown(toolbox, onChange) {
+      CommandState.off("changed", onChange);
+    }
   },
 ];
 
 /**
  * Lookup l10n string from a string bundle.
  *
  * @param {string} name
  *        The key to lookup.
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -569,35 +569,30 @@ Toolbox.prototype = {
    *                      memory leaks. The same arguments than `setup` function are
    *                      passed to `teardown`.
    * @property {Function} isTargetSupported - Function to automatically enable/disable
    *                      the button based on the target. If the target don't support
    *                      the button feature, this method should return false.
    * @property {Function} isChecked - Optional function called to known if the button
    *                      is toggled or not. The function should return true when
    *                      the button should be displayed as toggled on.
-   * @property {Boolean}  autoToggle - If true, the checked state is going to be
-   *                      automatically toggled on click.
    */
   _createButtonState: function (options) {
     let isCheckedValue = false;
     const { id, className, description, onClick, isInStartContainer, setup, teardown,
-            isTargetSupported, isChecked, autoToggle } = options;
+            isTargetSupported, isChecked } = options;
     const toolbox = this;
     const button = {
       id,
       className,
       description,
       onClick(event) {
         if (typeof onClick == "function") {
           onClick(event, toolbox);
         }
-        if (autoToggle) {
-          button.isChecked = !button.isChecked;
-        }
       },
       isTargetSupported,
       get isChecked() {
         if (typeof isChecked == "function") {
           return isChecked(toolbox);
         }
         return isCheckedValue;
       },
--- a/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
+++ b/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
@@ -39,18 +39,25 @@ function* testButton(toolbox, Telemetry)
 
 function* delayedClicks(toolbox, node, clicks) {
   for (let i = 0; i < clicks; i++) {
     yield new Promise(resolve => {
       // See TOOL_DELAY for why we need setTimeout here
       setTimeout(() => resolve(), TOOL_DELAY);
     });
 
-    let PaintFlashingCmd = require("devtools/shared/gcli/commands/paintflashing");
-    let clicked = PaintFlashingCmd.eventEmitter.once("changed");
+    let { CommandState } = require("devtools/shared/gcli/command-state");
+    let clicked = new Promise(resolve => {
+      CommandState.on("changed", function changed(type, { command }) {
+        if (command === "paintflashing") {
+          CommandState.off("changed", changed);
+          resolve();
+        }
+      });
+    });
 
     info("Clicking button " + node.id);
     node.click();
 
     yield clicked;
   }
 }
 
--- a/devtools/shared/event-emitter.js
+++ b/devtools/shared/event-emitter.js
@@ -79,23 +79,26 @@
   }
 
   /**
    * Decorate an object with event emitter functionality.
    *
    * @param Object objectToDecorate
    *        Bind all public methods of EventEmitter to
    *        the objectToDecorate object.
+   * @return Object the object given.
    */
   EventEmitter.decorate = function (objectToDecorate) {
     let emitter = new EventEmitter();
     objectToDecorate.on = emitter.on.bind(emitter);
     objectToDecorate.off = emitter.off.bind(emitter);
     objectToDecorate.once = emitter.once.bind(emitter);
     objectToDecorate.emit = emitter.emit.bind(emitter);
+
+    return objectToDecorate;
   };
 
   EventEmitter.prototype = {
     /**
      * Connect a listener.
      *
      * @param string event
      *        The event name to which we're connecting.
new file mode 100644
--- /dev/null
+++ b/devtools/shared/gcli/command-state.js
@@ -0,0 +1,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 EventEmitter = require("devtools/shared/event-emitter");
+
+loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+const getTargetId = ({tab}) => getBrowserForTab(tab).outerWindowID;
+const enabledCommands = new Map();
+
+/**
+ * The `CommandState` is a singleton that provides utility methods to keep the commands'
+ * state in sync between the toolbox, the toolbar and the content.
+ */
+const CommandState = EventEmitter.decorate({
+  /**
+   * Returns if a command is enabled on a given target.
+   *
+   * @param {Object} target
+   *                  The target object must have a tab's reference.
+   * @param {String} command
+   *                  The command's name used in gcli.
+   * @ returns {Boolean} returns `false` if the command is not enabled for the target
+   *                    given, or if the target given hasn't a tab; `true` otherwise.
+   */
+  isEnabledForTarget(target, command) {
+    if (!target.tab || !enabledCommands.has(command)) {
+      return false;
+    }
+
+    return enabledCommands.get(command).has(getTargetId(target));
+  },
+
+  /**
+   * Enables a command on a given target.
+   * Emits a "changed" event to notify potential observers about the new commands state.
+   *
+   * @param {Object} target
+   *                  The target object must have a tab's reference.
+   * @param {String} command
+   *                  The command's name used in gcli.
+   */
+  enableForTarget(target, command) {
+    if (!target.tab) {
+      return;
+    }
+
+    if (!enabledCommands.has(command)) {
+      enabledCommands.set(command, new Set());
+    }
+
+    enabledCommands.get(command).add(getTargetId(target));
+
+    CommandState.emit("changed", {target, command});
+  },
+
+  /**
+   * Disabled a command on a given target.
+   * Emits a "changed" event to notify potential observers about the new commands state.
+   *
+   * @param {Object} target
+   *                  The target object must have a tab's reference.
+   * @param {String} command
+   *                  The command's name used in gcli.
+   */
+  disableForTarget(target, command) {
+    if (!target.tab || !enabledCommands.has(command)) {
+      return;
+    }
+
+    enabledCommands.get(command).delete(getTargetId(target));
+
+    CommandState.emit("changed", {target, command});
+  },
+});
+exports.CommandState = CommandState;
+
--- a/devtools/shared/gcli/commands/measure.js
+++ b/devtools/shared/gcli/commands/measure.js
@@ -1,94 +1,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/. */
- /* globals getOuterId, getBrowserForTab */
 
 "use strict";
 
-const EventEmitter = require("devtools/shared/event-emitter");
-const eventEmitter = new EventEmitter();
 const events = require("sdk/event/core");
 
-loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
-loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+loader.lazyRequireGetter(this, "CommandState",
+  "devtools/shared/gcli/command-state", true);
 
 const l10n = require("gcli/l10n");
 require("devtools/server/actors/inspector");
 const { MeasuringToolHighlighter, HighlighterEnvironment } =
   require("devtools/server/actors/highlighters");
 
 const highlighters = new WeakMap();
-const visibleHighlighters = new Set();
-
-const isCheckedFor = (tab) =>
-  tab ? visibleHighlighters.has(getBrowserForTab(tab).outerWindowID) : false;
 
 exports.items = [
   // The client measure command is used to maintain the toolbar button state
   // only and redirects to the server command to actually toggle the measuring
   // tool (see `measure_server` below).
   {
     name: "measure",
     runAt: "client",
     description: l10n.lookup("measureDesc"),
     manual: l10n.lookup("measureManual"),
     buttonId: "command-button-measure",
     buttonClass: "command-button command-button-invertable",
     tooltipText: l10n.lookup("measureTooltip"),
     state: {
-      isChecked: ({_tab}) => isCheckedFor(_tab),
-      onChange: (target, handler) => eventEmitter.on("changed", handler),
-      offChange: (target, handler) => eventEmitter.off("changed", handler)
+      isChecked: (target) => CommandState.isEnabledForTarget(target, "measure"),
+      onChange: (target, handler) => CommandState.on("changed", handler),
+      offChange: (target, handler) => CommandState.off("changed", handler)
     },
     exec: function* (args, context) {
       let { target } = context.environment;
 
       // Pipe the call to the server command.
       let response = yield context.updateExec("measure_server");
-      let { visible, id } = response.data;
+      let isEnabled = response.data;
 
-      if (visible) {
-        visibleHighlighters.add(id);
+      if (isEnabled) {
+        CommandState.enableForTarget(target, "measure");
       } else {
-        visibleHighlighters.delete(id);
+        CommandState.disableForTarget(target, "measure");
       }
 
-      eventEmitter.emit("changed", { target });
-
       // Toggle off the button when the page navigates because the measuring
       // tool is removed automatically by the MeasuringToolHighlighter on the
       // server then.
-      let onNavigate = () => {
-        visibleHighlighters.delete(id);
-        eventEmitter.emit("changed", { target });
-      };
-      target.off("will-navigate", onNavigate);
-      target.once("will-navigate", onNavigate);
+      target.once("will-navigate", () =>
+        CommandState.disableForTarget(target, "measure"));
     }
   },
   // The server measure command is hidden by default, it's just used by the
   // client command.
   {
     name: "measure_server",
     runAt: "server",
     hidden: true,
     returnType: "highlighterVisibility",
     exec: function (args, context) {
       let env = context.environment;
       let { document } = env;
-      let id = getOuterId(env.window);
 
       // Calling the command again after the measuring tool has been shown once,
       // hides it.
       if (highlighters.has(document)) {
         let { highlighter } = highlighters.get(document);
         highlighter.destroy();
-        return {visible: false, id};
+        return false;
       }
 
       // Otherwise, display the measuring tool.
       let environment = new HighlighterEnvironment();
       environment.initFromWindow(env.window);
       let highlighter = new MeasuringToolHighlighter(environment);
 
       // Store the instance of the measuring tool highlighter for this document
@@ -101,12 +87,12 @@ exports.items = [
         if (highlighters.has(document)) {
           let { environment: toDestroy } = highlighters.get(document);
           toDestroy.destroy();
           highlighters.delete(document);
         }
       });
 
       highlighter.show();
-      return {visible: true, id};
+      return true;
     }
   }
 ];
--- a/devtools/shared/gcli/commands/paintflashing.js
+++ b/devtools/shared/gcli/commands/paintflashing.js
@@ -1,59 +1,42 @@
 /* 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");
-loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
-loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+
+loader.lazyRequireGetter(this, "CommandState",
+  "devtools/shared/gcli/command-state", true);
 
 var telemetry;
 try {
   const Telemetry = require("devtools/client/shared/telemetry");
   telemetry = new Telemetry();
 } catch (e) {
   // DevTools Telemetry module only available in Firefox
 }
 
-const EventEmitter = require("devtools/shared/event-emitter");
-const eventEmitter = new EventEmitter();
-
-// exports the event emitter to help test know when this command is toggled
-exports.eventEmitter = eventEmitter;
-
 const gcli = require("gcli/index");
 const l10n = require("gcli/l10n");
 
-const enabledPaintFlashing = new Set();
-
-const isCheckedFor = (tab) =>
-  tab ? enabledPaintFlashing.has(getBrowserForTab(tab).outerWindowID) : false;
-
 /**
  * Fire events and telemetry when paintFlashing happens
  */
-function onPaintFlashingChanged(target, state) {
-  const { flashing, id } = state;
-
+function onPaintFlashingChanged(target, flashing) {
   if (flashing) {
-    enabledPaintFlashing.add(id);
+    CommandState.enableForTarget(target, "paintflashing");
   } else {
-    enabledPaintFlashing.delete(id);
+    CommandState.disableForTarget(target, "paintflashing");
   }
 
-  eventEmitter.emit("changed", { target: target });
-  function fireChange() {
-    eventEmitter.emit("changed", { target: target });
-  }
-
-  target.off("navigate", fireChange);
-  target.once("navigate", fireChange);
+  target.once("will-navigate", () =>
+    CommandState.disableForTarget(target, "paintflashing"));
 
   if (!telemetry) {
     return;
   }
   if (flashing) {
     telemetry.toolOpened("paintflashing");
   } else {
     telemetry.toolClosed("paintflashing");
@@ -160,19 +143,19 @@ exports.items = [
   {
     item: "command",
     runAt: "client",
     name: "paintflashing toggle",
     hidden: true,
     buttonId: "command-button-paintflashing",
     buttonClass: "command-button command-button-invertable",
     state: {
-      isChecked: ({_tab}) => isCheckedFor(_tab),
-      onChange: (_, handler) => eventEmitter.on("changed", handler),
-      offChange: (_, handler) => eventEmitter.off("changed", handler),
+      isChecked: (target) => CommandState.isEnabledForTarget(target, "paintflashing"),
+      onChange: (_, handler) => CommandState.on("changed", handler),
+      offChange: (_, handler) => CommandState.off("changed", handler),
     },
     tooltipText: l10n.lookup("paintflashingTooltip"),
     description: l10n.lookup("paintflashingToggleDesc"),
     manual: l10n.lookup("paintflashingManual"),
     exec: function* (args, context) {
       const output = yield context.updateExec("paintflashing_server --state toggle");
 
       onPaintFlashingChanged(context.environment.target, output.data);
@@ -190,15 +173,13 @@ exports.items = [
           name: "selection",
           data: [ "on", "off", "toggle", "query" ]
         }
       },
     ],
     returnType: "paintFlashingState",
     exec: function (args, context) {
       let { window } = context.environment;
-      let id = getOuterId(window);
-      let flashing = setPaintFlashing(window, args.state);
 
-      return { flashing, id };
+      return setPaintFlashing(window, args.state);
     }
   }
 ];
--- a/devtools/shared/gcli/commands/rulers.js
+++ b/devtools/shared/gcli/commands/rulers.js
@@ -1,92 +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/. */
-/* globals getBrowserForTab */
 
 "use strict";
 
-const EventEmitter = require("devtools/shared/event-emitter");
-const eventEmitter = new EventEmitter();
 const events = require("sdk/event/core");
+
 loader.lazyRequireGetter(this, "getOuterId", "sdk/window/utils", true);
-loader.lazyRequireGetter(this, "getBrowserForTab", "sdk/tabs/utils", true);
+loader.lazyRequireGetter(this, "CommandState",
+  "devtools/shared/gcli/command-state", true);
 
 const l10n = require("gcli/l10n");
 require("devtools/server/actors/inspector");
 const { RulersHighlighter, HighlighterEnvironment } =
   require("devtools/server/actors/highlighters");
 
 const highlighters = new WeakMap();
-const visibleHighlighters = new Set();
-
-const isCheckedFor = (tab) =>
-  tab ? visibleHighlighters.has(getBrowserForTab(tab).outerWindowID) : false;
 
 exports.items = [
   // The client rulers command is used to maintain the toolbar button state only
   // and redirects to the server command to actually toggle the rulers (see
   // rulers_server below).
   {
     name: "rulers",
     runAt: "client",
     description: l10n.lookup("rulersDesc"),
     manual: l10n.lookup("rulersManual"),
     buttonId: "command-button-rulers",
     buttonClass: "command-button command-button-invertable",
     tooltipText: l10n.lookup("rulersTooltip"),
     state: {
-      isChecked: ({_tab}) => isCheckedFor(_tab),
-      onChange: (target, handler) => eventEmitter.on("changed", handler),
-      offChange: (target, handler) => eventEmitter.off("changed", handler)
+      isChecked: (target) => CommandState.isEnabledForTarget(target, "rulers"),
+      onChange: (target, handler) => CommandState.on("changed", handler),
+      offChange: (target, handler) => CommandState.off("changed", handler)
     },
     exec: function* (args, context) {
       let { target } = context.environment;
 
       // Pipe the call to the server command.
       let response = yield context.updateExec("rulers_server");
-      let { visible, id } = response.data;
+      let isEnabled = response.data;
 
-      if (visible) {
-        visibleHighlighters.add(id);
+      if (isEnabled) {
+        CommandState.enableForTarget(target, "rulers");
       } else {
-        visibleHighlighters.delete(id);
+        CommandState.disableForTarget(target, "rulers");
       }
 
-      eventEmitter.emit("changed", { target });
-
       // Toggle off the button when the page navigates because the rulers are
       // removed automatically by the RulersHighlighter on the server then.
-      let onNavigate = () => {
-        visibleHighlighters.delete(id);
-        eventEmitter.emit("changed", { target });
-      };
-      target.off("will-navigate", onNavigate);
-      target.once("will-navigate", onNavigate);
+      target.once("will-navigate", () => CommandState.disableForTarget(target, "rulers"));
     }
   },
   // The server rulers command is hidden by default, it's just used by the
   // client command.
   {
     name: "rulers_server",
     runAt: "server",
     hidden: true,
     returnType: "highlighterVisibility",
     exec: function (args, context) {
       let env = context.environment;
       let { document } = env;
-      let id = getOuterId(env.window);
 
       // Calling the command again after the rulers have been shown once hides
       // them.
       if (highlighters.has(document)) {
         let { highlighter } = highlighters.get(document);
         highlighter.destroy();
-        return {visible: false, id};
+        return false;
       }
 
       // Otherwise, display the rulers.
       let environment = new HighlighterEnvironment();
       environment.initFromWindow(env.window);
       let highlighter = new RulersHighlighter(environment);
 
       // Store the instance of the rulers highlighter for this document so we
@@ -99,12 +86,12 @@ exports.items = [
         if (highlighters.has(document)) {
           let { environment: toDestroy } = highlighters.get(document);
           toDestroy.destroy();
           highlighters.delete(document);
         }
       });
 
       highlighter.show();
-      return {visible: true, id};
+      return true;
     }
   }
 ];
--- a/devtools/shared/gcli/moz.build
+++ b/devtools/shared/gcli/moz.build
@@ -14,10 +14,11 @@ DIRS += [
     'source/lib/gcli/languages',
     'source/lib/gcli/mozui',
     'source/lib/gcli/types',
     'source/lib/gcli/ui',
     'source/lib/gcli/util',
 ]
 
 DevToolsModules(
+    'command-state.js',
     'templater.js'
 )