Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@opensourceframework/critters

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@opensourceframework/critters - npm Package Compare versions

Comparing version
1.0.0
to
1.0.1
+17
-20
package.json
{
"name": "@opensourceframework/critters",
"version": "1.0.0",
"version": "1.0.1",
"description": "Inline critical CSS and lazy-load the rest. Forked from GoogleChromeLabs/critters.",

@@ -15,10 +15,9 @@ "keywords": [

],
"homepage": "https://github.com/opensourceframework/opensourceframework/tree/main/packages/critters#readme",
"homepage": "https://github.com/riceharvest/critters#readme",
"bugs": {
"url": "https://github.com/opensourceframework/opensourceframework/issues"
"url": "https://github.com/riceharvest/critters/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/opensourceframework/opensourceframework.git",
"directory": "packages/critters"
"url": "git+https://github.com/riceharvest/critters.git"
},

@@ -62,2 +61,13 @@ "license": "Apache-2.0",

],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"dev": "tsup --watch",
"clean": "rm -rf dist",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "eslint src test",
"typecheck": "tsc --noEmit"
},
"dependencies": {

@@ -76,5 +86,3 @@ "chalk": "^4.1.0",

"typescript": "^5.3.0",
"vitest": "^1.0.0",
"@opensourceframework/eslint-config": "0.0.0",
"@opensourceframework/tsconfig": "0.0.0"
"vitest": "^1.0.0"
},

