@@ -179,24 +167,18 @@ exports[`options { async:true } should match snapshot 1`] = `
+}
+
+
@@ -258,24 +236,18 @@ exports[`publicPath should match snapshot 1`] = `
+}
+
+
@@ -332,9 +300,12 @@ footer {
`;
exports[`publicPath should prune external sheet 1`] = `
-".extra-style {
+"
+.extra-style {
font-size: 200%;
-}"
+}
+
+"
`;
exports[`webpack compilation 1`] = `
diff --git a/packages/critters-webpack-plugin/test/__snapshots__/standalone.test.js.snap b/packages/critters-webpack-plugin/test/__snapshots__/standalone.test.js.snap
index 5c56cc8..519a24d 100644
--- a/packages/critters-webpack-plugin/test/__snapshots__/standalone.test.js.snap
+++ b/packages/critters-webpack-plugin/test/__snapshots__/standalone.test.js.snap
@@ -1,18 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Usage without html-webpack-plugin should process the first html asset 1`] = `
-"h1 {
- color: green;
-}"
+"
+ h1 {
+ color: green;
+ }
+ "
`;
exports[`Usage without html-webpack-plugin should process the first html asset 2`] = `
"
Basic Demo
-
+
Some HTML Here
@@ -24,67 +28,60 @@ exports[`Usage without html-webpack-plugin should process the first html asset 2
exports[`webpack compilation 1`] = `
"
Basic Demo
-
+
diff --git a/packages/critters/package.json b/packages/critters/package.json
index 9fdf82f..b03213b 100644
--- a/packages/critters/package.json
+++ b/packages/critters/package.json
@@ -47,10 +47,10 @@
},
"dependencies": {
"chalk": "^4.1.0",
- "css": "^3.0.0",
"css-select": "^1.2.0",
"parse5": "^6.0.1",
"parse5-htmlparser2-tree-adapter": "^6.0.1",
+ "postcss": "^8.3.7",
"pretty-bytes": "^5.3.0"
}
}
diff --git a/packages/critters/src/css.js b/packages/critters/src/css.js
index cd4f048..f501345 100644
--- a/packages/critters/src/css.js
+++ b/packages/critters/src/css.js
@@ -14,29 +14,63 @@
* the License.
*/
-import css from 'css';
+import { parse, stringify } from 'postcss';
/**
* Parse a textual CSS Stylesheet into a Stylesheet instance.
- * Stylesheet is a mutable ReworkCSS AST with format similar to CSSOM.
- * @see https://github.com/reworkcss/css
+ * Stylesheet is a mutable postcss AST with format similar to CSSOM.
+ * @see https://github.com/postcss/postcss/
* @private
* @param {String} stylesheet
* @returns {css.Stylesheet} ast
*/
-export function parseStylesheet (stylesheet) {
- return css.parse(stylesheet);
+export function parseStylesheet(stylesheet) {
+ return parse(stylesheet);
}
/**
- * Serialize a ReworkCSS Stylesheet to a String of CSS.
+ * Serialize a postcss Stylesheet to a String of CSS.
* @private
* @param {css.Stylesheet} ast A Stylesheet to serialize, such as one returned from `parseStylesheet()`
- * @param {Object} options Options to pass to `css.stringify()`
+ * @param {Object} options Options used by the stringify logic
* @param {Boolean} [options.compress] Compress CSS output (removes comments, whitespace, etc)
*/
-export function serializeStylesheet (ast, options) {
- return css.stringify(ast, options);
+export function serializeStylesheet(ast, options) {
+ let cssStr = '';
+
+ stringify(ast, (result, node, type) => {
+ if (!options.compress) {
+ cssStr += result;
+ return;
+ }
+
+ // Simple minification logic
+ 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;
}
/**
@@ -46,8 +80,8 @@ export function serializeStylesheet (ast, options) {
* @param {Function} iterator Invoked on each node in the tree. Return `false` to remove that node.
* @returns {(rule) => void} nonDestructiveIterator
*/
-export function markOnly (predicate) {
- return rule => {
+export function markOnly(predicate) {
+ return (rule) => {
const sel = rule.selectors;
if (predicate(rule) === false) {
rule.$$remove = true;
@@ -64,8 +98,8 @@ export function markOnly (predicate) {
* Apply filtered selectors to a rule from a previous markOnly run.
* @private
* @param {css.Rule} rule The Rule to apply marked selectors to (if they exist).
-*/
-export function applyMarkedSelectors (rule) {
+ */
+export function applyMarkedSelectors(rule) {
if (rule.$$markedSelectors) {
rule.selectors = rule.$$markedSelectors;
}
@@ -80,11 +114,9 @@ export function applyMarkedSelectors (rule) {
* @param {css.Rule} node A Stylesheet or Rule to descend into.
* @param {Function} iterator Invoked on each node in the tree. Return `false` to remove that node.
*/
-export function walkStyleRules (node, iterator) {
- if (node.stylesheet) return walkStyleRules(node.stylesheet, iterator);
-
- node.rules = node.rules.filter(rule => {
- if (rule.rules) {
+export function walkStyleRules(node, iterator) {
+ node.nodes = node.nodes.filter((rule) => {
+ if (hasNestedRules(rule)) {
walkStyleRules(rule, iterator);
}
rule._other = undefined;
@@ -100,25 +132,38 @@ export function walkStyleRules (node, iterator) {
* @param {css.Rule} node2 A second tree identical to `node`
* @param {Function} iterator Invoked on each node in the tree. Return `false` to remove that node from the first tree, true to remove it from the second.
*/
-export function walkStyleRulesWithReverseMirror (node, node2, iterator) {
+export function walkStyleRulesWithReverseMirror(node, node2, iterator) {
if (node2 === null) return walkStyleRules(node, iterator);
- if (node.stylesheet) return walkStyleRulesWithReverseMirror(node.stylesheet, node2.stylesheet, iterator);
-
- [node.rules, node2.rules] = splitFilter(node.rules, node2.rules, (rule, index, rules, rules2) => {
- const rule2 = rules2[index];
- if (rule.rules) {
- walkStyleRulesWithReverseMirror(rule, rule2, iterator);
+ [node.nodes, node2.nodes] = splitFilter(
+ node.nodes,
+ node2.nodes,
+ (rule, index, rules, rules2) => {
+ const rule2 = rules2[index];
+ if (hasNestedRules(rule)) {
+ walkStyleRulesWithReverseMirror(rule, rule2, iterator);
+ }
+ rule._other = rule2;
+ rule.filterSelectors = filterSelectors;
+ return iterator(rule) !== false;
}
- rule._other = rule2;
- rule.filterSelectors = filterSelectors;
- return iterator(rule) !== false;
- });
+ );
+}
+
+// Checks if a node has nested rules, like @media
+// @keyframes are an exception since they are evaluated as a whole
+function hasNestedRules(rule) {
+ return (
+ rule.nodes &&
+ rule.nodes.length &&
+ rule.nodes.some((n) => n.type === 'rule' || n.type === 'atrule') &&
+ rule.name !== 'keyframes'
+ );
}
// Like [].filter(), but applies the opposite filtering result to a second copy of the Array without a second pass.
// This is just a quicker version of generating the compliment of the set returned from a filter operation.
-function splitFilter (a, b, predicate) {
+function splitFilter(a, b, predicate) {
const aOut = [];
const bOut = [];
for (let index = 0; index < a.length; index++) {
@@ -132,9 +177,13 @@ function splitFilter (a, b, predicate) {
}
// can be invoked on a style rule to subset its selectors (with reverse mirroring)
-function filterSelectors (predicate) {
+function filterSelectors(predicate) {
if (this._other) {
- const [a, b] = splitFilter(this.selectors, this._other.selectors, predicate);
+ const [a, b] = splitFilter(
+ this.selectors,
+ this._other.selectors,
+ predicate
+ );
this.selectors = a;
this._other.selectors = b;
} else {
diff --git a/packages/critters/src/index.js b/packages/critters/src/index.js
index 22aa47e..91f6103 100644
--- a/packages/critters/src/index.js
+++ b/packages/critters/src/index.js
@@ -118,7 +118,7 @@ export default class Critters {
publicPath: '',
reduceInlineStyles: true,
pruneSource: false,
- additionalStylesheets: [],
+ additionalStylesheets: []
},
options || {}
);
@@ -476,25 +476,23 @@ export default class Critters {
return false;
}
});
+
// If there are no matched selectors, remove the rule:
- if (rule.selectors.length === 0) {
+ if (!rule.selector) {
return false;
}
- if (rule.declarations) {
- for (let i = 0; i < rule.declarations.length; i++) {
- const decl = rule.declarations[i];
+ if (rule.nodes) {
+ for (let i = 0; i < rule.nodes.length; i++) {
+ const decl = rule.nodes[i];
// detect used fonts
- if (decl.property && decl.property.match(/\bfont(-family)?\b/i)) {
+ if (decl.prop && decl.prop.match(/\bfont(-family)?\b/i)) {
criticalFonts += ' ' + decl.value;
}
// detect used keyframes
- if (
- decl.property === 'animation' ||
- decl.property === 'animation-name'
- ) {
+ if (decl.prop === 'animation' || decl.prop === 'animation-name') {
// @todo: parse animation declarations and extract only the name. for now we'll do a lazy match.
const names = decl.value.split(/\s+/);
for (let j = 0; j < names.length; j++) {
@@ -507,10 +505,10 @@ export default class Critters {
}
// keep font rules, they're handled in the second pass:
- if (rule.type === 'font-face') return;
+ if (rule.type === 'atrule' && rule.name === 'font-face') return;
// If there are no remaining rules, remove the whole rule:
- const rules = rule.rules && rule.rules.filter((rule) => !rule.$$remove);
+ const rules = rule.nodes && rule.nodes.filter((rule) => !rule.$$remove);
return !rules || rules.length !== 0;
})
);
@@ -539,21 +537,21 @@ export default class Critters {
applyMarkedSelectors(rule);
// prune @keyframes rules
- if (rule.type === 'keyframes') {
+ if (rule.type === 'atrule' && rule.name === 'keyframes') {
if (keyframesMode === 'none') return false;
if (keyframesMode === 'all') return true;
- return criticalKeyframeNames.indexOf(rule.name) !== -1;
+ return criticalKeyframeNames.indexOf(rule.params) !== -1;
}
// prune @font-face rules
- if (rule.type === 'font-face') {
+ if (rule.type === 'atrule' && rule.name === 'font-face') {
let family, src;
- for (let i = 0; i < rule.declarations.length; i++) {
- const decl = rule.declarations[i];
- if (decl.property === 'src') {
+ for (let i = 0; i < rule.nodes.length; i++) {
+ const decl = rule.nodes[i];
+ if (decl.prop === 'src') {
// @todo parse this properly and generate multiple preloads with type="font/woff2" etc
src = (decl.value.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || [])[2];
- } else if (decl.property === 'font-family') {
+ } else if (decl.prop === 'font-family') {
family = decl.value;
}
}
@@ -582,7 +580,7 @@ export default class Critters {
sheet = serializeStylesheet(ast, {
compress: this.options.compress !== false
- }).trim();
+ });
// If all rules were removed, get rid of the style element entirely
if (sheet.trim().length === 0) {
diff --git a/packages/critters/test/__snapshots__/critters.test.js.snap b/packages/critters/test/__snapshots__/critters.test.js.snap
index 90b5fbd..f5e1249 100644
--- a/packages/critters/test/__snapshots__/critters.test.js.snap
+++ b/packages/critters/test/__snapshots__/critters.test.js.snap
@@ -2,7 +2,7 @@
exports[`Critters Basic Usage 1`] = `
"
-
+