Bug 1250002 - part1: add loader to globals exposed by require from browser-loader;r=jryans draft
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 23 Feb 2016 23:31:32 +0100
changeset 334525 bd31080b2d678ee4f003386145b3f954e3ee16ca
parent 334524 3dfcbfa56b0e1c182f74e761d70efbb707c1199d
child 334526 8fb3a46769291272cc018e9a0ee19ba2814c0470
child 334567 444aa09b859bd2ef04856dc0db4ca52bc05e7906
push id11557
push userjdescottes@mozilla.com
push dateThu, 25 Feb 2016 08:58:20 +0000
reviewersjryans
bugs1250002
milestone47.0a1
Bug 1250002 - part1: add loader to globals exposed by require from browser-loader;r=jryans MozReview-Commit-ID: Dzv5AdksdnP
devtools/client/shared/browser-loader.js
--- a/devtools/client/shared/browser-loader.js
+++ b/devtools/client/shared/browser-loader.js
@@ -13,50 +13,16 @@ Cu.import("resource://gre/modules/AppCon
 
 const BROWSER_BASED_DIRS = [
   "resource://devtools/client/jsonview",
   "resource://devtools/client/shared/vendor",
   "resource://devtools/client/shared/components",
   "resource://devtools/client/shared/redux"
 ];
 
-function clearCache() {
-  Services.obs.notifyObservers(null, "startupcache-invalidate", null);
-}
-
-function hotReloadFile(window, require, loader, componentProxies, fileURI) {
-  dump("Hot reloading: " + fileURI + "\n");
-
-  if (fileURI.match(/\.js$/)) {
-    // Test for React proxy components
-    const proxy = componentProxies.get(fileURI);
-    if (proxy) {
-      // Remove the old module and re-require the new one; the require
-      // hook in the loader will take care of the rest
-      delete loader.modules[fileURI];
-      clearCache();
-      require(fileURI);
-    }
-  } else if (fileURI.match(/\.css$/)) {
-    const links = [...window.document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "link")];
-    links.forEach(link => {
-      if (link.href.indexOf(fileURI) === 0) {
-        const parentNode = link.parentNode;
-        const newLink = window.document.createElementNS("http://www.w3.org/1999/xhtml", "link");
-        newLink.rel = "stylesheet";
-        newLink.type = "text/css";
-        newLink.href = fileURI + "?s=" + Math.random();
-
-        parentNode.insertBefore(newLink, link);
-        parentNode.removeChild(link);
-      }
-    });
-  }
-}
-
 /*
  * Create a loader to be used in a browser environment. This evaluates
  * modules in their own environment, but sets window (the normal
  * global object) as the sandbox prototype, so when a variable is not
  * defined it checks `window` before throwing an error. This makes all
  * browser APIs available to modules by default, like a normal browser
  * environment, but modules are still evaluated in their own scope.
  *
@@ -73,16 +39,33 @@ function hotReloadFile(window, require, 
  * @param Object window
  *        The window instance to evaluate modules within
  * @return Object
  *         An object with two properties:
  *         - loader: the Loader instance
  *         - require: a function to require modules with
  */
 function BrowserLoader(baseURI, window) {
+  const browserLoaderBuilder = new BrowserLoaderBuilder(baseURI, window);
+  return {
+    loader: browserLoaderBuilder.loader,
+    require: browserLoaderBuilder.require
+  };
+}
+
+/**
+ * Private class used to build the Loader instance and require method returned
+ * by BrowserLoader(baseURI, window).
+ *
+ * @param string baseURI
+ *        Base path to load modules from.
+ * @param Object window
+ *        The window instance to evaluate modules within
+ */
+function BrowserLoaderBuilder(baseURI, window) {
   const loaderOptions = devtools.require("@loader/options");
   const dynamicPaths = {};
   const componentProxies = new Map();
   const hotReloadEnabled = Services.prefs.getBoolPref("devtools.loader.hotreload");
 
   if(AppConstants.DEBUG || AppConstants.DEBUG_JS_MODULES) {
     dynamicPaths["devtools/client/shared/vendor/react"] =
       "resource://devtools/client/shared/vendor/react-dev";
@@ -121,16 +104,23 @@ function BrowserLoader(baseURI, window) 
       //     ... code ...
       //   });
       //
       // Bug 1248830 will work out a better plan here for our content module
       // loading needs, especially as we head towards devtools.html.
       define(factory) {
         factory(this.require, this.exports, this.module);
       },
+      // Allow modules to use the DevToolsLoader lazy loading helpers.
+      loader: {
+        lazyGetter: devtools.lazyGetter,
+        lazyImporter: devtools.lazyImporter,
+        lazyServiceGetter: devtools.lazyServiceGetter,
+        lazyRequireGetter: this.lazyRequireGetter.bind(this),
+      },
     }
   };
 
   if(hotReloadEnabled) {
     opts.loadModuleHook = (module, require) => {
       const { uri, exports } = module;
 
       if (exports.prototype &&
@@ -150,34 +140,86 @@ function BrowserLoader(baseURI, window) 
           instances.forEach(getForceUpdate(React));
           module.exports = proxy.get();
         }
       }
       return exports;
     }
   }
 
