fmd.js
Version:
another module writing system
1,302 lines (910 loc) • 30.4 kB
JavaScript
/*! fmd.js v1.1.0 | http://fmdjs.org/ | MIT */
/**
* @module fmd/boot
* @author Edgar <mail@edgar.im>
* @version v0.3
* @date 170104
* */
(function( global ){
'use strict';
if ( global.fmd ){
return;
}
var partsCache = {},
parts = [];
var require = function( id ){
return partsCache[id];
},
fmd = function( id, deps, factory ){
if ( partsCache[id] ){
return;
}
if ( !factory ){
factory = deps;
deps = [];
}
if ( 'function' === typeof factory ){
var args = [];
for ( var i = 0, l = deps.length; i < l; i++ ){
args.push( require( deps[i] ) );
}
factory = factory.apply( null, args );
}
partsCache[id] = factory || 1;
parts.push( id );
};
fmd.version = '1.1.0';
fmd.cache = {
parts: parts
};
fmd( 'global', global );
fmd( 'require', function(){
return require;
} );
fmd( 'env', function(){
return fmd;
} );
fmd( 'cache', function(){
return fmd.cache;
} );
global.fmd = fmd;
})( typeof window !== 'undefined' ? window : global );
/**
* @module fmd/lang
* @author Edgar <mail@edgar.im>
* @version v0.2
* @date 131009
* */
fmd( 'lang', function(){
'use strict';
var toString = {}.toString,
AP = Array.prototype;
var lang = {
isFunction: function( it ){
return toString.call( it ) === '[object Function]';
},
isArray: Array.isArray || function( it ){
return toString.call( it ) === '[object Array]';
},
isString: function( it ){
return typeof it === 'string';
},
forEach: AP.forEach ?
function( arr, fn, context ){
arr.forEach( fn, context );
} :
function( arr, fn, context ){
for ( var i = 0, l = arr.length; i < l; i++ ){
fn.call( context, arr[i], i, arr );
}
},
map: AP.map ? function( arr, fn, context ){
return arr.map( fn, context );
} :
function( arr, fn, context ){
var ret = [];
lang.forEach( arr, function( item, i, arr ){
ret.push( fn.call( context, item, i, arr ) );
} );
return ret;
},
inArray: AP.indexOf ?
function( arr, item ){
return arr.indexOf( item );
} :
function( arr, item ){
for ( var i = 0, l = arr.length; i < l; i++ ){
if ( arr[i] === item ){
return i;
}
}
return -1;
}
};
return lang;
} );
/**
* @module fmd/event
* @author Edgar <mail@edgar.im>
* @version v0.2
* @date 170104
* */
fmd( 'event', ['env','cache'],
function( env, cache ){
'use strict';
/**
* Thanks to:
* SeaJS, http://seajs.org/
* */
var eventsCache = cache.events = {},
slice = [].slice;
var event = {
on: function( name, callback ){
var list = eventsCache[name] || ( eventsCache[name] = [] );
list.push( callback );
},
emit: function( name ){
var args = slice.call( arguments, 1 ),
list = eventsCache[name],
fn, i = 0;
if ( list ){
while ( ( fn = list[i++] ) ){
fn.apply( null, args );
}
}
},
off: function( name, callback ){
var list = eventsCache[name];
if ( list ){
if ( callback ){
for ( var i = list.length - 1; i >= 0; i-- ){
( list[i] === callback ) && list.splice( i, 1 );
}
} else {
delete eventsCache[name];
}
}
}
};
/* exports API to fmd */
env.on = event.on;
env.emit = event.emit;
env.off = event.off;
return event;
} );
/**
* @module fmd/config
* @author Edgar <mail@edgar.im>
* @version v0.3
* @date 170117
* */
fmd( 'config', ['env','cache','lang'],
function( env, cache, lang ){
'use strict';
var configCache = cache.config = {},
configKeysCache = cache.configKeys = {},
configRulesCache = cache.configRules = {};
var ANONYMOUS_RULE_PREFIX = '_rule_';
var ruleUid = 0;
var applyRule = function( current, key, val, ruleName ){
var rule = configRulesCache[ruleName];
if ( rule ){
return rule.call( configCache, current, key, val ) === undefined;
}
return false;
};
var config = {
get: function( key ){
return configCache[key];
},
set: function( options ){
for ( var key in options ){
var current = configCache[key],
val = options[key],
ruleName = configKeysCache[key];
if ( !ruleName || !applyRule( current, key, val, ruleName ) ){
configCache[key] = val;
}
}
},
register: function( o ){
if ( lang.isFunction( o.rule ) ){
o.name || ( o.name = ANONYMOUS_RULE_PREFIX + ( ruleUid++ ) );
configRulesCache[o.name] = o.rule;
}
if ( configRulesCache[o.name] && ( o.key || o.keys ) ){
if ( lang.isArray( o.keys ) ){
lang.forEach( o.keys, function( key ){
configKeysCache[key] = o.name;
} );
} else {
configKeysCache[ o.key || o.keys ] = o.name;
}
}
return this;
}
};
/* default config rule */
config.register({
name: 'object',
rule: function( current, key, val ){
if ( current ){
for ( var i in val ){
current[i] = val[i];
}
return;
}
return false;
}
})
.register({
name: 'array',
rule: function( current, key, val ){
current ? current.push( val ) : ( this[key] = [val] );
}
});
/* exports API to fmd */
env.config = function( options ){
if ( lang.isString( options ) ){
return config.get( options );
}
config.set( options );
};
return config;
} );
/**
* @module fmd/module
* @author Edgar <mail@edgar.im>
* @version v0.5
* @date 170213
* */
fmd( 'module', ['global','env','cache','lang','event','config'],
function( global, env, cache, lang, event, config ){
'use strict';
/**
* Thanks to:
* RequireJS, http://requirejs.org/
* SeaJS, http://seajs.org/
* cujo.js, http://cujojs.com/
* HexJS, http://hexjs.edgarhoo.org/
* */
var EMPTY_ID = '',
EMPTY_DEPS = [],
ANONYMOUS_PREFIX = '_!_fmd_anonymous_',
UNDEFINED;
var anonymousUid = 0;
var modulesCache = cache.modules = {};
/**
* builtin-modules
* */
var builtinModules = {
'require': function( mod ){
mod.require || Module.makeRequire( mod );
event.emit( 'makeRequire', mod.require, mod );
return mod.require;
},
'exports': function( mod ){
return mod.exports;
},
'module': function( mod ){
mod.module = {
id: mod.id,
exports: mod.exports
};
return mod.module;
},
'@fmd': function(){
return env;
}
};
/**
* module constructor
* @param {string} module id
* @param {array} module's dependencies
* @param {object} module factory
* */
var Module = function( id, deps, factory ){
var mod = this;
mod.id = id;
mod.deps = deps || [];
mod.factory = factory;
mod.exports = {};
if ( mod.unnamed() ){
id = ANONYMOUS_PREFIX + anonymousUid;
anonymousUid++;
}
mod.uid = id;
};
Module.prototype = {
unnamed: function(){
return this.id === EMPTY_ID;
},
extract: function(){
var mod = this,
deps = mod.deps,
list = [];
if ( lang.isArray( deps ) ){
lang.forEach( deps, function( id ){
var mid, hook;
if ( hook = builtinModules[id] ){
mid = hook( mod );
} else {
mod.require || Module.makeRequire( mod );
mid = mod.require( id );
}
list.push( mid );
} );
}
return list;
},
compile: function(){
var mod = this;
if ( config.get('hasCatch') ){
try {
Module.compile( mod );
} catch ( ex ){
event.emit( 'compileFailed', ex, mod );
}
} else {
Module.compile( mod );
}
},
autocompile: function(){
this.unnamed() && this.compile();
}
};
Module.get = function( id ){
return modulesCache[id];
};
Module.has = function( id, deep ){
if ( builtinModules[id] ){
return true;
}
var meta = { id: id };
deep && event.emit( 'alias', meta );
return modulesCache[meta.id] ? true : false;
};
Module.save = function( mod ){
modulesCache[mod.uid] = mod;
event.emit( 'saved', mod );
mod.autocompile();
};
Module.require = function( id ){
var mod = Module.get( id );
if ( !mod ){
event.emit( 'requireFailed', { id: id } );
return null;
}
if ( !mod.compiled ){
mod.compiled = true;
mod.compile();
}
event.emit( 'required', mod );
return mod.exports;
};
Module.makeRequire = function( mod ){
mod.require = function( id ){
var meta = { id: id };
event.emit( 'alias', meta, mod );
return Module.require( meta.id );
};
};
Module.define = function( id, deps, factory ){
var argsLength = arguments.length;
if ( argsLength === 1 ){
factory = id;
id = EMPTY_ID;
} else if ( argsLength === 2 ){
factory = deps;
deps = EMPTY_DEPS;
if ( !lang.isString(id) ){
deps = id;
id = EMPTY_ID;
}
}
if ( Module.has( id, true ) ){
event.emit( 'existed', { id: id } );
return null;
}
Module.save( new Module( id, deps, factory ) );
};
Module.compile = function( mod ){
if ( lang.isFunction( mod.factory ) ){
var deps = mod.extract(),
exports = mod.factory.apply( null, deps );
if ( exports !== UNDEFINED ){
mod.exports = exports;
} else if ( mod.module && mod.module.exports !== UNDEFINED ){
mod.exports = mod.module.exports;
}
mod.module && ( delete mod.module );
} else if ( mod.factory !== UNDEFINED ) {
mod.exports = mod.factory;
}
event.emit( 'compiled', mod );
};
/* sign for FMD */
Module.define.fmd = {};
/* exports API to fmd */
var originalDefine = global.define;
env.noConflict = function(){
global.define = originalDefine;
};
env.define = global.define = Module.define;
return Module;
} );
/**
* @module fmd/relative
* @author Edgar <mail@edgar.im>
* @version v0.2
* @date 170117
* */
fmd( 'relative', ['lang','event'],
function( lang, event ){
'use strict';
var rCwd = /.*\//,
rDot = /\/\.\//,
rDoubleDot = /[^\/]+\/\.\.\//;
var relative = {
cwd: function( id ){
return id.match( rCwd )[0];
},
isDotStart: function( id ){
return id.charAt(0) === '.';
},
hasSlash: function( id ){
return id.lastIndexOf('/') > 0;
},
resolve: function( from, to ){
var id = ( from + to ).replace( rDot, '/' );
while ( id.match( rDoubleDot ) ){
id = id.replace( rDoubleDot, '' );
}
return id;
}
};
event.on( 'alias', function( meta, mod ){
if ( mod && mod.id && relative.isDotStart( meta.id ) && relative.hasSlash( mod.id ) ){
mod._cwd || ( mod._cwd = relative.cwd( mod.id ) );
meta.id = relative.resolve( mod._cwd, meta.id );
}
} );
return relative;
} );
/**
* @module fmd/alias
* @author Edgar <mail@edgar.im>
* @version v0.3
* @date 170118
* */
fmd( 'alias', ['config','event'],
function( config, event ){
'use strict';
config.register({
key: 'alias',
name: 'object'
});
event.on( 'alias', function( meta ){
var aliases = config.get( 'alias' ),
alias;
if ( aliases && ( alias = aliases[meta.id] ) ){
meta.nominalId = meta.id;
meta.id = alias;
}
} );
} );
/**
* @module fmd/resolve
* @author Edgar <mail@edgar.im>
* @version v0.2
* @date 170213
* */
fmd( 'resolve', ['event','config'],
function( event, config ){
'use strict';
config.register({
key: 'resolve',
name: 'array'
});
var parseResolve = function( asset ){
var resolveQueue = config.get( 'resolve' ),
url;
if ( resolveQueue ){
for ( var i = 0, l = resolveQueue.length; i < l; i++ ){
url = resolveQueue[i]( asset.id );
if ( url !== undefined && url !== asset.id ){
break;
}
}
}
asset.url = url ? url : asset.id;
};
event.on( 'resolve', parseResolve );
} );
/**
* @module fmd/id2url
* @author Edgar <mail@edgar.im>
* @version v0.4
* @date 170213
* */
fmd( 'id2url', ['global','event','config'],
function( global, event, config ){
'use strict';
var rAbsolute = /^https?:\/\//i;
var TIME_STAMP = ( new Date() ).getTime();
config.set({
baseUrl: (function(){
var rDomain = /^\w+\:\/\/[\w\-\.:]+\//i,
scripts = global.document.getElementsByTagName('script'),
selfScript = scripts[scripts.length-1],
src = selfScript.hasAttribute ? selfScript.src : selfScript.getAttribute( 'src', 4 ),
selfUrl = src ? src.match( rDomain ) : null;
return selfUrl ? selfUrl[0] : '';
})()
});
config.register({
key: 'stamp',
name: 'object'
});
var addBaseUrl = function( asset ){
rAbsolute.test( asset.url ) || ( asset.url = config.get('baseUrl') + asset.url );
},
addExtname = function( asset ){
var url = asset.url;
url.lastIndexOf('.') < url.lastIndexOf('/') && ( asset.url += '.js' );
},
addStamp = function( asset ){
var t = config.get('hasStamp') ? TIME_STAMP : null,
stampMap = config.get( 'stamp' );
if ( stampMap ){
for ( var key in stampMap ){
if ( ( new RegExp( key ) ).test( asset.id ) ){
t = stampMap[key];
break;
}
}
}
t && ( asset.url += '?fmd.stamp=' + t );
},
id2url = function( asset ){
event.emit( 'resolve', asset );
addBaseUrl( asset );
addExtname( asset );
event.emit( 'stamp', asset );
};
event.on( 'stamp', addStamp );
event.on( 'id2url', id2url );
} );
/**
* @module fmd/assets
* @author Edgar <mail@edgar.im>
* @version v0.3
* @date 170213
* */
fmd( 'assets', ['cache','lang','event','config','module'],
function( cache, lang, event, config, Module ){
'use strict';
var assetsCache = cache.assets = {},
id2urlMap = {};
var assets = {
make: function( id, meta ){
var asset = { id: id };
event.emit( 'analyze', asset );
event.emit( 'alias', asset, meta );
if ( id2urlMap[asset.id] ){
return assetsCache[ id2urlMap[asset.id] ];
}
Module.has( asset.id ) ? ( asset.url = asset.id ) : event.emit( 'id2url', asset );
id2urlMap[asset.id] = asset.url;
return ( assetsCache[asset.url] = asset );
},
group: function( meta ){
return lang.map( meta.deps, function( id ){
return assets.make( id, meta );
} );
}
};
return assets;
} );
/**
* @module fmd/when
* @author Edgar <mail@edgar.im>
* @version v0.1
* @date 130419
* */
fmd( 'when', function(){
'use strict';
/**
* Thanks to:
* cujo.js, https://github.com/cujojs/curl/blob/master/src/curl.js
* jQuery, https://github.com/jquery/jquery/blob/1.7.2/src/deferred.js
* */
var noop = function(){};
var Promise = function( len ){
var _this = this,
thens = [],
resolved = 0,
rejected = 0;
len = len || 0;
var probe = function(){
if ( resolved + rejected === len ){
complete();
}
},
complete = function(){
_this.then = !rejected ?
function( resolved, rejected ){ resolved && resolved(); } :
function( resolved, rejected ){ rejected && rejected(); };
complete = noop;
notify( !rejected ? 0 : 1 );
notify = noop;
thens = [];
},
notify = function( which ){
var then, callback, i = 0;
while ( ( then = thens[i++] ) ){
callback = then[which];
callback && callback();
}
};
this.then = function( resolved, rejected ){
thens.push( [resolved, rejected] );
};
this.resolve = function(){
resolved++;
probe();
};
this.reject = function(){
rejected++;
probe();
};
probe();
};
var when = function(){
var l = arguments.length,
promise = new Promise(l),
fn, i = 0;
while ( ( fn = arguments[i++] ) ){
fn( promise );
}
return promise;
};
return when;
} );
/**
* @module fmd/request
* @author Edgar <mail@edgar.im>
* @version v0.4
* @date 170105
* */
fmd( 'request', ['global','config','event'],
function( global, config, event ){
'use strict';
/**
* Thanks to:
* SeaJS, https://github.com/seajs/seajs/blob/master/src/util-request.js
* https://github.com/seajs/seajs/blob/master/tests/research/load-js-css/test.html
* https://github.com/seajs/seajs/blob/master/tests/research/load-js-css/load-css.html
* YUI3, https://github.com/yui/yui3/blob/v3.13.0/src/get/js/get.js
* HeadJS, https://github.com/headjs/headjs/blob/master/src/load.js
* lazyload, https://github.com/rgrove/lazyload/blob/master/lazyload.js
* RequireJS, https://github.com/jrburke/requirejs/blob/master/require.js
* cujo.js, https://github.com/cujojs/curl/blob/master/src/curl.js
* curl css! plugin, https://github.com/cujojs/curl/blob/master/src/curl/plugin/css.js
* cssx, https://github.com/unscriptable/cssx/blob/master/src/cssx/css.js
* LABjs, https://github.com/getify/LABjs/blob/2.0/LAB.src.js
* */
var doc = global.document,
setTimeout = global.setTimeout;
var rStyle = /\.css(?:\?|$)/i,
rReadyStates = /loaded|complete/,
rLoadXdSheetError = /security|denied/i,
rWebKit = /.*webkit\/?(\d+)\..*/,
rMobile = /mobile/;
var UA = global.navigator.userAgent.toLowerCase();
var webkitVersion = UA.match( rWebKit ),
isOldWebKit = webkitVersion ? webkitVersion[1] * 1 < 536 : false,
isPollCSS = isOldWebKit || ( !webkitVersion && rMobile.test( UA ) );
var head = doc && ( doc.head || doc.getElementsByTagName('head')[0] || doc.documentElement );
var createNode = function( asset, isStyle ){
var node;
if ( isStyle ){
node = doc.createElement('link');
node.rel = 'stylesheet';
node.href = asset.url;
} else {
node = doc.createElement('script');
node.async = true;
node.src = asset.url;
}
config.get( 'charset' ) && ( node.charset = config.get( 'charset' ) );
event.emit( 'createNode', node, asset );
return node;
},
poll = function( node, callback, asset ){
var isLoaded = false,
sheet, rules;
try {
sheet = node.sheet;
if ( sheet ){
rules = sheet.cssRules;
isLoaded = rules ? rules.length > 0 : rules !== undefined;
}
} catch( ex ){
isLoaded = rLoadXdSheetError.test( ex.message );
}
setTimeout( function(){
if ( isLoaded ){
callback && callback();
event.emit( 'requested', asset );
} else {
poll( node, callback, asset );
}
}, 20 );
};
var onLoadAsset = function( node, callback, isSupportOnload, asset, isStyle ){
if ( isSupportOnload ){
node.onload = function(){
finish();
event.emit( 'requested', asset );
};
node.onerror = function(){
finish();
event.emit( 'requestError', asset );
};
} else {
node.onreadystatechange = function(){
if ( rReadyStates.test( node.readyState ) ){
finish();
event.emit( 'requested', asset );
}
};
}
function finish(){
node.onload = node.onreadystatechange = node.onerror = null;
if ( !isStyle && !config.get('debug') ){
node.parentNode && node.parentNode.removeChild( node );
}
node = undefined;
callback && callback();
}
},
onLoadStyle = function( node, callback, isSupportOnload, asset ){
if ( !isSupportOnload || isPollCSS ){
setTimeout( function(){
poll( node, callback, asset );
}, 1 );
return;
}
onLoadAsset( node, callback, isSupportOnload, asset, true );
},
request = function( asset, callback ){
var isStyle = rStyle.test( asset.url ),
node = createNode( asset, isStyle ),
isSupportOnload = 'onload' in node;
isStyle ? onLoadStyle( node, callback, isSupportOnload, asset )
: onLoadAsset( node, callback, isSupportOnload, asset );
head.appendChild( node );
};
return request;
} );
/**
* @module fmd/loader
* @author Edgar <mail@edgar.im>
* @version v0.3
* @date 170105
* */
fmd( 'loader', ['global','event','config','request'],
function( global, event, config, request ){
'use strict';
var STATE_LOADING = 'loading',
STATE_LOADED = 'loaded';
var noop = function(){};
config.set({
timeout: 10000
});
event.on( 'requestComplete', function( asset ){
var call, queue;
asset.state = STATE_LOADED;
queue = asset.onload;
while ( call = queue.shift() ){
call();
}
} );
var loader = function( asset, callback ){
callback || ( callback = noop );
if ( asset.state === STATE_LOADED ){
callback();
return;
}
if ( asset.state === STATE_LOADING ){
asset.onload.push( callback );
return;
}
asset.state = STATE_LOADING;
asset.onload = [callback];
event.emit( 'request', asset, callback );
if ( asset.requested ){
return;
}
asset.timer = global.setTimeout( function(){
event.emit( 'requestTimeout', asset );
}, config.get('timeout') );
request( asset, function(){
global.clearTimeout( asset.timer );
event.emit( 'requestComplete', asset );
} );
};
return loader;
} );
/**
* @module fmd/remote
* @author Edgar <mail@edgar.im>
* @version v0.1
* @date 131112
* */
fmd( 'remote', ['lang','event','module','assets','when','loader'],
function( lang, event, Module, assets, when, loader ){
'use strict';
var remote = {};
remote.bring = remote.get = function( group, callback ){
when.apply( null, lang.map( group, function( asset ){
return function( promise ){
Module.has( asset.id ) ?
promise.resolve() : loader( asset, function(){
promise.resolve();
} );
};
} ) ).then( callback );
};
remote.fetch = function( meta, callback ){
var group = assets.group( meta );
event.emit( 'fetch', group );
remote.bring( group, function(){
when.apply( null, lang.map( group, function( asset ){
return function( promise ){
var mod = Module.get( asset.id );
mod && !mod.compiled && mod.deps.length ? remote.fetch( mod, function(){
promise.resolve();
} ) : promise.resolve();
};
} ) ).then( function(){
callback.call( null, group );
} );
} );
};
return remote;
} );
/**
* @module fmd/use
* @author Edgar <mail@edgar.im>
* @version v0.2
* @date 131015
* */
fmd( 'use', ['lang','event','module','remote'],
function( lang, event, Module, remote ){
'use strict';
event.on( 'makeRequire', function( require, mod ){
require.use = function( ids, callback ){
lang.isArray( ids ) || ( ids = [ids] );
remote.fetch( { id: mod.id, deps: ids }, function( group ){
var args = lang.map( group, function( asset ){
return Module.require( asset.id );
} );
callback && callback.apply( null, args );
} );
};
} );
} );
/**
* @module fmd/async
* @author Edgar <mail@edgar.im>
* @version v0.3
* @date 170118
* */
fmd( 'async', ['config','module','remote'],
function( config, Module, remote ){
'use strict';
var original = Module.prototype.autocompile;
var replacer = function(){
var mod = this;
if ( mod.unnamed() ){
remote.fetch( mod, function(){
mod.compile();
} );
}
};
config.register({
key: 'async',
rule: function( current, key, val ){
val = !!val;
if ( current !== val ){
this.async = val;
Module.prototype.autocompile = val === true ? replacer : original;
}
}
})
.set({
async: true
});
} );