Socket
Socket
Sign inDemoInstall

critters

Package Overview
Dependencies
16
Maintainers
2
Versions
22
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.0.16 to 0.0.17

667

dist/critters.js

@@ -6,9 +6,457 @@ function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var prettyBytes = _interopDefault(require('pretty-bytes'));
var parse5 = _interopDefault(require('parse5'));
var cssSelect = require('css-select');
var treeAdapter = _interopDefault(require('parse5-htmlparser2-tree-adapter'));
var htmlparser2 = require('htmlparser2');
var domhandler = require('domhandler');
var render = _interopDefault(require('dom-serializer'));
var postcss = require('postcss');
var chalk = _interopDefault(require('chalk'));
var SelectorType;
(function (SelectorType) {
SelectorType["Attribute"] = "attribute";
SelectorType["Pseudo"] = "pseudo";
SelectorType["PseudoElement"] = "pseudo-element";
SelectorType["Tag"] = "tag";
SelectorType["Universal"] = "universal";
// Traversals
SelectorType["Adjacent"] = "adjacent";
SelectorType["Child"] = "child";
SelectorType["Descendant"] = "descendant";
SelectorType["Parent"] = "parent";
SelectorType["Sibling"] = "sibling";
SelectorType["ColumnCombinator"] = "column-combinator";
})(SelectorType || (SelectorType = {}));
var AttributeAction;
(function (AttributeAction) {
AttributeAction["Any"] = "any";
AttributeAction["Element"] = "element";
AttributeAction["End"] = "end";
AttributeAction["Equals"] = "equals";
AttributeAction["Exists"] = "exists";
AttributeAction["Hyphen"] = "hyphen";
AttributeAction["Not"] = "not";
AttributeAction["Start"] = "start";
})(AttributeAction || (AttributeAction = {}));
const reName = /^[^\\#]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\-\u00b0-\uFFFF])+/;
const reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi;
const actionTypes = new Map([
[126 /* Tilde */, AttributeAction.Element],
[94 /* Circumflex */, AttributeAction.Start],
[36 /* Dollar */, AttributeAction.End],
[42 /* Asterisk */, AttributeAction.Any],
[33 /* ExclamationMark */, AttributeAction.Not],
[124 /* Pipe */, AttributeAction.Hyphen],
]);
// Pseudos, whose data property is parsed as well.
const unpackPseudos = new Set([
"has",
"not",
"matches",
"is",
"where",
"host",
"host-context",
]);
/**
* Checks whether a specific selector is a traversal.
* This is useful eg. in swapping the order of elements that
* are not traversals.
*
* @param selector Selector to check.
*/
function isTraversal(selector) {
switch (selector.type) {
case SelectorType.Adjacent:
case SelectorType.Child:
case SelectorType.Descendant:
case SelectorType.Parent:
case SelectorType.Sibling:
case SelectorType.ColumnCombinator:
return true;
default:
return false;
}
}
const stripQuotesFromPseudos = new Set(["contains", "icontains"]);
// Unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L152
function funescape(_, escaped, escapedWhitespace) {
const high = parseInt(escaped, 16) - 0x10000;
// NaN means non-codepoint
return high !== high || escapedWhitespace
? escaped
: high < 0
? // BMP codepoint
String.fromCharCode(high + 0x10000)
: // Supplemental Plane codepoint (surrogate pair)
String.fromCharCode((high >> 10) | 0xd800, (high & 0x3ff) | 0xdc00);
}
function unescapeCSS(str) {
return str.replace(reEscape, funescape);
}
function isQuote(c) {
return c === 39 /* SingleQuote */ || c === 34 /* DoubleQuote */;
}
function isWhitespace(c) {
return (c === 32 /* Space */ ||
c === 9 /* Tab */ ||
c === 10 /* NewLine */ ||
c === 12 /* FormFeed */ ||
c === 13 /* CarriageReturn */);
}
/**
* Parses `selector`, optionally with the passed `options`.
*
* @param selector Selector to parse.
* @param options Options for parsing.
* @returns Returns a two-dimensional array.
* The first dimension represents selectors separated by commas (eg. `sub1, sub2`),
* the second contains the relevant tokens for that selector.
*/
function parse(selector) {
const subselects = [];
const endIndex = parseSelector(subselects, `${selector}`, 0);
if (endIndex < selector.length) {
throw new Error(`Unmatched selector: ${selector.slice(endIndex)}`);
}
return subselects;
}
function parseSelector(subselects, selector, selectorIndex) {
let tokens = [];
function getName(offset) {
const match = selector.slice(selectorIndex + offset).match(reName);
if (!match) {
throw new Error(`Expected name, found ${selector.slice(selectorIndex)}`);
}
const [name] = match;
selectorIndex += offset + name.length;
return unescapeCSS(name);
}
function stripWhitespace(offset) {
selectorIndex += offset;
while (selectorIndex < selector.length &&
isWhitespace(selector.charCodeAt(selectorIndex))) {
selectorIndex++;
}
}
function readValueWithParenthesis() {
selectorIndex += 1;
const start = selectorIndex;
let counter = 1;
for (; counter > 0 && selectorIndex < selector.length; selectorIndex++) {
if (selector.charCodeAt(selectorIndex) ===
40 /* LeftParenthesis */ &&
!isEscaped(selectorIndex)) {
counter++;
}
else if (selector.charCodeAt(selectorIndex) ===
41 /* RightParenthesis */ &&
!isEscaped(selectorIndex)) {
counter--;
}
}
if (counter) {
throw new Error("Parenthesis not matched");
}
return unescapeCSS(selector.slice(start, selectorIndex - 1));
}
function isEscaped(pos) {
let slashCount = 0;
while (selector.charCodeAt(--pos) === 92 /* BackSlash */)
slashCount++;
return (slashCount & 1) === 1;
}
function ensureNotTraversal() {
if (tokens.length > 0 && isTraversal(tokens[tokens.length - 1])) {
throw new Error("Did not expect successive traversals.");
}
}
function addTraversal(type) {
if (tokens.length > 0 &&
tokens[tokens.length - 1].type === SelectorType.Descendant) {
tokens[tokens.length - 1].type = type;
return;
}
ensureNotTraversal();
tokens.push({ type });
}
function addSpecialAttribute(name, action) {
tokens.push({
type: SelectorType.Attribute,
name,
action,
value: getName(1),
namespace: null,
ignoreCase: "quirks",
});
}
/**
* We have finished parsing the current part of the selector.
*
* Remove descendant tokens at the end if they exist,
* and return the last index, so that parsing can be
* picked up from here.
*/
function finalizeSubselector() {
if (tokens.length &&
tokens[tokens.length - 1].type === SelectorType.Descendant) {
tokens.pop();
}
if (tokens.length === 0) {
throw new Error("Empty sub-selector");
}
subselects.push(tokens);
}
stripWhitespace(0);
if (selector.length === selectorIndex) {
return selectorIndex;
}
loop: while (selectorIndex < selector.length) {
const firstChar = selector.charCodeAt(selectorIndex);
switch (firstChar) {
// Whitespace
case 32 /* Space */:
case 9 /* Tab */:
case 10 /* NewLine */:
case 12 /* FormFeed */:
case 13 /* CarriageReturn */: {
if (tokens.length === 0 ||
tokens[0].type !== SelectorType.Descendant) {
ensureNotTraversal();
tokens.push({ type: SelectorType.Descendant });
}
stripWhitespace(1);
break;
}
// Traversals
case 62 /* GreaterThan */: {
addTraversal(SelectorType.Child);
stripWhitespace(1);
break;
}
case 60 /* LessThan */: {
addTraversal(SelectorType.Parent);
stripWhitespace(1);
break;
}
case 126 /* Tilde */: {
addTraversal(SelectorType.Sibling);
stripWhitespace(1);
break;
}
case 43 /* Plus */: {
addTraversal(SelectorType.Adjacent);
stripWhitespace(1);
break;
}
// Special attribute selectors: .class, #id
case 46 /* Period */: {
addSpecialAttribute("class", AttributeAction.Element);
break;
}
case 35 /* Hash */: {
addSpecialAttribute("id", AttributeAction.Equals);
break;
}
case 91 /* LeftSquareBracket */: {
stripWhitespace(1);
// Determine attribute name and namespace
let name;
let namespace = null;
if (selector.charCodeAt(selectorIndex) === 124 /* Pipe */) {
// Equivalent to no namespace
name = getName(1);
}
else if (selector.startsWith("*|", selectorIndex)) {
namespace = "*";
name = getName(2);
}
else {
name = getName(0);
if (selector.charCodeAt(selectorIndex) === 124 /* Pipe */ &&
selector.charCodeAt(selectorIndex + 1) !==
61 /* Equal */) {
namespace = name;
name = getName(1);
}
}
stripWhitespace(0);
// Determine comparison operation
let action = AttributeAction.Exists;
const possibleAction = actionTypes.get(selector.charCodeAt(selectorIndex));
if (possibleAction) {
action = possibleAction;
if (selector.charCodeAt(selectorIndex + 1) !==
61 /* Equal */) {
throw new Error("Expected `=`");
}
stripWhitespace(2);
}
else if (selector.charCodeAt(selectorIndex) === 61 /* Equal */) {
action = AttributeAction.Equals;
stripWhitespace(1);
}
// Determine value
let value = "";
let ignoreCase = null;
if (action !== "exists") {
if (isQuote(selector.charCodeAt(selectorIndex))) {
const quote = selector.charCodeAt(selectorIndex);
let sectionEnd = selectorIndex + 1;
while (sectionEnd < selector.length &&
(selector.charCodeAt(sectionEnd) !== quote ||
isEscaped(sectionEnd))) {
sectionEnd += 1;
}
if (selector.charCodeAt(sectionEnd) !== quote) {
throw new Error("Attribute value didn't end");
}
value = unescapeCSS(selector.slice(selectorIndex + 1, sectionEnd));
selectorIndex = sectionEnd + 1;
}
else {
const valueStart = selectorIndex;
while (selectorIndex < selector.length &&
((!isWhitespace(selector.charCodeAt(selectorIndex)) &&
selector.charCodeAt(selectorIndex) !==
93 /* RightSquareBracket */) ||
isEscaped(selectorIndex))) {
selectorIndex += 1;
}
value = unescapeCSS(selector.slice(valueStart, selectorIndex));
}
stripWhitespace(0);
// See if we have a force ignore flag
const forceIgnore = selector.charCodeAt(selectorIndex) | 0x20;
// If the forceIgnore flag is set (either `i` or `s`), use that value
if (forceIgnore === 115 /* LowerS */) {
ignoreCase = false;
stripWhitespace(1);
}
else if (forceIgnore === 105 /* LowerI */) {
ignoreCase = true;
stripWhitespace(1);
}
}
if (selector.charCodeAt(selectorIndex) !==
93 /* RightSquareBracket */) {
throw new Error("Attribute selector didn't terminate");
}
selectorIndex += 1;
const attributeSelector = {
type: SelectorType.Attribute,
name,
action,
value,
namespace,
ignoreCase,
};
tokens.push(attributeSelector);
break;
}
case 58 /* Colon */: {
if (selector.charCodeAt(selectorIndex + 1) === 58 /* Colon */) {
tokens.push({
type: SelectorType.PseudoElement,
name: getName(2).toLowerCase(),
data: selector.charCodeAt(selectorIndex) ===
40 /* LeftParenthesis */
? readValueWithParenthesis()
: null,
});
continue;
}
const name = getName(1).toLowerCase();
let data = null;
if (selector.charCodeAt(selectorIndex) ===
40 /* LeftParenthesis */) {
if (unpackPseudos.has(name)) {
if (isQuote(selector.charCodeAt(selectorIndex + 1))) {
throw new Error(`Pseudo-selector ${name} cannot be quoted`);
}
data = [];
selectorIndex = parseSelector(data, selector, selectorIndex + 1);
if (selector.charCodeAt(selectorIndex) !==
41 /* RightParenthesis */) {
throw new Error(`Missing closing parenthesis in :${name} (${selector})`);
}
selectorIndex += 1;
}
else {
data = readValueWithParenthesis();
if (stripQuotesFromPseudos.has(name)) {
const quot = data.charCodeAt(0);
if (quot === data.charCodeAt(data.length - 1) &&
isQuote(quot)) {
data = data.slice(1, -1);
}
}
data = unescapeCSS(data);
}
}
tokens.push({ type: SelectorType.Pseudo, name, data });
break;
}
case 44 /* Comma */: {
finalizeSubselector();
tokens = [];
stripWhitespace(1);
break;
}
default: {
if (selector.startsWith("/*", selectorIndex)) {
const endIndex = selector.indexOf("*/", selectorIndex + 2);
if (endIndex < 0) {
throw new Error("Comment was not terminated");
}
selectorIndex = endIndex + 2;
// Remove leading whitespace
if (tokens.length === 0) {
stripWhitespace(0);
}
break;
}
let namespace = null;
let name;
if (firstChar === 42 /* Asterisk */) {
selectorIndex += 1;
name = "*";
}
else if (firstChar === 124 /* Pipe */) {
name = "";
if (selector.charCodeAt(selectorIndex + 1) === 124 /* Pipe */) {
addTraversal(SelectorType.ColumnCombinator);
stripWhitespace(2);
break;
}
}
else if (reName.test(selector.slice(selectorIndex))) {
name = getName(0);
}
else {
break loop;
}
if (selector.charCodeAt(selectorIndex) === 124 /* Pipe */ &&
selector.charCodeAt(selectorIndex + 1) !== 124 /* Pipe */) {
namespace = name;
if (selector.charCodeAt(selectorIndex + 1) ===
42 /* Asterisk */) {
name = "*";
selectorIndex += 2;
}
else {
name = getName(1);
}
}
tokens.push(name === "*"
? { type: SelectorType.Universal, namespace }
: { type: SelectorType.Tag, name, namespace });
}
}
}
finalizeSubselector();
return selectorIndex;
}
/**
* Copyright 2018 Google LLC

@@ -28,6 +476,28 @@ *

*/
let classCache = null;
let idCache = null;
const PARSE5_OPTS = {
treeAdapter
};
function buildCache(container) {
classCache = new Set();
idCache = new Set();
const queue = [container];
while (queue.length) {
const node = queue.shift();
if (node.hasAttribute('class')) {
const classList = node.getAttribute('class').trim().split(' ');
classList.forEach(cls => {
classCache.add(cls);
});
}
if (node.hasAttribute('id')) {
const id = node.getAttribute('id').trim();
idCache.add(id);
}
queue.push(...node.children.filter(child => child.type === 'tag'));
}
}
/**

@@ -39,14 +509,20 @@ * Parse HTML into a mutable, serializable DOM Document.

function createDocument(html) {
const document =
/** @type {HTMLDocument} */
parse5.parse(html, PARSE5_OPTS);
htmlparser2.parseDocument(html);
defineProperties(document, DocumentExtensions); // Extend Element.prototype with DOM manipulation methods.
const scratch = document.createElement('div'); // Get a reference to the base Node class - used by createTextNode()
defineProperties(domhandler.Element.prototype, ElementExtensions); // Critters container is the viewport to evaluate critical CSS
document.$$Node = scratch.constructor;
const elementProto = Object.getPrototypeOf(scratch);
defineProperties(elementProto, ElementExtensions);
elementProto.ownerDocument = document;
let crittersContainer = document.querySelector('[data-critters-container]');
if (!crittersContainer) {
document.documentElement.setAttribute('data-critters-container', '');
crittersContainer = document.documentElement;
}
document.crittersContainer = crittersContainer;
buildCache(crittersContainer);
return document;

@@ -60,3 +536,3 @@ }

