UNPKG

includejs

Version:

IncludeJS Resource Builder Tool

888 lines (747 loc) 20.2 kB
var __eval = function(source, include) { "use strict"; if (!source) { console.error('error', include); } var iparams = include.route.params; return eval(source); }; ;(function(global, document) { "use strict"; /** * .cfg * : path := root path. @default current working path, im browser window.location; * : eval := in node.js this conf. is forced * : lockedToFolder := makes current url as root path * Example "/script/main.js" within this window.location "{domain}/apps/1.html" * will become "{domain}/apps/script/main.js" instead of "{domain}/script/main.js" */ var bin = {}, isWeb = !! (global.location && global.location.protocol && /^https?:/.test(global.location.protocol)), cfg = { eval: document == null }, handler = {}, hasOwnProp = {}.hasOwnProperty, //-currentParent = null, XMLHttpRequest = global.XMLHttpRequest; var Helper = { /** TODO: improve url handling*/ uri: { getDir: function(url) { var index = url.lastIndexOf('/'); return index == -1 ? '' : url.substring(index + 1, -index); }, /** @obsolete */ resolveCurrent: function() { var scripts = document.querySelectorAll('script'); return scripts[scripts.length - 1].getAttribute('src'); }, resolveUrl: function(url, parent) { if (cfg.path && url[0] == '/') { url = cfg.path + url.substring(1); } if (url[0] == '/') { if (isWeb === false || cfg.lockedToFolder === true) { return url.substring(1); } return url; } switch (url.substring(0, 4)) { case 'file': case 'http': return url; } if (parent != null && parent.location != null) { return parent.location + url; } return url; } }, extend: function(target) { for(var i = 1; i< arguments.length; i++){ var source = arguments[i]; if (typeof source === 'function'){ source = source.prototype; } for (var key in source) { target[key] = source[key]; } } return target; }, invokeEach: function(arr, args) { if (arr == null) { return; } if (arr instanceof Array) { for (var i = 0, x, length = arr.length; i < length; i++) { x = arr[i]; if (typeof x === 'function'){ (args != null ? x.apply(this, args) : x()); } } } }, doNothing: function(fn) { typeof fn == 'function' && fn(); }, reportError: function(e) { console.error('IncludeJS Error:', e, e.message, e.url); typeof handler.onerror == 'function' && handler.onerror(e); }, ensureArray: function(obj, xpath) { if (!xpath) { return obj; } var arr = xpath.split('.'); while (arr.length - 1) { var key = arr.shift(); obj = obj[key] || (obj[key] = {}); } return (obj[arr.shift()] = []); }, xhr: function(url, callback) { var xhr = new XMLHttpRequest(), s = Date.now(); xhr.onreadystatechange = function() { xhr.readyState == 4 && callback && callback(url, xhr.responseText); }; xhr.open('GET', url, true); xhr.send(); } }; var Routes = (function() { var routes = {}; return { /** * @param route {String} = Example: '.reference/libjs/{0}/{1}.js' */ register: function(namespace, route) { routes[namespace] = route instanceof Array ? route : route.split(/[\{\}]/g); }, /** * @param {String} template = Example: 'scroller/scroller.min?ui=black' */ resolve: function(namespace, template) { var questionMark = template.indexOf('?'), aliasIndex = template.indexOf('::'), alias, path, params, route, i, x, length; if (~aliasIndex){ alias = template.substring(aliasIndex + 2); template = template.substring(0, aliasIndex); } if (~questionMark) { var arr = template.substring(questionMark + 1).split('&'); params = {}; for (i = 0, length = arr.length; i < length; i++) { x = arr[i].split('='); params[x[0]] = x[1]; } template = template.substring(0, questionMark); } template = template.split('/'); route = routes[namespace]; if (route == null){ return { path: template.join('/'), params: params, alias: alias }; } path = route[0]; for (i = 1; i < route.length; i++) { if (i % 2 === 0) { path += route[i]; } else { /** if template provides less "breadcrumbs" than needed - * take always the last one for failed peaces */ var index = route[i] << 0; if (index > template.length - 1) { index = template.length - 1; } path += template[index]; if (i == route.length - 2){ for(index++;index < template.length; index++){ path += '/' + template[index]; } } } } return { path: path, params: params, alias: alias }; }, /** * @arg includeData : * 1. string - URL to resource * 2. array - URLs to resources * 3. object - {route: x} - route defines the route template to resource, * it must be set before in include.cfg. * example: * include.cfg('net','scripts/net/{name}.js') * include.js({net: 'downloader'}) // -> will load scipts/net/downloader.js * @arg namespace - route in case of resource url template, or namespace in case of LazyModule * * @arg fn - callback function, which receives namespace|route, url to resource and ?id in case of not relative url * @arg xpath - xpath string of a lazy object 'object.sub.and.othersub'; */ each: function(type, includeData, fn, namespace, xpath) { var key; if (includeData == null) { console.error('Include Item has no Data', type, namespace); return; } if (type == 'lazy' && xpath == null) { for (key in includeData) { this.each(type, includeData[key], fn, null, key); } return; } if (includeData instanceof Array) { for (var i = 0; i < includeData.length; i++) { this.each(type, includeData[i], fn, namespace, xpath); } return; } if (typeof includeData === 'object') { for (key in includeData) { if (hasOwnProp.call(includeData, key)) { this.each(type, includeData[key], fn, key, xpath); } } return; } if (typeof includeData === 'string') { var x = this.resolve(namespace, includeData); if (namespace){ namespace += '.' + includeData; } fn(namespace, x, xpath); return; } console.error('Include Package is invalid', arguments); }, getRoutes: function(){ return routes; } }; })(); /*{test} console.log(JSON.stringify(Routes.resolve(null,'scroller.js::Scroller'))); Routes.register('lib', '.reference/libjs/{0}/lib/{1}.js'); console.log(JSON.stringify(Routes.resolve('lib','scroller::Scroller'))); console.log(JSON.stringify(Routes.resolve('lib','scroller/scroller.mobile?ui=black'))); Routes.register('framework', '.reference/libjs/framework/{0}.js'); console.log(JSON.stringify(Routes.resolve('framework','dom/jquery'))); */ var Events = (function(document) { if (document == null) { return { ready: Helper.doNothing, load: Helper.doNothing }; } var readycollection = [], loadcollection = null, timer = Date.now(); document.onreadystatechange = function() { if (/complete|interactive/g.test(document.readyState) === false) { return; } if (timer) { console.log('DOMContentLoader', document.readyState, Date.now() - timer, 'ms'); } Events.ready = Helper.doNothing; Helper.invokeEach(readycollection); readycollection = null; if (document.readyState == 'complete') { Events.load = Helper.doNothing; Helper.invokeEach(loadcollection); loadcollection = null; } }; return { ready: function(callback) { readycollection.unshift(callback); }, load: function(callback) { (loadcollection || (loadcollection = [])).unshift(callback); } }; })(document); var IncludeDeferred = function() { this.callbacks = []; }; IncludeDeferred.prototype = { /** state observer */ on: function(state, callback) { state <= this.state ? callback(this) : this.callbacks.unshift({ state: state, callback: callback }); return this; }, readystatechanged: function(state) { this.state = state; for (var i = 0, x, length = this.callbacks.length; i < length; i++) { x = this.callbacks[i]; if (x.state > this.state || x.callback == null) { continue; } x.callback(this); x.callback = null; } }, /** idefer */ ready: function(callback) { return this.on(4, function() { Events.ready(this.resolve.bind(this, callback)); }.bind(this)); }, /** assest loaded and window is loaded */ loaded: function(callback) { return this.on(4, function() { Events.load(callback); }); }, /** assets loaded */ done: function(callback) { return this.on(4, this.resolve.bind(this, callback)); }, resolve: function(callback) { global.include = this; callback(this.response); } }; var Include = function(){}; Include.prototype = { setCurrent: function(resource) { var r = new Resource('js', {path: resource.id}, resource.namespace, null, null, resource.id); if (r.state != 4){ console.error("Current Resource should be loaded"); } global.include = r; }, incl: function(type, pckg) { if (this instanceof Resource) { return this.include(type, pckg); } var r = new Resource(); // //if (currentParent) { // r.id = currentParent.id; // r.url = currentParent.url; // r.namespace = currentParent.namespace; // r.location = Helper.uri.getDir(r.url); //} return r.include(type, pckg); }, js: function(pckg) { return this.incl('js', pckg); }, css: function(pckg) { return this.incl('css', pckg); }, load: function(pckg) { return this.incl('load', pckg); }, ajax: function(pckg) { return this.incl('ajax', pckg); }, embed: function(pckg) { return this.incl('embed', pckg); }, lazy: function(pckg) { return this.incl('lazy', pckg); }, cfg: function(arg) { switch (typeof arg) { case 'object': for (var key in arg) { cfg[key] = arg[key]; } break; case 'string': if (arguments.length == 1){ return cfg[arg]; } if (arguments.length == 2) { cfg[arg] = arguments[1]; } break; case 'undefined': return cfg; } return this; }, routes: function(arg){ if (arg == null){ return Routes.getRoutes(); } for (var key in arg) { Routes.register(key, arg[key]); } return this; }, promise: function(namespace) { var arr = namespace.split('.'), obj = global; while (arr.length) { var key = arr.shift(); obj = obj[key] || (obj[key] = {}); } return obj; }, register: function(_bin) { for (var key in _bin) { for (var i = 0; i < _bin[key].length; i++) { var id = _bin[key][i].id, url = _bin[key][i].url, namespace = _bin[key][i].namespace, resource = new Resource(); resource.state = 4; resource.namespace = namespace; resource.type = key; if (url) { if (url[0] == '/') { url = url.substring(1); } resource.location = Helper.uri.getDir(url); } switch (key) { case 'load': case 'lazy': var container = document.querySelector('#includejs-' + id); if (container == null) { console.error('"%s" Data was not embedded into html', id); return; } resource.obj = container.innerHTML; break; } (bin[key] || (bin[key] = {}))[id] = resource; } } } }; var ScriptStack = (function() { var head, currentResource, stack = [], loadScript = function(url, callback) { //console.log('load script', url); var tag = document.createElement('script'); tag.type = 'application/javascript'; tag.src = url; tag.onload = tag.onerror = callback; (head || (head = document.querySelector('head'))).appendChild(tag); }, afterScriptRun = function(resource) { var includes = resource.includes; if (includes != null && includes.length) { for (var i = 0; i < includes.length; i++) { if (includes[i].state != 4) { return; } } } resource.readystatechanged(4); }, loadByEmbedding = function() { if (stack.length === 0) { return; } if (currentResource != null){ return; } var resource = (currentResource = stack[0]); if (resource.state === 1) { return; } resource.state = 1; global.include = resource; global.iparams = resource.route.params; loadScript(resource.url, function(e) { if (e.type == 'error'){ console.log('Script Loaded Error', resource.url); } for (var i = 0, length = stack.length; i < length; i++) { if (stack[i] === resource) { stack.splice(i, 1); break; } } resource.state = 3; afterScriptRun(resource); currentResource = null; loadByEmbedding(); }); }, processByEval = function() { if (stack.length === 0){ return; } if (currentResource != null){ return; } var resource = stack[0]; if (resource && resource.state > 2) { currentResource = resource; resource.state = 1; //console.log('evaling', resource.url, stack.length); try { __eval(resource.source, resource); } catch (error) { error.url = resource.url; Helper.reportError(error); } for (var i = 0, x, length = stack.length; i < length; i++) { x = stack[i]; if (x == resource) { stack.splice(i, 1); break; } } resource.state = 3; afterScriptRun(resource); currentResource = null; processByEval(); } }; return { load: function(resource, parent) { //console.log('LOAD', resource.url, 'parent:',parent ? parent.url : ''); var added = false; if (parent) { for (var i = 0, length = stack.length; i < length; i++) { if (stack[i] === parent) { stack.splice(i, 0, resource); added = true; break; } } } if (!added) { stack.push(resource); } if (cfg.eval) { Helper.xhr(resource.url, function(url, response) { if (!response) { console.error('Not Loaded:', url); } resource.source = response; resource.readystatechanged(3); // process next processByEval(); }); } else { loadByEmbedding(); } }, afterScriptRun: afterScriptRun }; })(); var Resource = function(type, route, namespace, xpath, parent, id) { Include.call(this); IncludeDeferred.call(this); ////if (type == null) { //// this.state = 3; //// return this; ////} var url = route && route.path; if (url != null) { this.url = url = Helper.uri.resolveUrl(url, parent); } this.route = route; this.namespace = namespace; this.type = type; this.xpath = xpath; if (id == null && url){ id = (url[0] == '/' ? '' : '/') + url; } var resource = bin[type] && bin[type][id]; if (resource) { resource.route = route; return resource; } if (url == null){ this.state = 3; return this; } this.location = Helper.uri.getDir(url); (bin[type] || (bin[type] = {}))[id] = this; var tag; switch (type) { case 'js': ScriptStack.load(this, parent); break; case 'ajax': case 'load': case 'lazy': Helper.xhr(url, this.onXHRLoaded.bind(this)); break; case 'css': this.state = 4; tag = document.createElement('link'); tag.href = url; tag.rel = "stylesheet"; tag.type = "text/css"; break; case 'embed': tag = document.createElement('script'); tag.type = 'application/javascript'; tag.src = url; tag.onload = tag.onerror = this.readystatechanged.bind(this, 4); break; } if (tag != null) { document.querySelector('head').appendChild(tag); tag = null; } return this; }; Resource.prototype = Helper.extend({}, IncludeDeferred, Include, { include: function(type, pckg) { //-this.state = 1; this.state = this.state >= 3 ? 3 : 1; if (this.includes == null) { this.includes = []; } Routes.each(type, pckg, function(namespace, route, xpath) { var resource = new Resource(type, route, namespace, xpath, this); this.includes.push(resource); resource.index = this.calcIndex(type, namespace); resource.on(4, this.childLoaded.bind(this)); }.bind(this)); return this; }, /** Deprecated * Use Resource Alias instead */ calcIndex: function(type, namespace) { if (this.response == null) { this.response = {}; } switch (type) { case 'js': case 'load': case 'ajax': var key = type + 'Index'; if (this.response[key] == null) { this.response[key] = -1; } return ++this.response[key]; } return -1; }, childLoaded: function(resource) { if (resource && resource.exports) { switch (resource.type) { case 'js': case 'load': case 'ajax': //////if (this.response == null) { ////// this.response = {}; //////} if (resource.route.alias){ this.response[resource.route.alias] = resource.exports; break; } var obj = (this.response[resource.type] || (this.response[resource.type] = [])); if (resource.namespace != null) { obj = Helper.ensureArray(obj, resource.namespace); } obj[resource.index] = resource.exports; break; } } var includes = this.includes; if (includes && includes.length) { if (this.state < 3/* && this.url != null */){ /** resource still loading/include is in process, but one of sub resources are already done */ return; } for (var i = 0; i < includes.length; i++) { if (includes[i].state != 4) { return; } } } this.readystatechanged(4); }, onXHRLoaded: function(url, response) { if (response) { switch (this.type) { case 'load': case 'ajax': this.exports = response; break; case 'lazy': LazyModule.create(this.xpath, response); break; } } else { console.warn('Resource cannt be loaded', this.url); } this.readystatechanged(4); } }); var LazyModule = { create: function(xpath, code) { var arr = xpath.split('.'), obj = global, module = arr[arr.length - 1]; while (arr.length > 1) { var prop = arr.shift(); obj = obj[prop] || (obj[prop] = {}); } arr = null; Object.defineProperty(obj, module, { get: function() { delete obj[module]; try { var r = __eval(code, global.include); if (!(r == null || r instanceof Resource)){ obj[module] = r; } } catch (error) { error.xpath = xpath; Helper.reportError(error); } finally { code = null; xpath = null; return obj[module]; } } }); } }; global.include = new Include(); global.includeLib = { Helper: Helper, Routes: Routes, Resource: Resource, ScriptStack: ScriptStack }; var fs = require('fs'); XMLHttpRequest = function(){}; XMLHttpRequest.prototype = { open: function(method, url){ this.url = url; }, send: function(){ if (this.url.indexOf('file:///') > -1){ this.url = this.url.replace('file:///',''); } var that = this; fs.readFile(this.url, 'utf-8', function(err, data){ if (err) { throw err; } that.readyState = 4; that.responseText = data; that.onreadystatechange(); }); } }; })(typeof window === 'undefined' ? global : window, typeof document == 'undefined' ? null : document);