From bd8c6f1e8879bad1bbd28afad904c4c6d6f2b0c6 Mon Sep 17 00:00:00 2001 From: Ludovico Fischer Date: Wed, 12 May 2021 00:01:26 +0200 Subject: [PATCH] fix(postcss-merge-rules): do not merge unknown pseudo-selectors Do not merge if the pseudo-selector is not in the list of well-known pseudo-selectors and does not start with a vendor prefix. We let through vendor prefixes as they are handled elsewhere in the code. Fix #999 --- .../src/__tests__/index.js | 5 +++ packages/postcss-merge-rules/src/index.js | 40 +++-------------- .../src/lib/ensureCompatibility.js | 43 ++++++++++++++++--- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/packages/postcss-merge-rules/src/__tests__/index.js b/packages/postcss-merge-rules/src/__tests__/index.js index 43ed8e5a7..59cc1dabf 100644 --- a/packages/postcss-merge-rules/src/__tests__/index.js +++ b/packages/postcss-merge-rules/src/__tests__/index.js @@ -627,6 +627,11 @@ test( ) ); +test( + 'should not merge unknown and known selector', + passthroughCSS('p {color: blue}:nonsense {color: blue}') +); + test( 'should merge multiple media queries', processCSS( diff --git a/packages/postcss-merge-rules/src/index.js b/packages/postcss-merge-rules/src/index.js index eda11f530..04f6e9175 100644 --- a/packages/postcss-merge-rules/src/index.js +++ b/packages/postcss-merge-rules/src/index.js @@ -1,10 +1,10 @@ import browserslist from 'browserslist'; -import vendors from 'vendors'; import { sameParent } from 'cssnano-utils'; -import ensureCompatibility from './lib/ensureCompatibility'; - -/** @type {string[]} */ -const prefixes = vendors.map((v) => `-${v}-`); +import { + ensureCompatibility, + sameVendor, + noVendor, +} from './lib/ensureCompatibility'; /** * @param {postcss.Declaration} a @@ -52,36 +52,6 @@ function sameDeclarationsAndOrder(a, b) { return a.every((d, index) => declarationIsEqual(d, b[index])); } -// Internet Explorer use :-ms-input-placeholder. -// Microsoft Edge use ::-ms-input-placeholder. -const findMsInputPlaceholder = (selector) => - ~selector.search(/-ms-input-placeholder/i); - -/** - * @param {string} selector - * @return {string[]} - */ -function filterPrefixes(selector) { - return prefixes.filter((prefix) => selector.indexOf(prefix) !== -1); -} - -function sameVendor(selectorsA, selectorsB) { - let same = (selectors) => selectors.map(filterPrefixes).join(); - let findMsVendor = (selectors) => selectors.find(findMsInputPlaceholder); - return ( - same(selectorsA) === same(selectorsB) && - !(findMsVendor(selectorsA) && findMsVendor(selectorsB)) - ); -} - -/** - * @param {string} selector - * @return {boolean} - */ -function noVendor(selector) { - return !filterPrefixes(selector).length; -} - /** * @param {postcss.Rule} ruleA * @param {postcss.Rule} ruleB diff --git a/packages/postcss-merge-rules/src/lib/ensureCompatibility.js b/packages/postcss-merge-rules/src/lib/ensureCompatibility.js index 68e855d4c..c0457a6ff 100644 --- a/packages/postcss-merge-rules/src/lib/ensureCompatibility.js +++ b/packages/postcss-merge-rules/src/lib/ensureCompatibility.js @@ -1,5 +1,6 @@ import { isSupported } from 'caniuse-api'; import selectorParser from 'postcss-selector-parser'; +import vendors from 'vendors'; const simpleSelectorRe = /^#?[-._a-z0-9 ]+$/i; @@ -10,6 +11,39 @@ const cssFirstLetter = 'css-first-letter'; const cssFirstLine = 'css-first-line'; const cssInOutOfRange = 'css-in-out-of-range'; +/** @type {string[]} */ +const prefixes = vendors.map((v) => `-${v}-`); + +/** + * @param {string} selector + * @return {string[]} + */ +export function filterPrefixes(selector) { + return prefixes.filter((prefix) => selector.indexOf(prefix) !== -1); +} + +// Internet Explorer use :-ms-input-placeholder. +// Microsoft Edge use ::-ms-input-placeholder. +const findMsInputPlaceholder = (selector) => + ~selector.search(/-ms-input-placeholder/i); + +export function sameVendor(selectorsA, selectorsB) { + let same = (selectors) => selectors.map(filterPrefixes).join(); + let findMsVendor = (selectors) => selectors.find(findMsInputPlaceholder); + return ( + same(selectorsA) === same(selectorsB) && + !(findMsVendor(selectorsA) && findMsVendor(selectorsB)) + ); +} + +/** + * @param {string} selector + * @return {boolean} + */ +export function noVendor(selector) { + return !filterPrefixes(selector).length; +} + export const pseudoElements = { ':active': cssSel2, ':after': cssGencontent, @@ -80,11 +114,7 @@ function isSupportedCached(feature, browsers) { return result; } -export default function ensureCompatibility( - selectors, - browsers, - compatibilityCache -) { +export function ensureCompatibility(selectors, browsers, compatibilityCache) { // Should not merge mixins if (selectors.some(isCssMixin)) { return false; @@ -107,6 +137,9 @@ export default function ensureCompatibility( const { type, value } = node; if (type === 'pseudo') { const entry = pseudoElements[value]; + if (!entry && noVendor(value)) { + compatible = false; + } if (entry && compatible) { compatible = isSupportedCached(entry, browsers); }