-
   const mainModule = loaders.Module(baseURI, joinURI(baseURI, "main.js"));
-  const mainLoader = loaders.Loader(opts);
-  const require = loaders.Require(mainLoader, mainModule);
+  this.loader = loaders.Loader(opts);
+  this.require = loaders.Require(this.loader, mainModule);
 
   if (hotReloadEnabled) {
     const watcher = devtools.require("devtools/client/shared/file-watcher");
-    function onFileChanged(_, fileURI) {
-      hotReloadFile(window, require, mainLoader, componentProxies, fileURI);
-    }
+    const onFileChanged = (_, fileURI) => {
+      this.hotReloadFile(window, componentProxies, fileURI);
+    };
     watcher.on("file-changed", onFileChanged);
 
     window.addEventListener("unload", () => {
       watcher.off("file-changed", onFileChanged);
     });
   }
+}
 
-  return {
-    loader: mainLoader,
-    require: require
-  };
-}
+BrowserLoaderBuilder.prototype = {
+  /**
+   * Define a getter property on the given object that requires the given
+   * module. This enables delaying importing modules until the module is
+   * actually used.
+   *
+   * @param Object obj
+   *    The object to define the property on.
+   * @param String property
+   *    The property name.
+   * @param String module
+   *    The module path.
+   * @param Boolean destructure
+   *    Pass true if the property name is a member of the module's exports.
+   */
+  lazyRequireGetter: function(obj, property, module, destructure) {
+    devtools.lazyGetter(obj, property, () => {
+      return destructure
+          ? this.require(module)[property]
+          : this.require(module || property);
+    });
+  },
+
+  clearCache: function() {
+    Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+  },
+
+  hotReloadFile: function(window, componentProxies, fileURI) {
+    dump("Hot reloading: " + fileURI + "\n");
+
+    if (fileURI.match(/\.js$/)) {
+      // Test for React proxy components
+      const proxy = componentProxies.get(fileURI);
+      if (proxy) {
+        // Remove the old module and re-require the new one; the require
+        // hook in the loader will take care of the rest
+        delete this.loader.modules[fileURI];
+        this.clearCache();
+        this.require(fileURI);
+      }
+    } else if (fileURI.match(/\.css$/)) {
+      const links = [...window.document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "link")];
+      links.forEach(link => {
+        if (link.href.indexOf(fileURI) === 0) {
+          const parentNode = link.parentNode;
+          const newLink = window.document.createElementNS("http://www.w3.org/1999/xhtml", "link");
+          newLink.rel = "stylesheet";
+          newLink.type = "text/css";
+          newLink.href = fileURI + "?s=" + Math.random();
+
+          parentNode.insertBefore(newLink, link);
+          parentNode.removeChild(link);
+        }
+      });
+    }
+  }
+};
 
 this.BrowserLoader = BrowserLoader;
 
 this.EXPORTED_SYMBOLS = ["BrowserLoader"];