diff --git a/LAB.src.js b/LAB.src.js index 94a14ae..4b5197e 100644 --- a/LAB.src.js +++ b/LAB.src.js @@ -1,5 +1,5 @@ -/*! LAB.js (LABjs :: Loading And Blocking JavaScript) - v3.0.0-pre1 (c) Kyle Simpson +/*! LAB.js + v3.0.0-pre1 (c) 2017 Kyle Simpson MIT License */ @@ -11,40 +11,42 @@ var old$LAB = context.$LAB; - // constants for the valid keys of the options object - var keyAlwaysPreserveOrder = "AlwaysPreserveOrder"; - var keyAllowDuplicates = "AllowDuplicates"; - var keyCacheBust = "CacheBust"; - /*!START_DEBUG*/var debugMode = "Debug";/*!END_DEBUG*/ - var keyBasePath = "BasePath"; - - // stateless variables used across all $LAB instances - var rootPage = /^[^?#]*\//.exec(location.href)[0]; - var rootDomain = /^\w+\:\/\/\/?[^\/]+/.exec(rootPage)[0]; - var appendTo = document.head; - -/*!START_DEBUG*/ - // console.log() and console.error() wrappers + // placeholders for console output var logMsg = function NOOP(){}; var logError = logMsg; -/*!END_DEBUG*/ - - // feature sniffs (yay!) - var testScriptElem = document.createElement("script"), - var realPreloading; - - // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order - var scriptOrderedAsync = !realPreloading && testScriptElem.async === true; - -/*!START_DEBUG*/ // define console wrapper functions if applicable if (context.console && context.console.log) { if (!context.console.error) context.console.error = context.console.log; logMsg = function logMsg(msg) { context.console.log(msg); }; logError = function logError(msg,err) { context.console.error(msg,err); }; } -/*!END_DEBUG*/ + var linkElems = document.getElementsByTagName( "link" ); + // options keys + var optAlwaysPreserveOrder = "AlwaysPreserveOrder"; + var optCacheBust = "CacheBust"; + var optDebug = "Debug"; + var optBasePath = "BasePath"; + + // stateless variables used across all $LAB instances + var rootPageDir = /^[^?#]*\//.exec( location.href )[0]; + var rootURL = /^[\w\-]+\:\/\/\/?[^\/]+/.exec( rootPageDir )[0]; + var appendTo = document.head; + + // feature detections (yay!) + var realPreloading = (function featureTest(){ + // Adapted from: https://gist.github.com/igrigorik/a02f2359f3bc50ca7a9c + var tokenList = document.createElement( "link" ).relList; + try { + if (tokenList && tokenList.supports) { + return tokenList.supports( "preload" ); + } + } + catch (err) {} + return false; + })(); + var scriptOrderedAsync = document.createElement( "script" ).async === true; + var perfTiming = context.performance && context.performance.getEntriesByName; // create the main instance of $LAB return createSandbox(); @@ -52,401 +54,443 @@ // ************************************** - // make script URL absolute/canonical - function canonicalURI(src,basePath) { - var absoluteRegex = /^\w+\:\/\//; - - // is `src` is protocol-relative (begins with // or ///), prepend protocol - if (/^\/\/\/?/.test(src)) { - src = location.protocol + src; + function preloadResource(registryEntry) { + var elem = document.createElement( "link" ); + elem.setAttribute( "href", registryEntry.src ); + if (registryEntry.type == "script" || registryEntry.type == "module") { + elem.setAttribute( "as", "script" ); } - // is `src` page-relative? (not an absolute URL, and not a domain-relative path, beginning with /) - else if (!absoluteRegex.test(src) && src.charAt(0) != "/") { - // prepend `basePath`, if any - src = (basePath || "") + src; - } - - // make sure to return `src` as absolute - return absoluteRegex.test(src) ? - src : - ( - (src.charAt(0) == "/" ? rootDomain : rootPage) + src - ); + // TODO: handle more resource types + elem.setAttribute( "rel", "preload" ); + elem.setAttribute( "data-requested-with", "LABjs" ); + document.head.appendChild( elem ); + registryEntry.preloadRequested = true; + return elem; } - // merge `source` into `target` - function mergeObjs(source,target) { - for (var k in source) { - target[k] = source[k]; // TODO: does this need to be recursive for our purposes? + function loadResource(registryEntry) { + if (registryEntry.type == "script" || registryEntry.type == "module") { + var elem = document.createElement( "script" ); + elem.setAttribute( "src", registryEntry.src ); + elem.setAttribute( "data-requested-with", "LABjs" ); + elem.async = false; // ensure ordered execution + if (registryEntry.type == "module") { + elem.setAttribute( "type", "module" ); + } } - return target; + if (registryEntry.opts) { + // TODO + } + document.head.appendChild( elem ); + registryEntry.loadRequested = true; + return elem; } - - // creates a script load listener - function createScriptLoadListener(elem,registryItem,flag,onload) { - elem.onload = elem.onreadystatechange = function elemOnload() { - if ((elem.readyState && elem.readyState != "complete" && elem.readyState != "loaded") || registryItem[flag]) return; - elem.onload = elem.onreadystatechange = null; - onload(); - }; + function throwGlobalError(err) { + setTimeout( function globalError(){ throw err; }, 0 ); } - // script executed handler - function scriptExecuted(registryItem) { - registryItem.ready = registryItem.finished = true; - for (var i=0; i 0) { - val = queue.shift(); - $L = $L[val.type].apply(null,val.args); - } - return $L; - }, - - // rollback `context.$LAB` to what it was before this file - // was loaded, then return this current instance of $LAB - noConflict: function onConflict(){ - context.$LAB = old$LAB; - return instanceAPI; - }, - - // create another clean instance of $LAB - sandbox: function sandbox(){ - return createSandbox(); - } + var publicAPI = { + setGlobalDefaults: setGlobalDefaults, + setOptions: setOptions, + script: script, + wait: wait, + sandbox: createSandbox, + noConflict: noConflict, }; - return instanceAPI; + return publicAPI; // ************************************** - // execute a script that has been preloaded already - function executePreloadedScript(chainOpts,scriptObj,registryItem) { - var script; + function registerMarkupLinks() { + for (var i = 0; i < linkElems.length; i++) { + (function loopScope(elem){ + if ( + elem && + /\bpreload\b/i.test( elem.getAttribute( "rel" ) ) + ) { + // must have the `as` attribute + var preloadAs = elem.getAttribute( "as" ); + if (!preloadAs) return; + + // canonicalize resource URL and look it up in the global performance table + var href = elem.getAttribute( "href" ); + href = canonicalURL( href, document.baseURI ); // TODO: fix document.baseURI here + var perfEntries = context.performance.getEntriesByName( href ); + + // already registered this resource? + if (href in registry) return; + + // add global registry entry + var registryEntry = new createRegistryEntry( null, href ); + registryEntry.preloadRequested = true; + registry[href] = registryEntry; + + // resource already (pre)loaded? + if (perfEntries.length > 0) { + registryEntry.preloaded = true; + console.log( "markup link already preloaded", href ); + } + else { + console.log( "listening for markup link preload", href ); + // listen for preload to complete + elem.addEventListener( "load", function resourcePreloaded(){ + console.log("markup link preloaded!",href); + elem.removeEventListener( "load", resourcePreloaded ); + registryEntry.preloaded = true; + notifyRegistryListeners( registryEntry ); + } ); + } + } + })( linkElems[i] ); + } + } + + // rollback `context.$LAB` to what it was before this file + // was loaded, then return this current instance of $LAB + function noConflict() { + context.$LAB = old$LAB; + return publicAPI; + } - if (registry[scriptObj.src].finished) return; - if (!chainOpts[keyAllowDuplicates]) registry[scriptObj.src].finished = true; + function setGlobalDefaults(opts) { + defaults = assign( defaults, opts ); + return publicAPI; + } - script = registryItem.elem || document.createElement("script"); - if (scriptObj.type) script.type = scriptObj.type; - if (scriptObj.charset) script.charset = scriptObj.charset; - createScriptLoadListener(script,registryItem,"finished",preloadExecuteFinished); + function setOptions() { + return createChainInstance().setOptions.apply( null, arguments ); + } - script.src = scriptObj.realSrc; + function script() { + return createChainInstance().script.apply( null, arguments ); + } - appendTo.insertBefore(script,appendTo.firstChild); + function wait() { + return createChainInstance().wait.apply( null, arguments ); + } - // ************************************** + function createGroupEntry(check) { + this.isGroup = true; + this.resources = []; + this.ready = false; + this.complete = false; + this.check = check || function(){}; + } - function preloadExecuteFinished() { - if (script != null) { // make sure this only ever fires once - script = null; - scriptExecuted(registryItem); - } - } + function createRegistryEntry(type,src) { + this.type = type; + this.src = src; + this.listeners = []; + this.preloadRequested = false; + this.preloaded = false; + this.loadRequested = false; + this.complete = false; + this.opts = null; } - // process the script request setup - function setupScript(chainOpts,scriptObj,chainGroup,preloadThisScript) { - var registryItem; - var registryItems; - - scriptObj.src = canonicalURI(scriptObj.src,chainOpts[keyBasePath]); - scriptObj.realSrc = scriptObj.src + - // append cache-bust param to URL? - (chainOpts[keyCacheBust] ? ((/\?.*$/.test(scriptObj.src) ? "&_" : "?_") + ~~(Math.random()*1E9) + "=") : "") - ; - - if (!registry[scriptObj.src]) { - registry[scriptObj.src] = { - items: [], - finished: false - }; + function registerResource(resourceRecord) { + // registry entry doesn't exist yet? + if (!(resourceRecord.src in registry)) { + registry[resourceRecord.src] = new createRegistryEntry( resourceRecord.type, resourceRecord.src ); } - registryItems = registry[scriptObj.src].items; - - // allowing duplicates, or is this the first recorded load of this script? - if (chainOpts[keyAllowDuplicates] || registryItems.length == 0) { - registryItem = registryItems[registryItems.length] = { - ready: false, - finished: false, - readyListeners: [onReady], - finishedListeners: [onFinished] - }; - - requestScript(chainOpts,scriptObj,registryItem, - // which callback type to pass? - ( - (preloadThisScript) ? // depends on script-preloading - function onScriptPreloaded(){ - registryItem.ready = true; - for (var i=0; i 0) { + for (var i = 0; i < arguments.length; i++) { + addChainWait( arguments[i] ); } - execCursor++; } - // we've reached the end of the chain (so far) - if (execCursor == chain.length) { - scriptsCurrentlyLoading = false; - group = false; + else { + // placeholder wait entry + addChainWait( /*waitEntry=*/true ); + } + scheduleChainCheck(); + + return chainAPI; + } + + function addChainResource(resourceType,resourceRecord) { + // need to add next group to chain? + if ( + chain.length == 0 || + !chain[chain.length - 1].isGroup || + chain[chain.length - 1].complete || + (!realPreloading && !scriptOrderedAsync) || // TODO: only apply these checks for scripts + (chainOpts[optAlwaysPreserveOrder] && !scriptOrderedAsync) + ) { + var groupEntry = new createGroupEntry( scheduleChainCheck ); + chain.push( groupEntry ); + } + + var currentGroup = chain[chain.length - 1]; + currentGroup.complete = false; + + // format resource record and canonicalize URL + if (typeof resourceRecord == "string") { + resourceRecord = { + type: resourceType, + src: canonicalURL( resourceRecord, chainOpts[optBasePath] ), + group: currentGroup, + }; + } + else { + resourceRecord = { + type: resourceType, + src: canonicalURL( resourceRecord.src, chainOpts[optBasePath] ), + opts: resourceRecord, + group: currentGroup, + }; + } + + // add resource to current group + currentGroup.resources.push( resourceRecord ); + + // add/lookup resource in the global registry + var registryEntry = registerResource( resourceRecord ); + + // need to start preloading this resource? + if (realPreloading && !registryEntry.preloadRequested) { + var elem = preloadResource( registryEntry ); + + // listen for preload to complete + elem.addEventListener( "load", function resourcePreloaded(){ + console.log("resource preloaded!",resourceRecord.src); + elem.removeEventListener( "load", resourcePreloaded ); + elem.parentNode.removeChild( elem ); + registryEntry.preloaded = true; + notifyRegistryListeners( registryEntry ); + } ); + } + } + + function addChainWait(waitEntry) { + // need empty group placeholder at beginning of chain? + if (chain.length == 0) { + var groupEntry = new createGroupEntry(); + groupEntry.ready = groupEntry.complete = true; + chain.push( groupEntry ); } + + chain.push( {wait: waitEntry} ); } - // setup next chain script group - function initScriptChainGroup() { - if (!group || !group.scripts) { - chain.push(group = {scripts:[],finished:true}); + function scheduleChainCheck() { + if (checkHook == null) { + checkHook = setTimeout( advanceChain, 0 ); } } - // start loading one or more scripts - function script(){ - for (var i=0; i 0) { - for (var i=0; i