un-css
Version:
Remove unused CSS styles
237 lines (216 loc) • 7.81 kB
JavaScript
/* jshint phantom: true */
/* eslint-env phantomjs */
/* globals document: true */
var path = require('path'),
phridge = require('phridge'),
promise = require('bluebird'),
utility = require('./utility'),
_ = require('lodash');
var phantom;
/**
* Create the PhantomJS instances, or use the given one.
* @param {Object} instance The instance to use, if there is one
* @return {promise}
*/
function init(instance, options) {
if (instance) {
phantom = instance;
return null;
}
// Convert to bluebird promise
return new promise(function (resolve) {
resolve(phridge.spawn(options));
}).then(function (ph) {
/* Phridge outputs everything to stdout by default */
ph.childProcess.cleanStdout.unpipe();
ph.childProcess.cleanStdout.pipe(process.stderr);
phantom = ph;
}).disposer(phridge.disposeAll);
}
/**
* This function is called whenever a resource is requested by PhantomJS.
* If we are loading either raw HTML or a local page, PhantomJS needs to be able to find the
* resource with an absolute path.
* There are two possible cases:
* - 'file://': This might be either a protocol-less URL or a relative path. Since we
* can't handle both, we choose to handle the former.
* - 'file:///': This is an absolute path. If options.htmlroot is specified, we have a chance to
* redirect the request to the correct location.
*/
function ResourceHandler(htmlroot, isWindows, resolve) {
var ignoredExtensions = ['\\.css', '\\.png', '\\.gif', '\\.jpg', '\\.jpeg', ''],
ignoredEndpoints = ['fonts\\.googleapis'];
var ignoreRequests = new RegExp(ignoredExtensions.join('$|') + ignoredEndpoints.join('|'));
this.onResourceRequested = function (requestData, networkRequest) {
var originalUrl = requestData.url,
url = originalUrl.split('?')[0].split('#')[0];
if (url.substr(-3) === '.js' && url.substr(0, 7) === 'file://') {
/* Try and match protocol-less URLs and absolute ones.
* Relative URLs will still not load.
*/
if (url.substr(5, 3) === '///') {
/* Absolute URL
* Retry loading the resource appending the htmlroot option
*/
if (isWindows) {
/* Do not strip leading '/' */
url = originalUrl.substr(0, 8) + htmlroot + originalUrl.substr(7);
} else {
url = originalUrl.substr(0, 7) + htmlroot + originalUrl.substr(7);
}
} else {
/* Protocol-less URL */
url = 'http://' + originalUrl.substr(7);
}
networkRequest.changeUrl(url);
} else if (ignoreRequests.test(url)) {
networkRequest.abort();
}
};
resolve();
}
/**
* Helper for fromRaw, fromLocal, fromRemote;
* return the phantom page after the timeout
* has elapsed
* @param {phantom} page Page created by phantom
* @param {Object} options
* @return {promise}
*/
function resolveWithPage(page, options) {
return function () {
return new promise(function (resolve) {
setTimeout(function () {
return resolve(page);
}, options.timeout);
});
};
}
/**
* Load a page given an HTML string.
* @param {String} html
* @param {Object} options
* @return {promise}
*/
function fromRaw(html, options) {
var page = phantom.createPage(),
htmlroot = path.join(process.cwd(), options.htmlroot || '');
return page.run(htmlroot, utility.isWindows(), ResourceHandler).then(function () {
return page.run(html, function (raw) {
this.setContent(raw, 'local');
});
}).then(resolveWithPage(page, options));
}
/**
* Open a page given a filename.
* @param {String} filename
* @param {Object} options
* @return {promise}
*/
function fromLocal(filename, options) {
var page = phantom.createPage(),
htmlroot = path.join(process.cwd(), options.htmlroot || '');
return page.run(htmlroot, utility.isWindows(), ResourceHandler).then(function () {
return page.run(filename, function (source, resolve, reject) {
this.open(source, function (status) {
if (status !== 'success') {
return reject(new Error('PhantomJS: Cannot open ' + this.url));
}
resolve();
});
});
}).then(resolveWithPage(page, options));
}
/**
* Open a page given a URL.
* @param {String} url
* @param {Object} options
* @return {promise}
*/
function fromRemote(url, options) {
/* If the protocol is unspecified, default to HTTP */
if (!/^http/.test(url)) {
url = 'http:' + url;
}
return phantom.openPage(url).then(function (page) {
return resolveWithPage(page, options)();
});
}
/**
* Extract stylesheets' hrefs from dom
* @param {Object} page A PhantomJS page
* @param {Object} options Options, as passed to UnCSS
* @return {promise}
*/
function getStylesheets(page, options) {
if (_.isArray(options.media) === false) {
options.media = [options.media];
}
var media = _.union(['', 'all', 'screen'], options.media);
return page.run(function () {
return this.evaluate(function () {
return Array.prototype.map.call(document.querySelectorAll('link[rel="stylesheet"]'), function (link) {
return { href: link.href, media: link.media };
});
});
}).then(function (stylesheets) {
stylesheets = _
.toArray(stylesheets)
/* Match only specified media attributes, plus defaults */
.filter(function (sheet) {
return media.indexOf(sheet.media) !== -1;
})
.map(function (sheet) {
return sheet.href;
});
return stylesheets;
});
}
/**
* Filter unused selectors.
* @param {Object} page A PhantomJS page
* @param {Array} sels List of selectors to be filtered
* @return {promise}
*/
function findAll(page, sels) {
return page.run(sels, function (args) {
return this.evaluate(function (selectors) {
// Unwrap noscript elements
Array.prototype.forEach.call(document.getElementsByTagName('noscript'), function (ns) {
var wrapper = document.createElement('div');
wrapper.innerHTML = ns.innerText;
// Insert each child of the <noscript> as its sibling
Array.prototype.forEach.call(wrapper.children, function (child) {
ns.parentNode.insertBefore(child, ns);
});
});
// Do the filtering
selectors = selectors.filter(function (selector) {
try {
if (document.querySelector(selector)) {
return true;
}
} catch (e) {
return true;
}
});
return {
selectors: selectors
};
}, args);
}).then(function (res) {
if (res === null) {
return [];
}
return res.selectors;
});
}
module.exports = {
init: init,
fromLocal: fromLocal,
fromRaw: fromRaw,
fromRemote: fromRemote,
findAll: findAll,
getStylesheets: getStylesheets
};
;