function serializeDocument(document) {
return parse5.serialize(document, PARSE5_OPTS);
return render(document);
}

@@ -83,3 +559,3 @@ /** @typedef {treeAdapter.Document & typeof ElementExtensions} HTMLDocument */

if (!referenceNode) return this.appendChild(child);
treeAdapter.insertBefore(this, child, referenceNode);
htmlparser2.DomUtils.prepend(referenceNode, child);
return child;

@@ -89,3 +565,3 @@ },

appendChild(child) {
treeAdapter.appendChild(this, child);
htmlparser2.DomUtils.appendChild(this, child);
return child;

@@ -95,7 +571,7 @@ },

removeChild(child) {
treeAdapter.detachNode(child);
htmlparser2.DomUtils.removeElement(child);
},
remove() {
treeAdapter.detachNode(this);
htmlparser2.DomUtils.removeElement(this);
},

@@ -105,3 +581,3 @@

get() {
return getText(this);
return htmlparser2.DomUtils.getText(this);
},

@@ -111,3 +587,3 @@

this.children = [];
treeAdapter.insertText(this, text);
htmlparser2.DomUtils.appendChild(this, new domhandler.Text(text));
}

@@ -143,2 +619,14 @@

};
},
exists(sel) {
return cachedQuerySelector(sel, this);
},
querySelector(sel) {
return cssSelect.selectOne(sel, this);
},
querySelectorAll(sel) {
return cssSelect.selectAll(sel, this);
}