@@ -86,14 +94,3 @@ "engines": {

"access": "public"
},
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"dev": "tsup --watch",
"clean": "rm -rf dist",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "eslint src test",
"typecheck": "tsc --noEmit"
}
}
}
'use strict';
var fs = require('fs');
var cssSelect = require('css-select');
var htmlparser2 = require('htmlparser2');
var cssWhat = require('css-what');
var domhandler = require('domhandler');
var render = require('dom-serializer');
var path = require('path');
var postcss = require('postcss');
var mediaParser = require('postcss-media-query-parser');
var chalk = require('chalk');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var render__default = /*#__PURE__*/_interopDefault(render);
var path__default = /*#__PURE__*/_interopDefault(path);
var mediaParser__default = /*#__PURE__*/_interopDefault(mediaParser);
var chalk__default = /*#__PURE__*/_interopDefault(chalk);
// src/index.js
var classCache = null;
var idCache = null;
function buildCache(container) {
classCache = /* @__PURE__ */ new Set();
idCache = /* @__PURE__ */ 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"));
}
}
function createDocument(html) {
const document = (
/** @type {HTMLDocument} */
htmlparser2.parseDocument(html, { decodeEntities: false })
);
defineProperties(document, DocumentExtensions);
defineProperties(domhandler.Element.prototype, ElementExtensions);
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;
}
function serializeDocument(document) {
const htmlElement = document.documentElement;
if (htmlElement && htmlElement.hasAttribute("data-critters-container")) {
const value = htmlElement.getAttribute("data-critters-container");
if (value === "") {
htmlElement.removeAttribute("data-critters-container");
}
}
return render__default.default(document, { decodeEntities: false });
}
var ElementExtensions = {
/** @extends treeAdapter.Element.prototype */
nodeName: {
get() {
return this.tagName.toUpperCase();
}
},
id: reflectedProperty("id"),
className: reflectedProperty("class"),
insertBefore(child, referenceNode) {
if (!referenceNode) return this.appendChild(child);
htmlparser2.DomUtils.prepend(referenceNode, child);
return child;
},
appendChild(child) {
htmlparser2.DomUtils.appendChild(this, child);
return child;
},
removeChild(child) {
htmlparser2.DomUtils.removeElement(child);
},
remove() {
htmlparser2.DomUtils.removeElement(this);
},
textContent: {
get() {
return htmlparser2.DomUtils.getText(this);
},
set(text) {
this.children = [];
htmlparser2.DomUtils.appendChild(this, new domhandler.Text(text));
}
},
setAttribute(name, value) {
if (this.attribs == null) this.attribs = {};
if (value == null) value = "";
this.attribs[name] = value;
},
removeAttribute(name) {
if (this.attribs != null) {
delete this.attribs[name];
}
},
getAttribute(name) {
return this.attribs != null && this.attribs[name];
},
hasAttribute(name) {
return this.attribs != null && this.attribs[name] != null;
},
getAttributeNode(name) {
const value = this.getAttribute(name);
if (value != null) return { specified: true, value };
},
exists(sel) {
return cachedQuerySelector(sel, this);
},
querySelector(sel) {
return cssSelect.selectOne(sel, this);
},
querySelectorAll(sel) {
return cssSelect.selectAll(sel, this);
}
};
var DocumentExtensions = {
/** @extends treeAdapter.Document.prototype */
// document is just an Element in htmlparser2, giving it a nodeType of ELEMENT_NODE.
// TODO: verify if these are needed for css-select
nodeType: {
get() {
return 9;
}
},
contentType: {
get() {
return "text/html";
}
},
nodeName: {
get() {
return "#document";
}
},
documentElement: {
get() {
return this.children.find(
(child) => String(child.tagName).toLowerCase() === "html"
);
}
},
head: {
get() {
return this.querySelector("head");
}
},
body: {
get() {
return this.querySelector("body");
}
},
createElement(name) {
return new domhandler.Element(name);
},
createTextNode(text) {
return new domhandler.Text(text);
},
exists(sel) {
return cachedQuerySelector(sel, this);
},
querySelector(sel) {
return cssSelect.selectOne(sel, this);
},
querySelectorAll(sel) {
if (sel === ":root") {
return this;
}
return cssSelect.selectAll(sel, this);
}
};
function defineProperties(obj, properties) {
for (const i in properties) {
const value = properties[i];
Object.defineProperty(
obj,
i,
typeof value === "function" ? { value } : value
);
}
}
function reflectedProperty(attributeName) {
return {
get() {
return this.getAttribute(attributeName);
},
set(value) {
this.setAttribute(attributeName, value);
}
};
}
function cachedQuerySelector(sel, node) {
const selectorTokens = cssWhat.parse(sel);
for (const tokens of selectorTokens) {
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);
}
var DANGEROUS_CSS_URL_PATTERN = /^\s*(javascript|data\s*:\s*text\/html|data\s*:\s*text\/javascript)/i;
function hasDangerousContent(value) {
if (!value) return false;
if (/<\/style>/i.test(value)) return true;
if (/<script/i.test(value)) return true;
return false;
}
function parseStylesheet(stylesheet) {
return postcss.parse(stylesheet);
}
function serializeStylesheet(ast, options) {
let cssStr = "";
postcss.stringify(ast, (result, node, type) => {
if (node?.type === "decl") {
if (node.value.includes("</style>")) {
return;
}
if (hasDangerousContent(node.value)) {
return;
}
if (node.value.includes("url(") && DANGEROUS_CSS_URL_PATTERN.test(node.value)) {
return;
}
}
if (!options.compress) {
cssStr += result;
return;
}
if (node?.type === "comment") return;
if (node?.type === "decl") {
const prefix = node.prop + node.raws.between;
cssStr += result.replace(prefix, prefix.trim());
return;
}
if (type === "start") {
if (node.type === "rule" && node.selectors) {
cssStr += node.selectors.join(",") + "{";
} else {
cssStr += result.replace(/\s\{$/, "{");
}
return;
}
if (type === "end" && result === "}" && node?.raws?.semicolon) {
cssStr = cssStr.slice(0, -1);
}
cssStr += result.trim();
});
return cssStr;
}
function markOnly(predicate) {
return (rule) => {
const sel = rule.selectors;
if (predicate(rule) === false) {
rule.$$remove = true;
}
rule.$$markedSelectors = rule.selectors;
if (rule._other) {
rule._other.$$markedSelectors = rule._other.selectors;
}
rule.selectors = sel;
};
}
function applyMarkedSelectors(rule) {
if (rule.$$markedSelectors) {
rule.selectors = rule.$$markedSelectors;
}
if (rule._other) {
applyMarkedSelectors(rule._other);
}
}
function walkStyleRules(node, iterator) {
node.nodes = node.nodes.filter((rule) => {
if (hasNestedNodes(rule)) {
walkStyleRules(rule, iterator);
}
rule._other = void 0;
rule.filterSelectors = filterSelectors;
return iterator(rule) !== false;
});
}
function walkStyleRulesWithReverseMirror(node, node2, iterator) {
if (node2 === null) return walkStyleRules(node, iterator);
[node.nodes, node2.nodes] = splitFilter(
node.nodes,
node2.nodes,
(rule, index, rules, rules2) => {
const rule2 = rules2[index];
if (hasNestedNodes(rule)) {
walkStyleRulesWithReverseMirror(rule, rule2, iterator);
}
rule._other = rule2;
rule.filterSelectors = filterSelectors;
return iterator(rule) !== false;
}
);
}
function hasNestedNodes(rule) {
return rule.nodes?.length && rule.name !== "keyframes" && rule.name !== "-webkit-keyframes" && rule.nodes.some((n) => n.type === "rule" || n.type === "atrule");
}
function splitFilter(a, b, predicate) {
const aOut = [];
const bOut = [];
for (let index = 0; index < a.length; index++) {
if (predicate(a[index], index, a, b)) {
aOut.push(a[index]);
} else {
bOut.push(a[index]);
}
}
return [aOut, bOut];
}
function filterSelectors(predicate) {
if (this._other) {
const [a, b] = splitFilter(
this.selectors,
this._other.selectors,
predicate
);
this.selectors = a;
this._other.selectors = b;
} else {
this.selectors = this.selectors.filter(predicate);
}
}
var MEDIA_TYPES = /* @__PURE__ */ new Set(["all", "print", "screen", "speech"]);
var MEDIA_KEYWORDS = /* @__PURE__ */ new Set(["and", "not", ","]);
var MEDIA_FEATURES = new Set(
[
"width",
"aspect-ratio",
"color",
"color-index",
"grid",
"height",
"monochrome",
"orientation",
"resolution",
"scan"
].flatMap((feature) => [feature, `min-${feature}`, `max-${feature}`])
);
function validateMediaType(node) {
const { type: nodeType, value: nodeValue } = node;
if (nodeType === "media-type") {
return MEDIA_TYPES.has(nodeValue);
} else if (nodeType === "keyword") {
return MEDIA_KEYWORDS.has(nodeValue);
} else if (nodeType === "media-feature") {
return MEDIA_FEATURES.has(nodeValue);
}
}
function validateMediaQuery(query) {
const mediaParserFn = "default" in mediaParser__default.default ? mediaParser__default.default.default : mediaParser__default.default;
const mediaTree = mediaParserFn(query);
const nodeTypes = /* @__PURE__ */ new Set(["media-type", "keyword", "media-feature"]);
const stack = [mediaTree];
while (stack.length > 0) {
const node = stack.pop();
if (nodeTypes.has(node.type) && !validateMediaType(node)) {
return false;
}
if (node.nodes) {
stack.push(...node.nodes);
}
}
return true;
}
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "silent"];
var defaultLogger = {
trace(msg) {
globalThis.console.trace(msg);
},
debug(msg) {
globalThis.console.debug(msg);
},
warn(msg) {
globalThis.console.warn(chalk__default.default.yellow(msg));
},
error(msg) {
globalThis.console.error(chalk__default.default.bold.red(msg));
},
info(msg) {
globalThis.console.info(chalk__default.default.bold.blue(msg));
},
silent() {
}
};
function createLogger(logLevel) {
const logLevelIdx = LOG_LEVELS.indexOf(logLevel);
return LOG_LEVELS.reduce((logger, type, index) => {
if (index >= logLevelIdx) {
logger[type] = defaultLogger[type];
} else {
logger[type] = defaultLogger.silent;
}
return logger;
}, {});
}
function isSubpath(basePath, currentPath) {
return !path__default.default.relative(basePath, currentPath).startsWith("..");
}
// src/index.js
var SCRIPT_TAG_PATTERN = /<script[^>]*>[\s\S]*?<\/script>/gi;
var SCRIPT_BREAKOUT_PATTERN = /<\/script>/gi;
function sanitizeAttributeValue(value) {
if (!value) return value;
let sanitized = value.replace(SCRIPT_TAG_PATTERN, "");
sanitized = sanitized.replace(SCRIPT_BREAKOUT_PATTERN, "");
return sanitized;
}
function isDangerousAttribute(name) {
return /^on/i.test(name);
}
var Critters = class {
constructor(options) {
this.options = Object.assign(
{
logLevel: "info",
path: "",
publicPath: "",
reduceInlineStyles: true,
pruneSource: false,
preload: void 0,
noscriptFallback: true,
inlineFonts: false,
preloadFonts: true,
fonts: void 0,
keyframes: "critical",
compress: true,
mergeStylesheets: true,
external: true,
inlineThreshold: 0,
minimumExternalSize: 0,
additionalStylesheets: [],
allowRules: []
},
options || {}
);
this.logger = this.options.logger ? Object.assign(createLogger(this.options.logLevel), this.options.logger) : createLogger(this.options.logLevel);
this.fs = { readFile: fs.readFile };
}
/**
* Read the contents of a file from the specified filesystem or disk.
* Override this method to customize how stylesheets are loaded.
* @param {string} filename
* @returns {Promise<string>}
*/
readFile(filename) {
return new Promise((resolve, reject) => {
this.fs.readFile(filename, "utf8", (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
/**
* Given a stylesheet URL, returns the corresponding CSS asset.
* Overriding this method requires doing your own URL normalization, so it's generally better to override `readFile()`.
* @param {string} href
* @returns {Promise<string | undefined>}
*/
async getCssAsset(href) {
const outputPath = this.options.path;
const publicPath = this.options.publicPath;
let normalizedPath = href.replace(/^\//, "");
const pathPrefix = (publicPath || "").replace(/(^\/|\/$)/g, "") + "/";
if (normalizedPath.indexOf(pathPrefix) === 0) {
normalizedPath = normalizedPath.substring(pathPrefix.length).replace(/^\//, "");
}
const filename = path__default.default.resolve(outputPath, normalizedPath);
if (!isSubpath(outputPath, filename)) {
this.logger.warn(`Path "${normalizedPath}" is not a subpath of "${outputPath}"`);
return;
}
try {
return await this.readFile(filename);
} catch {
this.logger.warn(`Unable to locate stylesheet: ${normalizedPath}`);
}
}
/**
* Process an HTML document to inline critical CSS from its stylesheets.
* @param {string} html String containing a full HTML document to be parsed.
* @returns {Promise<string>} A modified copy of the provided HTML with critical CSS inlined.
*/
async process(html) {
const document = createDocument(html);
const sheets = [];
const inlineStyleSheets = [];
const externalSheets = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
const inlineStyles = Array.from(document.querySelectorAll("style"));
if (this.options.external !== false) {
for (const link of externalSheets) {
const href = link.getAttribute("href");
if (!href) continue;
link.getAttribute("media");
const style = document.createElement("style");
style.$$name = href;
style.$$external = true;
style.$$links = [link];
const sheet = await this.getCssAsset(href);
if (sheet) {
style.textContent = sheet;
link.parentNode.insertBefore(style, link);
if (this.checkInlineThreshold(link, style, sheet)) {
continue;
}
sheets.push(style);
}
}
}
if (this.options.reduceInlineStyles !== false) {
for (const style of inlineStyles) {
style.$$name = "inline";
style.$$reduce = true;
inlineStyleSheets.push(style);
}
sheets.push(...inlineStyleSheets);
}
const additionalStyles = await this.embedAdditionalStylesheet(document);
sheets.push(...additionalStyles);
for (const style of sheets) {
await this.processStyle(style, document);
}
if (this.options.preload !== void 0) {
await this.applyPreloadStrategy(document);
}
if (this.options.mergeStylesheets !== false && sheets.length > 1) {
this.mergeStylesheets(document, sheets);
}
return serializeDocument(document);
}
/**
* Check if an external stylesheet should be fully inlined based on size threshold.
* @param {Element} link
* @param {Element} style
* @param {string} sheet
* @returns {boolean}
*/
checkInlineThreshold(link, style, sheet) {
const inlineThreshold = this.options.inlineThreshold;
if (inlineThreshold && sheet.length < inlineThreshold) {
link.remove();
this.logger.info(
`\x1B[32mInlined all of ${style.$$name} (${sheet.length}b was below threshold of ${inlineThreshold}b)\x1B[39m`
);
return true;
}
return false;
}
/**
* Embed additional stylesheets specified in options.
* @param {Document} document
* @returns {Promise<Element[]>} Array of style elements created
*/
async embedAdditionalStylesheet(document) {
const additionalStylesheets = this.options.additionalStylesheets || [];
const styles = [];
for (const cssFile of additionalStylesheets) {
const sheet = await this.getCssAsset(cssFile);
if (sheet) {
const style = document.createElement("style");
style.$$name = cssFile;
style.$$external = true;
style.textContent = sheet;
document.head.appendChild(style);
styles.push(style);
}
}
return styles;
}
/**
* Apply the preload strategy to remaining external stylesheets.
* @param {Document} document
*/
async applyPreloadStrategy(document) {
const preloadMode = this.options.preload;
const links = document.querySelectorAll('link[rel="stylesheet"]');
for (const link of links) {
const href = link.getAttribute("href");
if (!href) continue;
const media = link.getAttribute("media");
const style = link.previousElementSibling;
if (media && !validateMediaQuery(media)) {
this.logger.warn(`Invalid media query: ${media}`);
link.removeAttribute("media");
}
let styleElement = style;
if (!styleElement || styleElement.tagName !== "STYLE") {
styleElement = { $$links: [] };
}
this.setupLinkPreload(link, href, link.getAttribute("media"), styleElement, document, preloadMode);
}
}
/**
* Setup link preload based on strategy.
* @param {Element} link
* @param {string} href
* @param {string} media
* @param {object} style
* @param {Document} document
* @param {string} preloadMode
*/
setupLinkPreload(link, href, media, style, document, preloadMode) {
let cssLoaderPreamble = "function $loadcss(u,m,l){(l=document.createElement('link')).rel='stylesheet';l.href=u;document.head.appendChild(l)}";
const lazy = preloadMode === "js-lazy";
if (lazy) {
cssLoaderPreamble = cssLoaderPreamble.replace(
"l.href",
"l.media='print';l.onload=function(){l.media=m};l.href"
);
}
if (preloadMode === false) return;
const dangerousAttrs = [];
if (link.attribs) {
for (const attrName of Object.keys(link.attribs)) {
if (isDangerousAttribute(attrName)) {
dangerousAttrs.push(attrName);
}
}
dangerousAttrs.forEach((attr) => link.removeAttribute(attr));
}
const safeHref = sanitizeAttributeValue(href);
if (safeHref !== href) {
link.setAttribute("href", safeHref);
}
let noscriptFallback = false;
let updateLinkToPreload = false;
const noscriptLink = link.cloneNode(false);
dangerousAttrs.forEach((attr) => noscriptLink.removeAttribute(attr));
noscriptLink.setAttribute("href", safeHref);
if (preloadMode === "body") {
document.body.appendChild(link);
} else {
if (preloadMode === "js" || preloadMode === "js-lazy") {
const script = document.createElement("script");
script.setAttribute("data-href", safeHref);
script.setAttribute("data-media", sanitizeAttributeValue(media || "all"));
const js = `${cssLoaderPreamble}$loadcss(document.currentScript.dataset.href,document.currentScript.dataset.media)`;
script.textContent = js;
link.parentNode.insertBefore(script, link.nextSibling);
style.$$links.push(script);
cssLoaderPreamble = "";
noscriptFallback = true;
updateLinkToPreload = true;
} else if (preloadMode === "media") {
const safeMedia = media && validateMediaQuery(media) ? media : "all";
link.setAttribute("media", "print");
link.setAttribute("onload", `this.media='${safeMedia.replace(/'/g, "\\'")}'`);
noscriptFallback = true;
} else if (preloadMode === "swap-high") {
link.setAttribute("rel", "alternate stylesheet preload");
link.setAttribute("title", "styles");
link.setAttribute("onload", `this.title='';this.rel='stylesheet'`);
noscriptFallback = true;
} else if (preloadMode === "swap") {
link.setAttribute("rel", "preload");
link.setAttribute("as", "style");
link.setAttribute("onload", "this.rel='stylesheet'");
noscriptFallback = true;
} else {
const bodyLink = link.cloneNode(false);
bodyLink.removeAttribute("id");
document.body.appendChild(bodyLink);
updateLinkToPreload = true;
}
}
if (this.options.noscriptFallback !== false && noscriptFallback && !safeHref.includes("</noscript>")) {
const noscript = document.createElement("noscript");
noscriptLink.removeAttribute("id");
noscript.appendChild(noscriptLink);
link.parentNode.insertBefore(noscript, link.nextSibling);
style.$$links.push(noscript);
}
if (updateLinkToPreload) {
link.setAttribute("rel", "preload");
link.setAttribute("as", "style");
}
}
/**
* Merge multiple stylesheets into a single style tag.
* @param {Document} document
* @param {Element[]} sheets
*/
mergeStylesheets(document, sheets) {
const firstStyle = sheets[0];
if (!firstStyle || firstStyle.tagName !== "STYLE") return;
const mergedContent = sheets.filter((s) => s.tagName === "STYLE" && s.textContent).map((s) => s.textContent).join("\n");
firstStyle.textContent = mergedContent;
for (let i = 1; i < sheets.length; i++) {
const sheet = sheets[i];
if (sheet.tagName === "STYLE" && sheet.parentNode) {
sheet.remove();
}
}
}
/**
* Prune the source CSS files
*/
pruneSource(style, before, sheetInverse) {
const minSize = this.options.minimumExternalSize;
const name = style.$$name;
if (minSize && sheetInverse.length < minSize) {
this.logger.info(
`\x1B[32mInlined all of ${name} (non-critical external stylesheet would have been ${sheetInverse.length}b, which was below the threshold of ${minSize})\x1B[39m`
);
style.textContent = before;
if (style.$$links) {
for (const link of style.$$links) {
const parent = link.parentNode;
if (parent) parent.removeChild(link);
}
}
return true;
}
return false;
}
/**
* Parse the stylesheet within a <style> element, then reduce it to contain only rules used by the document.
*/
async processStyle(style, document) {
if (style.$$reduce === false) return;
const name = style.$$name ? style.$$name.replace(/^\//, "") : "inline CSS";
const options = this.options;
const crittersContainer = document.crittersContainer;
let keyframesMode = options.keyframes || "critical";
if (keyframesMode === true) keyframesMode = "all";
if (keyframesMode === false) keyframesMode = "none";
let sheet = style.textContent;
const before = sheet;
if (!sheet) return;
const ast = parseStylesheet(sheet);
const astInverse = options.pruneSource ? parseStylesheet(sheet) : null;
let criticalFonts = "";
const failedSelectors = [];
const criticalKeyframeNames = /* @__PURE__ */ new Set();
let includeNext = false;
let includeAll = false;
let excludeNext = false;
let excludeAll = false;
const shouldPreloadFonts = options.fonts === true || options.preloadFonts === true;
const shouldInlineFonts = options.fonts !== false && options.inlineFonts === true;
walkStyleRules(
ast,
markOnly((rule) => {
if (rule.type === "comment") {
const crittersComment = rule.text.match(/^(?<! )critters:(.*)/);
const command = crittersComment && crittersComment[1];
if (command) {
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") {
if (includeNext) {
includeNext = false;
return true;
}
if (excludeNext) {
excludeNext = false;
return false;
}
if (includeAll) {
return true;
}
if (excludeAll) {
return false;
}
rule.filterSelectors((sel) => {
const isAllowedRule = options.allowRules.some((exp) => {
if (exp instanceof RegExp) {
return exp.test(sel);
}
return exp === sel;
});
if (isAllowedRule) return true;
if (sel === ":root" || sel === "html" || sel === "body" || /^::?(before|after)$/.test(sel)) {
return true;
}
sel = sel.replace(/(?<!\\)::?[a-z-]+(?![a-z-(])/gi, "").replace(/::?not\(\s*\)/g, "").replace(/\(\s*,/g, "(").replace(/,\s*\)/g, ")").trim();
if (!sel) return false;
try {
return crittersContainer.exists(sel);
} catch (err) {
failedSelectors.push(sel + " -> " + err.message);
return false;
}
});
if (!rule.selector) {
return false;
}
if (rule.nodes) {
for (const decl of rule.nodes) {
if (shouldInlineFonts && decl.prop && /\bfont(-family)?\b/i.test(decl.prop)) {
criticalFonts += " " + decl.value;
}
if (decl.prop === "animation" || decl.prop === "animation-name") {
for (const name2 of decl.value.split(/\s+/)) {
const nameTrimmed = name2.trim();
if (nameTrimmed) criticalKeyframeNames.add(nameTrimmed);
}
}
}
}
}
if (rule.type === "atrule" && rule.name === "font-face") return;
const rules = rule.nodes?.filter((rule2) => !rule2.$$remove);
return !rules || rules.length !== 0;
})
);
if (failedSelectors.length !== 0) {
this.logger.warn(
`${failedSelectors.length} rules skipped due to selector errors:
${failedSelectors.join(
"\n "
)}`
);
}
const preloadedFonts = /* @__PURE__ */ new Set();
walkStyleRulesWithReverseMirror(ast, astInverse, (rule) => {
if (rule.$$remove === true) return false;
applyMarkedSelectors(rule);
if (rule.type === "atrule" && rule.name === "keyframes") {
if (keyframesMode === "none") return false;
if (keyframesMode === "all") return true;
return criticalKeyframeNames.has(rule.params);
}
if (rule.type === "atrule" && rule.name === "font-face") {
let family, src;
for (const decl of rule.nodes) {
if (decl.prop === "src") {
src = (decl.value.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || [])[2];
} else if (decl.prop === "font-family") {
family = decl.value;
}
}
if (src && shouldPreloadFonts && !preloadedFonts.has(src)) {
preloadedFonts.add(src);
const preload = document.createElement("link");
preload.setAttribute("rel", "preload");
preload.setAttribute("as", "font");
preload.setAttribute("crossorigin", "anonymous");
preload.setAttribute("href", src.trim());
document.head.appendChild(preload);
}
if (!shouldInlineFonts || !family || !src || !criticalFonts.includes(family)) {
return false;
}
}
});
sheet = serializeStylesheet(ast, {
compress: this.options.compress !== false
});
if (sheet.trim().length === 0) {
if (style.parentNode) {
style.remove();
}
return;
}
let afterText = "";
let styleInlinedCompletely = false;
if (options.pruneSource) {
const sheetInverse = serializeStylesheet(astInverse, {
compress: this.options.compress !== false
});
styleInlinedCompletely = this.pruneSource(style, before, sheetInverse);
if (styleInlinedCompletely) {
const percent2 = sheetInverse.length / before.length * 100;
afterText = `, reducing non-inlined size ${percent2 | 0}% to ${formatSize(sheetInverse.length)}`;
}
}
if (!styleInlinedCompletely) {
style.textContent = sheet;
}
const percent = sheet.length / before.length * 100 | 0;
this.logger.info(
"\x1B[32mInlined " + formatSize(sheet.length) + " (" + percent + "% of original " + formatSize(before.length) + ") of " + name + afterText + ".\x1B[39m"
);
}
};
function formatSize(size) {
if (size <= 0) {
return "0 bytes";
}
const abbreviations = ["bytes", "kB", "MB", "GB"];
const index = Math.floor(Math.log(size) / Math.log(1024));
const roundedSize = size / Math.pow(1024, index);
const fractionDigits = index === 0 ? 0 : 2;
return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`;
}
module.exports = Critters;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map

Sorry, the diff of this file is too big to display

/**
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
declare class Critters {
/**
* Create an instance of Critters with custom options.
* The `.process()` method can be called repeatedly to re-use this instance and its cache.
*/
constructor(options?: Options);
/**
* Process an HTML document to inline critical CSS from its stylesheets.
* @param html String containing a full HTML document to be parsed.
* @returns A modified copy of the provided HTML with critical CSS inlined.
*/
process(html: string): Promise<string>;
/**
* Read the contents of a file from the specified filesystem or disk.
* Override this method to customize how stylesheets are loaded.
*/
readFile(filename: string): Promise<string> | string;
/**
* Given a stylesheet URL, returns the corresponding CSS asset.
* Overriding this method requires doing your own URL normalization, so it's generally better to override `readFile()`.
*/
getCssAsset(href: string): Promise<string | undefined> | string | undefined;
}
interface Options {
path?: string;
publicPath?: string;
external?: boolean;
inlineThreshold?: number;
minimumExternalSize?: number;
pruneSource?: boolean;
mergeStylesheets?: boolean;
additionalStylesheets?: string[];
preload?: 'body' | 'media' | 'swap' | 'swap-high' | 'js' | 'js-lazy' | false;
noscriptFallback?: boolean;
inlineFonts?: boolean;
preloadFonts?: boolean;
fonts?: boolean;
keyframes?: 'critical' | 'all' | 'none' | boolean;
compress?: boolean;
logLevel?: 'info' | 'warn' | 'error' | 'trace' | 'debug' | 'silent';
reduceInlineStyles?: boolean;
logger?: Logger;
allowRules?: (RegExp | string)[];
}
interface Logger {
trace?: (message: string) => void;
debug?: (message: string) => void;
info?: (message: string) => void;
warn?: (message: string) => void;
error?: (message: string) => void;
}
export { type Logger, type Options, Critters as default };
/**
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
declare class Critters {
/**
* Create an instance of Critters with custom options.
* The `.process()` method can be called repeatedly to re-use this instance and its cache.
*/
constructor(options?: Options);
/**
* Process an HTML document to inline critical CSS from its stylesheets.
* @param html String containing a full HTML document to be parsed.
* @returns A modified copy of the provided HTML with critical CSS inlined.
*/
process(html: string): Promise<string>;
/**
* Read the contents of a file from the specified filesystem or disk.
* Override this method to customize how stylesheets are loaded.
*/
readFile(filename: string): Promise<string> | string;
/**
* Given a stylesheet URL, returns the corresponding CSS asset.
* Overriding this method requires doing your own URL normalization, so it's generally better to override `readFile()`.
*/
getCssAsset(href: string): Promise<string | undefined> | string | undefined;
}
interface Options {
path?: string;
publicPath?: string;
external?: boolean;
inlineThreshold?: number;
minimumExternalSize?: number;
pruneSource?: boolean;
mergeStylesheets?: boolean;
additionalStylesheets?: string[];
preload?: 'body' | 'media' | 'swap' | 'swap-high' | 'js' | 'js-lazy' | false;
noscriptFallback?: boolean;
inlineFonts?: boolean;
preloadFonts?: boolean;
fonts?: boolean;
keyframes?: 'critical' | 'all' | 'none' | boolean;
compress?: boolean;
logLevel?: 'info' | 'warn' | 'error' | 'trace' | 'debug' | 'silent';
reduceInlineStyles?: boolean;
logger?: Logger;
allowRules?: (RegExp | string)[];
}
interface Logger {
trace?: (message: string) => void;
debug?: (message: string) => void;
info?: (message: string) => void;
warn?: (message: string) => void;
error?: (message: string) => void;
}
export { type Logger, type Options, Critters as default };
import { readFile } from 'fs';
import { selectAll, selectOne } from 'css-select';
import { parseDocument, DomUtils } from 'htmlparser2';
import { parse as parse$1 } from 'css-what';
import { Element, Text } from 'domhandler';
import render from 'dom-serializer';
import path from 'path';
import { parse, stringify } from 'postcss';
import mediaParser from 'postcss-media-query-parser';
import chalk from 'chalk';
// src/index.js
var classCache = null;
var idCache = null;
function buildCache(container) {
classCache = /* @__PURE__ */ new Set();
idCache = /* @__PURE__ */ 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"));
}
}
function createDocument(html) {
const document = (
/** @type {HTMLDocument} */
parseDocument(html, { decodeEntities: false })
);
defineProperties(document, DocumentExtensions);
defineProperties(Element.prototype, ElementExtensions);
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;
}
function serializeDocument(document) {
const htmlElement = document.documentElement;
if (htmlElement && htmlElement.hasAttribute("data-critters-container")) {
const value = htmlElement.getAttribute("data-critters-container");
if (value === "") {
htmlElement.removeAttribute("data-critters-container");
}
}
return render(document, { decodeEntities: false });
}
var ElementExtensions = {
/** @extends treeAdapter.Element.prototype */
nodeName: {
get() {
return this.tagName.toUpperCase();
}
},
id: reflectedProperty("id"),
className: reflectedProperty("class"),
insertBefore(child, referenceNode) {
if (!referenceNode) return this.appendChild(child);
DomUtils.prepend(referenceNode, child);
return child;
},
appendChild(child) {
DomUtils.appendChild(this, child);
return child;
},
removeChild(child) {
DomUtils.removeElement(child);
},
remove() {
DomUtils.removeElement(this);
},
textContent: {
get() {
return DomUtils.getText(this);
},
set(text) {
this.children = [];
DomUtils.appendChild(this, new Text(text));
}
},
setAttribute(name, value) {
if (this.attribs == null) this.attribs = {};
if (value == null) value = "";
this.attribs[name] = value;
},
removeAttribute(name) {
if (this.attribs != null) {
delete this.attribs[name];
}
},
getAttribute(name) {
return this.attribs != null && this.attribs[name];
},
hasAttribute(name) {
return this.attribs != null && this.attribs[name] != null;
},
getAttributeNode(name) {
const value = this.getAttribute(name);
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);
}
};
var DocumentExtensions = {
/** @extends treeAdapter.Document.prototype */
// document is just an Element in htmlparser2, giving it a nodeType of ELEMENT_NODE.
// TODO: verify if these are needed for css-select
nodeType: {
get() {
return 9;
}
},
contentType: {
get() {
return "text/html";
}
},
nodeName: {
get() {
return "#document";
}
},
documentElement: {
get() {
return this.children.find(
(child) => String(child.tagName).toLowerCase() === "html"
);
}
},
head: {
get() {
return this.querySelector("head");
}
},
body: {
get() {
return this.querySelector("body");
}
},
createElement(name) {
return new Element(name);
},
createTextNode(text) {
return new Text(text);
},
exists(sel) {
return cachedQuerySelector(sel, this);
},
querySelector(sel) {
return selectOne(sel, this);
},
querySelectorAll(sel) {
if (sel === ":root") {
return this;
}
return selectAll(sel, this);
}
};
function defineProperties(obj, properties) {
for (const i in properties) {
const value = properties[i];
Object.defineProperty(
obj,
i,
typeof value === "function" ? { value } : value
);
}
}
function reflectedProperty(attributeName) {
return {
get() {
return this.getAttribute(attributeName);
},
set(value) {
this.setAttribute(attributeName, value);
}
};
}
function cachedQuerySelector(sel, node) {
const selectorTokens = parse$1(sel);
for (const tokens of selectorTokens) {
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);
}
var DANGEROUS_CSS_URL_PATTERN = /^\s*(javascript|data\s*:\s*text\/html|data\s*:\s*text\/javascript)/i;
function hasDangerousContent(value) {
if (!value) return false;
if (/<\/style>/i.test(value)) return true;
if (/<script/i.test(value)) return true;
return false;
}
function parseStylesheet(stylesheet) {
return parse(stylesheet);
}
function serializeStylesheet(ast, options) {
let cssStr = "";
stringify(ast, (result, node, type) => {
if (node?.type === "decl") {
if (node.value.includes("</style>")) {
return;
}
if (hasDangerousContent(node.value)) {
return;
}
if (node.value.includes("url(") && DANGEROUS_CSS_URL_PATTERN.test(node.value)) {
return;
}
}
if (!options.compress) {
cssStr += result;
return;
}
if (node?.type === "comment") return;
if (node?.type === "decl") {
const prefix = node.prop + node.raws.between;
cssStr += result.replace(prefix, prefix.trim());
return;
}
if (type === "start") {
if (node.type === "rule" && node.selectors) {
cssStr += node.selectors.join(",") + "{";
} else {
cssStr += result.replace(/\s\{$/, "{");
}
return;
}
if (type === "end" && result === "}" && node?.raws?.semicolon) {
cssStr = cssStr.slice(0, -1);
}
cssStr += result.trim();
});
return cssStr;
}
function markOnly(predicate) {
return (rule) => {
const sel = rule.selectors;
if (predicate(rule) === false) {
rule.$$remove = true;
}
rule.$$markedSelectors = rule.selectors;
if (rule._other) {
rule._other.$$markedSelectors = rule._other.selectors;
}
rule.selectors = sel;
};
}
function applyMarkedSelectors(rule) {
if (rule.$$markedSelectors) {
rule.selectors = rule.$$markedSelectors;
}
if (rule._other) {
applyMarkedSelectors(rule._other);
}
}
function walkStyleRules(node, iterator) {
node.nodes = node.nodes.filter((rule) => {
if (hasNestedNodes(rule)) {
walkStyleRules(rule, iterator);
}
rule._other = void 0;
rule.filterSelectors = filterSelectors;
return iterator(rule) !== false;
});
}
function walkStyleRulesWithReverseMirror(node, node2, iterator) {
if (node2 === null) return walkStyleRules(node, iterator);
[node.nodes, node2.nodes] = splitFilter(
node.nodes,
node2.nodes,
(rule, index, rules, rules2) => {
const rule2 = rules2[index];
if (hasNestedNodes(rule)) {
walkStyleRulesWithReverseMirror(rule, rule2, iterator);
}
rule._other = rule2;
rule.filterSelectors = filterSelectors;
return iterator(rule) !== false;
}
);
}
function hasNestedNodes(rule) {
return rule.nodes?.length && rule.name !== "keyframes" && rule.name !== "-webkit-keyframes" && rule.nodes.some((n) => n.type === "rule" || n.type === "atrule");
}
function splitFilter(a, b, predicate) {
const aOut = [];
const bOut = [];
for (let index = 0; index < a.length; index++) {
if (predicate(a[index], index, a, b)) {
aOut.push(a[index]);
} else {
bOut.push(a[index]);
}
}
return [aOut, bOut];
}
function filterSelectors(predicate) {
if (this._other) {
const [a, b] = splitFilter(
this.selectors,
this._other.selectors,
predicate
);
this.selectors = a;
this._other.selectors = b;
} else {
this.selectors = this.selectors.filter(predicate);
}
}
var MEDIA_TYPES = /* @__PURE__ */ new Set(["all", "print", "screen", "speech"]);
var MEDIA_KEYWORDS = /* @__PURE__ */ new Set(["and", "not", ","]);
var MEDIA_FEATURES = new Set(
[
"width",
"aspect-ratio",
"color",
"color-index",
"grid",
"height",
"monochrome",
"orientation",
"resolution",
"scan"
].flatMap((feature) => [feature, `min-${feature}`, `max-${feature}`])
);
function validateMediaType(node) {
const { type: nodeType, value: nodeValue } = node;
if (nodeType === "media-type") {
return MEDIA_TYPES.has(nodeValue);
} else if (nodeType === "keyword") {
return MEDIA_KEYWORDS.has(nodeValue);
} else if (nodeType === "media-feature") {
return MEDIA_FEATURES.has(nodeValue);
}
}
function validateMediaQuery(query) {
const mediaParserFn = "default" in mediaParser ? mediaParser.default : mediaParser;
const mediaTree = mediaParserFn(query);
const nodeTypes = /* @__PURE__ */ new Set(["media-type", "keyword", "media-feature"]);
const stack = [mediaTree];
while (stack.length > 0) {
const node = stack.pop();
if (nodeTypes.has(node.type) && !validateMediaType(node)) {
return false;
}
if (node.nodes) {
stack.push(...node.nodes);
}
}
return true;
}
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "silent"];
var defaultLogger = {
trace(msg) {
globalThis.console.trace(msg);
},
debug(msg) {
globalThis.console.debug(msg);
},
warn(msg) {
globalThis.console.warn(chalk.yellow(msg));
},
error(msg) {
globalThis.console.error(chalk.bold.red(msg));
},
info(msg) {
globalThis.console.info(chalk.bold.blue(msg));
},
silent() {
}
};
function createLogger(logLevel) {
const logLevelIdx = LOG_LEVELS.indexOf(logLevel);
return LOG_LEVELS.reduce((logger, type, index) => {
if (index >= logLevelIdx) {
logger[type] = defaultLogger[type];
} else {
logger[type] = defaultLogger.silent;
}
return logger;
}, {});
}
function isSubpath(basePath, currentPath) {
return !path.relative(basePath, currentPath).startsWith("..");
}
// src/index.js
var SCRIPT_TAG_PATTERN = /<script[^>]*>[\s\S]*?<\/script>/gi;
var SCRIPT_BREAKOUT_PATTERN = /<\/script>/gi;
function sanitizeAttributeValue(value) {
if (!value) return value;
let sanitized = value.replace(SCRIPT_TAG_PATTERN, "");
sanitized = sanitized.replace(SCRIPT_BREAKOUT_PATTERN, "");
return sanitized;
}
function isDangerousAttribute(name) {
return /^on/i.test(name);
}
var Critters = class {
constructor(options) {
this.options = Object.assign(
{
logLevel: "info",
path: "",
publicPath: "",
reduceInlineStyles: true,
pruneSource: false,
preload: void 0,
noscriptFallback: true,
inlineFonts: false,
preloadFonts: true,
fonts: void 0,
keyframes: "critical",
compress: true,
mergeStylesheets: true,
external: true,
inlineThreshold: 0,
minimumExternalSize: 0,
additionalStylesheets: [],
allowRules: []
},
options || {}
);
this.logger = this.options.logger ? Object.assign(createLogger(this.options.logLevel), this.options.logger) : createLogger(this.options.logLevel);
this.fs = { readFile };
}
/**
* Read the contents of a file from the specified filesystem or disk.
* Override this method to customize how stylesheets are loaded.
* @param {string} filename
* @returns {Promise<string>}
*/
readFile(filename) {
return new Promise((resolve, reject) => {
this.fs.readFile(filename, "utf8", (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
/**
* Given a stylesheet URL, returns the corresponding CSS asset.
* Overriding this method requires doing your own URL normalization, so it's generally better to override `readFile()`.
* @param {string} href
* @returns {Promise<string | undefined>}
*/
async getCssAsset(href) {
const outputPath = this.options.path;
const publicPath = this.options.publicPath;
let normalizedPath = href.replace(/^\//, "");
const pathPrefix = (publicPath || "").replace(/(^\/|\/$)/g, "") + "/";
if (normalizedPath.indexOf(pathPrefix) === 0) {
normalizedPath = normalizedPath.substring(pathPrefix.length).replace(/^\//, "");
}
const filename = path.resolve(outputPath, normalizedPath);
if (!isSubpath(outputPath, filename)) {
this.logger.warn(`Path "${normalizedPath}" is not a subpath of "${outputPath}"`);
return;
}
try {
return await this.readFile(filename);
} catch {
this.logger.warn(`Unable to locate stylesheet: ${normalizedPath}`);
}
}
/**
* Process an HTML document to inline critical CSS from its stylesheets.
* @param {string} html String containing a full HTML document to be parsed.
* @returns {Promise<string>} A modified copy of the provided HTML with critical CSS inlined.
*/
async process(html) {
const document = createDocument(html);
const sheets = [];
const inlineStyleSheets = [];
const externalSheets = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
const inlineStyles = Array.from(document.querySelectorAll("style"));
if (this.options.external !== false) {
for (const link of externalSheets) {
const href = link.getAttribute("href");
if (!href) continue;
link.getAttribute("media");
const style = document.createElement("style");
style.$$name = href;
style.$$external = true;
style.$$links = [link];
const sheet = await this.getCssAsset(href);
if (sheet) {
style.textContent = sheet;
link.parentNode.insertBefore(style, link);
if (this.checkInlineThreshold(link, style, sheet)) {
continue;
}
sheets.push(style);
}
}
}
if (this.options.reduceInlineStyles !== false) {
for (const style of inlineStyles) {
style.$$name = "inline";
style.$$reduce = true;
inlineStyleSheets.push(style);
}
sheets.push(...inlineStyleSheets);
}
const additionalStyles = await this.embedAdditionalStylesheet(document);
sheets.push(...additionalStyles);
for (const style of sheets) {
await this.processStyle(style, document);
}
if (this.options.preload !== void 0) {
await this.applyPreloadStrategy(document);
}
if (this.options.mergeStylesheets !== false && sheets.length > 1) {
this.mergeStylesheets(document, sheets);
}
return serializeDocument(document);
}
/**
* Check if an external stylesheet should be fully inlined based on size threshold.
* @param {Element} link
* @param {Element} style
* @param {string} sheet
* @returns {boolean}
*/
checkInlineThreshold(link, style, sheet) {
const inlineThreshold = this.options.inlineThreshold;
if (inlineThreshold && sheet.length < inlineThreshold) {
link.remove();
this.logger.info(
`\x1B[32mInlined all of ${style.$$name} (${sheet.length}b was below threshold of ${inlineThreshold}b)\x1B[39m`
);
return true;
}
return false;
}
/**
* Embed additional stylesheets specified in options.
* @param {Document} document
* @returns {Promise<Element[]>} Array of style elements created
*/
async embedAdditionalStylesheet(document) {
const additionalStylesheets = this.options.additionalStylesheets || [];
const styles = [];
for (const cssFile of additionalStylesheets) {
const sheet = await this.getCssAsset(cssFile);
if (sheet) {
const style = document.createElement("style");
style.$$name = cssFile;
style.$$external = true;
style.textContent = sheet;
document.head.appendChild(style);
styles.push(style);
}
}
return styles;
}
/**
* Apply the preload strategy to remaining external stylesheets.
* @param {Document} document
*/
async applyPreloadStrategy(document) {
const preloadMode = this.options.preload;
const links = document.querySelectorAll('link[rel="stylesheet"]');
for (const link of links) {
const href = link.getAttribute("href");
if (!href) continue;
const media = link.getAttribute("media");
const style = link.previousElementSibling;
if (media && !validateMediaQuery(media)) {
this.logger.warn(`Invalid media query: ${media}`);
link.removeAttribute("media");
}
let styleElement = style;
if (!styleElement || styleElement.tagName !== "STYLE") {
styleElement = { $$links: [] };
}
this.setupLinkPreload(link, href, link.getAttribute("media"), styleElement, document, preloadMode);
}
}
/**
* Setup link preload based on strategy.
* @param {Element} link
* @param {string} href
* @param {string} media
* @param {object} style
* @param {Document} document
* @param {string} preloadMode
*/
setupLinkPreload(link, href, media, style, document, preloadMode) {
let cssLoaderPreamble = "function $loadcss(u,m,l){(l=document.createElement('link')).rel='stylesheet';l.href=u;document.head.appendChild(l)}";
const lazy = preloadMode === "js-lazy";
if (lazy) {
cssLoaderPreamble = cssLoaderPreamble.replace(
"l.href",
"l.media='print';l.onload=function(){l.media=m};l.href"
);
}
if (preloadMode === false) return;
const dangerousAttrs = [];
if (link.attribs) {
for (const attrName of Object.keys(link.attribs)) {
if (isDangerousAttribute(attrName)) {
dangerousAttrs.push(attrName);
}
}
dangerousAttrs.forEach((attr) => link.removeAttribute(attr));
}
const safeHref = sanitizeAttributeValue(href);
if (safeHref !== href) {
link.setAttribute("href", safeHref);
}
let noscriptFallback = false;
let updateLinkToPreload = false;
const noscriptLink = link.cloneNode(false);
dangerousAttrs.forEach((attr) => noscriptLink.removeAttribute(attr));
noscriptLink.setAttribute("href", safeHref);
if (preloadMode === "body") {
document.body.appendChild(link);
} else {
if (preloadMode === "js" || preloadMode === "js-lazy") {
const script = document.createElement("script");
script.setAttribute("data-href", safeHref);
script.setAttribute("data-media", sanitizeAttributeValue(media || "all"));
const js = `${cssLoaderPreamble}$loadcss(document.currentScript.dataset.href,document.currentScript.dataset.media)`;
script.textContent = js;
link.parentNode.insertBefore(script, link.nextSibling);
style.$$links.push(script);
cssLoaderPreamble = "";
noscriptFallback = true;
updateLinkToPreload = true;
} else if (preloadMode === "media") {
const safeMedia = media && validateMediaQuery(media) ? media : "all";
link.setAttribute("media", "print");
link.setAttribute("onload", `this.media='${safeMedia.replace(/'/g, "\\'")}'`);
noscriptFallback = true;
} else if (preloadMode === "swap-high") {
link.setAttribute("rel", "alternate stylesheet preload");
link.setAttribute("title", "styles");
link.setAttribute("onload", `this.title='';this.rel='stylesheet'`);
noscriptFallback = true;
} else if (preloadMode === "swap") {
link.setAttribute("rel", "preload");
link.setAttribute("as", "style");
link.setAttribute("onload", "this.rel='stylesheet'");
noscriptFallback = true;
} else {
const bodyLink = link.cloneNode(false);
bodyLink.removeAttribute("id");
document.body.appendChild(bodyLink);
updateLinkToPreload = true;
}
}
if (this.options.noscriptFallback !== false && noscriptFallback && !safeHref.includes("</noscript>")) {
const noscript = document.createElement("noscript");
noscriptLink.removeAttribute("id");
noscript.appendChild(noscriptLink);
link.parentNode.insertBefore(noscript, link.nextSibling);
style.$$links.push(noscript);
}
if (updateLinkToPreload) {
link.setAttribute("rel", "preload");
link.setAttribute("as", "style");
}
}
/**
* Merge multiple stylesheets into a single style tag.
* @param {Document} document
* @param {Element[]} sheets
*/
mergeStylesheets(document, sheets) {
const firstStyle = sheets[0];
if (!firstStyle || firstStyle.tagName !== "STYLE") return;
const mergedContent = sheets.filter((s) => s.tagName === "STYLE" && s.textContent).map((s) => s.textContent).join("\n");
firstStyle.textContent = mergedContent;
for (let i = 1; i < sheets.length; i++) {
const sheet = sheets[i];
if (sheet.tagName === "STYLE" && sheet.parentNode) {
sheet.remove();
}
}
}
/**
* Prune the source CSS files
*/
pruneSource(style, before, sheetInverse) {
const minSize = this.options.minimumExternalSize;
const name = style.$$name;
if (minSize && sheetInverse.length < minSize) {
this.logger.info(
`\x1B[32mInlined all of ${name} (non-critical external stylesheet would have been ${sheetInverse.length}b, which was below the threshold of ${minSize})\x1B[39m`
);
style.textContent = before;
if (style.$$links) {
for (const link of style.$$links) {
const parent = link.parentNode;
if (parent) parent.removeChild(link);
}
}
return true;
}
return false;
}
/**
* Parse the stylesheet within a <style> element, then reduce it to contain only rules used by the document.
*/
async processStyle(style, document) {
if (style.$$reduce === false) return;
const name = style.$$name ? style.$$name.replace(/^\//, "") : "inline CSS";
const options = this.options;
const crittersContainer = document.crittersContainer;
let keyframesMode = options.keyframes || "critical";
if (keyframesMode === true) keyframesMode = "all";
if (keyframesMode === false) keyframesMode = "none";
let sheet = style.textContent;
const before = sheet;
if (!sheet) return;
const ast = parseStylesheet(sheet);
const astInverse = options.pruneSource ? parseStylesheet(sheet) : null;
let criticalFonts = "";
const failedSelectors = [];
const criticalKeyframeNames = /* @__PURE__ */ new Set();
let includeNext = false;
let includeAll = false;
let excludeNext = false;
let excludeAll = false;
const shouldPreloadFonts = options.fonts === true || options.preloadFonts === true;
const shouldInlineFonts = options.fonts !== false && options.inlineFonts === true;
walkStyleRules(
ast,
markOnly((rule) => {
if (rule.type === "comment") {
const crittersComment = rule.text.match(/^(?<! )critters:(.*)/);
const command = crittersComment && crittersComment[1];
if (command) {
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") {
if (includeNext) {
includeNext = false;
return true;
}
if (excludeNext) {
excludeNext = false;
return false;
}
if (includeAll) {
return true;
}
if (excludeAll) {
return false;
}
rule.filterSelectors((sel) => {
const isAllowedRule = options.allowRules.some((exp) => {
if (exp instanceof RegExp) {
return exp.test(sel);
}
return exp === sel;
});
if (isAllowedRule) return true;
if (sel === ":root" || sel === "html" || sel === "body" || /^::?(before|after)$/.test(sel)) {
return true;
}
sel = sel.replace(/(?<!\\)::?[a-z-]+(?![a-z-(])/gi, "").replace(/::?not\(\s*\)/g, "").replace(/\(\s*,/g, "(").replace(/,\s*\)/g, ")").trim();
if (!sel) return false;
try {
return crittersContainer.exists(sel);
} catch (err) {
failedSelectors.push(sel + " -> " + err.message);
return false;
}
});
if (!rule.selector) {
return false;
}
if (rule.nodes) {
for (const decl of rule.nodes) {
if (shouldInlineFonts && decl.prop && /\bfont(-family)?\b/i.test(decl.prop)) {
criticalFonts += " " + decl.value;
}
if (decl.prop === "animation" || decl.prop === "animation-name") {
for (const name2 of decl.value.split(/\s+/)) {
const nameTrimmed = name2.trim();
if (nameTrimmed) criticalKeyframeNames.add(nameTrimmed);
}
}
}
}
}
if (rule.type === "atrule" && rule.name === "font-face") return;
const rules = rule.nodes?.filter((rule2) => !rule2.$$remove);
return !rules || rules.length !== 0;
})
);
if (failedSelectors.length !== 0) {
this.logger.warn(
`${failedSelectors.length} rules skipped due to selector errors:
${failedSelectors.join(
"\n "
)}`
);
}
const preloadedFonts = /* @__PURE__ */ new Set();
walkStyleRulesWithReverseMirror(ast, astInverse, (rule) => {
if (rule.$$remove === true) return false;
applyMarkedSelectors(rule);
if (rule.type === "atrule" && rule.name === "keyframes") {
if (keyframesMode === "none") return false;
if (keyframesMode === "all") return true;
return criticalKeyframeNames.has(rule.params);
}
if (rule.type === "atrule" && rule.name === "font-face") {
let family, src;
for (const decl of rule.nodes) {
if (decl.prop === "src") {
src = (decl.value.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || [])[2];
} else if (decl.prop === "font-family") {
family = decl.value;
}
}
if (src && shouldPreloadFonts && !preloadedFonts.has(src)) {
preloadedFonts.add(src);
const preload = document.createElement("link");
preload.setAttribute("rel", "preload");
preload.setAttribute("as", "font");
preload.setAttribute("crossorigin", "anonymous");
preload.setAttribute("href", src.trim());
document.head.appendChild(preload);
}
if (!shouldInlineFonts || !family || !src || !criticalFonts.includes(family)) {
return false;
}
}
});
sheet = serializeStylesheet(ast, {
compress: this.options.compress !== false
});
if (sheet.trim().length === 0) {
if (style.parentNode) {
style.remove();
}
return;
}
let afterText = "";
let styleInlinedCompletely = false;
if (options.pruneSource) {
const sheetInverse = serializeStylesheet(astInverse, {
compress: this.options.compress !== false
});
styleInlinedCompletely = this.pruneSource(style, before, sheetInverse);
if (styleInlinedCompletely) {
const percent2 = sheetInverse.length / before.length * 100;
afterText = `, reducing non-inlined size ${percent2 | 0}% to ${formatSize(sheetInverse.length)}`;
}
}
if (!styleInlinedCompletely) {
style.textContent = sheet;
}
const percent = sheet.length / before.length * 100 | 0;
this.logger.info(
"\x1B[32mInlined " + formatSize(sheet.length) + " (" + percent + "% of original " + formatSize(before.length) + ") of " + name + afterText + ".\x1B[39m"
);
}
};
function formatSize(size) {
if (size <= 0) {
return "0 bytes";
}
const abbreviations = ["bytes", "kB", "MB", "GB"];
const index = Math.floor(Math.log(size) / Math.log(1024));
const roundedSize = size / Math.pow(1024, index);
const fractionDigits = index === 0 ? 0 : 2;
return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`;
}
export { Critters as default };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

Sorry, the diff of this file is too big to display