@opensourceframework/critters
Advanced tools
+17
-20
| { | ||
| "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" | ||
| } | ||
| } | ||
| } |
-933
| '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 }; |
-924
| 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
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
3
-40%1
-66.67%71128
-75.85%9
-40%1401
-57.72%