@@ -177,17 +665,6 @@

// Find the first <html> element within the document
return this.childNodes.filter(child => String(child.tagName).toLowerCase() === 'html');
return this.children.find(child => String(child.tagName).toLowerCase() === 'html');
}
},
compatMode: {
get() {
const compatMode = {
'no-quirks': 'CSS1Compat',
quirks: 'BackCompat',
'limited-quirks': 'CSS1Compat'
};
return compatMode[treeAdapter.getDocumentMode(this)];
}
},
head: {

@@ -207,3 +684,3 @@ get() {

createElement(name) {
return treeAdapter.createElement(name, null, []);
return new domhandler.Element(name);
},

@@ -213,14 +690,11 @@

// there is no dedicated createTextNode equivalent exposed in htmlparser2's DOM
const Node = this.$$Node;
return new Node({
type: 'text',
data: text,
parent: null,
prev: null,
next: null
});
return new domhandler.Text(text);
},
exists(sel) {
return cachedQuerySelector(sel, this);
},
querySelector(sel) {
return cssSelect.selectOne(sel, this.documentElement);
return cssSelect.selectOne(sel, this);
},

@@ -233,3 +707,3 @@

return cssSelect.selectAll(sel, this.documentElement);
return cssSelect.selectAll(sel, this);
}

