inline-critical/index.js

166 lines
4.9 KiB
JavaScript
Raw Normal View History

2014-08-04 00:01:39 +02:00
/**
* Module to inline styles while loading the existing stylesheets async
*
* @author Ben Zörb @bezoerb https://github.com/bezoerb
* @copyright Copyright (c) 2014 Ben Zörb
*
* Licensed under the MIT license.
* http://bezoerb.mit-license.org/
* All rights reserved.
*/
'use strict';
2017-03-20 22:42:19 +01:00
const fs = require('fs');
const path = require('path');
2018-12-20 07:18:51 +01:00
const isString = require('lodash/isString');
const isRegExp = require('lodash/isRegExp');
2017-03-20 22:42:19 +01:00
const reaver = require('reaver');
const slash = require('slash');
2018-12-30 23:41:29 +01:00
const Dom = require('./src/dom');
const {prettifyCss, extractCss} = require('./src/css');
2018-12-20 07:18:51 +01:00
const DEFAULT_OPTIONS = {
minify: true,
extract: false,
polyfill: true,
ignore: [],
2018-12-30 23:41:29 +01:00
replaceStylesheets: [],
2018-12-20 07:18:51 +01:00
};
2014-11-25 17:21:43 +01:00
/**
* Fixup slashes in file paths for windows
*
2018-12-18 12:51:30 +01:00
* @param {string} str Filepath
* @return {string} Normalized path
2014-11-25 17:21:43 +01:00
*/
function normalizePath(str) {
2018-12-18 12:51:30 +01:00
return process.platform === 'win32' ? slash(str) : str;
2014-11-25 17:21:43 +01:00
}
2014-08-04 00:01:39 +02:00
2018-12-18 12:51:30 +01:00
/**
* Main function ;)
* @param {string} html HTML String
* @param {string} styles CSS String
* @param {object} options Options
* @returns {string} HTML Source with inlined critical css
*/
function inline(html, styles, options) {
2018-12-20 07:18:51 +01:00
const o = {...DEFAULT_OPTIONS, ...(options || {})};
if (!isString(html)) {
2018-12-18 12:51:30 +01:00
html = String(html);
}
2018-12-30 23:41:29 +01:00
if (!Array.isArray(o.ignore)) {
o.ignore = [o.ignore].filter(i => i);
}
2018-12-18 12:51:30 +01:00
2018-12-30 23:41:29 +01:00
const document = new Dom(html, o);
2018-12-30 23:41:29 +01:00
const inlineStyles = document.getInlineStyles();
const extarnalStyles = document.getExternalStyles();
const missingStyles = extractCss(styles, ...inlineStyles);
2018-12-20 07:18:51 +01:00
2018-12-30 23:41:29 +01:00
const links = extarnalStyles.filter(link => {
// Only take stylesheets
const stylesheet = link.getAttribute('rel') === 'stylesheet';
// Filter ignored links
const href = link.getAttribute('href');
return stylesheet && !o.ignore.some(i => (isRegExp(i) && i.test(href)) || i === href);
2018-12-18 12:51:30 +01:00
});
2018-12-30 23:41:29 +01:00
const targetSelectors = [
o.selector,
':not(noscript) > link[rel="stylesheet"]',
':not(noscript) > link[rel="preload"][as="style"]',
'head script',
];
2018-12-18 12:51:30 +01:00
2018-12-30 23:41:29 +01:00
const target = document.querySelector(targetSelectors);
const inlined = `${inlineStyles}\n${missingStyles}`;
2018-12-18 12:51:30 +01:00
2018-12-30 23:41:29 +01:00
if (missingStyles) {
if (o.minify) {
document.addInlineStyles(missingStyles, target);
2018-12-18 12:51:30 +01:00
} else {
2018-12-30 23:41:29 +01:00
document.addInlineStyles(prettifyCss(missingStyles, document.indent), target);
2016-04-18 06:35:08 +02:00
}
2018-12-18 12:51:30 +01:00
}
2018-12-30 23:41:29 +01:00
if (o.replaceStylesheets.length > 0 && links.length > 0) {
// Detect links to be removed
const [ref] = links;
const removable = [...document.querySelectorAll('link[rel="stylesheet"], link[rel="preload"][as="style"]')].filter(
link => {
// Filter ignored links
const href = link.getAttribute('href');
return !o.ignore.some(i => (isRegExp(i) && i.test(href)) || i === href);
2018-12-18 12:51:30 +01:00
}
2018-12-30 23:41:29 +01:00
);
// Add link tags before old links
// eslint-disable-next-line array-callback-return
o.replaceStylesheets.map(href => {
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', href);
const noscript = document.createElement('noscript');
noscript.append(link.cloneNode());
2018-12-18 12:51:30 +01:00
2018-12-30 23:41:29 +01:00
link.setAttribute('rel', 'preload');
link.setAttribute('as', 'style');
link.setAttribute('onload', "this.onload=null;this.rel='stylesheet'");
2018-12-18 12:51:30 +01:00
2018-12-30 23:41:29 +01:00
document.insertBefore(link, ref);
document.insertBefore(noscript, ref);
});
// Remove old links
// eslint-disable-next-line array-callback-return
removable.map(link => {
if (link.parentElement.tagName === 'NOSCRIPT') {
document.remove(link.parentElement);
} else {
document.remove(link);
}
});
} else {
// Modify links and add clones to noscript block
// eslint-disable-next-line array-callback-return
links.map(link => {
2018-12-18 12:51:30 +01:00
if (o.extract) {
2018-12-30 23:41:29 +01:00
const href = link.getAttribute('href');
const file = path.resolve(path.join(o.basePath || process.cwd, href));
2018-12-18 12:51:30 +01:00
if (fs.existsSync(file)) {
const orig = fs.readFileSync(file);
2018-12-30 23:41:29 +01:00
const diff = extractCss(orig, inlined, o.minify);
const filename = reaver.rev(file, diff);
2018-12-18 12:51:30 +01:00
fs.writeFileSync(filename, diff);
2018-12-30 23:41:29 +01:00
link.setAttribute('href', normalizePath(reaver.rev(href, diff)));
} else if (!/\/\//.test(href)) {
throw new Error(`Error: file "${href}" not found in "${o.basePath || process.cwd}". Specify base path.`);
2018-12-18 12:51:30 +01:00
}
}
2018-11-29 17:35:58 +01:00
2018-12-30 23:41:29 +01:00
const noscript = document.createElement('noscript');
noscript.append(link.cloneNode());
document.insertAfter(noscript, link);
2018-12-30 23:41:29 +01:00
link.setAttribute('rel', 'preload');
link.setAttribute('as', 'style');
link.setAttribute('onload', "this.onload=null;this.rel='stylesheet'");
2015-11-13 22:59:10 +01:00
});
2018-12-18 12:51:30 +01:00
}
2018-12-30 23:41:29 +01:00
// Add loadcss if it's not already loaded
if (o.polyfill) {
document.maybeAddLoadcss();
}
2015-11-13 22:59:10 +01:00
2018-12-30 23:41:29 +01:00
return Buffer.from(document.serialize());
2018-12-18 12:51:30 +01:00
}
2015-02-17 00:59:53 +01:00
2018-12-18 12:51:30 +01:00
module.exports = inline;