UNPKG

react-saasify-chrisvxd

Version:

React components for Saasify web clients.

261 lines (234 loc) 8.08 kB
'use strict'; const glob = require('glob'), isHTML = require('is-html'), isURL = require('is-absolute-url'), jsdom = require('./jsdom.js'), postcss = require('postcss'), uncss = require('./lib.js'), utility = require('./utility.js'), _ = require('lodash'); /** * Get the contents of HTML pages through jsdom. * @param {Array} files List of HTML files * @param {Object} options UnCSS options * @return {Array|Promise} */ function getHTML(files, options) { if (_.isString(files)) { files = [files]; } files = _.flatten(files.map((file) => { if (!isURL(file) && !isHTML(file)) { return glob.sync(file); } return file; })); if (!files.length) { return Promise.reject(new Error('UnCSS: no HTML files found')); } // Save files for later reference. options.files = files; return Promise.all(files.map((file) => jsdom.fromSource(file, options))); } /** * Get the contents of CSS files. * @param {Array} files List of HTML files * @param {Object} options UnCSS options * @param {Array} pages Pages opened by jsdom * @return {Promise} */ function getStylesheets(files, options, pages) { if (options.stylesheets && options.stylesheets.length) { /* Simulate the behavior below */ return Promise.resolve([files, options, pages, [options.stylesheets]]); } /* Extract the stylesheets from the HTML */ return Promise.all(pages.map((page) => jsdom.getStylesheets(page.window, options))) .then((stylesheets) => [files, options, pages, stylesheets]); } /** * Get the contents of CSS files. * @param {Array} files List of HTML files * @param {Object} options UnCSS options * @param {Array} pages Pages opened by jsdom * @param {Array} stylesheets List of CSS files * @return {Array} */ function getCSS([files, options, pages, stylesheets]) { /* Ignore specified stylesheets */ if (options.ignoreSheets.length) { stylesheets = stylesheets .map((arr) => { return arr.filter((sheet) => { return _.every(options.ignoreSheets, (ignore) => { if (_.isRegExp(ignore)) { return !ignore.test(sheet); } return sheet !== ignore; }); }); }); } if (_.flatten(stylesheets).length) { /* Only run this if we found links to stylesheets (there may be none...) * files = ['some_file.html', 'some_other_file.html'] * stylesheets = [['relative_css_path.css', ...], * ['maybe_a_duplicate.css', ...]] * We need to - make the stylesheets' paths relative to the HTML files, * - flatten the array, * - remove duplicates */ stylesheets = _.chain(stylesheets) .map((sheets, i) => utility.parsePaths(files[i], sheets, options)) .flatten() .uniq() .value(); } else { /* Reset the array if we didn't find any link tags */ stylesheets = []; } return Promise.all([options, pages, utility.readStylesheets(stylesheets, options.banner)]); } /** * Do the actual work * @param {Array} files List of HTML files * @param {Object} options UnCSS options * @param {Array} pages Pages opened by jsdom * @param {Array} stylesheets List of CSS files * @return {Promise} */ function processWithTextApi([options, pages, stylesheets]) { /* If we specified a raw string of CSS, add it to the stylesheets array */ if (options.raw) { if (_.isString(options.raw)) { stylesheets.push(options.raw); } else { throw new Error('UnCSS: options.raw - expected a string'); } } /* At this point, there isn't any point in running the rest of the task if: * - We didn't specify any stylesheet links in the options object * - We couldn't find any stylesheet links in the HTML itself * - We weren't passed a string of raw CSS in addition to, or to replace * either of the above */ if (!_.flatten(stylesheets).length) { throw new Error('UnCSS: no stylesheets found'); } /* OK, so we have some CSS to work with! * Three steps: * - Parse the CSS * - Remove the unused rules * - Return the optimized CSS as a string */ const cssStr = stylesheets.join(' \n'); let pcss, report; try { pcss = postcss.parse(cssStr); } catch (err) { /* Try and construct a helpful error message */ throw utility.parseErrorMessage(err, cssStr); } return uncss(pages, pcss, options.ignore).then(([css, rep]) => { let newCssStr = ''; postcss.stringify(css, (result) => { newCssStr += result; }); if (options.report) { report = { original: cssStr, selectors: rep }; } return [newCssStr, report]; }); } /** * Main exposed function. * Here we check the options and callback, then run the files through jsdom. * @param {Array} files Array of filenames * @param {Object} [options] options * @param {Function} callback(Error, String, Object) */ function init(files, options, callback) { if (_.isFunction(options)) { /* There were no options, this argument is actually the callback */ callback = options; options = {}; } else if (!_.isFunction(callback)) { throw new TypeError('UnCSS: expected a callback'); } /* Try and read options from the specified uncssrc file */ if (options.uncssrc) { try { /* Manually-specified options take precedence over uncssrc options */ options = _.merge(utility.parseUncssrc(options.uncssrc), options); } catch (err) { if (err instanceof SyntaxError) { callback(new SyntaxError('UnCSS: uncssrc file is invalid JSON.')); return; } callback(err); return; } } /* Assign default values to options, unless specified */ options = _.merge({ banner: true, csspath: '', html: files, htmlRoot: null, ignore: [], ignoreSheets: [], inject: null, jsdom: jsdom.defaultOptions(), media: [], raw: null, report: false, stylesheets: null, timeout: 0, uncssrc: null, userAgent: 'uncss' }, options); process(options).then(([css, report]) => callback(null, css, report), callback); } function processAsPostCss(options, pages) { return uncss(pages, options.rawPostCss, options.ignore); } function process(opts) { return getHTML(opts.html, opts).then((pages) => { function cleanup (result) { pages.forEach((page) => page.window.close()); return result; } if (opts.usePostCssInternal) { return processAsPostCss(opts, pages) .then(cleanup); } return getStylesheets(opts.files, opts, pages) .then(getCSS) .then(processWithTextApi) .then(cleanup); }); } const postcssPlugin = postcss.plugin('uncss', (opts) => { let options = _.merge({ usePostCssInternal: true, // Ignore stylesheets in the HTML files; only use those from the stream ignoreSheets: [/\s*/], html: [], ignore: [], jsdom: jsdom.defaultOptions() }, opts); return function (css, result) { // eslint-disable-line no-unused-vars options = _.merge(options, { // This is used to pass the css object in to processAsPostCSS rawPostCss: css }); return process(options); }; }); module.exports = init; module.exports.postcssPlugin = postcssPlugin;