@@ -269,14 +743,22 @@

}
/**
* Helper to get the text content of a node
* https://github.com/fb55/domutils/blob/master/src/stringify.ts#L21
* @private
*/
function cachedQuerySelector(sel, node) {
const selectorTokens = parse(sel);
function getText(node) {
if (Array.isArray(node)) return node.map(getText).join('');
if (treeAdapter.isElementNode(node)) return node.name === 'br' ? '\n' : getText(node.children);
if (treeAdapter.isTextNode(node)) return node.data;
return '';
for (const tokens of selectorTokens) {
// Check if the selector is a class selector
if (tokens.length === 1) {
const token = tokens[0];
if (token.type === 'attribute' && token.name === 'class') {
return classCache.has(token.value);
}
if (token.type === 'attribute' && token.name === 'id') {
return idCache.has(token.value);
}
}
}
return !!cssSelect.selectOne(sel, node);
}

@@ -616,3 +1098,4 @@

pruneSource: false,
additionalStylesheets: []
additionalStylesheets: [],
allowRules: []
}, options || {});

@@ -912,4 +1395,4 @@ this.urlFilter = this.options.filter;

const name = style.$$name ? style.$$name.replace(/^\//, '') : 'inline CSS';
const options = this.options; // const document = style.ownerDocument;
const options = this.options;
const crittersContainer = document.crittersContainer;
let keyframesMode = options.keyframes || 'critical'; // we also accept a boolean value for options.keyframes

@@ -929,12 +1412,78 @@

const failedSelectors = [];
const criticalKeyframeNames = []; // Walk all CSS rules, marking unused rules with `.$$remove=true` for removal in the second pass.
const criticalKeyframeNames = [];
let includeNext = false;
let includeAll = false;
let excludeNext = false;
let excludeAll = false; // Walk all CSS rules, marking unused rules with `.$$remove=true` for removal in the second pass.
// This first pass is also used to collect font and keyframe usage used in the second pass.
walkStyleRules(ast, markOnly(rule => {
if (rule.type === 'comment') {
const comment = rule.text.trim();
if (comment.startsWith('critters')) {
const command = comment.replace(/^critters:/, '');
switch (command) {
case 'include':
includeNext = true;
break;
case 'exclude':
excludeNext = true;
break;
case 'include start':
includeAll = true;
break;
case 'include end':
includeAll = false;
break;
case 'exclude start':
excludeAll = true;
break;
case 'exclude end':
excludeAll = false;
break;
}
}
}
if (rule.type === 'rule') {
// Filter the selector list down to only those match
// Handle comment based markers
if (includeNext) {
includeNext = false;
return true;
}
if (excludeNext) {
excludeNext = false;
return false;
}
if (includeAll) {
return true;
}
if (excludeAll) {
return false;
} // Filter the selector list down to only those match
rule.filterSelectors(sel => {
// Strip pseudo-elements and pseudo-classes, since we only care that their associated elements exist.
// Validate rule with 'allowRules' option
const isAllowedRule = options.allowRules.some(exp => {
if (exp instanceof RegExp) {
return exp.test(sel);
}
return exp === sel;
});
if (isAllowedRule) return true; // Strip pseudo-elements and pseudo-classes, since we only care that their associated elements exist.
// This means any selector for a pseudo-element or having a pseudo-class will be inlined if the rest of the selector matches.
if (sel === ':root' || sel.match(/^::?(before|after)$/)) {
if (sel === ':root' || sel.match(/^::?(before|after)$/) || sel === 'html' || sel === 'body') {
return true;

@@ -947,3 +1496,3 @@ }

try {
return document.querySelector(sel) != null;
return crittersContainer.exists(sel);
} catch (e) {

@@ -950,0 +1499,0 @@ failedSelectors.push(sel + ' -> ' + e.message);

15

package.json
{
"name": "critters",
"version": "0.0.16",
"version": "0.0.17",
"description": "Inline critical CSS and lazy-load the rest.",

@@ -49,8 +49,9 @@ "main": "dist/critters.js",

"dependencies": {
"chalk": "^4.1.0",
"css-select": "^4.2.0",
"parse5": "^6.0.1",
"parse5-htmlparser2-tree-adapter": "^6.0.1",
"postcss": "^8.3.7",
"pretty-bytes": "^5.3.0"
"chalk": "^5.2.0",
"css-select": "^5.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.2",
"htmlparser2": "^8.0.2",
"postcss": "^8.4.23",
"pretty-bytes": "^6.1.0"
},

@@ -57,0 +58,0 @@ "devDependencies": {

@@ -16,7 +16,7 @@ <p align="center">

* Fast - no browser, few dependencies
* Integrates with Webpack [critters-webpack-plugin]
* Supports preloading and/or inlining critical fonts
* Prunes unused CSS keyframes and media queries
* Removes inlined CSS rules from lazy-loaded stylesheets
- Fast - no browser, few dependencies
- Integrates with Webpack [critters-webpack-plugin]
- Supports preloading and/or inlining critical fonts
- Prunes unused CSS keyframes and media queries
- Removes inlined CSS rules from lazy-loaded stylesheets

@@ -73,3 +73,5 @@ ## Installation

+ new Critters({
+ // optional configuration (see below)
+ // optional configuration
+ preload: 'swap',
+ includeSelectors: [/^\.btn/, '.banner'],
+ })

@@ -92,29 +94,115 @@ ]

* `options`
- `options`
#### Properties
* `path` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Base path location of the CSS files *(default: `''`)*
* `publicPath` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Public path of the CSS resources. This prefix is removed from the href *(default: `''`)*
* `external` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Inline styles from external stylesheets *(default: `true`)*
* `inlineThreshold` **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** Inline external stylesheets smaller than a given size *(default: `0`)*
* `minimumExternalSize` **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** If the non-critical external stylesheet would be below this size, just inline it *(default: `0`)*
* `pruneSource` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Remove inlined rules from the external stylesheet *(default: `false`)*
* `mergeStylesheets` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Merged inlined stylesheets into a single `<style>` tag *(default: `true`)*
* `additionalStylesheets` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** Glob for matching other stylesheets to be used while looking for critical CSS.
* `preload` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Which [preload strategy](#preloadstrategy) to use
* `noscriptFallback` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Add `<noscript>` fallback to JS-based strategies
* `inlineFonts` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Inline critical font-face rules *(default: `false`)*
* `preloadFonts` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Preloads critical fonts *(default: `true`)*
* `fonts` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Shorthand for setting `inlineFonts` + `preloadFonts`* Values:
* `true` to inline critical font-face rules and preload the fonts
* `false` to don't inline any font-face rules and don't preload fonts
* `keyframes` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Controls which keyframes rules are inlined.* Values:
* `"critical"`: *(default)* inline keyframes rules used by the critical CSS
* `"all"` inline all keyframes rules
* `"none"` remove all keyframes rules
* `compress` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Compress resulting critical CSS *(default: `true`)*
* `logLevel` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Controls [log level](#loglevel) of the plugin *(default: `"info"`)*
* `logger` **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** Provide a custom logger interface [logger](#logger)
- `path` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Base path location of the CSS files _(default: `''`)_
- `publicPath` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Public path of the CSS resources. This prefix is removed from the href _(default: `''`)_
- `external` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Inline styles from external stylesheets _(default: `true`)_
- `inlineThreshold` **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** Inline external stylesheets smaller than a given size _(default: `0`)_
- `minimumExternalSize` **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** If the non-critical external stylesheet would be below this size, just inline it _(default: `0`)_
- `pruneSource` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Remove inlined rules from the external stylesheet _(default: `false`)_
- `mergeStylesheets` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Merged inlined stylesheets into a single `<style>` tag _(default: `true`)_
- `additionalStylesheets` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** Glob for matching other stylesheets to be used while looking for critical CSS.
- `reduceInlineStyles` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Option indicates if inline styles should be evaluated for critical CSS. By default inline style tags will be evaluated and rewritten to only contain critical CSS. Set it to `false` to skip processing inline styles. _(default: `true`)_
- `preload` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Which [preload strategy](#preloadstrategy) to use
- `noscriptFallback` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Add `<noscript>` fallback to JS-based strategies
- `inlineFonts` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Inline critical font-face rules _(default: `false`)_
- `preloadFonts` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Preloads critical fonts _(default: `true`)_
- `fonts` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Shorthand for setting `inlineFonts` + `preloadFonts`\* Values:
- `true` to inline critical font-face rules and preload the fonts
- `false` to don't inline any font-face rules and don't preload fonts
- `keyframes` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Controls which keyframes rules are inlined.\* Values:
- `"critical"`: _(default)_ inline keyframes rules used by the critical CSS
- `"all"` inline all keyframes rules
- `"none"` remove all keyframes rules
- `compress` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Compress resulting critical CSS _(default: `true`)_
- `logLevel` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Controls [log level](#loglevel) of the plugin _(default: `"info"`)_
- `logger` **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** Provide a custom logger interface [logger](#logger)
- `includeSelectors` **[RegExp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp)** | **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Provide a list of selectors that should be included in the critical CSS. Accepts both RegExp and string.
### Include/exclude rules
We can include or exclude rules to be part of critical CSS by adding comments in the CSS
Single line comments to include/exclude the next CSS rule
```css
/* critters:exclude */
.selector1 {
/* this rule will be excluded from critical CSS */
}
.selector2 {
/* this will be evaluated normally */
}
/* critters:include */
.selector3 {
/* this rule will be included in the critical CSS */
}
.selector4 {
/* this will be evaluated normally */
}
```
Including/Excluding multiple rules by adding start and end markers
```css
/* critters:exclude start */
.selector1 {
/* this rule will be excluded from critical CSS */
}
.selector2 {
/* this rule will be excluded from critical CSS */
}
/* critters:exclude end */
```
```css
/* critters:include start */
.selector3 {
/* this rule will be included in the critical CSS */
}
.selector4 {
/* this rule will be included in the critical CSS */
}
/* critters:include end */
```
### Critters container
By default Critters evaluates the CSS against the entire input HTML. Critters evaluates the Critical CSS by reconstructing the entire DOM and evaluating the CSS selectors to find matching nodes. Usually this works well as Critters is lightweight and fast.
For some cases, the input HTML can be very large or deeply nested which makes the reconstructed DOM much larger, which in turn can slow down the critical CSS generation. Critters is not aware of viewport size and what specific nodes are above the fold since there is not a headless browser involved.
To overcome this issue Critters makes use of **Critters containers**.
A Critters container mimics the viewport and can be enabled by adding `data-critters-container` into the top level container thats contains the HTML elements above the fold.
You can estimate the contents of your viewport roughly and add a <div `data-critters-container` > around the contents.
```html
<html>
<body>
<div class="container">
<div data-critters-container>
/* HTML inside this container are used to evaluate critical CSS */
</div>
/* HTML is ignored when evaluating critical CSS */
</div>
<footer></footer>
</body>
</html>
```
_Note: This is an easy way to improve the performance of Critters_
### Logger

@@ -128,7 +216,7 @@

* `trace` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints a trace message
* `debug` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints a debug message
* `info` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints an information message
* `warn` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints a warning message
* `error` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints an error message
- `trace` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints a trace message
- `debug` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints a debug message
- `info` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints an information message
- `warn` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints a warning message
- `error` **function ([String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String))** Prints an error message

@@ -141,8 +229,8 @@ ### LogLevel

* **"info"** *(default)*
* **"warn"**
* **"error"**
* **"trace"**
* **"debug"**
* **"silent"**
- **"info"** _(default)_
- **"warn"**
- **"error"**
- **"trace"**
- **"debug"**
- **"silent"**

@@ -157,10 +245,10 @@ Type: (`"info"` | `"warn"` | `"error"` | `"trace"` | `"debug"` | `"silent"`)

* **default:** Move stylesheet links to the end of the document and insert preload meta tags in their place.
* **"body":** Move all external stylesheet links to the end of the document.
* **"media":** Load stylesheets asynchronously by adding `media="not x"` and removing once loaded. <kbd>JS</kbd>
* **"swap":** Convert stylesheet links to preloads that swap to `rel="stylesheet"` once loaded ([details](https://www.filamentgroup.com/lab/load-css-simpler/#the-code)). <kbd>JS</kbd>
* **"swap-high":** Use `<link rel="alternate stylesheet preload">` and swap to `rel="stylesheet"` once loaded ([details](http://filamentgroup.github.io/loadCSS/test/new-high.html)). <kbd>JS</kbd>
* **"js":** Inject an asynchronous CSS loader similar to [LoadCSS](https://github.com/filamentgroup/loadCSS) and use it to load stylesheets. <kbd>JS</kbd>
* **"js-lazy":** Like `"js"`, but the stylesheet is disabled until fully loaded.
* **false:** Disables adding preload tags.
- **default:** Move stylesheet links to the end of the document and insert preload meta tags in their place.
- **"body":** Move all external stylesheet links to the end of the document.
- **"media":** Load stylesheets asynchronously by adding `media="not x"` and removing once loaded. <kbd>JS</kbd>
- **"swap":** Convert stylesheet links to preloads that swap to `rel="stylesheet"` once loaded ([details](https://www.filamentgroup.com/lab/load-css-simpler/#the-code)). <kbd>JS</kbd>
- **"swap-high":** Use `<link rel="alternate stylesheet preload">` and swap to `rel="stylesheet"` once loaded ([details](http://filamentgroup.github.io/loadCSS/test/new-high.html)). <kbd>JS</kbd>
- **"js":** Inject an asynchronous CSS loader similar to [LoadCSS](https://github.com/filamentgroup/loadCSS) and use it to load stylesheets. <kbd>JS</kbd>
- **"js-lazy":** Like `"js"`, but the stylesheet is disabled until fully loaded.
- **false:** Disables adding preload tags.

@@ -173,8 +261,8 @@ Type: (default | `"body"` | `"media"` | `"swap"` | `"swap-high"` | `"js"` | `"js-lazy"`)

* [Critical](https://github.com/addyosmani/critical)
* [Penthouse](https://github.com/pocketjoso/penthouse)
* [webpack-critical](https://github.com/lukeed/webpack-critical)
* [webpack-plugin-critical](https://github.com/nrwl/webpack-plugin-critical)
* [html-critical-webpack-plugin](https://github.com/anthonygore/html-critical-webpack-plugin)
* [react-snap](https://github.com/stereobooster/react-snap)
- [Critical](https://github.com/addyosmani/critical)
- [Penthouse](https://github.com/pocketjoso/penthouse)
- [webpack-critical](https://github.com/lukeed/webpack-critical)
- [webpack-plugin-critical](https://github.com/nrwl/webpack-plugin-critical)
- [html-critical-webpack-plugin](https://github.com/anthonygore/html-critical-webpack-plugin)
- [react-snap](https://github.com/stereobooster/react-snap)

@@ -188,5 +276,3 @@ ## License

[critters-webpack-plugin]: https://github.com/GoogleChromeLabs/critters/tree/main/packages/critters-webpack-plugin
[critical css]: https://www.smashingmagazine.com/2015/08/understanding-critical-css/
[html-webpack-plugin]: https://github.com/jantimon/html-webpack-plugin

@@ -17,11 +17,35 @@ /**

import parse5 from 'parse5';
import { selectAll, selectOne } from 'css-select';
import treeAdapter from 'parse5-htmlparser2-tree-adapter';
import { parseDocument, DomUtils } from 'htmlparser2';
import { parse as selectorParser } from 'css-what';
import { Element, Text } from 'domhandler';
import render from 'dom-serializer';
// htmlparser2 has a relatively DOM-like tree format, which we'll massage into a DOM elsewhere
const PARSE5_OPTS = {
treeAdapter
};
let classCache = null;
let idCache = null;
function buildCache(container) {
classCache = new Set();
idCache = new Set();
const queue = [container];
while (queue.length) {
const node = queue.shift();
if (node.hasAttribute('class')) {
const classList = node.getAttribute('class').trim().split(' ');
classList.forEach((cls) => {
classCache.add(cls);
});
}
if (node.hasAttribute('id')) {
const id = node.getAttribute('id').trim();
idCache.add(id);
}
queue.push(...node.children.filter((child) => child.type === 'tag'));
}
}
/**

@@ -33,5 +57,3 @@ * Parse HTML into a mutable, serializable DOM Document.

export function createDocument(html) {
const document = /** @type {HTMLDocument} */ (
parse5.parse(html, PARSE5_OPTS)
);
const document = /** @type {HTMLDocument} */ (parseDocument(html));

@@ -41,9 +63,15 @@ defineProperties(document, DocumentExtensions);

// Extend Element.prototype with DOM manipulation methods.
const scratch = document.createElement('div');
// Get a reference to the base Node class - used by createTextNode()
document.$$Node = scratch.constructor;
const elementProto = Object.getPrototypeOf(scratch);
defineProperties(elementProto, ElementExtensions);
elementProto.ownerDocument = document;
defineProperties(Element.prototype, ElementExtensions);
// Critters container is the viewport to evaluate critical CSS
let crittersContainer = document.querySelector('[data-critters-container]');
if (!crittersContainer) {
document.documentElement.setAttribute('data-critters-container', '');
crittersContainer = document.documentElement;
}
document.crittersContainer = crittersContainer;
buildCache(crittersContainer);
return document;

@@ -57,3 +85,3 @@ }

export function serializeDocument(document) {
return parse5.serialize(document, PARSE5_OPTS);
return render(document);
}

@@ -82,3 +110,3 @@

if (!referenceNode) return this.appendChild(child);
treeAdapter.insertBefore(this, child, referenceNode);
DomUtils.prepend(referenceNode, child);
return child;

@@ -88,3 +116,3 @@ },

appendChild(child) {
treeAdapter.appendChild(this, child);
DomUtils.appendChild(this, child);
return child;

@@ -94,7 +122,7 @@ },

removeChild(child) {
treeAdapter.detachNode(child);
DomUtils.removeElement(child);
},
remove() {
treeAdapter.detachNode(this);
DomUtils.removeElement(this);
},

@@ -104,3 +132,3 @@

get() {
return getText(this);
return DomUtils.getText(this);
},

@@ -110,3 +138,3 @@

this.children = [];
treeAdapter.insertText(this, text);
DomUtils.appendChild(this, new Text(text));
}

@@ -138,2 +166,14 @@ },

if (value != null) return { specified: true, value };
},
exists(sel) {
return cachedQuerySelector(sel, this);
},
querySelector(sel) {
return selectOne(sel, this);
},
querySelectorAll(sel) {
return selectAll(sel, this);
}

@@ -172,3 +212,3 @@ };

// Find the first <html> element within the document
return this.childNodes.filter(
return this.children.find(
(child) => String(child.tagName).toLowerCase() === 'html'

@@ -179,13 +219,2 @@ );

compatMode: {
get() {
const compatMode = {
'no-quirks': 'CSS1Compat',
quirks: 'BackCompat',
'limited-quirks': 'CSS1Compat'
};
return compatMode[treeAdapter.getDocumentMode(this)];
}
},
head: {

@@ -204,3 +233,3 @@ get() {

createElement(name) {
return treeAdapter.createElement(name, null, []);
return new Element(name);
},

@@ -210,14 +239,11 @@

// there is no dedicated createTextNode equivalent exposed in htmlparser2's DOM
const Node = this.$$Node;
return new Node({
type: 'text',
data: text,
parent: null,
prev: null,
next: null
});
return new Text(text);
},
exists(sel) {
return cachedQuerySelector(sel, this);
},
querySelector(sel) {
return selectOne(sel, this.documentElement);
return selectOne(sel, this);
},

@@ -229,3 +255,3 @@

}
return selectAll(sel, this.documentElement);
return selectAll(sel, this);
}

@@ -264,13 +290,17 @@ };

/**
* Helper to get the text content of a node
* https://github.com/fb55/domutils/blob/master/src/stringify.ts#L21
* @private
*/
function getText(node) {
if (Array.isArray(node)) return node.map(getText).join('');
if (treeAdapter.isElementNode(node))
return node.name === 'br' ? '\n' : getText(node.children);
if (treeAdapter.isTextNode(node)) return node.data;
return '';
function cachedQuerySelector(sel, node) {
const selectorTokens = selectorParser(sel);
for (const tokens of selectorTokens) {
// Check if the selector is a class selector
if (tokens.length === 1) {
const token = tokens[0];
if (token.type === 'attribute' && token.name === 'class') {
return classCache.has(token.value);
}
if (token.type === 'attribute' && token.name === 'id') {
return idCache.has(token.value);
}
}
}
return !!selectOne(sel, node);
}

@@ -125,3 +125,4 @@ /**

pruneSource: false,
additionalStylesheets: []
additionalStylesheets: [],
allowRules: []
},

@@ -445,3 +446,3 @@ options || {}

const options = this.options;
// const document = style.ownerDocument;
const crittersContainer = document.crittersContainer;
let keyframesMode = options.keyframes || 'critical';

@@ -470,2 +471,7 @@ // we also accept a boolean value for options.keyframes

let includeNext = false;
let includeAll = false;
let excludeNext = false;
let excludeAll = false;
// Walk all CSS rules, marking unused rules with `.$$remove=true` for removal in the second pass.

@@ -476,8 +482,69 @@ // This first pass is also used to collect font and keyframe usage used in the second pass.

markOnly((rule) => {
if (rule.type === 'comment') {
const comment = rule.text.trim();
if (comment.startsWith('critters')) {
const command = comment.replace(/^critters:/, '');
switch (command) {
case 'include':
includeNext = true;
break;
case 'exclude':
excludeNext = true;
break;
case 'include start':
includeAll = true;
break;
case 'include end':
includeAll = false;
break;
case 'exclude start':
excludeAll = true;
break;
case 'exclude end':
excludeAll = false;
break;
}
}
}
if (rule.type === 'rule') {
// Handle comment based markers
if (includeNext) {
includeNext = false;
return true;
}
if (excludeNext) {
excludeNext = false;
return false;
}
if (includeAll) {
return true;
}
if (excludeAll) {
return false;
}
// Filter the selector list down to only those match
rule.filterSelectors((sel) => {
// Validate rule with 'allowRules' option
const isAllowedRule = options.allowRules.some((exp) => {
if (exp instanceof RegExp) {
return exp.test(sel);
}
return exp === sel;
});
if (isAllowedRule) return true;
// Strip pseudo-elements and pseudo-classes, since we only care that their associated elements exist.
// This means any selector for a pseudo-element or having a pseudo-class will be inlined if the rest of the selector matches.
if (sel === ':root' || sel.match(/^::?(before|after)$/)) {
if (
sel === ':root' ||
sel.match(/^::?(before|after)$/) ||
sel === 'html' ||
sel === 'body'
) {
return true;

@@ -492,3 +559,3 @@ }

try {
return document.querySelector(sel) != null;
return crittersContainer.exists(sel);
} catch (e) {

@@ -495,0 +562,0 @@ failedSelectors.push(sel + ' -> ' + e.message);

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc