Bug 1291049 - use devtools-local-toolbox to load the inspector in content;r=bgrins,jlast,tromey draft
authorJulian Descottes <jdescottes@mozilla.com>
Wed, 30 Nov 2016 22:24:23 +0100
changeset 446055 bbbdb782a33c6af2533c8fc2a94967563b7c0c43
parent 446054 c60ddb8b9c254ceedd808251250d5932b627bd27
child 446056 d7340153b73e51bb989fd45968bc59ca0fd54894
push id37689
push userjdescottes@mozilla.com
push dateWed, 30 Nov 2016 21:30:01 +0000
reviewersbgrins, jlast, tromey
bugs1291049
milestone53.0a1
Bug 1291049 - use devtools-local-toolbox to load the inspector in content;r=bgrins,jlast,tromey Add package.json to devtools/client/inspector Integration with devtools-local-toolbox: - provides development server - default webpack config - landing page to select a tab In this patch: - bin/dev-server-js contains the inspector specific routes - webpack/*-sham.js : inspector dependencies that had to be mocked ideally, decoupling work should continue and the inspector client should only ever require files that require no chrome priviledged APIs. - toolbox.js contains the interface with devtools-local-toolbox bootstrap and the inspector panel initialization - configs/development.json is the inspector config for the devtools-local-toolbox MozReview-Commit-ID: Cuphp7VS2OT
addon-sdk/source/lib/sdk/util/object.js
devtools/client/framework/devtools.js
devtools/client/inspector/bin/dev-server.js
devtools/client/inspector/configs/development.json
devtools/client/inspector/inspector.js
devtools/client/inspector/inspector.xhtml
devtools/client/inspector/local-toolbox.js
devtools/client/inspector/markup/markup.xhtml
devtools/client/inspector/package.json
devtools/client/inspector/webpack.config.js
devtools/client/inspector/webpack/about-devtools-sham.js
devtools/client/inspector/webpack/attach-thread-sham.js
devtools/client/inspector/webpack/editor-sham.js
devtools/client/inspector/webpack/jsonview-sham.js
devtools/client/inspector/webpack/prefs-loader.js
devtools/client/inspector/webpack/rewrite-browser-require.js
devtools/client/inspector/webpack/rewrite-event-emitter.js
devtools/client/inspector/webpack/system-unload-sham.js
devtools/client/inspector/webpack/target-from-url-sham.js
devtools/client/shared/shim/Services.js
devtools/client/shared/undo.js
devtools/client/shared/widgets/tooltip/TooltipToggle.js
devtools/shared/DevToolsUtils.js
--- a/addon-sdk/source/lib/sdk/util/object.js
+++ b/addon-sdk/source/lib/sdk/util/object.js
@@ -29,17 +29,17 @@ const { flatten } = require('./array');
  *    b.name    // 'b'
  */
 function merge(source) {
   let descriptor = {};
 
   // `Boolean` converts the first parameter to a boolean value. Any object is
   // converted to `true` where `null` and `undefined` becames `false`. Therefore
   // the `filter` method will keep only objects that are defined and not null.
-  Array.slice(arguments, 1).filter(Boolean).forEach(function onEach(properties) {
+  [].slice.call(arguments, 1).filter(Boolean).forEach(function onEach(properties) {
     getOwnPropertyIdentifiers(properties).forEach(function(name) {
       descriptor[name] = Object.getOwnPropertyDescriptor(properties, name);
     });
   });
   return Object.defineProperties(source, descriptor);
 }
 exports.merge = merge;
 
@@ -67,17 +67,17 @@ function each(obj, fn) {
 exports.each = each;
 
 /**
  * Like `merge`, except no property descriptors are manipulated, for use
  * with platform objects. Identical to underscore's `extend`. Useful for
  * merging XPCOM objects
  */
 function safeMerge(source) {
-  Array.slice(arguments, 1).forEach(function onEach (obj) {
+  [].slice.call(arguments, 1).forEach(function onEach (obj) {
     for (let prop in obj) source[prop] = obj[prop];
   });
   return source;
 }
 exports.safeMerge = safeMerge;
 
 /*
  * Returns a copy of the object without omitted properties
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -23,17 +23,17 @@ const {Task} = require("devtools/shared/
 
 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
 const MAX_ORDINAL = 99;
 
 /**
  * DevTools is a class that represents a set of developer tools, it holds a
  * set of tools and keeps track of open toolboxes in the browser.
  */
-this.DevTools = function DevTools() {
+const DevTools = function DevTools() {
   this._tools = new Map();     // Map<toolId, tool>
   this._themes = new Map();    // Map<themeId, theme>
   this._toolboxes = new Map(); // Map<target, toolbox>
 
   // destroy() is an observer's handler so we need to preserve context.
   this.destroy = this.destroy.bind(this);
 
   // JSON Viewer for 'application/json' documents.
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/bin/dev-server.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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/. */
+
+/* global __dirname */
+
+"use strict";
+
+const toolbox = require("../node_modules/devtools-local-toolbox/index");
+const feature = require("devtools-config");
+const envConfig = require("../configs/development.json");
+
+const fs = require("fs");
+const path = require("path");
+
+feature.setConfig(envConfig);
+const webpackConfig = require("../webpack.config")(envConfig);
+
+let {app} = toolbox.startDevServer(envConfig, webpackConfig);
+
+function sendFile(res, src, encoding) {
+  const filePath = path.join(__dirname, src);
+  const file = encoding ? fs.readFileSync(filePath, encoding) : fs.readFileSync(filePath);
+  res.send(file);
+}
+
+function addFileRoute(from, to) {
+  app.get(from, function (req, res) {
+    sendFile(res, to, "utf-8");
+  });
+}
+
+// Routes
+addFileRoute("/", "../inspector.xhtml");
+addFileRoute("/markup/markup.xhtml", "../markup/markup.xhtml");
+
+app.get("/devtools/skin/images/:file.png", function (req, res) {
+  res.contentType("image/png");
+  sendFile(res, "../../themes/images/" + req.params.file + ".png");
+});
+
+app.get("/devtools/skin/images/:file.svg", function (req, res) {
+  res.contentType("image/svg+xml");
+  sendFile(res, "../../themes/images/" + req.params.file + ".svg", "utf-8");
+});
+
+app.get("/images/:file.svg", function (req, res) {
+  res.contentType("image/svg+xml");
+  sendFile(res, "../../themes/images/" + req.params.file + ".svg", "utf-8");
+});
+
+// Redirect chrome:devtools/skin/file.css to ../../themes/file.css
+app.get("/devtools/skin/:file.css", function (req, res) {
+  res.contentType("text/css; charset=utf-8");
+  sendFile(res, "../../themes/" + req.params.file + ".css", "utf-8");
+});
+
+// Redirect chrome:devtools/client/path/to/file.css to ../../path/to/file.css
+//      and chrome:devtools/content/path/to/file.css to ../../path/to/file.css
+app.get(/^\/devtools\/(?:client|content)\/(.*)\.css$/, function (req, res) {
+  res.contentType("text/css; charset=utf-8");
+  sendFile(res, "../../" + req.params[0] + ".css");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/configs/development.json
@@ -0,0 +1,22 @@
+{
+  "title": "Inspector",
+  "environment": "development",
+  "baseWorkerURL": "public/build/",
+  "theme": "light",
+  "host": "",
+  "logging": {
+    "client": false,
+    "firefoxProxy": false
+  },
+  "features": {
+  },
+  "firefox": {
+    "proxyHost": "localhost:9000",
+    "webSocketConnection": false,
+    "webSocketHost": "localhost:6080"
+  },
+  "development": {
+    "serverPort": 8000,
+    "customIndex": true
+  }
+}
\ No newline at end of file
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -1,20 +1,18 @@
 /* -*- 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/. */
 
-/* global window */
+/* global window, BrowserLoader */
 
 "use strict";
 
-var Cu = Components.utils;
-var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 var Services = require("Services");
 var promise = require("promise");
 var defer = require("devtools/shared/defer");
 var EventEmitter = require("devtools/shared/event-emitter");
 const {executeSoon} = require("devtools/shared/DevToolsUtils");
 var {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
 var {Task} = require("devtools/shared/task");
 const {initCssProperties} = require("devtools/shared/fronts/css-properties");
@@ -1316,25 +1314,25 @@ Inspector.prototype = {
   _initMarkup: function () {
     let doc = this.panelDoc;
 
     this._markupBox = doc.getElementById("markup-box");
 
     // create tool iframe
     this._markupFrame = doc.createElement("iframe");
     this._markupFrame.setAttribute("flex", "1");
+    // This is needed to enable tooltips inside the iframe document.
     this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
     this._markupFrame.addEventListener("contextmenu", this._onContextMenu);
 
-    // This is needed to enable tooltips inside the iframe document.
-    this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
-
     this._markupBox.setAttribute("collapsed", true);
     this._markupBox.appendChild(this._markupFrame);
-    this._markupFrame.setAttribute("src", "chrome://devtools/content/inspector/markup/markup.xhtml");
+
+    this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
+    this._markupFrame.setAttribute("src", "markup/markup.xhtml");
     this._markupFrame.setAttribute("aria-label",
       INSPECTOR_L10N.getStr("inspector.panelLabel.markupView"));
   },
 
   _onMarkupFrameLoad: function () {
     this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
 
     this._markupFrame.contentWindow.focus();
@@ -1850,94 +1848,116 @@ Inspector.prototype = {
     // When the inspector menu was setup on click (see _getNodeLinkMenuItems), we
     // already checked that resolveRelativeURL existed.
     this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
       clipboardHelper.copyString(url);
     }, console.error);
   }
 };
 
+/**
+ * Create a fake toolbox when running the inspector standalone, either in a chrome tab or
+ * in a content tab.
+ *
+ * @param {Target} target to debug
+ * @param {Function} createThreadClient
+ *        When supported the thread client needs a reference to the toolbox.
+ *        This callback will be called right after the toolbox object is created.
+ * @param {Object} dependencies
+ *        - react
+ *        - reactDOM
+ *        - browserRequire
+ */
+const buildFakeToolbox = Task.async(function* (
+  target, createThreadClient, {
+    React,
+    ReactDOM,
+    browserRequire
+  }) {
+  const { InspectorFront } = require("devtools/shared/fronts/inspector");
+  const { Selection } = require("devtools/client/framework/selection");
+  const { getHighlighterUtils } = require("devtools/client/framework/toolbox-highlighter-utils");
+
+  let notImplemented = function () {
+    throw new Error("Not implemented in a tab");
+  };
+  let fakeToolbox = {
+    target,
+    hostType: "bottom",
+    doc: window.document,
+    win: window,
+    on() {}, emit() {}, off() {},
+    initInspector() {},
+    browserRequire,
+    React,
+    ReactDOM,
+    isToolRegistered() {
+      return false;
+    },
+    currentToolId: "inspector",
+    getCurrentPanel() {
+      return "inspector";
+    },
+    get textboxContextMenuPopup() {
+      notImplemented();
+    },
+    getPanel: notImplemented,
+    openSplitConsole: notImplemented,
+    viewCssSourceInStyleEditor: notImplemented,
+    viewJsSourceInDebugger: notImplemented,
+    viewSource: notImplemented,
+    viewSourceInDebugger: notImplemented,
+    viewSourceInStyleEditor: notImplemented,
+
+    // For attachThread:
+    highlightTool() {},
+    unhighlightTool() {},
+    selectTool() {},
+    raise() {},
+    getNotificationBox() {}
+  };
+
+  fakeToolbox.threadClient = yield createThreadClient(fakeToolbox);
+
+  let inspector = InspectorFront(target.client, target.form);
+  let showAllAnonymousContent =
+    Services.prefs.getBoolPref("devtools.inspector.showAllAnonymousContent");
+  let walker = yield inspector.getWalker({ showAllAnonymousContent });
+  let selection = new Selection(walker);
+  let highlighter = yield inspector.getHighlighter(false);
+  fakeToolbox.highlighterUtils = getHighlighterUtils(fakeToolbox);
+
+  fakeToolbox.inspector = inspector;
+  fakeToolbox.walker = walker;
+  fakeToolbox.selection = selection;
+  fakeToolbox.highlighter = highlighter;
+  return fakeToolbox;
+});
+
 // URL constructor doesn't support chrome: scheme
 let href = window.location.href.replace(/chrome:/, "http://");
 let url = new window.URL(href);
 
-// Only use this method to attach the toolbox if some query parameters are given
-if (url.search.length > 1) {
+// If query parameters are given in a chrome tab, the inspector is running in standalone.
+if (window.location.protocol === "chrome:" && url.search.length > 1) {
   const { targetFromURL } = require("devtools/client/framework/target-from-url");
   const { attachThread } = require("devtools/client/framework/attach-thread");
-  const { BrowserLoader } =
-    Cu.import("resource://devtools/client/shared/browser-loader.js", {});
 
-  const { Selection } = require("devtools/client/framework/selection");
-  const { InspectorFront } = require("devtools/shared/fronts/inspector");
-  const { getHighlighterUtils } = require("devtools/client/framework/toolbox-highlighter-utils");
+  const browserRequire = BrowserLoader({ window, useOnlyShared: true }).require;
+  const React = browserRequire("devtools/client/shared/vendor/react");
+  const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
 
   Task.spawn(function* () {
     let target = yield targetFromURL(url);
-
-    let notImplemented = function () {
-      throw new Error("Not implemented in a tab");
-    };
-    let fakeToolbox = {
+    let fakeToolbox = yield buildFakeToolbox(
       target,
-      hostType: "bottom",
-      doc: window.document,
-      win: window,
-      on() {}, emit() {}, off() {},
-      initInspector() {},
-      browserRequire: BrowserLoader({
-        window: window,
-        useOnlyShared: true
-      }).require,
-      get React() {
-        return this.browserRequire("devtools/client/shared/vendor/react");
-      },
-      get ReactDOM() {
-        return this.browserRequire("devtools/client/shared/vendor/react-dom");
-      },
-      isToolRegistered() {
-        return false;
-      },
-      currentToolId: "inspector",
-      getCurrentPanel() {
-        return "inspector";
-      },
-      get textboxContextMenuPopup() {
-        notImplemented();
-      },
-      getPanel: notImplemented,
-      openSplitConsole: notImplemented,
-      viewCssSourceInStyleEditor: notImplemented,
-      viewJsSourceInDebugger: notImplemented,
-      viewSource: notImplemented,
-      viewSourceInDebugger: notImplemented,
-      viewSourceInStyleEditor: notImplemented,
-
-      // For attachThread:
-      highlightTool() {},
-      unhighlightTool() {},
-      selectTool() {},
-      raise() {},
-      getNotificationBox() {}
-    };
-
-    // attachThread also expect a toolbox as argument
-    fakeToolbox.threadClient = yield attachThread(fakeToolbox);
-
-    let inspector = InspectorFront(target.client, target.form);
-    let showAllAnonymousContent =
-      Services.prefs.getBoolPref("devtools.inspector.showAllAnonymousContent");
-    let walker = yield inspector.getWalker({ showAllAnonymousContent });
-    let selection = new Selection(walker);
-    let highlighter = yield inspector.getHighlighter(false);
-
-    fakeToolbox.inspector = inspector;
-    fakeToolbox.walker = walker;
-    fakeToolbox.selection = selection;
-    fakeToolbox.highlighter = highlighter;
-    fakeToolbox.highlighterUtils = getHighlighterUtils(fakeToolbox);
-
+      (toolbox) => attachThread(toolbox),
+      { React, ReactDOM, browserRequire }
+    );
     let inspectorUI = new Inspector(fakeToolbox);
     inspectorUI.init();
   }).then(null, e => {
     window.alert("Unable to start the inspector:" + e.message + "\n" + e.stack);
   });
 }
+
+exports.Inspector = Inspector;
+exports.buildFakeToolbox = buildFakeToolbox;
--- a/devtools/client/inspector/inspector.xhtml
+++ b/devtools/client/inspector/inspector.xhtml
@@ -21,16 +21,27 @@
   <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabs.css"/>
   <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabbar.css"/>
   <link rel="stylesheet" href="resource://devtools/client/inspector/components/inspector-tab-panel.css"/>
   <link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/split-box.css"/>
   <link rel="stylesheet" href="resource://devtools/client/inspector/layout/components/Accordion.css"/>
 
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"></script>
+  <script type="text/javascript">
+    var isInChrome = window.location.href.includes("chrome:");
+    if (isInChrome) {
+      var exports = {};
+      var Cu = Components.utils;
+      var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+      var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+    }
+  </script>
+
+  <!-- in content, inspector.js is mapped to the dynamically generated webpack bundle -->
   <script type="application/javascript;version=1.8" src="inspector.js" defer="true"></script>
 </head>
 <body class="theme-body" role="application">
   <div class="inspector-responsive-container theme-body inspector">
 
     <!-- Main Panel Content -->
     <div id="inspector-main-content" class="devtools-main-content">
       <div id="inspector-toolbar" class="devtools-toolbar" nowindowdrag="true"
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/local-toolbox.js
@@ -0,0 +1,122 @@
+/* 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/. */
+
+/* global window, document */
+
+"use strict";
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const { buildFakeToolbox, Inspector } = require("./inspector");
+
+function onConnect(arg) {
+  if (!arg || !arg.client) {
+    return;
+  }
+
+  let client = arg.client;
+
+  const tabTarget = client.getTabTarget();
+  let threadClient = { paused: false };
+  buildFakeToolbox(
+    tabTarget,
+    () => threadClient,
+    { React, ReactDOM, browserRequire: () => {} }
+  ).then(function (fakeToolbox) {
+    let inspector = new Inspector(fakeToolbox);
+    inspector.init();
+  });
+}
+
+/**
+ * Stylesheet links in devtools xhtml files are using chrome or resource URLs.
+ * Rewrite the href attribute to remove the protocol. web-server.js contains redirects
+ * to map CSS urls to the proper file. Supports urls using:
+ *   - devtools/client/
+ *   - devtools/content/
+ *   - skin/
+ * The css for the light-theme will additionally be loaded.
+ * Will also add mandatory classnames and attributes to be compatible with devtools theme
+ * stylesheet.
+ *
+ */
+function fixStylesheets(doc) {
+  let links = doc.head.querySelectorAll("link");
+  for (let link of links) {
+    link.href = link.href.replace(/(resource|chrome)\:\/\//, "/");
+  }
+
+  // Add the light theme stylesheet to compensate for the missing theme-switching.js
+  let themeLink = doc.createElement("link");
+  themeLink.setAttribute("rel", "stylesheet");
+  themeLink.setAttribute("href", "/devtools/skin/light-theme.css");
+
+  doc.head.appendChild(themeLink);
+  doc.documentElement.classList.add("theme-light");
+  doc.body.classList.add("theme-light");
+
+  let isMac = /Mac/.test(window.navigator.userAgent);
+  let isLinux = /(Linux)|(X11)/.test(window.navigator.userAgent);
+  if (isMac) {
+    doc.documentElement.setAttribute("platform", "mac");
+  } else if (isLinux) {
+    doc.documentElement.setAttribute("platform", "linux");
+  } else {
+    doc.documentElement.setAttribute("platform", "win");
+  }
+}
+
+/**
+ * Called each time a childList mutation is received on the main document.
+ * Check the iframes currently loaded in the document and call fixStylesheets if needed.
+ */
+function fixStylesheetsOnMutation() {
+  let frames = document.body.querySelectorAll("iframe");
+  for (let frame of frames) {
+    let doc = frame.contentDocument || frame.contentWindow.document;
+    if (doc.__fixStylesheetsFlag) {
+      continue;
+    }
+
+    // Mark the document as processed to avoid extra changes.
+    doc.__fixStylesheetsFlag = true;
+    if (doc.readyState !== "complete") {
+      // If the document is not loaded yet, wait for DOMContentLoaded.
+      frame.contentWindow.addEventListener("DOMContentLoaded", () => {
+        fixStylesheets(doc);
+      }, { once: true });
+    } else {
+      fixStylesheets(doc);
+    }
+  }
+}
+
+window.addEventListener("DOMContentLoaded", function onInspectorDOMLoaded() {
+  window.removeEventListener("DOMContentLoaded", onInspectorDOMLoaded);
+
+  // Add styling for the main document.
+  fixStylesheets(document);
+
+  // Add a mutation observer to check if new iframes have been loaded and need to have
+  //  their stylesheet links updated.
+  new window.MutationObserver(mutations => {
+    fixStylesheetsOnMutation();
+  }).observe(document.body, { childList: true, subtree: true });
+
+  const hasFirefoxTabParam = window.location.href.indexOf("firefox-tab") != -1;
+  if (!hasFirefoxTabParam) {
+    const inspectorRoot = document.querySelector(".inspector");
+    // Remove the inspector specific markup and add the landing page root element.
+    inspectorRoot.remove();
+    let mount = document.createElement("div");
+    mount.setAttribute("id", "mount");
+    document.body.appendChild(mount);
+  }
+
+  // Toolbox tries to add a theme classname on the documentElement and should only be
+  // required after DOMContentLoaded.
+  const { bootstrap } = require("devtools-local-toolbox");
+  bootstrap(React, ReactDOM).then(onConnect);
+});
--- a/devtools/client/inspector/markup/markup.xhtml
+++ b/devtools/client/inspector/markup/markup.xhtml
@@ -11,17 +11,16 @@
   <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/addon/dialog/dialog.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/mozilla.css" type="text/css"/>
 
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"></script>
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/sourceeditor/codemirror/codemirror.bundle.js"></script>
-
 </head>
 <body class="theme-body devtools-monospace" role="application">
 
 <!-- NOTE THAT WE MAKE EXTENSIVE USE OF HTML COMMENTS IN THIS FILE IN ORDER -->
 <!-- TO MAKE SPANS READABLE WHILST AVOIDING SIGNIFICANT WHITESPACE          -->
 
   <div id="root-wrapper" role="presentation">
     <div id="root" role="presentation"></div>
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "inspector.html",
+  "version": "0.0.1",
+  "description": "The Firefox Inspector",
+  "scripts": {
+    "start": "node bin/dev-server"
+  },
+  "author": "",
+  "dependencies": {
+    "devtools-local-toolbox": "0.0.10",
+    "devtools-modules": "0.0.9",
+    "devtools-sham-modules": "0.0.9",
+    "devtools-config": "0.0.9",
+    "raw-loader": "^0.5.1",
+    "json-loader": "^0.5.4"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack.config.js
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* global __dirname */
+
+"use strict";
+
+const {toolboxConfig} = require("devtools-local-toolbox/index");
+
+const path = require("path");
+const webpack = require("webpack");
+
+module.exports = envConfig => {
+  let webpackConfig = {
+    bail: true,
+    entry: [
+      path.join(__dirname, "local-toolbox.js")
+    ],
+    output: {
+      path: path.join(__dirname, "assets/build"),
+      filename: "inspector.js",
+      publicPath: "/"
+    },
+    module: {
+      // Disable handling of unknown requires
+      unknownContextRegExp: /$^/,
+      unknownContextCritical: false,
+
+      // Disable handling of requires with a single expression
+      exprContextRegExp: /$^/,
+      exprContextCritical: false,
+
+      // Warn for every expression in require
+      wrappedContextCritical: true,
+
+      loaders: [
+        {
+          test: /event-emitter/,
+          exclude: /node_modules/,
+          loaders: [path.join(__dirname, "./webpack/rewrite-event-emitter")],
+        }, {
+          // Replace all references to this.browserRequire() by require() in
+          // client/inspector/*.js files
+          test: /client\/inspector\/.*\.js$/,
+          loaders: [path.join(__dirname, "./webpack/rewrite-browser-require")],
+        }
+      ]
+    },
+    resolveLoader: {
+      root: [
+        path.resolve("./node_modules"),
+        path.resolve("./webpack"),
+      ]
+    },
+    resolve: {
+      alias: {
+        "acorn/util/walk": path.join(__dirname, "../../shared/acorn/walk"),
+        "acorn": path.join(__dirname, "../../shared/acorn"),
+        "devtools/client/framework/about-devtools-toolbox":
+          path.join(__dirname, "./webpack/about-devtools-sham.js"),
+        "devtools/client/framework/attach-thread":
+          path.join(__dirname, "./webpack/attach-thread-sham.js"),
+        "devtools/client/framework/target-from-url":
+          path.join(__dirname, "./webpack/target-from-url-sham.js"),
+        "devtools/client/jsonview/main":
+          path.join(__dirname, "./webpack/jsonview-sham.js"),
+        "devtools/client/sourceeditor/editor":
+          path.join(__dirname, "./webpack/editor-sham.js"),
+        "devtools/client/locales": path.join(__dirname, "../locales/en-US"),
+        "devtools/shared/locales": path.join(__dirname, "../../shared/locales/en-US"),
+        "devtools/shared/platform": path.join(__dirname, "../../shared/platform/content"),
+        "devtools": path.join(__dirname, "../../"),
+        "gcli": path.join(__dirname, "../../shared/gcli/source/lib/gcli"),
+        "method": path.join(__dirname, "../../../addon-sdk/source/lib/method"),
+        "modules/libpref/init/all":
+          path.join(__dirname, "../../../modules/libpref/init/all.js"),
+        "sdk/system/unload": path.join(__dirname, "./webpack/system-unload-sham.js"),
+        "sdk": path.join(__dirname, "../../../addon-sdk/source/lib/sdk"),
+        "Services": path.join(__dirname, "../shared/shim/Services.js"),
+        "toolkit/locales":
+          path.join(__dirname, "../../../toolkit/locales/en-US/chrome/global"),
+      },
+    },
+
+    plugins: [
+      new webpack.DefinePlugin({
+        "isWorker": JSON.stringify(false),
+        "reportError": "console.error",
+        "AppConstants": "{ DEBUG: true, DEBUG_JS_MODULES: true }",
+        "loader": `{
+                      lazyRequireGetter: () => {},
+                      lazyGetter: () => {}
+                    }`,
+        "dump": "console.log",
+      }),
+    ]
+  };
+
+  webpackConfig.externals = [
+    /codemirror\//,
+    {
+      "promise": "var Promise",
+      "devtools/server/main": "{}",
+
+      // Just trying to get build to work.  These should be removed eventually:
+      "chrome": "{}",
+
+      // In case you end up in chrome-land you can use this to help track down issues.
+      // SDK for instance does a bunch of this so if you somehow end up importing an SDK
+      // dependency this might help for debugging:
+      // "chrome": `{
+      //   Cc: {
+      //     "@mozilla.org/uuid-generator;1": { getService: () => { return {} } },
+      //     "@mozilla.org/observer-service;1": { getService: () => { return {} } },
+      //   },
+      //   Ci: {},
+      //   Cr: {},
+      //   Cm: {},
+      //   components: { classesByID: () => {} , ID: () => {} }
+      // }`,
+
+      "resource://gre/modules/XPCOMUtils.jsm": "{}",
+      "resource://devtools/client/styleeditor/StyleEditorUI.jsm": "{}",
+      "resource://devtools/client/styleeditor/StyleEditorUtil.jsm": "{}",
+      "devtools/client/inspector/inspector-panel": "{}",
+      "devtools/server/actors/utils/audionodes.json": "{}",
+
+      "devtools/client/shared/developer-toolbar": "{}",
+
+      // From sdk.
+      "resource://gre/modules/Preferences.jsm": "{}",
+      "@loader/options": "{}",
+      "@loader/unload": "{}",
+    },
+  ];
+
+  // Exclude all files from devtools/ or addon-sdk/ or modules/ .
+  webpackConfig.babelExcludes = /(devtools\/|addon-sdk\/|modules\/)/;
+
+  return toolboxConfig(webpackConfig, envConfig);
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack/about-devtools-sham.js
@@ -0,0 +1,12 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+module.exports = {
+  register: () => {},
+  unregister: () => {},
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack/attach-thread-sham.js
@@ -0,0 +1,11 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+module.exports = {
+  attachThread: () => {},
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack/editor-sham.js
@@ -0,0 +1,13 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+Editor.modes = {};
+function Editor() {}
+Editor.prototype.appendToLocalElement = () => {};
+
+module.exports = Editor;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack/jsonview-sham.js
@@ -0,0 +1,14 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+module.exports = {
+  JsonView: {
+    initialize: () => {},
+    destroy: () => {},
+  }
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack/prefs-loader.js
@@ -0,0 +1,58 @@
+/* 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/. */
+
+// Rewrite devtools.js or all.js, leaving just the relevant pref() calls.
+
+"use strict";
+
+const PREF_RX = new RegExp("^ *pref\\(\"devtools");
+
+module.exports = function (content) {
+  this.cacheable && this.cacheable();
+
+  // If we're reading devtools.js we have to do some reprocessing.
+  // If we're reading all.js we just assume we can dump all the
+  // conditionals.
+  let isDevtools = this.request.endsWith("/devtools.js");
+
+  // This maps the text of a "#if" to its truth value.  This has to
+  // cover all uses of #if in devtools.js.
+  const ifMap = {
+    "#if MOZ_UPDATE_CHANNEL == beta": false,
+    "#if defined(NIGHTLY_BUILD)": false,
+    "#ifdef NIGHTLY_BUILD": false,
+    "#ifdef MOZ_DEV_EDITION": false,
+    "#ifdef RELEASE_OR_BETA": true,
+    "#ifdef RELEASE_BUILD": true,
+  };
+
+  let lines = content.split("\n");
+  let ignoring = false;
+  let newLines = [];
+  let continuation = false;
+  for (let line of lines) {
+    if (line.startsWith("sticky_pref")) {
+      line = line.slice(7);
+    }
+
+    if (isDevtools) {
+      if (line.startsWith("#if")) {
+        if (!(line in ifMap)) {
+          throw new Error("missing line in ifMap: " + line);
+        }
+        ignoring = !ifMap[line];
+      } else if (line.startsWith("#else")) {
+        ignoring = !ignoring;
+      }
+    }
+
+    if (continuation || (!ignoring && PREF_RX.test(line))) {
+      newLines.push(line);
+
+      // The call to pref(...); might span more than one line.
+      continuation = !/\);/.test(line);
+    }
+  }
+  return newLines.join("\n");
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack/rewrite-browser-require.js
@@ -0,0 +1,12 @@
+/* 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/. */
+
+// Replace all occurrences of "this.browserRequire(" by "require(".
+
+"use strict";
+
+module.exports = function (content) {
+  this.cacheable && this.cacheable();
+  return content.replace(/this\.browserRequire\(/g, "require(");
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack/rewrite-event-emitter.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Remove the header code from event-emitter.js.  This code confuses
+// webpack.
+
+"use strict";
+
+module.exports = function (content) {
+  this.cacheable && this.cacheable();
+
+  let lines = content.split("\n");
+  let ignoring = false;
+  let newLines = [];
+  for (let line of lines) {
+    if (/function \(factory\)/.test(line)) {
+      ignoring = true;
+    } else if (/call\(this, function /.test(line)) {
+      ignoring = false;
+    } else if (!ignoring && line !== "});") {
+      newLines.push(line);
+    }
+  }
+  return newLines.join("\n");
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack/system-unload-sham.js
@@ -0,0 +1,11 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+module.exports = {
+  when: () => {},
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/webpack/target-from-url-sham.js
@@ -0,0 +1,11 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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";
+
+module.exports = {
+  targetFromURL: () => {},
+};
--- a/devtools/client/shared/shim/Services.js
+++ b/devtools/client/shared/shim/Services.js
@@ -601,16 +601,24 @@ const Services = {
       // sufficient for our purposes.
       return {
         openUILinkIn: function (url) {
           window.open(url, "_blank");
         },
       };
     },
   },
+
+  /**
+   * Shims for Services.obs.add/removeObserver.
+   */
+  obs: {
+    addObserver: () => {},
+    removeObserver: () => {},
+  },
 };
 
 /**
  * Create a new preference.  This is used during startup (see
  * devtools/client/preferences/devtools.js) to install the
  * default preferences.
  *
  * @param {String} name the name of the preference
--- a/devtools/client/shared/undo.js
+++ b/devtools/client/shared/undo.js
@@ -148,18 +148,24 @@ UndoStack.prototype = {
   /**
    * ViewController implementation for undo/redo.
    */
 
   /**
    * Install this object as a command controller.
    */
   installController: function (controllerWindow) {
+    let controllers = controllerWindow.controllers;
+    // Only available when running in a Firefox panel.
+    if (!controllers || !controllers.appendController) {
+      return;
+    }
+
     this._controllerWindow = controllerWindow;
-    controllerWindow.controllers.appendController(this);
+    controllers.appendController(this);
   },
 
   /**
    * Uninstall this object from the command controller.
    */
   uninstallController: function () {
     if (!this._controllerWindow) {
       return;
--- a/devtools/client/shared/widgets/tooltip/TooltipToggle.js
+++ b/devtools/client/shared/widgets/tooltip/TooltipToggle.js
@@ -64,25 +64,28 @@ TooltipToggle.prototype = {
    *        - {Number} toggleDelay
    *          An optional delay (in ms) that will be observed before showing
    *          and before hiding the tooltip. Defaults to DEFAULT_TOGGLE_DELAY.
    *        - {Boolean} interactive
    *          If enabled, the tooltip is not hidden when mouse leaves the
    *          target element and enters the tooltip. Allows the tooltip
    *          content to be interactive.
    */
-  start: function (baseNode, targetNodeCb,
-                   {toggleDelay = DEFAULT_TOGGLE_DELAY, interactive = false} = {}) {
+  start: function (baseNode, targetNodeCb, {toggleDelay, interactive = false} = {}) {
     this.stop();
 
     if (!baseNode) {
       // Calling tool is in the process of being destroyed.
       return;
     }
 
+    if (typeof toggleDelay === "undefined") {
+      toggleDelay = DEFAULT_TOGGLE_DELAY;
+    }
+
     this._baseNode = baseNode;
     this._targetNodeCb = targetNodeCb || (() => true);
     this._toggleDelay = toggleDelay;
     this._interactive = interactive;
 
     baseNode.addEventListener("mousemove", this._onMouseMove);
     baseNode.addEventListener("mouseout", this._onMouseOut);
 
--- a/devtools/shared/DevToolsUtils.js
+++ b/devtools/shared/DevToolsUtils.js
@@ -21,17 +21,18 @@ const ThreadSafeDevToolsUtils = require(
 for (let key of Object.keys(ThreadSafeDevToolsUtils)) {
   exports[key] = ThreadSafeDevToolsUtils[key];
 }
 
 /**
  * Waits for the next tick in the event loop to execute a callback.
  */
 exports.executeSoon = function executeSoon(aFn) {
-  if (isWorker) {
+  // XXX: Move setImmmediate chrome implementation to loader
+  if (typeof setImmediate !== "undefined") {
     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 = getStack();
       executor = () => {