resolveDefaultsAtRules.js
4.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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()
/** @type {Set<import('postcss').AtRule>} */
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()
})
if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) {
for (let universal of universals) {
/** @type {Map<string, Set<string>>} */
let selectorGroups = new Map()
let rules = variableNodeMap.get(universal.params) ?? []
for (let rule of rules) {
for (let selector of extractElementSelector(rule.selector)) {
// If selector contains a vendor prefix after a pseudo element or class,
// we consider them separately because merging the declarations into
// a single rule will cause browsers that do not understand the
// vendor prefix to throw out the whole rule
let selectorGroupName =
selector.includes(':-') || selector.includes('::-') ? selector : '__DEFAULT__'
let selectors = selectorGroups.get(selectorGroupName) ?? new Set()
selectorGroups.set(selectorGroupName, selectors)
selectors.add(selector)
}
}
if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) {
if (selectorGroups.size === 0) {
universal.remove()
continue
}
for (let [, selectors] of selectorGroups) {
let universalRule = postcss.rule({
source: universal.source,
})
universalRule.selectors = [...selectors]
universalRule.append(universal.nodes.map((node) => node.clone()))
universal.before(universalRule)
}
}
universal.remove()
}
} else if (universals.size) {
let universalRule = postcss.rule({
selectors: ['*', '::before', '::after'],
})
for (let universal of universals) {
universalRule.append(universal.nodes)
if (!universalRule.parent) {
universal.before(universalRule)
}
if (!universalRule.source) {
universalRule.source = universal.source
}
universal.remove()
}
let backdropRule = universalRule.clone({
selectors: ['::backdrop'],
})
universalRule.after(backdropRule)
}
}
}