fixjs/define.js

View on GitHub
examples/regular-amd-style/define.js

Summary

Maintainability
F
1 mo
Test Coverage
/**
 * DefineJS v0.2.9 2015-04-16T23:09Z
 * 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 (g, undefined) {
  
  var global = g();
  var fix = {
    options: {
      paths: null
    },
    modules: {},
    installed: {},
    waitingList: {},
    failedList: {},
    definedModules: {}
  };
  var urlCache = {};

  function isObject(value) {
    // Avoid a V8 JIT bug in Chrome 19-20.
    // See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
    var type = typeof value;
    return type === 'function' || (value && type === 'object') || false;
  }

  function toObject(value) {
    return isObject(value) ? value : Object(value);
  }

  function forOwn(object, iteratee) {
    var iterable = toObject(object),
      props = Object.keys(iterable),
      length = props.length,
      index = -1,
      key;
    while (++index < length) {
      key = props[index];
      if (iteratee(iterable[key], key, iterable) === false) {
        break;
      }
    }
    return object;
  }

  //This function solves #10 issue
  function loadMap(modulePath) {
    var depMap = fix.options.dependencyMap;
    forOwn(depMap, function (modulesList, fileName) {
      if (modulesList.indexOf(modulePath) > -1) {
        modulePath = fileName;
        return false;
      }
    });
    return modulePath;
  }
  var tags = {
    func: '[object Function]',
    opera: '[object Opera]',
    array: '[object Array]',
    string: '[object String]'
  };
  var objToString = Object.prototype.toString;

  var isFunction = function (value) {
    // Avoid a Chakra JIT bug in compatibility modes of IE 11.
    // See https://github.com/jashkenas/underscore/issues/1621 for more details.
    return typeof value === 'function' || false;
  };
  // Fallback for environments that return incorrect `typeof` operator results.
  if (isFunction(/x/) || (Uint8Array && !isFunction(Uint8Array))) {
    isFunction = function (value) {
      return objToString.call(value) === tags.func;
    };
  }
  var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;

  function isLength(value) {
    return typeof value === 'number' && value > -1 && value % 1 === 0 && value <= MAX_SAFE_INTEGER;
  }

  function each(array, iteratee) {
    var length = array ? array.length : 0;
    if (!isLength(length)) {
      return forOwn(array, iteratee);
    }
    var index = -1,
      iterable = toObject(array);

    while (++index < length) {
      if (iteratee(iterable[index], index, iterable) === false) {
        break;
      }
    }
    return array;
  }

  function extract(base, path) {
    if (typeof path !== 'string') {
      return;
    }
    var parts = path.split('.');
    each(parts, function (part) {
      return isObject(base = base[part]);
    });
    return base;
  }

  function getShimObject(moduleName) {
    var shim = fix.options.shim && fix.options.shim[moduleName];
    if (!shim) {
      return false;
    }
    if (!isObject(shim.object)) {
      if (isFunction(shim.init)) {
        shim.object = shim.init.apply(global, arguments);
      }
      if (!isObject(shim.object)) {
        shim.object = extract(global, shim.exports);
      }
    }
    return shim.object;
  }
  var doc = global.document;

  function baseInfo() {
    var currentScript = doc.currentScript,
      filePathRgx = /^(.*[\\\/])/;
    //script injection when using BASE tag is now supported
    baseInfo.head = doc.head || doc.getElementsByTagName('head')[0];
    baseInfo.baseElement = doc.getElementsByTagName('base')[0];

    if (baseInfo.baseElement) {
      baseInfo.head = baseInfo.baseElement.parentNode;
    }

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

  function will(promise) {
    return {
      done: function (onFulfilled, onRejected) {
        var self = arguments.length ? promise.then.apply(promise, arguments) : promise;
        self.then(null, function (err) {
          setTimeout(function () {
            throw err;
          }, 0);
        });
      }
    };
  }

  function isObjectLike(value) {
    return (value && typeof value === 'object') || false;
  }

  function isPromiseAlike(obj) {
    return isObjectLike(obj) && isFunction(obj.then) || false;
  }

  function deferImpl(Promise) {
    function resolve(value, baseFulfill, baseReject, save) {
      var promise;
      if (isPromiseAlike(value)) {
        will(value).done(baseFulfill, baseReject);
        promise = value;
      } else {
        promise = new Promise(function (fulfill) {
          fulfill(value);
          baseFulfill(value);
        });
      }
      save(promise);
    }

    function reject(reason, baseReject, save) {
      save(new Promise(function (fulfill, reject) {
        reject(reason);
        baseReject(reason);
      }));
    }

    function defer() {
      var resolvedPromise,
        baseFulfill,
        baseReject,
        dfd = {},
        promise = new Promise(function (fulfill, reject) {
          baseFulfill = fulfill;
          baseReject = reject;
        });

      function save(newPromise) {
        resolvedPromise = newPromise;
        promise.source = newPromise;
      }
      dfd.promise = promise;
      dfd.resolve = function (value) {
        if (resolvedPromise) {
          return;
        }
        resolve(value, baseFulfill, baseReject, save);
      };
      dfd.reject = function (reason) {
        if (resolvedPromise) {
          return;
        }
        reject(reason, baseReject, save);
      };
      return dfd;
    }
    return defer;
  }

  var defer = deferImpl(global.Promise);

  function makeUrl(modulePath) {
    var url,
      urlArgs = (typeof fix.options.urlArgs === 'string') ?
      ('?' + fix.options.urlArgs) :
      (typeof fix.options.urlArgs === 'function') ? ('?' + fix.options.urlArgs()) : '';

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

    forOwn(fix.options.paths, function (pathUrl, path) {
      if (typeof pathUrl === 'string' && modulePath.indexOf(path + '/') === 0) {
        modulePath = modulePath.replace(path, pathUrl);
        return false;
      }
    });

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

  function getUrl(url) {
    return urlCache[url] || (urlCache[url] = makeUrl(url));
  }

  var isOldOpera = isObjectLike(global.opera) && global.opera.toString() === tags.opera;

  var readyStateLoadedRgx = /^(complete|loaded)$/;

  function loadFN(callback) {
    return function fn(e) {
      var el = e.currentTarget || e.srcElement;
      if (e.type === 'load' || readyStateLoadedRgx.test(el.readyState)) {
        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)) {
        callback('error');
      }
      if (typeof el.removeEventListener === 'function') {
        el.removeEventListener('error', fn, false);
      }
    };
  }

  function createScript(url) {
    var el,
      dfd = defer();
    //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 (fix.options.xhtml) {
      el = doc.createElementNS('http://www.w3.org/1999/xhtml', 'script');
    } else {
      el = doc.createElement('script');
    }
    el.async = true;
    el.type = fix.options.scriptType || 'text/javascript';
    el.charset = 'utf-8';
    
    url = getUrl(url);

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

    if (baseInfo.baseElement) {
      baseInfo.head.insertBefore(el, baseInfo.baseElement);
    } else {
      baseInfo.head.appendChild(el);
    }
    el.src = url;
    return dfd.promise;
  }

  var isArray = Array.isArray || function (value) {
    return (isObjectLike(value) && isLength(value.length) && objToString.call(value) === tags.array) || false;
  };

  function install(moduleName, status) {
    var callbacks;
    if (status === 'success') {
      if (fix.installed[moduleName]) {
        console.warn('[DefineJS][install][' + moduleName + ']: this module is already installed!');
        return;
      }
      fix.installed[moduleName] = true;
    } else {
      fix.failedList[moduleName] = true;
    }
    callbacks = fix.waitingList[moduleName];
    if (isArray(callbacks)) {
      each(callbacks, function (dfd) {
        try {
          dfd.resolve(fix.modules[moduleName]);
        } catch (err) {
          dfd.reject(err);
        }
      });
      callbacks.length = 0;
    }
  }

  function loadDemand(name, url, dfd) {
    var shimObject;
    //This solves #10 issue
    url = loadMap(url);

    //for those which are already loaded in the page
    shimObject = getShimObject(name);
    if (shimObject) {
      fix.modules[name] = shimObject;
      fix.installed[name] = true;
      dfd.resolve(shimObject);
    } else {
      if (urlCache[url] || fix.definedModules[name] || loader.loadShim(name, url, dfd)) {
        return;
      } else {
        createScript(url).then(function (status) {
          if (!fix.definedModules[name]) {
            install(name, status);
            dfd.resolve(fix.modules[name]);
          }
        });
      }
    }
  }

  var cleanUrlRgx = /[\?|#]([^]*)$/,
    fileNameRgx = /\/([^/]*)$/,
    cleanExtRgx = /.*?(?=\.|$)/;
  function matchUrl(url) {
    var fileName,
      matchResult;
    url = url.replace(cleanUrlRgx, '');
    fileName = (matchResult = url.match(fileNameRgx)) ? matchResult[1] : url;
    fileName = fileName.match(cleanExtRgx)[0];
    return fileName;
  }

  var files = {};
  function getFileName(url) {
    return files[url] || (files[url] = matchUrl(url));
  }

  function loadPromise(modulePath) {
    var dfd = defer(),
      isFirstLoadDemand = false,
      moduleName = getFileName(modulePath);

    if (fix.installed[moduleName]) {
      if (fix.modules[moduleName] !== undefined) {
        dfd.resolve(fix.modules[moduleName]);
      } else {
        dfd.reject(new Error(moduleName + ': has no returned module definition.'));
      }
    } else {
      if (!isArray(fix.waitingList[moduleName])) {
        fix.waitingList[moduleName] = [];
        isFirstLoadDemand = true;
      }
      fix.waitingList[moduleName].push(dfd);
      if (isFirstLoadDemand) {
        loadDemand(moduleName, modulePath, dfd);
      }
    }
    return dfd.promise;
  }

  function getShim(moduleName, modulePath, dfd) {
    return createScript(modulePath)
      .then(function (status) {
        fix.modules[moduleName] = getShimObject(moduleName);
        fix.waitingList[moduleName].push(dfd);
        install(moduleName, status);
      });
  }

  var globalPromise = new Promise(function (fulfill) {
      fulfill(global);
    }),
    promiseStorage = {
      global: globalPromise,
      g: globalPromise
    },
    loader;

  loader = {
    load: function load(modulePath) {
      if (promiseStorage[modulePath] === undefined) {
        promiseStorage[modulePath] = loadPromise(modulePath);
      }
      return promiseStorage[modulePath];
    },
    loadAll: function loadAll(list) {
      return Promise.all(list.map(loader.load));
    },
    loadShim: function (moduleName, modulePath, dfd) {
      var shim = fix.options.shim && fix.options.shim[moduleName];
      if (isObject(shim)) {
        if (shim.deps && shim.deps.length) {
          loader
            .loadAll(shim.deps)
            .then(function () {
              getShim(moduleName, modulePath, dfd);
            });
        } else {
          getShim(moduleName, modulePath, dfd);
        }
        return true;
      }
      return false;
    }
  };
  var emptyArray = [];

  function isGenerator(fn) {
    if (typeof fn === 'function') {
      //Function.prototype.isGenerator is supported in Firefox 5.0 or later
      //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/isGenerator
      if (typeof fn.isGenerator === 'function') {
        return fn.isGenerator();
      }
      return /^function\s*\*/.test(fn.toString());
    }
    return false;
  }

  function execute(fn, args) {
    var fnData,
      dfd = defer();
    if (!isArray(args)) {
      args = emptyArray;
    }
    if (isGenerator(fn)) {
      fn.invokeWith(args).then(dfd.resolve, dfd.reject);
    } else if (isFunction(fn)) {
      try {
        fnData = fn.apply(undefined, args);
        dfd.resolve(fnData);
      } catch (err) {
        dfd.reject(err);
      }
    } else {
      dfd.resolve(args);
    }
    return dfd.promise;
  }

  function isString(value) {
    return typeof value === 'string' || (isObjectLike(value) && objToString.call(value) === tags.string);
  }

  function setup(name, definition, deps) {
    var dfd = defer();
    if (!isString(name) || !isFunction(definition)) {
      dfd.reject(new TypeError('Expected a string and a function'));
      return;
    } else {
      return execute(definition, deps)
        .then(function (value) {
          fix.modules[name] = value;
          install(name, 'success');
          dfd.resolve(fix.modules[name]);
        });
    }
    return dfd.promise;
  }

  function fixDefine(name, list, definition) {
    fix.definedModules[name] = true;
    return loader
      .loadAll(list)
      .then(function (deps) {
        return setup(name, definition, deps);
      });
  }

  function setDepsHash(list, deps) {
    if (isArray(deps) && deps.length) {
      each(list, function (dep, index) {
        deps[dep] = deps[index];
      });
    }
  }

  function fixRequire(list, fn) {
    return loader
      .loadAll(list)
      .then(function (deps) {
        setDepsHash(list, deps);
        return execute(fn, deps);
      });
  }

  function core(_, amd) {
    if (!isObject(_)) {
      _ = global;
    }
    _.define = function (moduleName, array, moduleDefinition) {
      return core.define(amd, moduleName, array, moduleDefinition);
    };
    _.require = function (array, fn) {
      return amd.require(array, fn);
    };
    _.use = function (array) {
      return _.require(array);
    };
    _.config = function (cnfOptions) {
      if (!isObject(cnfOptions)) {
        console.error('Invalid parameter to set up the config');
        return;
      }
      forOwn(cnfOptions, function (option, key) {
        fix.options[key] = option;
      });
    };
    _.require.config = _.config;
    _.define.amd = {};
    _.define.fix = fix;
    _.define.defer = defer;
    return _;
  }

  core.define = function (amd, moduleName, array, moduleDefinition) {
    if (typeof moduleName === 'function') {
      //define(moduleDefinition)
      moduleDefinition = moduleName;
      moduleName = undefined;
      array = emptyArray;
    } else if (isArray(moduleName)) {
      //define(array, moduleDefinition)
      moduleDefinition = array;
      array = moduleName;
      moduleName = undefined;
    } 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 false;
    }
    if (moduleName === undefined) {
      moduleName = getFileName(document.currentScript.src);
    }
    return amd.define(moduleName, array, moduleDefinition);
  };

  function amd() {
    if (amd.definejs) {
      return amd.definejs;
    }
    var definejs = function (_) {
      _ = core(_, amd);
      amd.define = fixDefine;
      amd.require = fixRequire;
    };
    amd.definejs = definejs;
    return definejs;
  }

  if (typeof exports === 'object') {
    module.exports = amd();
  } else if (typeof define === 'function' && define.amd) {
    define([], amd);
  } else {
    var definejs = amd();
    if (baseInfo.baseGlobal && isObject(global[baseInfo.baseGlobal])) {
      definejs(global[baseInfo.baseGlobal]);
    } else {
      global.definejs = definejs;
    }
  }
}(function g() {
  return this;
}));