fixjs/define.js

View on GitHub
build/old/define.js

Summary

Maintainability
F
2 wks
Test Coverage
/**
 * DefineJS v0.2.31
 * Copyright (c) 2014 Mehran Hatami and define.js contributors.
 * Available via the MIT license.
 * license found at http://github.com/fixjs/define.js/raw/master/LICENSE
 */
(function (global, undefined) {
  'use strict';

  var doc = global.document,
    objToString = Object.prototype.toString,
    types = {},
    noop = function () {},
    isArray = Array.isArray || isType('Array');

  function isType(type) {
    if (type) {
      return (types[type] || (types[type] = function (arg) {
        return objToString.call(arg) === '[object ' + type + ']';
      }));
    } else {
      return noop;
    }
  }

  function isObject(obj) {
    return obj !== null && typeof obj === 'object';
  }

  function isPromiseAlike(obj) {
    return obj && isType('Function')(obj.then);
  }

  function defineModuleDefinition() {
    var
      isOldOpera = typeof global.opera !== 'undefined' && global.opera.toString() === '[object Opera]',
      currentScript = document.currentScript,
      emptyArray = [],
      options = {
        paths: null
      },
      baseUrl = '',
      baseGlobal = '',
      waitingList = {},
      urlCache = {},
      definedModules = {},
      modules = {},
      installed = {},
      //So far we we only capture the failure, we need to define a scenario for failed items
      failedList = {},
      files = {},
      cleanUrlRgx = /[\?|#]([^]*)$/,
      fileNameRgx = /\/([^/]*)$/,
      cleanExtRgx = /.*?(?=\.|$)/,
      filePathRgx = /^(.*[\\\/])/,
      readyStateLoadedRgx = /^(complete|loaded)$/,

      baseElement,
      head;

    function getFileName(url) {
      var fileName = files[url],
        matchResult;
      if (typeof fileName === 'string') {
        return fileName;
      }
      url = url.replace(cleanUrlRgx, '');
      matchResult = url.match(fileNameRgx);
      if (matchResult) {
        fileName = matchResult[1];
      } else {
        fileName = url;
      }
      fileName = fileName.match(cleanExtRgx)[0];
      files[url] = fileName;
      return fileName;
    }

    function getUrl(modulePath) {
      var moduleName = getFileName(modulePath),
        url,
        urlArgs,
        path,
        pathUrl;

      if (typeof urlCache[moduleName] === 'string') {
        return urlCache[moduleName];
      }

      urlArgs = (typeof options.urlArgs === 'string') ?
        ('?' + options.urlArgs) :
        (typeof options.urlArgs === 'function') ? ('?' + options.urlArgs()) : '';

      if (options.baseUrl) {
        url = options.baseUrl;
      } else {
        url = baseUrl;
      }

      if (isObject(options.paths)) {
        for (path in options.paths) {
          if (options.paths.hasOwnProperty(path)) {
            pathUrl = options.paths[path];

            if (typeof pathUrl === 'string' &&
              modulePath.indexOf(path + '/') === 0) {
              modulePath = modulePath.replace(path, pathUrl);
              break;
            }

          }
        }
      }

      if (url.charAt(url.length - 1) !== '/' && modulePath.charAt(0) !== '/') {
        url += '/';
      }

      url += modulePath + '.js' + urlArgs;

      urlCache[moduleName] = url;

      return url;
    }

    //phantomjs does not provide the "currentScript" property in global document object
    if (currentScript !== undefined) {
      baseUrl = currentScript.getAttribute('base') || currentScript.src.match(filePathRgx)[1];
      baseGlobal = currentScript.getAttribute('global');
    }

    //script injection when using BASE tag is now supported
    head = doc.head || doc.getElementsByTagName('head')[0];
    baseElement = doc.getElementsByTagName('base')[0];
    if (baseElement) {
      head = baseElement.parentNode;
    }

    function createScript() {
      var el;
      //in case DefineJS were used along with something like svg in XML based use-cases,
      //then "xhtml" should be set to "true" like config({ xhtml: true });
      if (options.xhtml) {
        el = doc.createElementNS('http://www.w3.org/1999/xhtml', 'script');
      } else {
        el = doc.createElement('script');
      }
      el.async = true;
      el.type = options.scriptType || 'text/javascript';
      el.charset = 'utf-8';
      return el;
    }

    function loadFN(callback, url) {
      return function fn(e) {
        var el = e.currentTarget || e.srcElement;
        if (e.type === 'load' || readyStateLoadedRgx.test(el.readyState)) {
          //dependency is loaded successfully
          if (typeof callback === 'function') {
            callback('success');
          }
        }
        if (el.detachEvent && !isOldOpera) {
          el.detachEvent('onreadystatechange', fn);
        } else {
          el.removeEventListener('load', fn, false);
        }
      };
    }

    function errorFN(callback) {
      return function fn(e) {
        var el = e.currentTarget || e.srcElement;
        if (e.type === 'load' || readyStateLoadedRgx.test(el.readyState)) {
          if (typeof callback === 'function') {
            callback('error');
          }
        }
        if (typeof el.removeEventListener === 'function') {
          el.removeEventListener('error', fn, false);
        }
      };
    }

    function getScript(url, callback) {
      var el = createScript(),
        scriptUrl = getUrl(url);

      if (el.attachEvent && !isOldOpera) {
        el.attachEvent('onreadystatechange', loadFN(callback, scriptUrl));
      } else {
        el.addEventListener('load', loadFN(callback, scriptUrl), false);
        el.addEventListener('error', errorFN(callback), false);
      }

      if (baseElement) {
        head.insertBefore(el, baseElement);
      } else {
        head.appendChild(el);
      }
      el.src = scriptUrl;
    }

    function executeFN(fn, args) {
      var fnData;
      if (!isArray(args)) {
        args = emptyArray;
      }

      try {
        fnData = fn.apply(undefined, args);
      } catch (ignore) {}

      return fnData;
    }

    function installModule(moduleName, status) {
      var callbacks, fn,
        i, len;

      if (status === 'success') {
        if (!installed[moduleName]) {
          installed[moduleName] = true;
        }
      } else {
        failedList[moduleName] = true;
      }

      callbacks = waitingList[moduleName];

      if (isArray(callbacks)) {
        i = 0;
        len = callbacks.length;

        for (; i < len; i += 1) {
          fn = callbacks[i];
          try {
            fn(status);
          } catch (ignored) {}
        }
        waitingList[moduleName] = [];
      }
    }

    function loadModule(modulePath, callback) {
      var isFirstLoadDemand = false,
        moduleName = getFileName(modulePath),
        fileName,
        modulesList;

      if (installed[moduleName]) {
        callback(modules[moduleName]);
      } else {
        if (!isArray(waitingList[moduleName])) {
          waitingList[moduleName] = [];
          isFirstLoadDemand = true;
        }

        waitingList[moduleName].push(callback);

        if (isFirstLoadDemand) {
          //This code blog solves #10 issue but it still needs some review
          if (isObject(options.dependencyMap)) {
            for (fileName in options.dependencyMap) {
              if (options.dependencyMap.hasOwnProperty(fileName)) {
                modulesList = options.dependencyMap[fileName];
                if (modulesList.indexOf(modulePath) > -1) {
                  modulePath = fileName;
                  break;
                }
              }
            }
          }
          getScript(modulePath, function (status) {
            if (definedModules[moduleName] === true) {
              //Do not need to do anything so far
            } else {
              //This code block allows using this library for regular javascript files
              //with no "define" or "require"
              installModule(moduleName, status);
            }
          });
        }
      }
    }

    function loadModules(array, callback) {
      var i = 0,
        len = array.length,
        loaded = [];

      function pCallback(status) {
        loaded.push(status);
        if (loaded.length === len && typeof callback === 'function') {
          callback(loaded);
        }
      }

      for (; i < len; i += 1) {
        loadModule(array[i], pCallback);
      }
    }

    function setUpModule(moduleName, moduleDefinition, args) {
      var moduleData = executeFN(moduleDefinition, args),
        setUp,
        //we could have checked it out with (moduleData instanceof Promise)
        //But this way we are actually being nice to nonnative promise libraries
        isPromise = isPromiseAlike(moduleData);

      setUp = (function (moduleData, moduleName, isPromise) {
        return function setUp(value) {
          if (isPromise) {
            modules[moduleName] = value;
          } else {
            modules[moduleName] = moduleData;
          }

          installModule(moduleName, 'success');

        };
      }(moduleData, moduleName, isPromise));

      if (isPromise) {
        moduleData.then(setUp);
      } else {
        setUp();
        // setTimeout(setUp, 0);
      }
    }

    function fxdefine(moduleName, array, moduleDefinition) {
      //define(moduleDefinition)
      if (typeof moduleName === 'function') {
        moduleDefinition = moduleName;
        moduleName = undefined;
        array = emptyArray;
      }
      //define(array, moduleDefinition)
      else if (isArray(moduleName)) {
        moduleDefinition = array;
        array = moduleName;
        moduleName = undefined;
      }
      /*
       * Note: (Not a good practice)
       * You can explicitly name modules yourself, but it makes the modules less portable
       * if you move the file to another directory you will need to change the name.
       */
      else if (typeof moduleName === 'string') {
        //define(moduleName, moduleDefinition)
        if (typeof array === 'function') {
          moduleDefinition = array;
          array = emptyArray;
        }
      }

      if (typeof moduleDefinition !== 'function') {
        console.error('Invalid input parameter to define a module');
        return;
      }

      if (moduleName === undefined) {
        moduleName = getFileName(document.currentScript.src);
      }

      definedModules[moduleName] = true;

      if (isArray(array) && array.length) {
        loadModules(array, function () {
          var args = [],
            i = 0,
            len = array.length;
          for (; i < len; i += 1) {
            args.push(modules[getFileName(array[i])]);
          }
          setUpModule(moduleName, moduleDefinition, args);
        });
      } else {
        setUpModule(moduleName, moduleDefinition);
      }
    }

    function fxrequire(array, fn) {
      if (typeof fn !== 'function') {
        console.error('Invalid input parameter to require a module');
        return;
      }

      if (isArray(array) && array.length) {
        loadModules(array, function () {
          var args = [],
            i = 0,
            len = array.length;
          for (; i < len; i += 1) {
            args.push(modules[getFileName(array[i])]);
          }
          executeFN(fn, args);
        });
      } else {
        executeFN(fn);
      }
    }

    function promiseUse(array) {
      return new Promise(function (fulfill) {
        //this function accepts two params: fulfill, reject

        fxrequire(array, fulfill);

        //FIXME: think of a useful pattern for promised module rejection

        console.log('Great to see that you are using this function on DefineJS (https://www.npmjs.org/package/definejs)');
        console.log('This function provides a way of requiring your defined modules using a Promise based coding style.');
        console.log('Actually as you might have noticed we are actively extending this module loader, and that\'s why we need your feedback on this.');
        console.log('You could join the chat room on Gitter (https://gitter.im/fixjs/define.js) and provide us with your great ideas and feedbacks.');

        /*
         * OPEN DISCUSSION:
         * module rejection could offer a cool pattern when developing new modules
         * but we first need to have the community's feedback on this
         */
      });
    }

    function fxconfig(cnfOptions) {
      if (!isObject(cnfOptions)) {
        return;
      }

      var key;

      for (key in cnfOptions) {
        if (cnfOptions.hasOwnProperty(key)) {
          options[key] = cnfOptions[key];
        }
      }
    }

    fxdefine.amd = {};

    function fixDefine(g) {
      g.require = fxrequire;
      g.define = fxdefine;
      g.config = fxconfig;
      g.options = options;

      //Nonstandards
      g.use = promiseUse;

    }

    if (baseGlobal && isObject(global[baseGlobal])) {
      fixDefine(global[baseGlobal]);
      fixDefine._exposed = true;
    }

    return fixDefine;
  }

  if (typeof exports === 'object') {
    module.exports = defineModuleDefinition();
  } else if (typeof define === 'function' && define.amd) {
    define([], defineModuleDefinition);
  } else {
    var moduleFN = defineModuleDefinition();
    if (!moduleFN._exposed) {
      global.fixDefine = moduleFN;
    }
  }
}(this));