asdf-games/node_modules/reload-css/index.js

245 lines
6.3 KiB
JavaScript

var qs = require('query-string');
var URL = require('./lib/url');
var baseHosts = getBaseHosts();
module.exports = function (url, opt) {
// by default, only reloads local style sheets
var localOnly = true;
if (opt && opt.local === false) {
localOnly = false;
}
// determine base URL
var baseUrl = document.location.pathname;
var baseTag = document.querySelector('base');
if (baseTag) {
baseUrl = baseTag.getAttribute('href');
var parsedBase = URL.parse(baseUrl);
parsedBase.pathname = '/';
parsedBase.hash = null;
parsedBase.query = null;
parsedBase.search = null;
baseUrl = URL.format(parsedBase);
}
// Find all <link> and <style> tags
var nodes = [ 'link', 'style' ]
.map(elements)
.reduce(function (a, b) {
return a.concat(b);
}, [])
.filter(function (el) {
return filterStyleSheet(el, localOnly);
})
.map(function (el) {
var data = {
element: el
};
var href = el.getAttribute('href');
if (el.tagName === 'LINK' && href) {
data.key = URL.key(href, baseUrl);
}
return data;
});
// Now gather all imports in those tags
var imports = [];
nodes.forEach(function (node) {
recursiveFindImports(node, node.element.sheet, imports, baseUrl);
});
// Now try to update the matched URLs
var keyToMatch = url ? URL.key(url, baseUrl) : null;
var matchImports = imports;
if (keyToMatch) {
// only match target imports
matchImports = matchImports.filter(function (imported) {
return imported.key === keyToMatch;
});
}
// Map them to the "top most" import that needs update
// This isn't actually the root, just the most shallow
// style sheet we need to update for it to work in Chrome/FF/Safari
// (Chrome has an issue where updating a deep import will break)
matchImports = matchImports.map(getTopmostImport);
// Filter out any potential duplicate top most imports
// And reverse so we update deep to shallow
matchImports = uniq(matchImports).reverse();
// Now cache bust each import
matchImports.forEach(bust);
// Now find any URLs referenced by a <link> tag
var matchLinks = nodes.filter(function (node) {
// no keyToMatch just means bust all link tags
var isMatch = keyToMatch
? node.key === keyToMatch
: true;
return node.element.tagName === 'LINK' && isMatch;
});
// And re-attach each link tag
matchLinks.forEach(function (node) {
node.element = reattachLink(node.element);
});
};
function bust (imported) {
if (!imported.busted) {
imported.rule = cacheBustImportRule(imported.rule, imported.index);
}
imported.busted = true;
return imported;
}
function reattachLink (link, cb) {
var href = link.getAttribute('href');
var cloned = link.cloneNode(false);
cloned.href = getCacheBustUrl(href);
var parent = link.parentNode;
if (parent.lastChild === link) {
parent.appendChild(cloned);
} else {
parent.insertBefore(cloned, link.nextSibling);
}
cloned.onload = function () {
if (link.parentNode) link.parentNode.removeChild(link);
if (cb) cb();
};
return cloned;
}
function filterStyleSheet (element, localOnly) {
if (isPrintMedia(element)) return false;
if (element.tagName === 'LINK') {
if (!element.getAttribute('href')) return false;
if (localOnly && !isLocalStylesheet(element)) return false;
}
return true;
}
function isLocalStylesheet (link) {
var href = link.getAttribute('href');
if (!href || link.getAttribute('rel') !== 'stylesheet') return false;
var parsed = URL.parse(href);
if (parsed.protocol && parsed.protocol !== window.document.location.protocol) {
// different protocol, let's assume not local
return false;
}
if (parsed.host) {
// see if domain matches
return baseHosts.indexOf(parsed.host.toLowerCase()) >= 0;
}
// no host / protocol... assume relative and thus local
return true;
}
function uniq (list) {
var result = [];
list.forEach(function (item) {
if (result.indexOf(item) === -1) {
result.push(item);
}
});
return result;
}
function isPrintMedia (link) {
return link.getAttribute('media') === 'print';
}
function elements (tag) {
return Array.prototype.slice.call(document.getElementsByTagName(tag));
}
function getBaseHosts () {
var baseHosts = [
'localhost', '127.0.0.1'
].map(function (h) {
return h + ':' + window.document.location.port;
});
// handle current
if (window.document.location.hostname !== 'localhost') {
baseHosts = baseHosts.concat([ window.document.location.host ]);
}
// normalize case
return baseHosts.map(function (h) {
return h.toLowerCase();
});
}
function cacheBustImportRule (rule, index) {
var parent = rule.parentStyleSheet;
var newHref = getCacheBustUrl(rule.href);
var media = '';
try {
media = rule.media.length
? Array.prototype.join.call(rule.media, ', ')
: '';
} catch (err) {
// might get here if permission is denied for some reason
}
var newRule = '@import url("' + newHref + '") ' + media + ';';
parent.insertRule(newRule, index);
parent.deleteRule(index + 1);
return parent.cssRules[index];
}
function getTopmostImport (imported) {
var topmost = imported;
while (topmost.parentImport) {
topmost = topmost.parentImport;
}
return topmost;
}
function recursiveFindImports (node, styleSheet, result, baseUrl, lastImport) {
if (!styleSheet) return;
var rules;
try {
rules = styleSheet.cssRules;
} catch (err) {
// some sort of security error
}
if (!rules || rules.length === 0) {
return;
}
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (rule.type === window.CSSRule.IMPORT_RULE) {
var parentHref = rule.parentStyleSheet.href || document.location.href;
var absoluteHref = URL.resolve(parentHref, rule.href);
var key = URL.key(absoluteHref, baseUrl);
var newImport = {
index: i,
rule: rule,
parentImport: lastImport,
key: key,
href: rule.href
};
result.push(newImport);
recursiveFindImports(node, rule.styleSheet, result, baseUrl, newImport);
}
}
}
function getCacheBustUrl (href) {
var parsed = URL.parse(href);
var qsObj = qs.parse(parsed.search);
qsObj._livereload = String(Date.now());
parsed.query = undefined;
parsed.search = qs.stringify(qsObj);
return URL.format(parsed);
}