Bug 1291049 - Attempt to bundle the inspector with webpack draft
authorTom Tromey <tom@tromey.com>
Tue, 09 Aug 2016 10:52:58 -0600
changeset 421402 1fc36f62f932
parent 421270 4f41a594f88d
child 533061 21f6cdd03c88
push id31480
push userbgrinstead@mozilla.com
push dateThu, 06 Oct 2016 00:14:49 +0000
bugs1291049
milestone52.0a1
Bug 1291049 - Attempt to bundle the inspector with webpack MozReview-Commit-ID: Lk5pRrDCLV4 * * * Bug 1291049 - Some more work towards bundling the inspector
.gitignore
.hgignore
addon-sdk/source/lib/sdk/util/object.js
addon-sdk/source/lib/toolkit/loader.js
devtools/client/inspector/inspector.js
devtools/client/inspector/inspector.xhtml
devtools/client/shared/undo.js
devtools/client/webpack.config.js
devtools/client/webpack/prefs-loader.js
devtools/client/webpack/rewrite-event-emitter.js
devtools/shared/DevToolsUtils.js
devtools/shared/l10n.js
--- a/.gitignore
+++ b/.gitignore
@@ -71,16 +71,17 @@ python/psutil/**/*.pyd
 python/psutil/build/
 
 # Ignore chrome.manifest files from the devtools loader
 devtools/client/chrome.manifest
 devtools/shared/chrome.manifest
 
 # Ignore node_modules directories in devtools
 devtools/**/node_modules
+devtools/client/inspector/inspector.bundle.js
 
 # Tag files generated by GNU Global
 GTAGS
 GRTAGS
 GSYMS
 GPATH
 
 # Git clone directory for updating web-platform-tests
--- a/.hgignore
+++ b/.hgignore
@@ -75,16 +75,17 @@
 .git/
 
 # Ignore chrome.manifest files from the devtools loader
 ^devtools/client/chrome.manifest$
 ^devtools/shared/chrome.manifest$
 
 # Ignore node_modules directories in devtools
 ^devtools/.*/node_modules/
+^devtools/client/inspector/inspector.bundle.js
 
 # git checkout of libstagefright
 ^media/libstagefright/android$
 
 # Tag files generated by GNU Global
 GTAGS
 GRTAGS
 GSYMS
