diff --git a/src/jit/lib/resolveDefaultsAtRules.js b/src/jit/lib/resolveDefaultsAtRules.js index 14e7bf19a..2f3f7543f 100644 --- a/src/jit/lib/resolveDefaultsAtRules.js +++ b/src/jit/lib/resolveDefaultsAtRules.js @@ -1,14 +1,43 @@ import postcss from 'postcss' import selectorParser from 'postcss-selector-parser' +function minimumImpactSelector(nodes) { + let pseudos = nodes.filter((n) => n.type === 'pseudo') + let [bestNode] = nodes + + for (let [type, getNode = (n) => n] of [ + ['class'], + [ + 'id', + (n) => + selectorParser.attribute({ + attribute: 'id', + operator: '=', + value: n.value, + quoteMark: '"', + }), + ], + ['attribute'], + ]) { + let match = nodes.find((n) => n.type === type) + + if (match) { + bestNode = getNode(match) + break + } + } + + return [bestNode, ...pseudos].join('').trim() +} + let elementSelectorParser = selectorParser((selectors) => { return selectors.map((s) => { - return s + let nodes = s .split((n) => n.type === 'combinator') .pop() .filter((n) => n.type !== 'pseudo' || n.value.startsWith('::')) - .join('') - .trim() + + return minimumImpactSelector(nodes) }) }) @@ -28,7 +57,7 @@ export default function resolveDefaultsAtRules() { let universals = new Set() root.walkAtRules('defaults', (rule) => { - if (rule.nodes.length > 0) { + if (rule.nodes && rule.nodes.length > 0) { universals.add(rule) return } diff --git a/tests/jit/resolve-defaults-at-rules.test.js b/tests/jit/resolve-defaults-at-rules.test.js index 4ca62e163..240422994 100644 --- a/tests/jit/resolve-defaults-at-rules.test.js +++ b/tests/jit/resolve-defaults-at-rules.test.js @@ -605,3 +605,104 @@ test('when a utility uses defaults but they do not exist', async () => { `) }) }) + +test('selectors are reduced to the lowest possible specificity', async () => { + let config = { + mode: 'jit', + purge: [ + { + raw: '
', + }, + ], + theme: {}, + plugins: [], + corePlugins: [], + } + + let css = ` + @defaults test { + --color: black; + } + + /* --- */ + + .foo { + @defaults test; + background-color: var(--color); + } + + #app { + @defaults test; + border-color: var(--color); + } + + span#page { + @defaults test; + color: var(--color); + } + + div[data-foo="bar"]#other { + @defaults test; + fill: var(--color); + } + + div[data-bar="baz"] { + @defaults test; + stroke: var(--color); + } + + article { + @defaults test; + --article: var(--color); + } + + div[data-foo="bar"]#another::before { + @defaults test; + fill: var(--color); + } + ` + + return run(css, config).then((result) => { + expect(result.css).toMatchFormattedCss(` + .foo, + [id="app"], + [id="page"], + [id="other"], + [data-bar="baz"], + article, + [id="another"]::before { + --color: black; + } + + /* --- */ + + .foo { + background-color: var(--color); + } + + #app { + border-color: var(--color); + } + + span#page { + color: var(--color); + } + + div[data-foo="bar"]#other { + fill: var(--color); + } + + div[data-bar="baz"] { + stroke: var(--color); + } + + article { + --article: var(--color); + } + + div[data-foo="bar"]#another::before { + fill: var(--color); + } + `) + }) +})