Skip to content

Commit bc00445

Browse files
authored
Expose context.getVariants for intellisense (#9505)
* add `context.getVariants` * use `modifier` instead of `label` * handle `modifySelectors` version * use reference * reverse engineer manual format strings if container was touched * use new positional API for `matchVariant` * update changelog
1 parent b7d5a2f commit bc00445

File tree

5 files changed

+308
-26
lines changed

5 files changed

+308
-26
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
- Add aria variants ([#9557](https://github.com/tailwindlabs/tailwindcss/pull/9557))
3333
- Add `data-*` variants ([#9559](https://github.com/tailwindlabs/tailwindcss/pull/9559))
3434
- Upgrade to `postcss-nested` v6.0 ([#9546](https://github.com/tailwindlabs/tailwindcss/pull/9546))
35+
- Expose `context.getVariants` for intellisense ([#9505](https://github.com/tailwindlabs/tailwindcss/pull/9505))
3536

3637
### Fixed
3738

src/corePlugins.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ export let variantPlugins = {
375375
check = `(${check})`
376376
}
377377

378-
return `@supports ${check} `
378+
return `@supports ${check}`
379379
},
380380
{ values: theme('supports') ?? {} }
381381
)

src/lib/generateRules.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let classNameParser = selectorParser((selectors) => {
1818
return selectors.first.filter(({ type }) => type === 'class').pop().value
1919
})
2020

21-
function getClassNameFromSelector(selector) {
21+
export function getClassNameFromSelector(selector) {
2222
return classNameParser.transformSync(selector)
2323
}
2424

src/lib/setupContextUtils.js

+165-24
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@ import { toPath } from '../util/toPath'
1818
import log from '../util/log'
1919
import negateValue from '../util/negateValue'
2020
import isValidArbitraryValue from '../util/isValidArbitraryValue'
21-
import { generateRules } from './generateRules'
21+
import { generateRules, getClassNameFromSelector } from './generateRules'
2222
import { hasContentChanged } from './cacheInvalidation.js'
2323
import { Offsets } from './offsets.js'
2424
import { flagEnabled } from '../featureFlags.js'
25+
import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector'
2526

26-
let MATCH_VARIANT = Symbol()
27+
const VARIANT_TYPES = {
28+
AddVariant: Symbol.for('ADD_VARIANT'),
29+
MatchVariant: Symbol.for('MATCH_VARIANT'),
30+
}
31+
32+
const VARIANT_INFO = {
33+
Base: 1 << 0,
34+
Dynamic: 1 << 1,
35+
}
2736

2837
function prefix(context, selector) {
2938
let prefix = context.tailwindConfig.prefix
@@ -524,7 +533,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
524533
let result = variantFunction(
525534
Object.assign(
526535
{ modifySelectors, container, separator },
527-
variantFunction[MATCH_VARIANT] && { args, wrap, format }
536+
options.type === VARIANT_TYPES.MatchVariant && { args, wrap, format }
528537
)
529538
)
530539

@@ -570,33 +579,34 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
570579
for (let [key, value] of Object.entries(options?.values ?? {})) {
571580
api.addVariant(
572581
isSpecial ? `${variant}${key}` : `${variant}-${key}`,
573-
Object.assign(
574-
({ args, container }) =>
575-
variantFn(
576-
value,
577-
modifiersEnabled ? { modifier: args.modifier, container } : { container }
578-
),
579-
{
580-
[MATCH_VARIANT]: true,
581-
}
582-
),
583-
{ ...options, value, id }
584-
)
585-
}
586-
587-
api.addVariant(
588-
variant,
589-
Object.assign(
590582
({ args, container }) =>
591583
variantFn(
592-
args.value,
584+
value,
593585
modifiersEnabled ? { modifier: args.modifier, container } : { container }
594586
),
595587
{
596-
[MATCH_VARIANT]: true,
588+
...options,
589+
value,
590+
id,
591+
type: VARIANT_TYPES.MatchVariant,
592+
variantInfo: VARIANT_INFO.Base,
597593
}
598-
),
599-
{ ...options, id }
594+
)
595+
}
596+
597+
api.addVariant(
598+
variant,
599+
({ args, container }) =>
600+
variantFn(
601+
args.value,
602+
modifiersEnabled ? { modifier: args.modifier, container } : { container }
603+
),
604+
{
605+
...options,
606+
id,
607+
type: VARIANT_TYPES.MatchVariant,
608+
variantInfo: VARIANT_INFO.Dynamic,
609+
}
600610
)
601611
},
602612
}
@@ -948,6 +958,137 @@ function registerPlugins(plugins, context) {
948958

949959
return output
950960
}
961+
962+
// Generate a list of available variants with meta information of the type of variant.
963+
context.getVariants = function getVariants() {
964+
let result = []
965+
for (let [name, options] of context.variantOptions.entries()) {
966+
if (options.variantInfo === VARIANT_INFO.Base) continue
967+
968+
result.push({
969+
name,
970+
isArbitrary: options.type === Symbol.for('MATCH_VARIANT'),
971+
values: Object.keys(options.values ?? {}),
972+
selectors({ modifier, value } = {}) {
973+
let candidate = '__TAILWIND_PLACEHOLDER__'
974+
975+
let rule = postcss.rule({ selector: `.${candidate}` })
976+
let container = postcss.root({ nodes: [rule.clone()] })
977+
978+
let before = container.toString()
979+
980+
let fns = (context.variantMap.get(name) ?? []).flatMap(([_, fn]) => fn)
981+
let formatStrings = []
982+
for (let fn of fns) {
983+
let localFormatStrings = []
984+
985+
let api = {
986+
args: { modifier, value: options.values?.[value] ?? value },
987+
separator: context.tailwindConfig.separator,
988+
modifySelectors(modifierFunction) {
989+
// Run the modifierFunction over each rule
990+
container.each((rule) => {
991+
if (rule.type !== 'rule') {
992+
return
993+
}
994+
995+
rule.selectors = rule.selectors.map((selector) => {
996+
return modifierFunction({
997+
get className() {
998+
return getClassNameFromSelector(selector)
999+
},
1000+
selector,
1001+
})
1002+
})
1003+
})
1004+
1005+
return container
1006+
},
1007+
format(str) {
1008+
localFormatStrings.push(str)
1009+
},
1010+
wrap(wrapper) {
1011+
localFormatStrings.push(`@${wrapper.name} ${wrapper.params} { & }`)
1012+
},
1013+
container,
1014+
}
1015+
1016+
let ruleWithVariant = fn(api)
1017+
if (localFormatStrings.length > 0) {
1018+
formatStrings.push(localFormatStrings)
1019+
}
1020+
1021+
if (Array.isArray(ruleWithVariant)) {
1022+
for (let variantFunction of ruleWithVariant) {
1023+
localFormatStrings = []
1024+
variantFunction(api)
1025+
formatStrings.push(localFormatStrings)
1026+
}
1027+
}
1028+
}
1029+
1030+
// Reverse engineer the result of the `container`
1031+
let manualFormatStrings = []
1032+
let after = container.toString()
1033+
1034+
if (before !== after) {
1035+
// Figure out all selectors
1036+
container.walkRules((rule) => {
1037+
let modified = rule.selector
1038+
1039+
// Rebuild the base selector, this is what plugin authors would do
1040+
// as well. E.g.: `${variant}${separator}${className}`.
1041+
// However, plugin authors probably also prepend or append certain
1042+
// classes, pseudos, ids, ...
1043+
let rebuiltBase = selectorParser((selectors) => {
1044+
selectors.walkClasses((classNode) => {
1045+
classNode.value = `${name}${context.tailwindConfig.separator}${classNode.value}`
1046+
})
1047+
}).processSync(modified)
1048+
1049+
// Now that we know the original selector, the new selector, and
1050+
// the rebuild part in between, we can replace the part that plugin
1051+
// authors need to rebuild with `&`, and eventually store it in the
1052+
// collectedFormats. Similar to what `format('...')` would do.
1053+
//
1054+
// E.g.:
1055+
// variant: foo
1056+
// selector: .markdown > p
1057+
// modified (by plugin): .foo .foo\\:markdown > p
1058+
// rebuiltBase (internal): .foo\\:markdown > p
1059+
// format: .foo &
1060+
manualFormatStrings.push(modified.replace(rebuiltBase, '&').replace(candidate, '&'))
1061+
})
1062+
1063+
// Figure out all atrules
1064+
container.walkAtRules((atrule) => {
1065+
manualFormatStrings.push(`@${atrule.name} (${atrule.params}) { & }`)
1066+
})
1067+
}
1068+
1069+
let result = formatStrings.map((formatString) =>
1070+
finalizeSelector(formatVariantSelector('&', ...formatString), {
1071+
selector: `.${candidate}`,
1072+
candidate,
1073+
context,
1074+
isArbitraryVariant: !(value in (options.values ?? {})),
1075+
})
1076+
.replace(`.${candidate}`, '&')
1077+
.replace('{ & }', '')
1078+
.trim()
1079+
)
1080+
1081+
if (manualFormatStrings.length > 0) {
1082+
result.push(formatVariantSelector('&', ...manualFormatStrings))
1083+
}
1084+
1085+
return result
1086+
},
1087+
})
1088+
}
1089+
1090+
return result
1091+
}
9511092
}
9521093

9531094
/**

tests/getVariants.test.js

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import postcss from 'postcss'
2+
import selectorParser from 'postcss-selector-parser'
3+
import resolveConfig from '../src/public/resolve-config'
4+
import { createContext } from '../src/lib/setupContextUtils'
5+
6+
it('should return a list of variants with meta information about the variant', () => {
7+
let config = {}
8+
let context = createContext(resolveConfig(config))
9+
10+
let variants = context.getVariants()
11+
12+
expect(variants).toContainEqual({
13+
name: 'hover',
14+
isArbitrary: false,
15+
values: [],
16+
selectors: expect.any(Function),
17+
})
18+
19+
expect(variants).toContainEqual({
20+
name: 'group',
21+
isArbitrary: true,
22+
values: expect.any(Array),
23+
selectors: expect.any(Function),
24+
})
25+
26+
// `group-hover` now belongs to the `group` variant. The information exposed for the `group`
27+
// variant is all you need.
28+
expect(variants.find((v) => v.name === 'group-hover')).toBeUndefined()
29+
})
30+
31+
it('should provide selectors for simple variants', () => {
32+
let config = {}
33+
let context = createContext(resolveConfig(config))
34+
35+
let variants = context.getVariants()
36+
37+
let variant = variants.find((v) => v.name === 'hover')
38+
expect(variant.selectors()).toEqual(['&:hover'])
39+
})
40+
41+
it('should provide selectors for parallel variants', () => {
42+
let config = {}
43+
let context = createContext(resolveConfig(config))
44+
45+
let variants = context.getVariants()
46+
47+
let variant = variants.find((v) => v.name === 'marker')
48+
expect(variant.selectors()).toEqual(['& *::marker', '&::marker'])
49+
})
50+
51+
it('should provide selectors for complex matchVariant variants like `group`', () => {
52+
let config = {}
53+
let context = createContext(resolveConfig(config))
54+
55+
let variants = context.getVariants()
56+
57+
let variant = variants.find((v) => v.name === 'group')
58+
expect(variant.selectors()).toEqual(['.group &'])
59+
expect(variant.selectors({})).toEqual(['.group &'])
60+
expect(variant.selectors({ value: 'hover' })).toEqual(['.group:hover &'])
61+
expect(variant.selectors({ value: '.foo_&' })).toEqual(['.foo .group &'])
62+
expect(variant.selectors({ modifier: 'foo', value: 'hover' })).toEqual(['.group\\/foo:hover &'])
63+
expect(variant.selectors({ modifier: 'foo', value: '.foo_&' })).toEqual(['.foo .group\\/foo &'])
64+
})
65+
66+
it('should provide selectors for variants with atrules', () => {
67+
let config = {}
68+
let context = createContext(resolveConfig(config))
69+
70+
let variants = context.getVariants()
71+
72+
let variant = variants.find((v) => v.name === 'supports')
73+
expect(variant.selectors({ value: 'display:grid' })).toEqual(['@supports (display:grid)'])
74+
expect(variant.selectors({ value: 'aspect-ratio' })).toEqual([
75+
'@supports (aspect-ratio: var(--tw))',
76+
])
77+
})
78+
79+
it('should provide selectors for custom plugins that do a combination of parallel variants with modifiers with arbitrary values and with atrules', () => {
80+
let config = {
81+
plugins: [
82+
function ({ matchVariant }) {
83+
matchVariant('foo', (value, { modifier }) => {
84+
return [
85+
`
86+
@supports (foo: ${modifier}) {
87+
@media (width <= 400px) {
88+
&:hover
89+
}
90+
}
91+
`,
92+
`.${modifier}\\/${value} &:focus`,
93+
]
94+
})
95+
},
96+
],
97+
}
98+
let context = createContext(resolveConfig(config))
99+
100+
let variants = context.getVariants()
101+
102+
let variant = variants.find((v) => v.name === 'foo')
103+
expect(variant.selectors({ modifier: 'bar', value: 'baz' })).toEqual([
104+
'@supports (foo: bar) { @media (width <= 400px) { &:hover } }',
105+
'.bar\\/baz &:focus',
106+
])
107+
})
108+
109+
it('should work for plugins that still use the modifySelectors API', () => {
110+
let config = {
111+
plugins: [
112+
function ({ addVariant }) {
113+
addVariant('foo', ({ modifySelectors, container }) => {
114+
// Manually mutating the selector
115+
modifySelectors(({ selector }) => {
116+
return selectorParser((selectors) => {
117+
selectors.walkClasses((classNode) => {
118+
classNode.value = `foo:${classNode.value}`
119+
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
120+
})
121+
}).processSync(selector)
122+
})
123+
124+
// Manually wrap in supports query
125+
let wrapper = postcss.atRule({ name: 'supports', params: 'display: grid' })
126+
let nodes = container.nodes
127+
container.removeAll()
128+
wrapper.append(nodes)
129+
container.append(wrapper)
130+
})
131+
},
132+
],
133+
}
134+
let context = createContext(resolveConfig(config))
135+
136+
let variants = context.getVariants()
137+
138+
let variant = variants.find((v) => v.name === 'foo')
139+
expect(variant.selectors({})).toEqual(['@supports (display: grid) { .foo .foo\\:& }'])
140+
})

0 commit comments

Comments
 (0)