--- 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/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -765,16 +765,22 @@ const Require = iced(function Require(lo
   }
 
   // Expose the `resolve` function for this `Require` instance
   require.resolve = _require.resolve = function resolve(id) {
     let { uri } = getRequirements(id);
     return uri;
   }
 
+  require.context = (prefix) => {
+    return (id) => {
+      return require(prefix + id);
+    };
+  };
+
   // Make `require.main === module` evaluate to true in main module scope.
   require.main = loader.main === requirer ? requirer : undefined;
   return iced(require);
 });
 Loader.Require = Require;
 
 const main = iced(function main(loader, id) {
   // If no main entry provided, and native loader is used,
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -3,18 +3,16 @@
 /* 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 */
 
 "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");
--- a/devtools/client/inspector/inspector.xhtml
+++ b/devtools/client/inspector/inspector.xhtml
@@ -20,17 +20,31 @@
 
 <!DOCTYPE html>
 
 <html xmlns="http://www.w3.org/1999/xhtml" dir="">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"></script>
-  <script type="application/javascript;version=1.8" src="inspector.js" defer="true"></script>
+  <script>
+  let loadInChrome = window.location.href.includes("chrome:");
+  let inspectorScript = document.createElement("script");
+  inspectorScript.setAttribute("type", "application/javascript;version=1.8");
+  inspectorScript.setAttribute("defer", "true");
+
+  if (loadInChrome) {
+    var Cu = Components.utils;
+    var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+    inspectorScript.src = "inspector.js";
+  } else {
+    inspectorScript.src = "inspector.bundle.js";
+  }
+  document.head.appendChild(inspectorScript);
+  </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"
                 data-localization-bundle="devtools/locale/inspector.properties">
--- a/devtools/client/shared/undo.js
+++ b/devtools/client/shared/undo.js
@@ -149,27 +149,27 @@ UndoStack.prototype = {
    * ViewController implementation for undo/redo.
    */
 
   /**
    * Install this object as a command controller.
    */
   installController: function (controllerWindow) {
     this._controllerWindow = controllerWindow;
-    controllerWindow.controllers.appendController(this);
+    // controllerWindow.controllers.appendController(this);
   },
 
   /**
    * Uninstall this object from the command controller.
    */
   uninstallController: function () {
     if (!this._controllerWindow) {
       return;
     }
-    this._controllerWindow.controllers.removeController(this);
+    // this._controllerWindow.controllers.removeController(this);
   },
 
   supportsCommand: function (command) {
     return (command == "cmd_undo" ||
             command == "cmd_redo");
   },
 
   isCommandEnabled: function (command) {
--- a/devtools/client/webpack.config.js
+++ b/devtools/client/webpack.config.js
@@ -1,14 +1,17 @@
 /* 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 path = require("path");
+const webpack = require("webpack");
+
 module.exports = [{
   bail: true,
   entry: [
     "./sourceeditor/codemirror/addon/dialog/dialog.js",
     "./sourceeditor/codemirror/addon/search/searchcursor.js",
     "./sourceeditor/codemirror/addon/search/search.js",
     "./sourceeditor/codemirror/addon/edit/matchbrackets.js",
     "./sourceeditor/codemirror/addon/edit/closebrackets.js",
@@ -31,9 +34,108 @@ module.exports = [{
     "./sourceeditor/codemirror/addon/fold/foldgutter.js",
     "./sourceeditor/codemirror/lib/codemirror.js",
   ],
   output: {
     filename: "./sourceeditor/codemirror/codemirror.bundle.js",
     libraryTarget: "var",
     library: "CodeMirror",
   },
+}, {
+  bail: true,
+
+  entry: './inspector/inspector.js',
+  output: {
+    filename: './inspector/inspector.bundle.js',
+    library: 'Inspector',
+  },
+  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: [__dirname + '/webpack/rewrite-event-emitter'],
+      },
+    ]
+  },
+  resolveLoader: {
+    root: [
+      path.resolve('./node_modules'),
+      path.resolve("./webpack"),
+    ]
+  },
+  resolve: {
+    alias: {
+      "devtools-shared/locale": path.join(__dirname, "../shared/locales/en-US"),
+      "devtools/locale": path.join(__dirname, "./locales/en-US"),
+      "global/locale": path.join(__dirname, "../../toolkit/locales/en-US/chrome/global"),
+      "devtools/shared/platform": path.join(__dirname, "../shared/platform/content"),
+      devtools: path.join(__dirname, "../"),
+      Services: path.join(__dirname, "./shared/shim/Services.js"),
+      gcli: path.join(__dirname, "../shared/gcli/source/lib/gcli"),
+      acorn: path.join(__dirname, "../shared/acorn"),
+      "acorn/util/walk": path.join(__dirname, "../shared/acorn/walk"),
+      sdk: path.join(__dirname, "../../addon-sdk/source/lib/sdk"),
+      method: path.join(__dirname, "../../addon-sdk/source/lib/method"),
+      "modules/libpref/init/all": path.join(__dirname,
+                                            "../../modules/libpref/init/all.js"),
+    },
+  },
+
+  plugins: [
+    new webpack.DefinePlugin({
+      "isWorker": JSON.stringify(false),
+      "reportError": "console.error",
+      "AppConstants": "{ DEBUG: true, DEBUG_JS_MODULES: true }",
+      "loader": "{ lazyRequireGetter: () => {} }",
+      "dump": "console.log",
+    }),
+  ],
+
+  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": "{}",
+    },
+  ],
 }];
new file mode 100644
--- /dev/null
+++ b/devtools/client/webpack/prefs-loader.js
@@ -0,0 +1,53 @@
+// 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_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/webpack/rewrite-event-emitter.js
@@ -0,0 +1,22 @@
+// 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");
+};
--- 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 = () => {
--- a/devtools/shared/l10n.js
+++ b/devtools/shared/l10n.js
@@ -3,27 +3,54 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const parsePropertiesFile = require("devtools/shared/node-properties/node-properties");
 const { sprintf } = require("devtools/shared/sprintfjs/sprintf");
 
 const propertiesMap = {};
 
+// Some shenanigans are needed for LocalizationHelper's dynamic
+// require to work with Webpack.  Here we a create different context
+// require for each possible locale directory.  Then later we use
+// these functions, rather than plain |require|, to load the resource.
+const reqShared = require.context("raw!devtools-shared/locale/",
+                                  true, /^.*\.properties$/);
+const reqClient = require.context("raw!devtools/locale/",
+                                  true, /^.*\.properties$/);
+const reqGlobal = require.context("raw!global/locale/",
+                                  true, /^.*\.properties$/);
+
 /**
  * Memoized getter for properties files that ensures a given url is only required and
  * parsed once.
  *
  * @param {String} url
  *        The URL of the properties file to parse.
  * @return {Object} parsed properties mapped in an object.
  */
 function getProperties(url) {
   if (!propertiesMap[url]) {
-    propertiesMap[url] = parsePropertiesFile(require(`raw!${url}`));
+    // More shenanigans.  Here we take an input like
+    // "devtools-shared/locale/debugger.properties" and decide which
+    // context require function to use.  Despite the string processing
+    // here, in the end a string identical to |url| ends up being
+    // passed to "require".
+    let index = url.lastIndexOf("/");
+    // Turn "mumble/locale/resource.properties" => "./resource.properties".
+    let baseName = "." + url.substr(index);
+    let reqFn;
+    if (/^global/.test(url)) {
+      reqFn = reqGlobal;
+    } else if (/^devtools-shared/.test(url)) {
+      reqFn = reqShared;
+    } else {
+      reqFn = reqClient;
+    }
+    propertiesMap[url] = parsePropertiesFile(reqFn(baseName));
   }
 
   return propertiesMap[url];
 }
 
 /**
  * Localization convenience methods.
  *