Bug 1291049 - Add development server for inspector.html draft
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 17 Oct 2016 18:19:08 +0200
changeset 437815 6dc26546730ed1ab04e55edd3ccff540bc9a14d2
parent 437814 94adeecaa6475e53550996ee94a45a19d524c955
child 437816 6d3af18a261ab2395c26c6919d5aba22b06554dd
push id35517
push userjdescottes@mozilla.com
push dateFri, 11 Nov 2016 16:33:44 +0000
bugs1291049
milestone52.0a1
Bug 1291049 - Add development server for inspector.html MozReview-Commit-ID: EDUoWPT2ckM
devtools/client/inspector/inspector-html-server/bin/cypress-server.js
devtools/client/inspector/inspector-html-server/bin/development-server.js
devtools/client/inspector/inspector-html-server/bin/download-firefox-artifact
devtools/client/inspector/inspector-html-server/bin/firefox-driver.js
devtools/client/inspector/inspector-html-server/bin/firefox-proxy
devtools/client/inspector/inspector-html-server/bin/import-deps.js
devtools/client/inspector/inspector-html-server/bin/install-chrome
devtools/client/inspector/inspector-html-server/bin/install-firefox
devtools/client/inspector/inspector-html-server/bin/make-firefox-bundle
devtools/client/inspector/inspector-html-server/bin/mocha-server.js
devtools/client/inspector/inspector-html-server/bin/prepare-mochitests-dev
devtools/client/inspector/inspector-html-server/bin/run-mochitests-docker
devtools/client/inspector/inspector-html-server/config/README.md
devtools/client/inspector/inspector-html-server/config/ci.json
devtools/client/inspector/inspector-html-server/config/config.js
devtools/client/inspector/inspector-html-server/config/development.json
devtools/client/inspector/inspector-html-server/config/feature.js
devtools/client/inspector/inspector-html-server/config/firefox-panel.json
devtools/client/inspector/inspector-html-server/config/local.sample.json
devtools/client/inspector/inspector-html-server/config/tests/.eslintrc
devtools/client/inspector/inspector-html-server/config/tests/feature.js
devtools/client/inspector/inspector-html-server/npm-debug.log
devtools/client/inspector/inspector-html-server/package.json
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/cypress-server.js
@@ -0,0 +1,47 @@
+"use strict";
+
+const express = require("express");
+const bodyParser = require("body-parser");
+const fs = require('fs');
+const path = require('path');
+
+/**
+ saves a fixture file to public/js/test/fixtures
+
+ @param name - name of the fixture file
+ @param text - fixture json text
+*/
+function saveFixture(name, text) {
+  function getFixtureFile(name) {
+    const fixturePath = path.join(__dirname, "../public/js/test/fixtures");
+    const fixtureFile = path.join(fixturePath, name + ".json");
+    if (!fs.existsSync(fixturePath)) {
+      throw new Error("Could not find fixture " + name);
+    }
+
+    return fixtureFile;
+  }
+
+  const fixtureFile = getFixtureFile(name);
+  fs.writeFileSync(fixtureFile, text)
+}
+
+const app = express();
+app.use(bodyParser.urlencoded({ extended: false }));
+app.use(bodyParser.json({ limit: "50mb" }));
+
+app.post("/save-fixture", function(req, res) {
+  const fixtureName = req.body.fixtureName;
+  const fixtureText = req.body.fixtureText;
+  saveFixture(fixtureName, fixtureText);
+
+  res.send(`saved fixture ${fixtureName}`);
+});
+
+app.listen(8001, "localhost", function(err, result) {
+  if (err) {
+    console.log(err);
+  }
+
+  console.log("Development Server Listening at http://localhost:8001");
+});
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/development-server.js
@@ -0,0 +1,158 @@
+#!/usr/bin/env node
+
+"use strict";
+
+require("babel-register");
+
+const path = require("path");
+const fs = require("fs");
+const Mustache = require("mustache");
+const webpack = require("webpack");
+const express = require("express");
+const webpackDevMiddleware = require("webpack-dev-middleware");
+const webpackHotMiddleware = require("webpack-hot-middleware");
+const http = require("http");
+const serveIndex = require("serve-index");
+
+// Setup Config
+const getConfig = require("../config/config").getConfig;
+const feature = require("../config/feature");
+const config = getConfig();
+
+feature.setConfig(config);
+
+if (!feature.getValue("firefox.webSocketConnection")) {
+  const firefoxProxy = require("./firefox-proxy");
+  firefoxProxy({ logging: feature.getValue("logging.firefoxProxy") });
+}
+
+function httpGet(url, onResponse) {
+  return http.get(url, (response) => {
+    if (response.statusCode !== 200) {
+      console.error(`error response: ${response.statusCode} to ${url}`);
+      response.emit("statusCode", new Error(response.statusCode));
+      return onResponse("{}");
+    }
+    let body = "";
+    response.on("data", function(d) {
+      body += d;
+    });
+    response.on("end", () => onResponse(body));
+  });
+}
+
+const app = express();
+
+// Webpack middleware
+// const webpackConfig = require("../../webpack.config");
+// const compiler = webpack(webpackConfig);
+
+// app.use(webpackDevMiddleware(compiler, {
+//   publicPath: webpackConfig.output.publicPath,
+//   noInfo: true,
+//   stats: { colors: true }
+// }));
+
+if (feature.getValue("hotReloading")) {
+  app.use(webpackHotMiddleware(compiler));
+} else {
+  console.log("Hot Reloading can be enabled by adding " +
+  "\"hotReloading\": true to your local.json config");
+}
+
+// Static middleware
+app.use(express.static("public"));
+
+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");
+addFileRoute("/inspector.bundle.js", "../../inspector.bundle.js");
+addFileRoute("/devtools/content/inspector/inspector-html-bootstrap.js", "../../inspector-html-bootstrap.js");
+
+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");
+});
+
+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");
+});
+
+app.get(/^\/devtools\/client\/(.*)\.css$/, function (req, res) {
+  res.contentType("text/css; charset=utf-8");
+  sendFile(res, "../../../" + req.params[0] + ".css");
+});
+
+app.get(/^\/devtools\/content\/(.*)\.css$/, function (req, res) {
+  res.contentType("text/css; charset=utf-8");
+  sendFile(res, "../../../" + req.params[0] + ".css");
+});
+
+app.get("/get", function(req, res) {
+  const url = req.query.url;
+  if(url.indexOf("file://") === 0) {
+    const path = url.replace("file://", "");
+    res.json(JSON.parse(fs.readFileSync(path, "utf8")));
+  }
+  else {
+    const httpReq = httpGet(
+      req.query.url,
+      body => {
+        try {
+          res.send(body);
+        } catch (e) {
+          res.status(500).send("Malformed json");
+        }
+      }
+    );
+
+    httpReq.on("error", err => res.status(500).send(err.code));
+    httpReq.on("statusCode", err => res.status(err.message).send(err.message));
+  }
+});
+
+// Listen'
+const serverPort = feature.getValue("development.serverPort");
+app.listen(serverPort, "0.0.0.0", function(err, result) {
+  if (err) {
+    console.log(err);
+  } else {
+    console.log(`Development Server Listening at http://localhost:${serverPort}`);
+  }
+});
+
+const examples = express();
+examples.use(express.static("public/js/test/examples"));
+examples.use(serveIndex("public/js/test/examples", { icons: true }));
+
+const examplesPort = feature.getValue("development.examplesPort");
+examples.listen(examplesPort, "0.0.0.0", function(err, result) {
+  if (err) {
+    console.log(err);
+  } else {
+    console.log(`View debugger examples at http://localhost:${examplesPort}`);
+  }
+});
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/download-firefox-artifact
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+
+ROOT=`dirname $0`
+FIREFOX_PATH="$ROOT/../firefox"
+
+# check that mercurial is installed
+if [ -z "`command -v hg`" ]; then
+  echo >&2 "mercurial is required for mochitests, use 'brew install mercurial' on MacOS";
+  exit 1;
+fi
+
+if [ -d "$FIREFOX_PATH" ]; then
+    # convert path to absolute path
+    FIREFOX_PATH=$(cd "$ROOT/../firefox"; pwd)
+
+    # If we already have Firefox locally, just update it
+    cd "$FIREFOX_PATH";
+
+    if [ -n "`hg status`" ]; then
+        read -p "There are local changes to Firefox which will be overwritten. Are you sure? [Y/n] " -r
+        if [[ $REPLY == "n" ]]; then
+            exit 0;
+        fi
+
+        # If the mochitest dir has been symlinked, remove it as revert
+        # does not follow symlinks.
+        if [ -h "$FIREFOX_PATH/devtools/client/debugger/new/test/mochitest" ]; then
+           rm "$FIREFOX_PATH/devtools/client/debugger/new/test/mochitest";
+        fi
+
+        hg revert -a
+    fi
+
+    hg pull
+    hg update -C
+else
+    echo "Downloading Firefox source code, requires about 10-30min depending on connection"
+    hg clone https://hg.mozilla.org/mozilla-central/ "$FIREFOX_PATH"
+    # if somebody cancels (ctrl-c) out of the long download don't continue
+    if [ $? -ne 0 ]; then
+        exit 1;
+    fi
+    cd "$FIREFOX_PATH"
+
+    # Make an artifact build so it builds much faster
+    echo "
+ac_add_options --enable-artifact-builds
+mk_add_options MOZ_OBJDIR=./objdir-frontend
+" > .mozconfig
+fi
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/firefox-driver.js
@@ -0,0 +1,69 @@
+"use strict";
+
+const webdriver = require("selenium-webdriver");
+const firefox = require("selenium-webdriver/firefox");
+const By = webdriver.By;
+const until = webdriver.until;
+const Key = webdriver.Key;
+const minimist = require("minimist");
+
+const args = minimist(process.argv.slice(2),
+{ boolean: ["start", "tests", "websocket"] });
+
+const isWindows = /^win/.test(process.platform);
+const shouldStart = args.start;
+const isTests = args.tests;
+const useWebSocket = args.websocket;
+
+function firefoxBinary() {
+  let binary = new firefox.Binary();
+  binary.addArguments((!isWindows ? "-" : "") + "-start-debugger-server",
+    useWebSocket ? "ws:6080" : "6080");
+
+  return binary;
+}
+
+function firefoxProfile() {
+  let profile = new firefox.Profile();
+  profile.setPreference("devtools.debugger.remote-port", 6080);
+  profile.setPreference("devtools.debugger.remote-enabled", true);
+  profile.setPreference("devtools.chrome.enabled", true);
+  profile.setPreference("devtools.debugger.prompt-connection", false);
+  profile.setPreference("devtools.debugger.remote-websocket", useWebSocket);
+
+  return profile;
+}
+
+function start() {
+  let options = new firefox.Options();
+  options.setProfile(firefoxProfile());
+  options.setBinary(firefoxBinary());
+
+  const driver = new firefox.Driver(options);
+  return driver;
+}
+
+if (shouldStart) {
+  const driver = start();
+  driver.get("http://localhost:7999/todomvc");
+  setInterval(() => {}, 100);
+}
+
+function getResults(driver) {
+  driver
+    .findElement(By.id("mocha-stats"))
+    .getText().then(results => {
+      console.log("results ", results);
+      const match = results.match(/failures: (\d*)/);
+      const resultCode = parseInt(match[1], 10) > 0 ? 1 : 0;
+      process.exit(resultCode);
+    });
+}
+
+if (isTests) {
+  const driver = start();
+  driver.get("http://localhost:8003");
+  setTimeout(() => getResults(driver), 5000);
+}
+
+module.exports = { start, By, Key, until };
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/firefox-proxy
@@ -0,0 +1,77 @@
+#!/usr/bin/env node
+"use strict";
+
+const minimist = require("minimist");
+const ws = require("ws");
+const net = require("net");
+
+function proxy(webSocketPort, tcpPort, logging) {
+  console.log("Listening for WS on *:" + webSocketPort);
+  console.log("Will proxy to TCP on *:" + tcpPort + " on first WS connection");
+  if (!logging) {
+    console.log("Protocol messages can be logged by enabling " +
+    "logging.firefoxProtocol in your local.json config");
+  }
+  let wsServer = new ws.Server({ port: webSocketPort });
+  wsServer.on("connection", function onConnection(wsConnection) {
+    let tcpClient = net.connect({ port: tcpPort });
+    tcpClient.setEncoding("utf8");
+
+    tcpClient.on("connect", () => {
+      console.log("TCP connection succeeded");
+    });
+
+    tcpClient.on("error", e => {
+      wsConnection.close();
+      console.log("TCP connection failed: " + e);
+    });
+
+    tcpClient.on("data", data => {
+      if (logging) {
+        console.log("TCP -> WS: " + data);
+      }
+      try {
+        wsConnection.send(data);
+      } catch (e) {
+        tcpClient.end();
+        console.log("WS send failed, disconnected from TCP");
+      }
+    });
+
+    wsConnection.on("message", msg => {
+      if (logging) {
+        console.log("WS -> TCP: " + msg);
+      }
+      tcpClient.write(msg);
+    });
+
+    wsConnection.on("close", () => {
+      tcpClient.end();
+      console.log("WS connection closed, disconnected from TCP");
+    });
+
+    wsConnection.on("error", () => {
+      tcpClient.end();
+      console.log("WS connection error, disconnected from TCP");
+    });
+  });
+}
+
+const args = minimist(process.argv.slice(2));
+
+const WEB_SOCKET_PORT = args["web-socket-port"] || 9000;
+const TCP_PORT = args["tcp-port"] || 6080;
+const shouldStart = args.start;
+
+function start(options) {
+  const webSocketPort = options.webSocketPort || 9000;
+  const tcpPort = options.tcpPort || 6080;
+  const logging = !!options.logging;
+  proxy(webSocketPort, tcpPort, logging);
+}
+
+if (shouldStart) {
+  start({ webSocketPort: WEB_SOCKET_PORT, tcpPort: TCP_PORT });
+} else {
+  module.exports = start;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/import-deps.js
@@ -0,0 +1,26 @@
+"use strict";
+
+const fs = require("fs");
+const glob = require("glob").sync;
+const path = require("path");
+
+const getConfig = require("../config/config").getConfig;
+const feature = require("../config/feature");
+const config = getConfig();
+feature.setConfig(config);
+
+const geckoDir = feature.getValue("firefox.geckoDir");
+if (!geckoDir) {
+  console.log("Set firefox.geckoDir in your local.json config.")
+  exit();
+}
+
+glob("public/js/lib/devtools/**/*.js").forEach((debuggerFile) => {
+  const geckoFilePath = path.join(
+    geckoDir,
+    path.relative("public/js/lib/", debuggerFile)
+  );
+
+  const fileText = fs.readFileSync(geckoFilePath, 'utf8');
+  fs.writeFileSync(debuggerFile, fileText);
+})
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/install-chrome
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
+sudo dpkg -i google-chrome.deb
+sudo sed -i 's|HERE/chrome\"|HERE/chrome\" --disable-setuid-sandbox|g' /opt/google/chrome/google-chrome
+rm google-chrome.deb
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/install-firefox
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+wget https://ftp.mozilla.org/pub/firefox/releases/46.0/linux-x86_64/en-US/firefox-46.0.tar.bz2
+tar -xjf firefox-46.0.tar.bz2
+sudo rm -rf  /opt/firefox46
+sudo rm  /usr/bin/firefox
+sudo mv firefox /opt/firefox46
+sudo ln -s /opt/firefox46/firefox /usr/bin/firefox
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/make-firefox-bundle
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+if [ -z "$1" ]; then
+   echo "Usage: $0 <path-to-firefox-repo>"
+   exit 1
+fi
+
+if [ "$2" == "--symlink-mochitests" ]; then
+  SYMLINK_MOCHITESTS=1
+fi
+
+ROOT=`dirname $0`
+DEBUGGER_PATH="$1/devtools/client/debugger/new"
+REV=`git log -1 --pretty=oneline`
+
+if [ ! -d "$DEBUGGER_PATH" ]; then
+   echo "Cannot find debugger at $DEBUGGER_PATH"
+   exit 2
+fi
+
+(
+    cd "$DEBUGGER_PATH";
+    if [ -d "DEBUGGER_PATH/.git" ]; then
+        if ! git log --oneline bundle.js | head -n 1 |
+                grep "UPDATE_BUNDLE" > /dev/null; then
+            echo "\033[31mWARNING\033[0m: bundle has changed on mozilla-central";
+        fi
+    fi
+);
+
+TARGET=firefox-panel node_modules/.bin/webpack
+echo "// Generated from: $REV\n" | cat - public/build/bundle.js > "$DEBUGGER_PATH/bundle.js"
+cp public/build/pretty-print-worker.js "$DEBUGGER_PATH"
+cp public/build/styles.css "$DEBUGGER_PATH"
+cp public/images/* "$DEBUGGER_PATH/images"
+
+rm -r "$DEBUGGER_PATH/test/mochitest"
+if [ -n "$SYMLINK_MOCHITESTS" ]; then
+    ln -s `pwd -P`"/$ROOT/../public/js/test/mochitest/" "$DEBUGGER_PATH/test/mochitest"
+else
+    rsync -avz public/js/test/mochitest/ "$DEBUGGER_PATH/test/mochitest"
+fi
+
+# Make sure a rebuild uses the new tests
+touch "$DEBUGGER_PATH/test/mochitest/browser.ini"
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/mocha-server.js
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+"use strict";
+
+const path = require("path");
+const webpack = require("webpack");
+const express = require("express");
+const projectConfig = require("../webpack.config");
+const webpackDevMiddleware = require("webpack-dev-middleware");
+const fs = require("fs");
+
+function recursiveReaddirSync(dir) {
+  let list = [];
+  const files = fs.readdirSync(dir);
+
+  files.forEach(function(file) {
+    const stats = fs.lstatSync(path.join(dir, file));
+    if (stats.isDirectory()) {
+      list = list.concat(recursiveReaddirSync(path.join(dir, file)));
+    } else {
+      list.push(path.join(dir, file));
+    }
+  });
+
+  return list;
+}
+
+function getTestPaths(dir) {
+  const paths = recursiveReaddirSync(dir);
+
+  return paths.filter(p => {
+    const inTestDirectory = path.dirname(p).includes("tests");
+    const inIntegrationDir = path.dirname(p).includes("integration");
+    const aHiddenFile = path.basename(p).charAt(0) == ".";
+    return inTestDirectory && !aHiddenFile && !inIntegrationDir;
+  });
+}
+
+const testPaths = getTestPaths(path.join(__dirname, "../public/js"));
+
+projectConfig.entry.bundle = projectConfig.entry.bundle.concat(testPaths);
+const config = Object.assign({}, projectConfig, {});
+
+const app = express();
+const compiler = webpack(config);
+
+app.use(express.static("public"));
+app.use(express.static("node_modules"));
+
+app.use(webpackDevMiddleware(compiler, {
+  publicPath: projectConfig.output.publicPath,
+  noInfo: true,
+  stats: {
+    colors: true
+  }
+}));
+
+app.get("/", function(req, res) {
+  res.sendFile(path.join(__dirname, "../mocha-runner.html"));
+});
+
+app.listen(8003, "localhost", function(err, result) {
+  if (err) {
+    console.log(err);
+  }
+
+  console.log("Listening at http://localhost:8003");
+});
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/prepare-mochitests-dev
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+ROOT=`dirname $0`
+FIREFOX_PATH="$ROOT/../firefox"
+
+# This will either download or update the local Firefox repo
+"$ROOT/download-firefox-artifact"
+
+# Update the debugger files, build firefox, and run all the mochitests
+"$ROOT/make-firefox-bundle" "$FIREFOX_PATH" --symlink-mochitests
+cd "$FIREFOX_PATH"
+./mach build
new file mode 100755
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/bin/run-mochitests-docker
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+TARGET=firefox-panel ./node_modules/.bin/webpack
+
+docker run -it \
+  -v `pwd`/public/build/bundle.js:/firefox/devtools/client/debugger/new/bundle.js \
+  -v `pwd`/public/build/pretty-print-worker.js:/firefox/devtools/client/debugger/new/pretty-print-worker.js \
+  -v `pwd`/public/build/styles.css:/firefox/devtools/client/debugger/new/styles.css \
+  -v `pwd`/public/images:/firefox/devtools/client/debugger/new/images \
+  -v `pwd`/public/js/test/mochitest:/firefox/devtools/client/debugger/new/test/mochitest \
+  -v "/tmp/.X11-unix:/tmp/.X11-unix:rw" \
+  -e "DISPLAY=unix$DISPLAY" \
+  --ipc host \
+  jlongster/mochitest-runner \
+  /bin/bash -c "export SHELL=/bin/bash; touch devtools/client/debugger/new/test/mochitest/browser.ini && ./mach mochitest --subsuite devtools devtools/client/debugger/new/test/mochitest/"
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/config/README.md
@@ -0,0 +1,31 @@
+## Configuration
+
+All default config values are in [`config/development.json`](./development.json), to override these values you need to [create a local config file](#create-a-local-config-file).
+
+* `logging`
+  * `client` Enables logging the Firefox protocol in the devtools console of the debugger
+  * `firefoxProxy` Enables logging the Firefox protocol in the terminal running `npm start`
+  * `actions` Enables logging the redux actions
+* `features` debugger related features
+  * `tabs` Enables source view tabs in the editor (CodeMirror)
+  * `sourceMaps` Enables source map loading when available
+  * `watchExpressions` Enables accordion component for working with watch expressions
+* `chrome` Chrome browser related flags
+  * `debug` Enables listening for remotely debuggable Chrome browsers
+  * `webSocketPort` Configures the web socket port specified when launching Chrome from the command line
+* `firefox` Firefox browser related flags
+  * `proxyPort` Port used by the development server run with `npm start`
+  * `webSocketConnection` Favours Firefox WebSocket connection over the [firefox-proxy](../bin/firefox-proxy), :warning: Experimental feature and requires [bug 1286281](https://bugzilla.mozilla.org/show_bug.cgi?id=1286281)
+  * `geckoDir` Local location of Firefox source code _only needed by project maintainers_
+*  `development` Development server related settings
+  * `serverPort` Listen Port used by the development server
+  * `examplesPort` Listen Port used to serve examples
+* `hotReloading` enables [Hot Reloading](../docs/local-development.md#hot-reloading) of CSS and React
+
+### Create a local config file
+
+To override any of the default configuration values above you need to create a new file in the config directory called `local.json`; it is easiest if you copy the `development.json` file.
+
+* Copy the [`config/development.json`](./development.json) to `config/local.json`
+
+> The `local.json` will be ignored by git so any changes you make won't be published, only make changes to the `development.json` file when related to features removed or added to the project.
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/config/ci.json
@@ -0,0 +1,10 @@
+{
+  "features": {
+    "sourceMaps": true,
+    "prettyPrint": true,
+    "watchExpressions": true
+  },
+  "chrome": {
+    "debug": true
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/config/config.js
@@ -0,0 +1,30 @@
+"use strict";
+
+const _ = require("lodash");
+const fs = require("fs");
+const path = require("path");
+
+const firefoxPanel = require("./firefox-panel.json");
+const development = require("./development.json");
+const envConfig = process.env.TARGET === "firefox-panel" ?
+   firefoxPanel : development;
+
+let config;
+
+if(process.env.TARGET === "firefox-panel") {
+  config = firefoxPanel;
+}
+else {
+  const localConfig = fs.existsSync(path.join(__dirname, "./local.json")) ?
+        require("./local.json") : {};
+
+  config = _.merge({}, envConfig, localConfig);
+}
+
+function getConfig() {
+  return config;
+}
+
+module.exports = {
+  getConfig
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/config/development.json
@@ -0,0 +1,29 @@
+{
+  "environment": "development",
+  "baseWorkerURL": "public/build/",
+  "logging": {
+    "client": false,
+    "firefoxProxy": false,
+    "actions": false
+  },
+  "features": {
+    "tabs": true,
+    "sourceMaps": true,
+    "prettyPrint": false,
+    "watchExpressions": false,
+    "search": true
+  },
+  "chrome": {
+    "debug": true,
+    "webSocketPort": 9222
+  },
+  "firefox": {
+    "proxyPort": 9000,
+    "webSocketConnection": false,
+    "webSocketPort": 6080
+  },
+  "development": {
+    "serverPort": 8000,
+    "examplesPort": 7999
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/config/feature.js
@@ -0,0 +1,56 @@
+"use strict";
+
+const flag = false;
+const pick = require("lodash/get");
+let config;
+
+/**
+ * Gets a config value for a given key
+ * e.g "chrome.webSocketPort"
+ */
+function getValue(key) {
+  return pick(config, key);
+}
+
+function isEnabled(key) {
+  return config.features[key];
+}
+
+function isDevelopment() {
+  if(isFirefoxPanel()) {
+    // Default to production if compiling for the Firefox panel
+    return process.env.NODE_ENV === "development";
+  }
+  return process.env.NODE_ENV !== "production";
+}
+
+function isTesting() {
+  return flag.testing;
+}
+
+function isFirefoxPanel() {
+  return process.env.TARGET == "firefox-panel";
+}
+
+function isFirefox() {
+  return /firefox/i.test(navigator.userAgent);
+}
+
+function setConfig(value) {
+  config = value;
+}
+
+function getConfig() {
+  return config;
+}
+
+module.exports = {
+  isEnabled,
+  getValue,
+  isDevelopment,
+  isTesting,
+  isFirefoxPanel,
+  isFirefox,
+  getConfig,
+  setConfig
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/config/firefox-panel.json
@@ -0,0 +1,9 @@
+{
+  "environment": "firefox-panel",
+  "baseWorkerURL": "resource://devtools/client/debugger/new/",
+  "logging": false,
+  "clientLogging": false,
+  "features": {
+    "tabs": true
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/config/local.sample.json
@@ -0,0 +1,5 @@
+{
+  "firefox": {
+    "geckoDir": "/Users/jlaster/src/mozilla/gecko"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/config/tests/.eslintrc
@@ -0,0 +1,32 @@
+{
+  "globals": {
+    "after": true,
+    "afterEach": true,
+    "before": true,
+    "beforeEach": true,
+    "context": true,
+    "describe": true,
+    "it": true,
+    "mocha": true,
+    "setup": true,
+    "specify": true,
+    "suite": true,
+    "suiteSetup": true,
+    "suiteTeardown": true,
+    "teardown": true,
+    "test": true,
+    "xcontext": true,
+    "xdescribe": true,
+    "xit": true,
+    "xspecify": true,
+    "assert": true,
+    "expect": true,
+    "equal": true,
+    "ok": true
+  },
+  "rules": {
+    "camelcase": 0,
+    "no-unused-vars": [2, {"vars": "all", "args": "none", "varsIgnorePattern": "run_test"}],
+    "max-nested-callbacks": [2, 4],
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/config/tests/feature.js
@@ -0,0 +1,39 @@
+const { isDevelopment, getValue, isEnabled, setConfig } = require("../feature");
+const expect = require("expect.js");
+
+describe("feature", () => {
+  it("isDevelopment", () => {
+    setConfig({ development: true });
+    expect(isDevelopment()).to.be.truthy;
+  });
+
+  it("isDevelopment - not defined", () => {
+    setConfig({ });
+    expect(isDevelopment()).to.be.falsey;
+  });
+
+  it("getValue - enabled", function() {
+    setConfig({ featureA: true });
+    expect(getValue("featureA")).to.be.truthy;
+  });
+
+  it("getValue - disabled", function() {
+    setConfig({ featureA: false });
+    expect(getValue("featureA")).to.be.falsey;
+  });
+
+  it("getValue - not present", function() {
+    setConfig({});
+    expect(getValue("featureA")).to.be.undefined;
+  });
+
+  it("isEnabled - enabled", function() {
+    setConfig({ features: { featureA: true }});
+    expect(isEnabled("featureA")).to.be.truthy;
+  });
+
+  it("isEnabled - disabled", function() {
+    setConfig({ features: { featureA: false }});
+    expect(isEnabled("featureA")).to.be.falsey;
+  });
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/npm-debug.log
@@ -0,0 +1,45 @@
+0 info it worked if it ends with ok
+1 verbose cli [ '/usr/local/bin/node', '/usr/local/bin/npm', 'start' ]
+2 info using npm@3.8.3
+3 info using node@v5.10.1
+4 verbose run-script [ 'prestart', 'start', 'poststart' ]
+5 info lifecycle inspector.html@0.0.1~prestart: inspector.html@0.0.1
+6 silly lifecycle inspector.html@0.0.1~prestart: no script for prestart, continuing
+7 info lifecycle inspector.html@0.0.1~start: inspector.html@0.0.1
+8 verbose lifecycle inspector.html@0.0.1~start: unsafe-perm in lifecycle true
+9 verbose lifecycle inspector.html@0.0.1~start: PATH: /usr/local/lib/node_modules/npm/bin/node-gyp-bin:/Users/jdescottes/Development/hg/fx-team/devtools/client/inspector/inspector.html/node_modules/.bin:/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+10 verbose lifecycle inspector.html@0.0.1~start: CWD: /Users/jdescottes/Development/hg/fx-team/devtools/client/inspector/inspector.html
+11 silly lifecycle inspector.html@0.0.1~start: Args: [ '-c', 'node bin/development-server' ]
+12 silly lifecycle inspector.html@0.0.1~start: Returned: code: 1  signal: null
+13 info lifecycle inspector.html@0.0.1~start: Failed to exec start script
+14 verbose stack Error: inspector.html@0.0.1 start: `node bin/development-server`
+14 verbose stack Exit status 1
+14 verbose stack     at EventEmitter.<anonymous> (/usr/local/lib/node_modules/npm/lib/utils/lifecycle.js:239:16)
+14 verbose stack     at emitTwo (events.js:100:13)
+14 verbose stack     at EventEmitter.emit (events.js:185:7)
+14 verbose stack     at ChildProcess.<anonymous> (/usr/local/lib/node_modules/npm/lib/utils/spawn.js:24:14)
+14 verbose stack     at emitTwo (events.js:100:13)
+14 verbose stack     at ChildProcess.emit (events.js:185:7)
+14 verbose stack     at maybeClose (internal/child_process.js:850:16)
+14 verbose stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:215:5)
+15 verbose pkgid inspector.html@0.0.1
+16 verbose cwd /Users/jdescottes/Development/hg/fx-team/devtools/client/inspector/inspector.html
+17 error Darwin 15.6.0
+18 error argv "/usr/local/bin/node" "/usr/local/bin/npm" "start"
+19 error node v5.10.1
+20 error npm  v3.8.3
+21 error code ELIFECYCLE
+22 error inspector.html@0.0.1 start: `node bin/development-server`
+22 error Exit status 1
+23 error Failed at the inspector.html@0.0.1 start script 'node bin/development-server'.
+23 error Make sure you have the latest version of node.js and npm installed.
+23 error If you do, this is most likely a problem with the inspector.html package,
+23 error not with npm itself.
+23 error Tell the author that this fails on your system:
+23 error     node bin/development-server
+23 error You can get information on how to open an issue for this project with:
+23 error     npm bugs inspector.html
+23 error Or if that isn't available, you can get their info via:
+23 error     npm owner ls inspector.html
+23 error There is likely additional logging output above.
+24 verbose exit [ 1, true ]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/inspector-html-server/package.json
@@ -0,0 +1,97 @@
+{
+  "name": "inspector.html",
+  "version": "0.0.1",
+  "scripts": {
+    "start": "node bin/development-server"
+  },
+  "engineStrict": true,
+  "engines": {
+    "node": ">=5.0.0"
+  },
+  "dependencies": {
+    "classnames": "^2.2.5",
+    "codemirror": "^5.1.0",
+    "express": "^4.13.4",
+    "fuzzaldrin-plus": "^0.3.1",
+    "immutable": "^3.7.6",
+    "invariant": "^2.2.1",
+    "lodash": "^4.13.1",
+    "md5": "^2.2.1",
+    "mock-require": "^1.3.0",
+    "pretty-fast": "^0.2.0",
+    "react": "=0.14.7",
+    "react-dom": "=0.14.7",
+    "react-immutable-proptypes": "^1.7.1",
+    "react-inlinesvg": "^0.5.3",
+    "react-redux": "4.4.5",
+    "redux": "3.5.2",
+    "source-map": "^0.5.6",
+    "svg-inline-loader": "^0.7.1",
+    "svg-inline-react": "^1.0.2",
+    "tcomb": "^3.1.0"
+  },
+  "devDependencies": {
+    "amd-loader": "0.0.5",
+    "babel": "^6.5.2",
+    "babel-cli": "^6.7.5",
+    "babel-core": "^6.7.6",
+    "babel-eslint": "^6.1.2",
+    "babel-loader": "^6.2.4",
+    "babel-plugin-module-alias": "^1.4.0",
+    "babel-plugin-transform-async-to-generator": "^6.8.0",
+    "babel-plugin-transform-es2015-block-scoping": "^6.7.1",
+    "babel-plugin-transform-es2015-destructuring": "^6.6.5",
+    "babel-plugin-transform-es2015-parameters": "^6.7.0",
+    "babel-plugin-transform-es2015-spread": "^6.6.5",
+    "babel-plugin-transform-flow-strip-types": "^6.8.0",
+    "babel-plugin-transform-runtime": "^6.7.5",
+    "babel-polyfill": "^6.7.4",
+    "babel-preset-es2015": "^6.6.0",
+    "babel-preset-stage-0": "^6.5.0",
+    "babel-register": "^6.7.2",
+    "body-parser": "^1.15.0",
+    "check-node-version": "^1.1.2",
+    "co": "=4.6.0",
+    "css-loader": "^0.25.0",
+    "documentation": "^4.0.0-beta10",
+    "expect.js": "^0.3.1",
+    "extract-text-webpack-plugin": "^1.0.1",
+    "firefox-profile": "^0.4.0",
+    "flow-bin": "^0.33.0",
+    "geckodriver": "^1.1.2",
+    "glob": "^7.0.3",
+    "husky": "^0.11.7",
+    "install": "^0.8.1",
+    "json-loader": "^0.5.4",
+    "minimist": "^1.2.0",
+    "mocha": "^2.4.5",
+    "mocha-circleci-reporter": "0.0.1",
+    "mustache": "^2.2.1",
+    "net": "^1.0.2",
+    "node-static": "^0.7.7",
+    "npm": "^3.10.7",
+    "react-hot-loader": "^1.3.0",
+    "rimraf": "^2.5.2",
+    "selenium-webdriver": "^3.0.0-beta-2",
+    "serve-index": "^1.8.0",
+    "style-loader": "^0.13.1",
+    "stylelint": "^7.2.0",
+    "webpack": "1.13.1",
+    "webpack-dev-middleware": "^1.6.1",
+    "webpack-hot-middleware": "^2.12.0",
+    "workerjs": "^0.1.1",
+    "ws": "^1.0.1"
+  },
+  "files": [
+    "public"
+  ],
+  "greenkeeper": {
+    "ignore": [
+      "react",
+      "react-dom",
+      "react-redux",
+      "redux",
+      "codemirror"
+    ]
+  }
+}