UNPKG

grunt-ozjs

Version:

grunt tasks for oz.js and ozma.js

596 lines (572 loc) 16.8 kB
/** * OzJS: microkernel for modular javascript * compatible with CommonJS Asynchronous Modules * Copyright (C) 2010-1011, Dexter.Yy * Licensed under The MIT License * @example https://github.com/dexteryy/OzJS/tree/master/tests * vim:set ts=4 sw=4 sts=4 et: */ (function(undefined){ var window = this, _toString = Object.prototype.toString, _RE_PLUGIN = /(.*)!(.+)/, _RE_DEPS = /\Wrequire\((['"]).+?\1\)/g, _RE_SUFFIX = /\.(js|json)$/, _builtin_mods = { "require": 1, "exports": 1, "module": 1, "host": 1, "finish": 1 }, _muid = 0, _config = { mods: {} }, _scripts = {}, _delays = {}, _refers = {}, _waitings = {}, _latestMod, _resets = {}, forEach = Array.prototype.forEach || function(fn, sc){ for(var i = 0, l = this.length; i < l; i++){ if (i in this) fn.call(sc, this[i], i, this); } }; function isFunction(obj) { return _toString.call(obj) === "[object Function]"; } function isArray(obj) { return _toString.call(obj) === "[object Array]"; } function isWindow(obj) { return "setInterval" in obj; } function clone(obj) { function NewObj(){} NewObj.prototype = obj; return new NewObj(); } /** * @public compare version number (Semantic Versioning format) * @param {string} * @param {string} * @return {boolean} v1 >= v2 == true */ function semver(v1, v2){ v1 = v1.split('.'); v2 = v2.split('.'); var result, l = v1.length; if (v2.length > l) l = v2.length; for (var i = 0; i < l; i++) { result = (v1[i] || 0) - (v2[i] || 0); if (result === 0) continue; else break; } return result >= 0; } /** * @public define / register a module and its meta information * @param {string} module name. optional as unique module in a script file * @param {string[]} dependencies. optional * @param {function} module code, execute only once on the first call * * @note * * define('', [""], func) * define('', [""], "") * * define('', [""]) * define('', "") * * define('', func) * define([""], func) * define(func) */ function define(fullname, deps, block){ var is_remote = typeof block === 'string'; if (!block) { if (deps) { if (isArray(deps)) { block = autoname(fullname); } else { block = deps; deps = null; } } else { block = fullname; fullname = ""; } if (typeof fullname !== 'string') { deps = fullname; fullname = ""; } else { is_remote = typeof block === 'string'; if (!is_remote && !deps) { deps = seek(block); } } } var mod = fullname && _config.mods[fullname]; if (mod && mod.fullname && (is_remote && mod.loaded == 2 || mod.exports)) { return; } if (is_remote && _config.enable_ozma) { deps = null; } var name = fullname.split('@'), host = isWindow(this) ? this : window, ver = name[1]; name = name[0]; mod = _config.mods[fullname] = { name: name, fullname: fullname, id: ++_muid, version: ver, url: mod && mod.url, host: host, deps: deps || [] }; if (fullname === "") { // capture anonymous module _latestMod = mod; } if (typeof block !== 'string') { mod.block = block; mod.loaded = 2; } else { // remote module mod.url = block; } if (mod.block && !isFunction(mod.block)) { // json module mod.exports = block; } if (name !== fullname) { // compare version number, link to the newest version var current = _config.mods[name]; if (!current || !current.block && (!current.url || current.loaded) || current.version && semver(ver, current.version)) { _config.mods[name] = mod; } } } /** * @public run a code block its dependencies * @param {string[]} [module fullname] dependencies * @param {function} */ function require(deps, block) { if (!block) { block = deps; deps = seek(block); } else if (typeof deps === 'string') { deps = [deps]; } var m, remotes = 0, // counter for remote scripts host = isWindow(this) ? this : window, list = scan.call(host, deps); // calculate dependencies, find all required modules for (var i = 0, l = list.length; i < l; i++) { m = list[i]; if (m.is_reset) { m = _config.mods[m.fullname]; } if (m.url && m.loaded !== 2) { // remote module remotes++; m.loaded = 1; // status: loading fetch(m, function(){ this.loaded = 2; // status: loaded var lm = _latestMod; if (lm) { // capture anonymous module lm.name = this.name; lm.fullname = this.fullname; lm.version = this.version; lm.url = this.url; var mods = _config.mods; mods[lm.fullname] = lm; if (mods[lm.name] && mods[lm.name].fullname === lm.fullname) { mods[lm.name] = lm; } _latestMod = null; } // loaded all modules, calculate dependencies all over again if (--remotes <= 0) { require.call(host, deps, block); } }); } } if (!remotes) { list.push({ deps: deps, host: host, block: block }); setTimeout(function(){ exec(list.reverse()); }, 0); } } /** * @private execute modules in a sequence of dependency * @param {object[]} [module object] */ function exec(list){ var mod, mid, tid, result, isAsync, deps, depObjs, exportObj, rmod, wt = _waitings; while (mod = list.pop()) { if (mod.is_reset) { rmod = clone(_config.mods[mod.fullname]); rmod.host = mod.host; rmod.newname = mod.newname; mod = rmod; if (!_resets[mod.newname]) { _resets[mod.newname] = []; } _resets[mod.newname].push(mod); mod.exports = undefined; } else if (mod.fullname) { mod = _config.mods[mod.fullname] || mod; } if (!mod.block || !mod.running && mod.exports !== undefined) { continue; } depObjs = []; exportObj = {}; // for "exports" module deps = mod.deps.slice(); deps[mod.block.hiddenDeps ? 'unshift' : 'push']("require", "exports", "module"); for (var i = 0, l = deps.length; i < l; i++) { mid = deps[i]; switch(mid) { case 'require': depObjs.push(requireFn); break; case 'exports': depObjs.push(exportObj); break; case 'module': depObjs.push(mod); break; case 'host': depObjs.push(mod.host); break; case 'finish': // execute asynchronously tid = mod.fullname; if (!wt[tid]) // for delay execute wt[tid] = [list]; else wt[tid].push(list); depObjs.push(function(result){ // HACK: no guarantee that this function will be invoked after while() loop termination in Chrome/Safari setTimeout(function(){ // 'mod' equal to 'list[list.length-1]' if (result) { mod.exports = result; } if (!wt[tid]) return; forEach.call(wt[tid], function(list){ this(list); }, exec); delete wt[tid]; mod.running = 0; }, 0); }); isAsync = 1; break; default: depObjs.push(( (_resets[mid] || []).pop() || _config.mods[mid] || {} ).exports); break; } } if (!mod.running) { // execute module code. arguments: [dep1, dep2, ..., require, exports, module] result = mod.block.apply(oz, depObjs) || null; mod.exports = result || exportObj; // use empty exportObj for "finish" for (var v in exportObj) { if (v) { mod.exports = exportObj; } break; } //console.log(mod.fullname, mod.exports) } if (isAsync) { // skip, wait for finish() mod.running = 1; break; } } } /** * @private observer for script loader, prevent duplicate requests * @param {object} module object * @param {function} callback */ function fetch(m, cb){ var url = m.url, observers = _scripts[url]; if (!observers) { var mname = m.fullname, delays = _delays; if (m.deps && m.deps.length && delays[mname] !== 1) { delays[mname] = [m.deps.length, cb]; m.deps.forEach(function(dep){ var d = _config.mods[dep]; if (this[dep] !== 1 && d.url && d.loaded !== 2) { if (!this[dep]) { this[dep] = []; } this[dep].push(m); } else { delays[mname][0]--; } }, _refers); if (delays[mname][0] > 0) { return; } else { delays[mname] = 1; } } observers = _scripts[url] = [[cb, m]]; var alias = _config.aliases; if (alias) { url = url.replace(/\{(\w+)\}/g, function(e1, e2){ return alias[e2] || ""; }); } var true_url = /^http:\/\//.test(url) ? url : (_config.enable_ozma && _config.distUrl || _config.baseUrl || '') + (_config.enableAutoSuffix ? truename(url) : url); getScript.call(m.host || this, true_url, function(){ forEach.call(observers, function(args){ args[0].call(args[1]); }); _scripts[url] = 1; if (_refers[mname] && _refers[mname] !== 1) { _refers[mname].forEach(function(dm){ var b = this[dm.fullname]; if (--b[0] <= 0) { this[dm.fullname] = 1; fetch(dm, b[1]); } }, delays); _refers[mname] = 1; } }); } else if (observers === 1) { cb.call(m); } else { observers.push([cb, m]); } } /** * @private search and sequence all dependencies, based on DFS * @param {string[]} a set of module names * @param {object[]} a sequence of modules, for recursion * @return {object[]} a sequence of modules */ function scan(m, list){ list = list || []; if (!m[0]) { return list; } var history = list.history; if (!history) { history = list.history = {}; } var deps, dep, mid, plugin, truename; if (m[1]) { deps = m; m = false; } else { mid = m[0]; plugin = _RE_PLUGIN.exec(mid); if (plugin) { mid = plugin[2]; plugin = plugin[1]; } if (!_config.mods[mid] && !_builtin_mods[mid]) { define(mid, autoname(mid)); } m = _config.mods[mid]; if (m) { if (plugin === "new") { m = { is_reset: true, deps: m.deps, fullname: mid, newname: plugin + "!" + mid, host: this }; } else { truename = m.fullname; } if (history[truename]) { return list; } } else { return list; } if (!history[truename]) { deps = m.deps || []; // find require information within the code // for server-side style module //deps = deps.concat(seek(m)); if (truename) { history[truename] = true; } } else { deps = []; } } for (var i = deps.length - 1; i >= 0; i--) { if (!history[deps[i]]) { scan.call(this, [deps[i]], list); } } if (m) { list.push(m); } return list; } /** * @experiment * @private analyse module code * to find out dependencies which have no explicit declaration * @param {object} module object */ function seek(block){ var hdeps = block.hiddenDeps || []; if (!block.hiddenDeps) { var code = block.toString(), h = null; hdeps = block.hiddenDeps = []; while (h = _RE_DEPS.exec(code)) { hdeps.push(h[0].slice(10, -2)); } } return hdeps.slice(); } function autoname(mid){ var ver = mid.split('@'); if (_RE_SUFFIX.test(ver[0])) { ver = ver[1] ? ver[0].replace(_RE_SUFFIX, function($0){ return '-' + ver[1] + $0; }) : ver[0]; } else { ver = (ver[1] ? (ver[0] + '-' + ver[1]) : ver[0]) + '.js'; } return ver; } /** * @note naming pattern: * _g_src.js * _g_combo.js * * jquery.js * jquery_pack.js * * _yy_src.pack.js * _yy_combo.js * * _yy_bak.pack.js * _yy_bak.pack_pack.js */ function truename(file){ return file.replace(/(.+?)(_src.*)?(\.\w+)$/, function($0, $1, $2, $3){ return $1 + ($2 && '_combo' || '_pack') + $3; }); } /** * @private for "require" module */ function requireFn(name, cb){ if (!cb) { return (_config.mods[name] || {}).exports; } else { return require(name, cb); } } /** * @public non-blocking script loader * @param {string} * @param {object} config */ function getScript(url, op){ var doc = isWindow(this) ? this.document : document, s = doc.createElement("script"); s.type = "text/javascript"; s.async = "async"; //for firefox3.6 if (!op) op = {}; else if (isFunction(op)) op = { callback: op }; if (op.charset) s.charset = op.charset; s.src = url; var h = doc.getElementsByTagName("head")[0]; s.onload = s.onreadystatechange = function(__, isAbort){ if ( isAbort || !s.readyState || /loaded|complete/.test(s.readyState) ) { s.onload = s.onreadystatechange = null; if (h && s.parentNode) { h.removeChild(s); } s = undefined; if (!isAbort && op.callback) { op.callback(); } } }; h.insertBefore(s, h.firstChild); } function config(opt){ for (var i in opt) { if (i === 'aliases') { if (!_config[i]) { _config[i] = {}; } for (var j in opt[i]) { _config[i][j] = opt[i][j]; } } else { _config[i] = opt[i]; } } } var oz = { define: define, require: require, config: config, seek: seek, fetch: fetch, autoname: autoname, truename: truename, // non-core _semver: semver, _getScript: getScript, _clone: clone, _forEach: forEach, _isFunction: isFunction, _isWindow: isWindow }; require.config = config; define.amd = { jQuery: true }; if (!window.window) { // for nodejs exports.oz = oz; exports._config = _config; // hook for build tool for (var i in oz) { exports[i] = oz[i]; } var hooking = function(fname){ return function(){ return exports[fname].apply(this, arguments); }; }; exec = hooking('exec'); fetch = hooking('fetch'); require = hooking('require'); require.config = config; } else { window.oz = oz; window.define = define; window.require = require; } })();