Skip to content

Commit d1e9632

Browse files
adamwathanEpicEricneupauerRobinMalfait
authored
JIT: Add exhaustive pseudo-class and pseudo-element variant support (#4482)
* Add first-line, first-letter, and marker variants * Add selection variant Co-Authored-By: Eric Rodrigues Pires <[email protected]> * Add remaining pseudo-class variants * Add target pseudo-class Co-Authored-By: Peter Neupauer <[email protected]> * add test for parallel variants * implement parallel variants Co-authored-by: Eric Rodrigues Pires <[email protected]> Co-authored-by: Peter Neupauer <[email protected]> Co-authored-by: Robin Malfait <[email protected]>
1 parent 8cd6017 commit d1e9632

6 files changed

+437
-53
lines changed

src/jit/corePlugins.js

+75-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,55 @@ import {
1111
} from '../util/pluginUtils'
1212

1313
export default {
14-
pseudoClassVariants: function ({ config, addVariant }) {
14+
pseudoElementVariants: function ({ config, addVariant }) {
15+
addVariant(
16+
'first-letter',
17+
transformAllSelectors((selector) => {
18+
return updateAllClasses(selector, (className, { withPseudo }) => {
19+
return withPseudo(`first-letter${config('separator')}${className}`, '::first-letter')
20+
})
21+
})
22+
)
23+
24+
addVariant(
25+
'first-line',
26+
transformAllSelectors((selector) => {
27+
return updateAllClasses(selector, (className, { withPseudo }) => {
28+
return withPseudo(`first-line${config('separator')}${className}`, '::first-line')
29+
})
30+
})
31+
)
32+
33+
addVariant('marker', [
34+
transformAllSelectors((selector) => {
35+
let variantSelector = updateAllClasses(selector, (className) => {
36+
return `marker${config('separator')}${className}`
37+
})
38+
39+
return `${variantSelector} *::marker`
40+
}),
41+
transformAllSelectors((selector) => {
42+
return updateAllClasses(selector, (className, { withPseudo }) => {
43+
return withPseudo(`marker${config('separator')}${className}`, '::marker')
44+
})
45+
}),
46+
])
47+
48+
addVariant('selection', [
49+
transformAllSelectors((selector) => {
50+
let variantSelector = updateAllClasses(selector, (className) => {
51+
return `selection${config('separator')}${className}`
52+
})
53+
54+
return `${variantSelector} *::selection`
55+
}),
56+
transformAllSelectors((selector) => {
57+
return updateAllClasses(selector, (className, { withPseudo }) => {
58+
return withPseudo(`selection${config('separator')}${className}`, '::selection')
59+
})
60+
}),
61+
])
62+
1563
addVariant(
1664
'before',
1765
transformAllSelectors(
@@ -55,16 +103,40 @@ export default {
55103
}
56104
)
57105
)
58-
106+
},
107+
pseudoClassVariants: function ({ config, addVariant }) {
59108
let pseudoVariants = [
109+
// Positional
60110
['first', 'first-child'],
61111
['last', 'last-child'],
112+
['only', 'only-child'],
62113
['odd', 'nth-child(odd)'],
63114
['even', 'nth-child(even)'],
115+
'first-of-type',
116+
'last-of-type',
117+
'only-of-type',
118+
119+
// State
64120
'visited',
121+
'target',
122+
123+
// Forms
124+
'default',
65125
'checked',
66-
'empty',
126+
'indeterminate',
127+
'placeholder-shown',
128+
'autofill',
129+
'required',
130+
'valid',
131+
'invalid',
132+
'in-range',
133+
'out-of-range',
67134
'read-only',
135+
136+
// Content
137+
'empty',
138+
139+
// Interactive
68140
'focus-within',
69141
'hover',
70142
'focus',

src/jit/lib/generateRules.js

+28-25
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function applyVariant(variant, matches, context) {
100100
}
101101

102102
if (context.variantMap.has(variant)) {
103-
let [variantSort, applyThisVariant] = context.variantMap.get(variant)
103+
let variantFunctionTuples = context.variantMap.get(variant)
104104
let result = []
105105

106106
for (let [{ sort, layer, options }, rule] of matches) {
@@ -112,36 +112,39 @@ function applyVariant(variant, matches, context) {
112112
let container = postcss.root()
113113
container.append(rule.clone())
114114

115-
function modifySelectors(modifierFunction) {
116-
container.each((rule) => {
117-
if (rule.type !== 'rule') {
118-
return
119-
}
120-
121-
rule.selectors = rule.selectors.map((selector) => {
122-
return modifierFunction({
123-
get className() {
124-
return getClassNameFromSelector(selector)
125-
},
126-
selector,
115+
for (let [variantSort, variantFunction] of variantFunctionTuples) {
116+
let clone = container.clone()
117+
function modifySelectors(modifierFunction) {
118+
clone.each((rule) => {
119+
if (rule.type !== 'rule') {
120+
return
121+
}
122+
123+
rule.selectors = rule.selectors.map((selector) => {
124+
return modifierFunction({
125+
get className() {
126+
return getClassNameFromSelector(selector)
127+
},
128+
selector,
129+
})
127130
})
128131
})
132+
return clone
133+
}
134+
135+
let ruleWithVariant = variantFunction({
136+
container: clone,
137+
separator: context.tailwindConfig.separator,
138+
modifySelectors,
129139
})
130-
return container
131-
}
132140

133-
let ruleWithVariant = applyThisVariant({
134-
container,
135-
separator: context.tailwindConfig.separator,
136-
modifySelectors,
137-
})
141+
if (ruleWithVariant === null) {
142+
continue
143+
}
138144

139-
if (ruleWithVariant === null) {
140-
continue
145+
let withOffset = [{ sort: variantSort | sort, layer, options }, clone.nodes[0]]
146+
result.push(withOffset)
141147
}
142-
143-
let withOffset = [{ sort: variantSort | sort, layer, options }, container.nodes[0]]
144-
result.push(withOffset)
145148
}
146149

147150
return result

src/jit/lib/setupContextUtils.js

+22-8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import isPlainObject from '../../util/isPlainObject'
1313
import escapeClassName from '../../util/escapeClassName'
1414
import nameClass from '../../util/nameClass'
1515
import { coerceValue } from '../../util/pluginUtils'
16+
import bigSign from '../../util/bigSign'
1617
import corePlugins from '../corePlugins'
1718
import * as sharedState from './sharedState'
1819
import { env } from './sharedState'
@@ -152,9 +153,11 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
152153
}
153154

154155
return {
155-
addVariant(variantName, applyThisVariant, options = {}) {
156+
addVariant(variantName, variantFunctions, options = {}) {
157+
variantFunctions = [].concat(variantFunctions)
158+
156159
insertInto(variantList, variantName, options)
157-
variantMap.set(variantName, applyThisVariant)
160+
variantMap.set(variantName, variantFunctions)
158161
},
159162
postcss,
160163
prefix: applyConfiguredPrefix,
@@ -395,7 +398,7 @@ function resolvePlugins(context, tailwindDirectives, root) {
395398

396399
// TODO: This is a workaround for backwards compatibility, since custom variants
397400
// were historically sorted before screen/stackable variants.
398-
let beforeVariants = [corePlugins['pseudoClassVariants']]
401+
let beforeVariants = [corePlugins['pseudoElementVariants'], corePlugins['pseudoClassVariants']]
399402
let afterVariants = [
400403
corePlugins['directionVariants'],
401404
corePlugins['reducedMotionVariants'],
@@ -445,17 +448,28 @@ function registerPlugins(plugins, context) {
445448
}
446449

447450
reservedBits += 3n
448-
context.variantOrder = variantList.reduce(
449-
(map, variant, i) => map.set(variant, (1n << BigInt(i)) << reservedBits),
450-
new Map()
451+
452+
let offset = 0
453+
context.variantOrder = new Map(
454+
variantList
455+
.map((variant, i) => {
456+
let variantFunctions = variantMap.get(variant).length
457+
let bits = (1n << BigInt(i + offset)) << reservedBits
458+
offset += variantFunctions - 1
459+
return [variant, bits]
460+
})
461+
.sort(([, a], [, z]) => bigSign(a - z))
451462
)
452463

453464
context.minimumScreen = [...context.variantOrder.values()].shift()
454465

455466
// Build variantMap
456-
for (let [variantName, variantFunction] of variantMap.entries()) {
467+
for (let [variantName, variantFunctions] of variantMap.entries()) {
457468
let sort = context.variantOrder.get(variantName)
458-
context.variantMap.set(variantName, [sort, variantFunction])
469+
context.variantMap.set(
470+
variantName,
471+
variantFunctions.map((variantFunction, idx) => [sort << BigInt(idx), variantFunction])
472+
)
459473
}
460474
}
461475

tests/jit/parallel-variants.test.js

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import postcss from 'postcss'
2+
import path from 'path'
3+
import tailwind from '../../src/jit/index.js'
4+
import { transformAllSelectors, updateAllClasses } from '../../src/util/pluginUtils.js'
5+
6+
function run(input, config = {}) {
7+
const { currentTestName } = expect.getState()
8+
9+
return postcss(tailwind(config)).process(input, {
10+
from: `${path.resolve(__filename)}?test=${currentTestName}`,
11+
})
12+
}
13+
14+
test('basic parallel variants', async () => {
15+
let config = {
16+
mode: 'jit',
17+
purge: [
18+
{
19+
raw: '<div class="font-normal hover:test:font-black test:font-bold test:font-medium"></div>',
20+
},
21+
],
22+
theme: {},
23+
plugins: [
24+
function test({ addVariant, config }) {
25+
addVariant('test', [
26+
transformAllSelectors((selector) => {
27+
let variantSelector = updateAllClasses(selector, (className) => {
28+
return `test${config('separator')}${className}`
29+
})
30+
31+
return `${variantSelector} *::test`
32+
}),
33+
transformAllSelectors((selector) => {
34+
return updateAllClasses(selector, (className, { withPseudo }) => {
35+
return withPseudo(`test${config('separator')}${className}`, '::test')
36+
})
37+
}),
38+
])
39+
},
40+
],
41+
}
42+
43+
let css = `@tailwind utilities`
44+
45+
return run(css, config).then((result) => {
46+
expect(result.css).toMatchFormattedCss(`
47+
.font-normal {
48+
font-weight: 400;
49+
}
50+
.test\\:font-bold *::test {
51+
font-weight: 700;
52+
}
53+
.test\\:font-medium *::test {
54+
font-weight: 500;
55+
}
56+
.hover\\:test\\:font-black:hover *::test {
57+
font-weight: 900;
58+
}
59+
.test\\:font-bold::test {
60+
font-weight: 700;
61+
}
62+
.test\\:font-medium::test {
63+
font-weight: 500;
64+
}
65+
.hover\\:test\\:font-black:hover::test {
66+
font-weight: 900;
67+
}
68+
`)
69+
})
70+
})

0 commit comments

Comments
 (0)