fix: prevent multiple loadcss includes

This commit is contained in:
bezoerb 2018-12-20 07:18:51 +01:00
parent 44c0cd38dd
commit 226bf160ea
4 changed files with 172 additions and 42 deletions

View File

@ -11,6 +11,9 @@
'use strict';
const fs = require('fs');
const path = require('path');
const isString = require('lodash/isString');
const isRegExp = require('lodash/isRegExp');
const filter = require('lodash/filter');
const _ = require('lodash');
const UglifyJS = require('uglify-js');
const reaver = require('reaver');
@ -25,6 +28,14 @@ const resolve = require('resolve');
const detectIndent = require('detect-indent');
const prettier = require('prettier');
const DEFAULT_OPTIONS = {
minify: true,
extract: false,
polyfill: true,
ignore: [],
stylesheets: [],
};
/**
* Get loadcss + cssrelpreload script
*
@ -86,10 +97,15 @@ function prettifyCSS(styles) {
return prettier.format(styles, {parser: 'css'});
}
function extract(css, critical) {
css = minifyCSS(css);
critical = minifyCSS(critical);
return normalizeNewline(postcss(discard({css: critical})).process(css).css);
function extract(css, critical, minify = true) {
const minCss = minifyCSS(css);
const minCritical = minifyCSS(critical);
const diff = normalizeNewline(postcss(discard({css: minCritical})).process(minCss).css);
if (minify) {
return diff;
}
return prettifyCSS(diff);
}
/**
@ -119,34 +135,26 @@ const getSvgs = (str = '') => {
* @returns {string} HTML Source with inlined critical css
*/
function inline(html, styles, options) {
if (!_.isString(html)) {
const o = {...DEFAULT_OPTIONS, ...(options || {})};
if (!isString(html)) {
html = String(html);
}
const o = _.assign(
{
minify: true,
},
options || {}
);
const $ = cheerio.load(html, {
decodeEntities: false,
});
// Fetch styles already inlined
// Process style tags
const inlineStyles = $('head style')
.map((i, el) => $(el).html())
.get()
.join('\n');
// Only missing styles
let missing = extract(styles, inlineStyles);
if (!o.minify) {
missing = prettifyCSS(missing);
}
// Only inline the missing styles
const missing = extract(styles, inlineStyles, o.minify);
const inlined = `${inlineStyles}\n${missing}`;
const allLinks = $('link[rel="stylesheet"], link[rel="preload"][as="style"]').filter(function() {
return !$(this).parents('noscript').length;
});
@ -158,18 +166,14 @@ function inline(html, styles, options) {
const targetIndent = getIndent(html, target);
const $target = $(target);
if (_.isString(o.ignore)) {
o.ignore = [o.ignore];
if (!Array.isArray(o.ignore)) {
o.ignore = [o.ignore].filter(i => i);
}
if (o.ignore) {
links = _.filter(links, link => {
if (o.ignore.length > 0) {
links = filter(links, link => {
const href = $(link).attr('href');
return (
_.findIndex(o.ignore, arg => {
return (_.isRegExp(arg) && arg.test(href)) || arg === href;
}) === -1
);
return !o.ignore.some(i => (isRegExp(i) && i.test(href)) || i === href);
});
}
@ -211,12 +215,7 @@ function inline(html, styles, options) {
const file = path.resolve(path.join(o.basePath, href));
if (fs.existsSync(file)) {
const orig = fs.readFileSync(file);
let diff = extract(orig, inlined);
if (!o.minify) {
diff = prettifyCSS(diff);
}
const diff = extract(orig, inlined, o.minify);
const filename = reaver.rev(file, diff);
fs.writeFileSync(filename, diff);
@ -233,15 +232,22 @@ function inline(html, styles, options) {
$el.attr('onload', "this.onload=null;this.rel='stylesheet'");
});
// Add loadcss + cssrelpreload polyfill
const scriptAnchor = $('link[rel="stylesheet"], noscript')
.filter(function() {
return !$(this).parents('noscript').length;
})
.last()
.get(0);
// Only add loadcss if it's not already included
const loadCssIncluded = $('script')
.get()
.some(tag => ($(tag).html() || '').includes('loadCSS'));
$(scriptAnchor).after('\n' + targetIndent + '<script>' + getScript() + '</script>');
if (!loadCssIncluded && o.polyfill) {
// Add loadcss + cssrelpreload polyfill
const scriptAnchor = $('link[rel="stylesheet"], noscript')
.filter(function() {
return !$(this).parents('noscript').length;
})
.last()
.get(0);
$(scriptAnchor).after('\n' + targetIndent + '<script>' + getScript() + '</script>');
}
}
const output = $.html();

View File

@ -0,0 +1,59 @@
<!doctype html>
<html class="no-js">
<head>
<meta charset="utf-8">
<title>critical css test</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css styles/main.css -->
<style>
body{padding-top:20px;padding-bottom:20px}.header{padding-left:15px;padding-right:15px}.header{border-bottom:1px solid #e5e5e5}.header h3{margin-top:0;margin-bottom:0;line-height:40px;padding-bottom:19px}.jumbotron{text-align:center;border-bottom:1px solid #e5e5e5}.jumbotron .btn{font-size:21px;padding:14px 24px}@media screen and (min-width:768px){.container{max-width:730px}.header{padding-left:0;padding-right:0}.header{margin-bottom:30px}.jumbotron{border-bottom:0}}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:0 0}h1{margin:.67em 0;font-size:2em}@media print{*{color:#000!important;text-shadow:none!important;background:0 0!important;box-shadow:none!important}a{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}a[href^="#"]:after{content:""}h3,p{orphans:3;widows:3}h3{page-break-after:avoid}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:transparent}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}a{color:#428bca;text-decoration:none}h1,h3{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1,h3{margin-top:20px;margin-bottom:10px}h1{font-size:36px}h3{font-size:24px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.text-muted{color:#999}ul{margin-top:0;margin-bottom:10px}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a{color:#fff;background-color:#428bca}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1{font-size:63px}}.container:after,.container:before,.nav:after,.nav:before{display:table;content:" "}.container:after,.nav:after{clear:both}.pull-right{float:right!important}
</style>
<link rel="preload" href="css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="css/main.css"></noscript>
<script id="loadcss">
!function(t){"use strict";t.loadCSS||(t.loadCSS=function(){});var e=loadCSS.relpreload={};if(e.support=function(){var e;try{e=t.document.createElement("link").relList.supports("preload")}catch(t){e=!1}return function(){return e}}(),e.bindMediaToggle=function(t){function e(){t.media=a}var a=t.media||"all";t.addEventListener?t.addEventListener("load",e):t.attachEvent&&t.attachEvent("onload",e),setTimeout(function(){t.rel="stylesheet",t.media="only x"}),setTimeout(e,3e3)},e.poly=function(){if(!e.support())for(var a=t.document.getElementsByTagName("link"),n=0;n<a.length;n++){var o=a[n];"preload"!==o.rel||"style"!==o.getAttribute("as")||o.getAttribute("data-loadcss")||(o.setAttribute("data-loadcss",!0),e.bindMediaToggle(o))}},!e.support()){e.poly();var a=t.setInterval(e.poly,500);t.addEventListener?t.addEventListener("load",function(){e.poly(),t.clearInterval(a)}):t.attachEvent&&t.attachEvent("onload",function(){e.poly(),t.clearInterval(a)})}"undefined"!=typeof exports?exports.loadCSS=loadCSS:t.loadCSS=loadCSS}("undefined"!=typeof global?global:this);
</script>
<!-- endbuild -->
</head>
<body>
<!--[if lt IE 10]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div class="container">
<div class="header">
<ul class="nav nav-pills pull-right">
<li class="active"><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<h3 class="text-muted">critical css test</h3>
</div>
<div class="jumbotron">
<h1>'Allo, 'Allo!</h1>
<p class="lead">Always a pleasure scaffolding your apps.</p>
<p><a class="btn btn-lg btn-success" href="#">Splendid!</a></p>
</div>
<div class="row marketing">
<div class="col-lg-6">
<h4>HTML5 Boilerplate</h4>
<p>HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.</p>
<h4>Bootstrap</h4>
<p>Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.</p>
</div>
</div>
<div class="footer">
<p>♥ from the Yeoman team</p>
</div>
</div>
</body>
</html>

55
test/fixtures/loadcss-again.html vendored Normal file
View File

@ -0,0 +1,55 @@
<!doctype html>
<html class="no-js">
<head>
<meta charset="utf-8">
<title>critical css test</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css styles/main.css -->
<link rel="stylesheet" href="css/main.css">
<script id="loadcss">
!function(t){"use strict";t.loadCSS||(t.loadCSS=function(){});var e=loadCSS.relpreload={};if(e.support=function(){var e;try{e=t.document.createElement("link").relList.supports("preload")}catch(t){e=!1}return function(){return e}}(),e.bindMediaToggle=function(t){function e(){t.media=a}var a=t.media||"all";t.addEventListener?t.addEventListener("load",e):t.attachEvent&&t.attachEvent("onload",e),setTimeout(function(){t.rel="stylesheet",t.media="only x"}),setTimeout(e,3e3)},e.poly=function(){if(!e.support())for(var a=t.document.getElementsByTagName("link"),n=0;n<a.length;n++){var o=a[n];"preload"!==o.rel||"style"!==o.getAttribute("as")||o.getAttribute("data-loadcss")||(o.setAttribute("data-loadcss",!0),e.bindMediaToggle(o))}},!e.support()){e.poly();var a=t.setInterval(e.poly,500);t.addEventListener?t.addEventListener("load",function(){e.poly(),t.clearInterval(a)}):t.attachEvent&&t.attachEvent("onload",function(){e.poly(),t.clearInterval(a)})}"undefined"!=typeof exports?exports.loadCSS=loadCSS:t.loadCSS=loadCSS}("undefined"!=typeof global?global:this);
</script>
<!-- endbuild -->
</head>
<body>
<!--[if lt IE 10]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div class="container">
<div class="header">
<ul class="nav nav-pills pull-right">
<li class="active"><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<h3 class="text-muted">critical css test</h3>
</div>
<div class="jumbotron">
<h1>'Allo, 'Allo!</h1>
<p class="lead">Always a pleasure scaffolding your apps.</p>
<p><a class="btn btn-lg btn-success" href="#">Splendid!</a></p>
</div>
<div class="row marketing">
<div class="col-lg-6">
<h4>HTML5 Boilerplate</h4>
<p>HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.</p>
<h4>Bootstrap</h4>
<p>Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.</p>
</div>
</div>
<div class="footer">
<p>♥ from the Yeoman team</p>
</div>
</div>
</body>
</html>

View File

@ -299,3 +299,13 @@ test('consider existing style tags', async () => {
expect(out.toString('utf-8')).toBe(expected);
});
test("don't add loadcss twice", async () => {
const html = await read('fixtures/loadcss-again.html');
const css = await read('fixtures/critical.css');
const expected = await read('expected/loadcss-again.html');
const out = inline(html, css);
expect(out.toString('utf-8')).toBe(expected);
});