interlockjs/interlock

View on GitHub
src/compile/construct/templates/runtime.jst

Summary

Maintainability
Test Coverage
/**
 * Behavior identical to _.extend.  Copies key/value pairs from source
 * objects to destination object.
 *
 * @param  {Object} dest     Destination for key/value pairs.
 * @param  {Object} sources  One or more sources for key/value pairs.
 *
 * @return {Object}          Destination object.
 */
function copyProps (dest/*, sources... */) {
  var len = arguments.length;
  if (arguments.length < 2 || dest === null) { return dest; }
  Array.prototype.splice.call(arguments, 1).forEach(function (src) {
    Object.keys(src).forEach(function (key) { dest[key] = src[key]; });
  });
  return dest;
}

/**
 * This object is exposed to the browser environment and is accessible
 * to chunks that are loaded asynchronously.  It represents the core
 * of the Interlock runtime.
 */
var r = window[GLOBAL_NAME] = window[GLOBAL_NAME] || { // eslint-disable-line no-undef
  modules: {},
  urls: {},
  providers: [
    /**
     * Default module provider.  Builds a <script> element to download and
     * execute the module-set that contains the requested module. Once a module
     * has completed loading, the <script> tag will be removed from the DOM.
     **/
    function (moduleHash, next) {
      var head = document.getElementsByTagName("head")[0];
      var script = document.createElement("script");
      script.type = "text/javascript";
      script.charset = "utf-8";
      script.async = true;
      script.src = r.getUrl(moduleHash);

      script.onload = script.onreadystatechange = function () {
        script.onload = script.onreadystatechange = null;
        head.removeChild(script);
      };
      script.onerror = next;

      head.appendChild(script);
    }
  ],

  /**
   * Loads a module set into the Interlock runtime.
   *
   * If the loaded module requires other modules that are not yet loaded, requests
   * for those modules will be made.  If all dependencies are satisfied, the module
   * will be resolved and made available for requiring.
   *
   * @param  {Object}   moduleSet
   * @param  {Array}    moduleSet.deps  Array of module hashes that represent the dependencies
   *                                    for the module being defined.
   * @param  {Function} moduleSet.fn    The module function itself, taking three args,
   *                                    (require, module, exports).
   */
  load: function (moduleSet) {
    Object.keys(moduleSet).forEach(function (moduleHash) {
      var loadedModule = moduleSet[moduleHash];

      var unloadedDeps = loadedModule.deps.filter(function (dependencyHash) {
        var dependency = r.modules[dependencyHash];

        if (!dependency || dependency.blocking) {
          if (dependencyHash in moduleSet) {
            // Avoid submitting request for module that will soon be provided.
            r.modules[dependencyHash] = copyProps(r.modules[dependencyHash] || {}, {
              requested: true
            });
          }
          r.request(dependencyHash, moduleHash);
          return true;
        }

        return false;
      });

      var module = r.modules[moduleHash] = copyProps(r.modules[moduleHash] || {}, loadedModule);

      if (unloadedDeps.length) {
        module.unloadedDeps = unloadedDeps;
      } else {
        r.resolve(moduleHash);
      }
    });
  },

  /**
   * Initiates a request for the specified module.  Providers specified as
   * part of `r.providers` will be run in sequence in reverse order.  When
   * one provider fails, it should call the provided `next` method, ultimately
   * resulting in a thrown error if the module cannot be loaded.
   *
   * If a blocked module is also specified, a record of that module will be kept
   * in association with the requested module.  This way, the blocked module can
   * be resolved when the dependency becomes available.  If a particular module
   * has already been requested, the blocked module will be tracked, but an
   * additional request for the module will not be initiated.
   *
   * @param  {String} moduleHash         Hash of the module that is needed.
   * @param  {String} blockedModuleHash  Hash of the module that needs it.
   */
  request: function (moduleHash, blockedModuleHash) {
    var module = r.modules[moduleHash] = r.modules[moduleHash] || {};

    if (blockedModuleHash) {
      module.blocking = (module.blocking || []).concat(blockedModuleHash);
    }

    if (!module.requested) {
      module.requested = true;
      r.requestSequence(moduleHash, [].concat(r.providers).reverse());
    }
  },

  /**
   * Recursive function to invoke module providers.
   *
   * When a request comes in for a particular module that is as yet
   * unloaded, providers are invoked in reverse order with two parameters:
   * the `moduleHash` and the `next` provider.  It is the responsibility of
   * each provider to invoke the next one in cases where the provider fails
   * or is otherwise unable to fulfill the request.
   *
   * @param  {String} moduleHash  Hash of the requested module.
   * @param  {Array}  providers   Remaining providers to try.
   */
  requestSequence: function (moduleHash, providers) {
    var head = providers[0];
    if (!head) {
      throw new Error("Unable to load module with hash " + moduleHash + ".");
    }
    head(moduleHash, function () {
      r.requestSequence(moduleHash, providers.slice(1));
    });
  },

  /**
   * Returns a URL for the module set that contains the requested module.
   *
   * @param  {String} moduleHash  Hash of the module that is needed.
   *
   * @return {String}             URL of the module set.
   */
  getUrl: function (moduleHash) {
    var url = r.urls[moduleHash];
    return url;
  },

  /**
   * Registers a mapping of module hashes to URLs.  Duplicate values may occur
   * for modules that are contained by the same module set.  However, this
   * duplication is not an issue if JavaScript is sent GZip'd.
   *
   * @param  {Object} urls  Keys are module hashes; values are URLs.
   */
  registerUrls: function (urls) {
    copyProps(r.urls, urls);
  },

  /**
   * Called when the fn for a module is available and ready to be executed.  Any
   * modules which specified the given moduleHash as a dependency will be
   * recursively resolved if all dependencies and their dependecies are now met.
   * Similarly, any callbacks waiting for the module to be resolved will be
   * invoked.
   *
   * @param  {String} moduleHash  Hash of the module that is needed.
   */
  resolve: function (moduleHash) {
    var module = r.modules[moduleHash];

    (module.blocking || []).forEach(function (blockedModuleHash) {
      var blockedModule = r.modules[blockedModuleHash];
      blockedModule.unloadedDeps = blockedModule.unloadedDeps.filter(function (dependencyHash) {
        return dependencyHash !== moduleHash;
      });
      if (!blockedModule.unloadedDeps.length) {
        r.resolve(blockedModuleHash);
      }
    });
    (module.cbs || []).forEach(function (cb) { cb(); });

    // This indicates that the module and all of its dependencies have successfully loaded.
    module.blocking = null;
    if (module.entry) { r.require(moduleHash); }
  },

  /**
   * Synchronous function that will return the exports of the specified module.
   * Exports will be cached and returned for any subsequent requests for the
   * same module.
   *
   * @param  {String}  moduleHash Hash of the module that is needed.
   *
   * @return {Exports}            Whatever the module set as exports, either the
   *                              default exports object, a mutated object, or something
   *                              else if `module.exports` is set directly.
   */
  require: function (moduleHash) {
    var module = r.modules[moduleHash];
    if ("exports" in module) { return module.exports; }

    var moduleObj = { exports: {} };
    module.fn.call(window, r.require.bind(r), moduleObj, moduleObj.exports);
    return module.exports = moduleObj.exports;
  }
};