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
This commit is contained in:
Robin Malfait 2022-10-17 12:38:21 +02:00 committed by GitHub
parent b7d5a2f247
commit bc004455bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 309 additions and 27 deletions

View File

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

View File

@ -375,7 +375,7 @@ export let variantPlugins = {
check = `(${check})`
}
return `@supports ${check} `
return `@supports ${check}`
},
{ values: theme('supports') ?? {} }
)

View File

@ -18,7 +18,7 @@ let classNameParser = selectorParser((selectors) => {
return selectors.first.filter(({ type }) => type === 'class').pop().value
})
function getClassNameFromSelector(selector) {
export function getClassNameFromSelector(selector) {
return classNameParser.transformSync(selector)
}

View File

@ -18,12 +18,21 @@ import { toPath } from '../util/toPath'
import log from '../util/log'
import negateValue from '../util/negateValue'
import isValidArbitraryValue from '../util/isValidArbitraryValue'
import { generateRules } from './generateRules'
import { generateRules, getClassNameFromSelector } from './generateRules'
import { hasContentChanged } from './cacheInvalidation.js'
import { Offsets } from './offsets.js'
import { flagEnabled } from '../featureFlags.js'
import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector'
let MATCH_VARIANT = Symbol()
const VARIANT_TYPES = {
AddVariant: Symbol.for('ADD_VARIANT'),
MatchVariant: Symbol.for('MATCH_VARIANT'),
}
const VARIANT_INFO = {
Base: 1 << 0,
Dynamic: 1 << 1,
}
function prefix(context, selector) {
let prefix = context.tailwindConfig.prefix
@ -524,7 +533,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
let result = variantFunction(
Object.assign(
{ modifySelectors, container, separator },
variantFunction[MATCH_VARIANT] && { args, wrap, format }
options.type === VARIANT_TYPES.MatchVariant && { args, wrap, format }
)
)
@ -570,33 +579,34 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
for (let [key, value] of Object.entries(options?.values ?? {})) {
api.addVariant(
isSpecial ? `${variant}${key}` : `${variant}-${key}`,
Object.assign(
({ args, container }) =>
variantFn(
value,
modifiersEnabled ? { modifier: args.modifier, container } : { container }
),
{
[MATCH_VARIANT]: true,
}
),
{ ...options, value, id }
({ args, container }) =>
variantFn(
value,
modifiersEnabled ? { modifier: args.modifier, container } : { container }
),
{
...options,
value,
id,
type: VARIANT_TYPES.MatchVariant,
variantInfo: VARIANT_INFO.Base,
}
)
}
api.addVariant(
variant,
Object.assign(
({ args, container }) =>
variantFn(
args.value,
modifiersEnabled ? { modifier: args.modifier, container } : { container }
),
{
[MATCH_VARIANT]: true,
}
),
{ ...options, id }
({ args, container }) =>
variantFn(
args.value,
modifiersEnabled ? { modifier: args.modifier, container } : { container }
),
{
...options,
id,
type: VARIANT_TYPES.MatchVariant,
variantInfo: VARIANT_INFO.Dynamic,
}
)
},
}
@ -948,6 +958,137 @@ function registerPlugins(plugins, context) {
return output
}
// Generate a list of available variants with meta information of the type of variant.
context.getVariants = function getVariants() {
let result = []
for (let [name, options] of context.variantOptions.entries()) {
if (options.variantInfo === VARIANT_INFO.Base) continue
result.push({
name,
isArbitrary: options.type === Symbol.for('MATCH_VARIANT'),
values: Object.keys(options.values ?? {}),
selectors({ modifier, value } = {}) {
let candidate = '__TAILWIND_PLACEHOLDER__'
let rule = postcss.rule({ selector: `.${candidate}` })
let container = postcss.root({ nodes: [rule.clone()] })
let before = container.toString()
let fns = (context.variantMap.get(name) ?? []).flatMap(([_, fn]) => fn)
let formatStrings = []
for (let fn of fns) {
let localFormatStrings = []
let api = {
args: { modifier, value: options.values?.[value] ?? value },
separator: context.tailwindConfig.separator,
modifySelectors(modifierFunction) {
// Run the modifierFunction over each rule
container.each((rule) => {
if (rule.type !== 'rule') {
return
}
rule.selectors = rule.selectors.map((selector) => {
return modifierFunction({
get className() {
return getClassNameFromSelector(selector)
},
selector,
})
})
})
return container
},
format(str) {
localFormatStrings.push(str)
},
wrap(wrapper) {
localFormatStrings.push(`@${wrapper.name} ${wrapper.params} { & }`)
},
container,
}
let ruleWithVariant = fn(api)
if (localFormatStrings.length > 0) {
formatStrings.push(localFormatStrings)
}
if (Array.isArray(ruleWithVariant)) {
for (let variantFunction of ruleWithVariant) {
localFormatStrings = []
variantFunction(api)
formatStrings.push(localFormatStrings)
}
}
}
// Reverse engineer the result of the `container`
let manualFormatStrings = []
let after = container.toString()
if (before !== after) {
// Figure out all selectors
container.walkRules((rule) => {
let modified = rule.selector
// Rebuild the base selector, this is what plugin authors would do
// as well. E.g.: `${variant}${separator}${className}`.
// However, plugin authors probably also prepend or append certain
// classes, pseudos, ids, ...
let rebuiltBase = selectorParser((selectors) => {
selectors.walkClasses((classNode) => {
classNode.value = `${name}${context.tailwindConfig.separator}${classNode.value}`
})
}).processSync(modified)
// Now that we know the original selector, the new selector, and
// the rebuild part in between, we can replace the part that plugin
// authors need to rebuild with `&`, and eventually store it in the
// collectedFormats. Similar to what `format('...')` would do.
//
// E.g.:
// variant: foo
// selector: .markdown > p
// modified (by plugin): .foo .foo\\:markdown > p
// rebuiltBase (internal): .foo\\:markdown > p
// format: .foo &
manualFormatStrings.push(modified.replace(rebuiltBase, '&').replace(candidate, '&'))
})
// Figure out all atrules
container.walkAtRules((atrule) => {
manualFormatStrings.push(`@${atrule.name} (${atrule.params}) { & }`)
})
}
let result = formatStrings.map((formatString) =>
finalizeSelector(formatVariantSelector('&', ...formatString), {
selector: `.${candidate}`,
candidate,
context,
isArbitraryVariant: !(value in (options.values ?? {})),
})
.replace(`.${candidate}`, '&')
.replace('{ & }', '')
.trim()
)
if (manualFormatStrings.length > 0) {
result.push(formatVariantSelector('&', ...manualFormatStrings))
}
return result
},
})
}
return result
}
}
/**

140
tests/getVariants.test.js Normal file
View File

@ -0,0 +1,140 @@
import postcss from 'postcss'
import selectorParser from 'postcss-selector-parser'
import resolveConfig from '../src/public/resolve-config'
import { createContext } from '../src/lib/setupContextUtils'
it('should return a list of variants with meta information about the variant', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
expect(variants).toContainEqual({
name: 'hover',
isArbitrary: false,
values: [],
selectors: expect.any(Function),
})
expect(variants).toContainEqual({
name: 'group',
isArbitrary: true,
values: expect.any(Array),
selectors: expect.any(Function),
})
// `group-hover` now belongs to the `group` variant. The information exposed for the `group`
// variant is all you need.
expect(variants.find((v) => v.name === 'group-hover')).toBeUndefined()
})
it('should provide selectors for simple variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'hover')
expect(variant.selectors()).toEqual(['&:hover'])
})
it('should provide selectors for parallel variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'marker')
expect(variant.selectors()).toEqual(['& *::marker', '&::marker'])
})
it('should provide selectors for complex matchVariant variants like `group`', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'group')
expect(variant.selectors()).toEqual(['.group &'])
expect(variant.selectors({})).toEqual(['.group &'])
expect(variant.selectors({ value: 'hover' })).toEqual(['.group:hover &'])
expect(variant.selectors({ value: '.foo_&' })).toEqual(['.foo .group &'])
expect(variant.selectors({ modifier: 'foo', value: 'hover' })).toEqual(['.group\\/foo:hover &'])
expect(variant.selectors({ modifier: 'foo', value: '.foo_&' })).toEqual(['.foo .group\\/foo &'])
})
it('should provide selectors for variants with atrules', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'supports')
expect(variant.selectors({ value: 'display:grid' })).toEqual(['@supports (display:grid)'])
expect(variant.selectors({ value: 'aspect-ratio' })).toEqual([
'@supports (aspect-ratio: var(--tw))',
])
})
it('should provide selectors for custom plugins that do a combination of parallel variants with modifiers with arbitrary values and with atrules', () => {
let config = {
plugins: [
function ({ matchVariant }) {
matchVariant('foo', (value, { modifier }) => {
return [
`
@supports (foo: ${modifier}) {
@media (width <= 400px) {
&:hover
}
}
`,
`.${modifier}\\/${value} &:focus`,
]
})
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({ modifier: 'bar', value: 'baz' })).toEqual([
'@supports (foo: bar) { @media (width <= 400px) { &:hover } }',
'.bar\\/baz &:focus',
])
})
it('should work for plugins that still use the modifySelectors API', () => {
let config = {
plugins: [
function ({ addVariant }) {
addVariant('foo', ({ modifySelectors, container }) => {
// Manually mutating the selector
modifySelectors(({ selector }) => {
return selectorParser((selectors) => {
selectors.walkClasses((classNode) => {
classNode.value = `foo:${classNode.value}`
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
})
}).processSync(selector)
})
// Manually wrap in supports query
let wrapper = postcss.atRule({ name: 'supports', params: 'display: grid' })
let nodes = container.nodes
container.removeAll()
wrapper.append(nodes)
container.append(wrapper)
})
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({})).toEqual(['@supports (display: grid) { .foo .foo\\:& }'])
})