Skip to content

Commit b661614

Browse files
authored
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
1 parent 7b94fea commit b661614

11 files changed

+301
-110
lines changed

integrations/tailwindcss-cli/tests/cli.test.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ describe('Build command', () => {
2121
// `-i` is omitted, therefore the default `@tailwind base; @tailwind
2222
// components; @tailwind utilities` is used. However `preflight` is
2323
// disabled. I still want to verify that the `base` got included.
24-
expect(contents).toContain('*')
25-
expect(contents).toContain('::before')
26-
expect(contents).toContain('::after')
27-
expect(contents).toContain('--tw-shadow')
24+
expect(contents).toContain('--tw-ring-offset-shadow: 0 0 #0000')
25+
expect(contents).toContain('--tw-ring-shadow: 0 0 #0000')
26+
expect(contents).toContain('--tw-shadow: 0 0 #0000')
2827

2928
// Verify `utilities` output is correct
3029
expect(contents).toIncludeCss(

src/featureFlags.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import chalk from 'chalk'
22
import log from './util/log'
33

4-
const featureFlags = {
4+
let defaults = {
5+
optimizeUniversalDefaults: true,
6+
}
7+
8+
let featureFlags = {
59
future: [],
610
experimental: ['optimizeUniversalDefaults'],
711
}
812

913
export function flagEnabled(config, flag) {
1014
if (featureFlags.future.includes(flag)) {
11-
return config.future === 'all' || (config?.future?.[flag] ?? false)
15+
return config.future === 'all' || (config?.future?.[flag] ?? defaults[flag] ?? false)
1216
}
1317

1418
if (featureFlags.experimental.includes(flag)) {
15-
return config.experimental === 'all' || (config?.experimental?.[flag] ?? false)
19+
return (
20+
config.experimental === 'all' || (config?.experimental?.[flag] ?? defaults[flag] ?? false)
21+
)
1622
}
1723

1824
return false
@@ -34,7 +40,7 @@ export function issueFlagNotices(config) {
3440
}
3541

3642
if (experimentalFlagsEnabled(config).length > 0) {
37-
const changes = experimentalFlagsEnabled(config)
43+
let changes = experimentalFlagsEnabled(config)
3844
.map((s) => chalk.yellow(s))
3945
.join(', ')
4046

src/lib/resolveDefaultsAtRules.js

+39-43
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,56 @@ import postcss from 'postcss'
22
import selectorParser from 'postcss-selector-parser'
33
import { flagEnabled } from '../featureFlags'
44

5-
function isPseudoElement(n) {
6-
if (n.type !== 'pseudo') {
7-
return false
8-
}
9-
10-
return (
11-
n.value.startsWith('::') ||
12-
[':before', ':after', ':first-line', ':first-letter'].includes(n.value)
13-
)
5+
let getNode = {
6+
id(node) {
7+
return selectorParser.attribute({
8+
attribute: 'id',
9+
operator: '=',
10+
value: node.value,
11+
quoteMark: '"',
12+
})
13+
},
1414
}
1515

1616
function minimumImpactSelector(nodes) {
1717
let rest = nodes
18-
// Keep all pseudo & combinator types (:not([hidden]) ~ :not([hidden]))
19-
.filter((n) => n.type === 'pseudo' || n.type === 'combinator')
20-
// Remove leading pseudo's (:hover, :focus, ...)
21-
.filter((n, idx, all) => {
22-
// Keep pseudo elements
23-
if (isPseudoElement(n)) return true
18+
.filter((node) => {
19+
// Keep non-pseudo nodes
20+
if (node.type !== 'pseudo') return true
21+
22+
// Keep pseudo nodes that have subnodes
23+
// E.g.: `:not()` contains subnodes inside the parentheses
24+
if (node.nodes.length > 0) return true
25+
26+
// Keep pseudo `elements`
27+
// This implicitly means that we ignore pseudo `classes`
28+
return (
29+
node.value.startsWith('::') ||
30+
[':before', ':after', ':first-line', ':first-letter'].includes(node.value)
31+
)
32+
})
33+
.reverse()
2434

25-
if (idx === 0 && n.type === 'pseudo') return false
26-
if (idx > 0 && n.type === 'pseudo' && all[idx - 1].type === 'pseudo') return false
35+
let searchFor = new Set(['tag', 'class', 'id', 'attribute'])
2736

28-
return true
29-
})
37+
let splitPointIdx = rest.findIndex((n) => searchFor.has(n.type))
38+
if (splitPointIdx === -1) return rest.reverse().join('').trim()
3039

31-
let [bestNode] = nodes
32-
33-
for (let [type, getNode = (n) => n] of [
34-
['class'],
35-
[
36-
'id',
37-
(n) =>
38-
selectorParser.attribute({
39-
attribute: 'id',
40-
operator: '=',
41-
value: n.value,
42-
quoteMark: '"',
43-
}),
44-
],
45-
['attribute'],
46-
]) {
47-
let match = nodes.find((n) => n.type === type)
48-
49-
if (match) {
50-
bestNode = getNode(match)
51-
break
52-
}
40+
let node = rest[splitPointIdx]
41+
let bestNode = getNode[node.type] ? getNode[node.type](node) : node
42+
43+
rest = rest.slice(0, splitPointIdx)
44+
45+
let combinatorIdx = rest.findIndex((n) => n.type === 'combinator' && n.value === '>')
46+
if (combinatorIdx !== -1) {
47+
rest.splice(0, combinatorIdx)
48+
rest.unshift(selectorParser.universal())
5349
}
5450

55-
return [bestNode, ...rest].join('').trim()
51+
return [bestNode, ...rest.reverse()].join('').trim()
5652
}
5753

58-
let elementSelectorParser = selectorParser((selectors) => {
54+
export let elementSelectorParser = selectorParser((selectors) => {
5955
return selectors.map((s) => {
6056
let nodes = s.split((n) => n.type === 'combinator' && n.value === ' ').pop()
6157
return minimumImpactSelector(nodes)

tests/basic-usage.test.css

+59-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
*,
2-
::before,
3-
::after {
1+
.translate-x-5,
2+
.-translate-x-4,
3+
.translate-y-6,
4+
.-translate-x-3,
5+
.rotate-3,
6+
.skew-y-12,
7+
.skew-x-12,
8+
.scale-95,
9+
.transform {
410
--tw-translate-x: 0;
511
--tw-translate-y: 0;
612
--tw-rotate: 0;
@@ -11,19 +17,55 @@
1117
--tw-transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y))
1218
rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
1319
scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
20+
}
21+
22+
.snap-x {
1423
--tw-scroll-snap-strictness: proximity;
24+
}
25+
26+
.divide-x-2 > *,
27+
.divide-y-4 > *,
28+
.divide-x-0 > *,
29+
.divide-y-0 > *,
30+
.border,
31+
.border-2,
32+
.border-x-4,
33+
.border-y-4,
34+
.border-t,
35+
.border-b-4 {
1536
--tw-border-opacity: 1;
1637
border-color: rgb(229 231 235 / var(--tw-border-opacity));
38+
}
39+
40+
.shadow,
41+
.shadow-md,
42+
.shadow-lg {
1743
--tw-ring-offset-shadow: 0 0 #0000;
1844
--tw-ring-shadow: 0 0 #0000;
1945
--tw-shadow: 0 0 #0000;
46+
}
47+
48+
.ring,
49+
.ring-4 {
2050
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
2151
--tw-ring-offset-width: 0px;
2252
--tw-ring-offset-color: #fff;
2353
--tw-ring-color: rgb(59 130 246 / 0.5);
2454
--tw-ring-offset-shadow: 0 0 #0000;
2555
--tw-ring-shadow: 0 0 #0000;
2656
--tw-shadow: 0 0 #0000;
57+
}
58+
59+
.blur-md,
60+
.brightness-150,
61+
.contrast-50,
62+
.drop-shadow-md,
63+
.grayscale,
64+
.hue-rotate-60,
65+
.invert,
66+
.saturate-200,
67+
.sepia,
68+
.filter {
2769
--tw-blur: var(--tw-empty, /*!*/ /*!*/);
2870
--tw-brightness: var(--tw-empty, /*!*/ /*!*/);
2971
--tw-contrast: var(--tw-empty, /*!*/ /*!*/);
@@ -35,6 +77,18 @@
3577
--tw-drop-shadow: var(--tw-empty, /*!*/ /*!*/);
3678
--tw-filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale)
3779
var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
80+
}
81+
82+
.backdrop-blur-lg,
83+
.backdrop-brightness-50,
84+
.backdrop-contrast-0,
85+
.backdrop-grayscale,
86+
.backdrop-hue-rotate-90,
87+
.backdrop-invert,
88+
.backdrop-opacity-75,
89+
.backdrop-saturate-150,
90+
.backdrop-sepia,
91+
.backdrop-filter {
3892
--tw-backdrop-blur: var(--tw-empty, /*!*/ /*!*/);
3993
--tw-backdrop-brightness: var(--tw-empty, /*!*/ /*!*/);
4094
--tw-backdrop-contrast: var(--tw-empty, /*!*/ /*!*/);
@@ -351,10 +405,10 @@
351405
.columns-md {
352406
columns: 28rem;
353407
}
354-
.break-before-page {
408+
.break-before-page {
355409
break-before: page;
356410
}
357-
.break-inside-avoid-column {
411+
.break-inside-avoid-column {
358412
break-inside: avoid-column;
359413
}
360414
.break-after-auto {

tests/experimental.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ test('experimental universal selector improvements (child selectors: divide-y)',
113113

114114
return run(input, config).then((result) => {
115115
expect(result.css).toMatchCss(css`
116-
.divide-y > :not([hidden]) ~ :not([hidden]) {
116+
.divide-y > * {
117117
--tw-border-opacity: 1;
118118
border-color: rgb(229 231 235 / var(--tw-border-opacity));
119119
}
@@ -145,7 +145,7 @@ test('experimental universal selector improvements (hover:divide-y)', () => {
145145

146146
return run(input, config).then((result) => {
147147
expect(result.css).toMatchCss(css`
148-
.hover\\:divide-y > :not([hidden]) ~ :not([hidden]) {
148+
.hover\\:divide-y > * {
149149
--tw-border-opacity: 1;
150150
border-color: rgb(229 231 235 / var(--tw-border-opacity));
151151
}
@@ -178,7 +178,7 @@ test('experimental universal selector improvements (#app important)', () => {
178178

179179
return run(input, config).then((result) => {
180180
expect(result.css).toMatchCss(css`
181-
.divide-y > :not([hidden]) ~ :not([hidden]) {
181+
.divide-y > * {
182182
--tw-border-opacity: 1;
183183
border-color: rgb(229 231 235 / var(--tw-border-opacity));
184184
}

tests/kitchen-sink.test.css

+12-3
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@
126126
}
127127
}
128128
}
129-
*,
130-
::before,
131-
::after {
129+
.scale-50,
130+
.transform,
131+
.hover\:scale-75 {
132132
--tw-translate-x: 0;
133133
--tw-translate-y: 0;
134134
--tw-rotate: 0;
@@ -139,9 +139,18 @@
139139
--tw-transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y))
140140
rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
141141
scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
142+
}
143+
144+
.shadow-sm,
145+
.shadow-md,
146+
.hover\:shadow-lg,
147+
.md\:shadow-sm {
142148
--tw-ring-offset-shadow: 0 0 #0000;
143149
--tw-ring-shadow: 0 0 #0000;
144150
--tw-shadow: 0 0 #0000;
151+
}
152+
153+
.focus\:ring-2 {
145154
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
146155
--tw-ring-offset-width: 0px;
147156
--tw-ring-offset-color: #fff;

tests/match-components.test.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ it('should be possible to matchComponents', () => {
4141

4242
return run('@tailwind base; @tailwind components; @tailwind utilities', config).then((result) => {
4343
return expect(result.css).toMatchFormattedCss(css`
44-
*,
45-
::before,
46-
::after {
44+
.shadow {
4745
--tw-ring-offset-shadow: 0 0 #0000;
4846
--tw-ring-shadow: 0 0 #0000;
4947
--tw-shadow: 0 0 #0000;

tests/minimum-impact-selector.test.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { elementSelectorParser } from '../src/lib/resolveDefaultsAtRules'
2+
3+
it.each`
4+
before | after
5+
${'*'} | ${'*'}
6+
${'*:hover'} | ${'*'}
7+
${'* > *'} | ${'* > *'}
8+
${'.foo'} | ${'.foo'}
9+
${'.foo:hover'} | ${'.foo'}
10+
${'.foo:focus:hover'} | ${'.foo'}
11+
${'li:first-child'} | ${'li'}
12+
${'li:before'} | ${'li:before'}
13+
${'li::before'} | ${'li::before'}
14+
${'#app .foo'} | ${'.foo'}
15+
${'#app'} | ${'[id=app]'}
16+
${'#app.other'} | ${'.other'}
17+
${'input[type="text"]'} | ${'[type="text"]'}
18+
${'input[type="text"].foo'} | ${'.foo'}
19+
${'.group .group\\:foo'} | ${'.group\\:foo'}
20+
${'.group:hover .group-hover\\:foo'} | ${'.group-hover\\:foo'}
21+
${'.owl > * + *'} | ${'.owl > *'}
22+
${'.owl > :not([hidden]) + :not([hidden])'} | ${'.owl > *'}
23+
${'.group:hover .group-hover\\:owl > :not([hidden]) + :not([hidden])'} | ${'.group-hover\\:owl > *'}
24+
${'.peer:first-child ~ .peer-first\\:shadow-md'} | ${'.peer-first\\:shadow-md'}
25+
${'.whats ~ .next > span:hover'} | ${'span'}
26+
${'.foo .bar ~ .baz > .next > span > article:hover'} | ${'article'}
27+
`('should generate "$after" from "$before"', ({ before, after }) => {
28+
expect(elementSelectorParser.transformSync(before).join(', ')).toEqual(after)
29+
})

0 commit comments

Comments
 (0)