tailwindcss/src/lib/resolveDefaultsAtRules.js
Robin Malfait b661614265
Enable optimize universal defaults by default (#5635)
* enabled `optimizeUniversalDefaults` by default

This PR is done in a way so that the default is set to `true`, but you
can still disable it if it causes issues. In this case we do appreciate
an issue in that case 😅.

* update tests to use optimized universal selector

* update integration tests

* add dedicated tests for the optimized universal selector

* improve minimumImpactSelector algorithm

I think I cracked the algorithm, but I will probably need another pair
of eyes on the subject.

The current implementation works like this:

Prerequisites:

- The selector should already have been parsed using the selectorParser
  from 'postcss-selector-parser'.

Algorithm:

1. Remove all of the pseudo classes from the list of nodes.
  1.1. We do want to keep pseudo elements (E.g.: `::before`, `::first-line`, ...)
  1.2. We do want to keep pseudo classes that contain nodes (E.g.:
    `:not(...)`)
2. Reverse the list of nodes.
  This will make it easier to search from the end to the start. For
  example `.group:hover .group-hover` should result in `.group-hover`
  not `.group`.
  2.1. Find the index of the best match (class, id, attribute), and
    convert the node if required. (E.g.: `span#app` -> `#app` => `[id="app"]`)
  2.2. Remove the rest of the selector that is not important anymore
  2.3. Re-join the left-over nodes together

* update tests using new algorithm

* also look for `tag` types

* take `tag` into account

* simplify logic

* add test to prove `rest.reverse()` in first case is required

In case we don't find a match (idx === -1), we use `rest.reverse()`.
However, it looks like you can just use `nodes` instead.
This is not entirely true, because the `rest` variable will contain only
the nodes that are not pseudo elements.

`*:hover` would result in `*:hover` instead of just `*`

* replace all nodes after > with a single universal selector
2021-10-06 17:45:26 +02:00

121 lines
3.1 KiB
JavaScript

import postcss from 'postcss'
import selectorParser from 'postcss-selector-parser'
import { flagEnabled } from '../featureFlags'
let getNode = {
id(node) {
return selectorParser.attribute({
attribute: 'id',
operator: '=',
value: node.value,
quoteMark: '"',
})
},
}
function minimumImpactSelector(nodes) {
let rest = nodes
.filter((node) => {
// Keep non-pseudo nodes
if (node.type !== 'pseudo') return true
// Keep pseudo nodes that have subnodes
// E.g.: `:not()` contains subnodes inside the parentheses
if (node.nodes.length > 0) return true
// Keep pseudo `elements`
// This implicitly means that we ignore pseudo `classes`
return (
node.value.startsWith('::') ||
[':before', ':after', ':first-line', ':first-letter'].includes(node.value)
)
})
.reverse()
let searchFor = new Set(['tag', 'class', 'id', 'attribute'])
let splitPointIdx = rest.findIndex((n) => searchFor.has(n.type))
if (splitPointIdx === -1) return rest.reverse().join('').trim()
let node = rest[splitPointIdx]
let bestNode = getNode[node.type] ? getNode[node.type](node) : node
rest = rest.slice(0, splitPointIdx)
let combinatorIdx = rest.findIndex((n) => n.type === 'combinator' && n.value === '>')
if (combinatorIdx !== -1) {
rest.splice(0, combinatorIdx)
rest.unshift(selectorParser.universal())
}
return [bestNode, ...rest.reverse()].join('').trim()
}
export let elementSelectorParser = selectorParser((selectors) => {
return selectors.map((s) => {
let nodes = s.split((n) => n.type === 'combinator' && n.value === ' ').pop()
return minimumImpactSelector(nodes)
})
})
let cache = new Map()
function extractElementSelector(selector) {
if (!cache.has(selector)) {
cache.set(selector, elementSelectorParser.transformSync(selector))
}
return cache.get(selector)
}
export default function resolveDefaultsAtRules({ tailwindConfig }) {
return (root) => {
let variableNodeMap = new Map()
let universals = new Set()
root.walkAtRules('defaults', (rule) => {
if (rule.nodes && rule.nodes.length > 0) {
universals.add(rule)
return
}
let variable = rule.params
if (!variableNodeMap.has(variable)) {
variableNodeMap.set(variable, new Set())
}
variableNodeMap.get(variable).add(rule.parent)
rule.remove()
})
for (let universal of universals) {
let selectors = new Set()
let rules = variableNodeMap.get(universal.params) ?? []
for (let rule of rules) {
for (let selector of extractElementSelector(rule.selector)) {
selectors.add(selector)
}
}
if (selectors.size === 0) {
universal.remove()
continue
}
let universalRule = postcss.rule()
if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) {
universalRule.selectors = [...selectors]
} else {
universalRule.selectors = ['*', '::before', '::after']
}
universalRule.append(universal.nodes)
universal.before(universalRule)
universal.remove()
}
}
}