Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement fallback plugins when arbitrary values result in css from multiple plugins #9376

Merged
merged 14 commits into from Sep 29, 2022
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Don't emit generated utilities with invalid uses of theme functions ([#9319](https://github.com/tailwindlabs/tailwindcss/pull/9319))
- Revert change that only listened for stdin close on TTYs ([#9331](https://github.com/tailwindlabs/tailwindcss/pull/9331))
- Ignore unset values (like `null` or `undefined`) when resolving the classList for intellisense ([#9385](https://github.com/tailwindlabs/tailwindcss/pull/9385))
- Implement fallback plugins when arbitrary values result in css from multiple plugins ([#9376](https://github.com/tailwindlabs/tailwindcss/pull/9376))

## [3.1.8] - 2022-08-05

Expand Down
34 changes: 17 additions & 17 deletions src/corePlugins.js
Expand Up @@ -1062,7 +1062,7 @@ export let corePlugins = {
}
},
},
{ values: theme('divideWidth'), type: ['line-width', 'length'] }
{ values: theme('divideWidth'), type: ['line-width', 'length', 'any'] }
)

addUtilities({
Expand Down Expand Up @@ -1110,7 +1110,7 @@ export let corePlugins = {
},
{
values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('divideColor'))),
type: 'color',
type: ['color', 'any'],
}
)
},
Expand Down Expand Up @@ -1290,7 +1290,7 @@ export let corePlugins = {
},
{
values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))),
type: ['color'],
type: ['color', 'any'],
}
)

Expand Down Expand Up @@ -1327,7 +1327,7 @@ export let corePlugins = {
},
{
values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))),
type: 'color',
type: ['color', 'any'],
}
)

Expand Down Expand Up @@ -1388,7 +1388,7 @@ export let corePlugins = {
},
{
values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))),
type: 'color',
type: ['color', 'any'],
}
)
},
Expand All @@ -1414,7 +1414,7 @@ export let corePlugins = {
})
},
},
{ values: flattenColorPalette(theme('backgroundColor')), type: 'color' }
{ values: flattenColorPalette(theme('backgroundColor')), type: ['color', 'any'] }
)
},

Expand Down Expand Up @@ -1482,7 +1482,7 @@ export let corePlugins = {
},

backgroundSize: createUtilityPlugin('backgroundSize', [['bg', ['background-size']]], {
type: ['lookup', 'length', 'percentage'],
type: ['lookup', ['length', { preferOnConflict: true }], 'percentage'],
}),

backgroundAttachment: ({ addUtilities }) => {
Expand Down Expand Up @@ -1543,7 +1543,7 @@ export let corePlugins = {
return { stroke: toColorValue(value) }
},
},
{ values: flattenColorPalette(theme('stroke')), type: ['color', 'url'] }
{ values: flattenColorPalette(theme('stroke')), type: ['color', 'url', 'any'] }
)
},

Expand Down Expand Up @@ -1654,7 +1654,7 @@ export let corePlugins = {
},

fontWeight: createUtilityPlugin('fontWeight', [['font', ['fontWeight']]], {
type: ['lookup', 'number'],
type: ['lookup', 'number', 'any'],
}),

textTransform: ({ addUtilities }) => {
Expand Down Expand Up @@ -1750,7 +1750,7 @@ export let corePlugins = {
})
},
},
{ values: flattenColorPalette(theme('textColor')), type: 'color' }
{ values: flattenColorPalette(theme('textColor')), type: ['color', 'any'] }
)
},

Expand All @@ -1772,7 +1772,7 @@ export let corePlugins = {
return { 'text-decoration-color': toColorValue(value) }
},
},
{ values: flattenColorPalette(theme('textDecorationColor')), type: ['color'] }
{ values: flattenColorPalette(theme('textDecorationColor')), type: ['color', 'any'] }
)
},

Expand All @@ -1795,7 +1795,7 @@ export let corePlugins = {
textUnderlineOffset: createUtilityPlugin(
'textUnderlineOffset',
[['underline-offset', ['text-underline-offset']]],
{ type: ['length', 'percentage'] }
{ type: ['length', 'percentage', 'any'] }
),

fontSmoothing: ({ addUtilities }) => {
Expand Down Expand Up @@ -1968,7 +1968,7 @@ export let corePlugins = {
}
},
},
{ values: flattenColorPalette(theme('boxShadowColor')), type: ['color'] }
{ values: flattenColorPalette(theme('boxShadowColor')), type: ['color', 'any'] }
)
},

Expand All @@ -1990,7 +1990,7 @@ export let corePlugins = {
}),

outlineOffset: createUtilityPlugin('outlineOffset', [['outline-offset', ['outline-offset']]], {
type: ['length', 'number', 'percentage'],
type: ['length', 'number', 'percentage', 'any'],
supportsNegativeValues: true,
}),

Expand All @@ -2001,7 +2001,7 @@ export let corePlugins = {
return { 'outline-color': toColorValue(value) }
},
},
{ values: flattenColorPalette(theme('outlineColor')), type: ['color'] }
{ values: flattenColorPalette(theme('outlineColor')), type: ['color', 'any'] }
)
},

Expand Down Expand Up @@ -2081,7 +2081,7 @@ export let corePlugins = {
([modifier]) => modifier !== 'DEFAULT'
)
),
type: 'color',
type: ['color', 'any'],
}
)
},
Expand All @@ -2108,7 +2108,7 @@ export let corePlugins = {
}
},
},
{ values: flattenColorPalette(theme('ringOffsetColor')), type: 'color' }
{ values: flattenColorPalette(theme('ringOffsetColor')), type: ['color', 'any'] }
)
},

Expand Down
161 changes: 114 additions & 47 deletions src/lib/generateRules.js
Expand Up @@ -3,7 +3,7 @@ import selectorParser from 'postcss-selector-parser'
import parseObjectStyles from '../util/parseObjectStyles'
import isPlainObject from '../util/isPlainObject'
import prefixSelector from '../util/prefixSelector'
import { updateAllClasses } from '../util/pluginUtils'
import { updateAllClasses, typeMap } from '../util/pluginUtils'
import log from '../util/log'
import * as sharedState from './sharedState'
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
Expand Down Expand Up @@ -539,68 +539,135 @@ function* resolveMatches(candidate, context, original = candidate) {
}

if (matchesPerPlugin.length > 0) {
typesByMatches.set(matchesPerPlugin, sort.options?.type)
let matchingTypes = (sort.options?.types ?? [])
.map(({ type }) => type)
// Only track the types for this plugin that resulted in some result
.filter((type) => {
return Boolean(
typeMap[type](modifier, sort.options, {
tailwindConfig: context.tailwindConfig,
})
)
})

if (matchingTypes.length > 0) {
typesByMatches.set(matchesPerPlugin, matchingTypes)
}

matches.push(matchesPerPlugin)
}
}

if (isArbitraryValue(modifier)) {
// When generated arbitrary values are ambiguous, we can't know
// which to pick so don't generate any utilities for them
if (matches.length > 1) {
let typesPerPlugin = matches.map((match) => new Set([...(typesByMatches.get(match) ?? [])]))
// Partition plugins in 2 categories so that we can start searching in the plugins that
// don't have `any` as a type first.
let [withAny, withoutAny] = matches.reduce(
(group, plugin) => {
let hasAnyType = plugin.some(([{ options }]) =>
options.types.some(({ type }) => type === 'any')
)

// Remove duplicates, so that we can detect proper unique types for each plugin.
for (let pluginTypes of typesPerPlugin) {
for (let type of pluginTypes) {
let removeFromOwnGroup = false
if (hasAnyType) {
group[0].push(plugin)
} else {
group[1].push(plugin)
}
return group
},
[[], []]
)

for (let otherGroup of typesPerPlugin) {
if (pluginTypes === otherGroup) continue
function findFallback(matches) {
// If only a single plugin matches, let's take that one
if (matches.length === 1) {
return matches[0]
}

if (otherGroup.has(type)) {
otherGroup.delete(type)
removeFromOwnGroup = true
// Otherwise, find the plugin that creates a valid rule given the arbitrary value, and
// also has the correct type which preferOnConflicts the plugin in case of clashes.
return matches.find((rules) => {
let matchingTypes = typesByMatches.get(rules)
return rules.some(([{ options }, rule]) => {
if (!isParsableNode(rule)) {
return false
}
}

if (removeFromOwnGroup) pluginTypes.delete(type)
}
return options.types.some(
({ type, preferOnConflict }) => matchingTypes.includes(type) && preferOnConflict
)
})
})
}

let messages = []

for (let [idx, group] of typesPerPlugin.entries()) {
for (let type of group) {
let rules = matches[idx]
.map(([, rule]) => rule)
.flat()
.map((rule) =>
rule
.toString()
.split('\n')
.slice(1, -1) // Remove selector and closing '}'
.map((line) => line.trim())
.map((x) => ` ${x}`) // Re-indent
.join('\n')
)
.join('\n\n')
// Try to find a fallback plugin, because we already know that multiple plugins matched for
// the given arbitrary value.
let fallback = findFallback(withoutAny) ?? findFallback(withAny)
if (fallback) {
matches = [fallback]
}

messages.push(
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
)
break
// We couldn't find a fallback plugin which means that there are now multiple plugins that
// generated css for the current candidate. This means that the result is ambiguous and this
// should not happen. We won't generate anything right now, so let's report this to the user
// by logging some options about what they can do.
else {
let typesPerPlugin = matches.map(
(match) => new Set([...(typesByMatches.get(match) ?? [])])
)

// Remove duplicates, so that we can detect proper unique types for each plugin.
for (let pluginTypes of typesPerPlugin) {
for (let type of pluginTypes) {
let removeFromOwnGroup = false

for (let otherGroup of typesPerPlugin) {
if (pluginTypes === otherGroup) continue

if (otherGroup.has(type)) {
otherGroup.delete(type)
removeFromOwnGroup = true
}
}

if (removeFromOwnGroup) pluginTypes.delete(type)
}
}
}

log.warn([
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
...messages,
`If this is content and not a class, replace it with \`${candidate
.replace('[', '[')
.replace(']', ']')}\` to silence this warning.`,
])
continue
let messages = []

for (let [idx, group] of typesPerPlugin.entries()) {
for (let type of group) {
let rules = matches[idx]
.map(([, rule]) => rule)
.flat()
.map((rule) =>
rule
.toString()
.split('\n')
.slice(1, -1) // Remove selector and closing '}'
.map((line) => line.trim())
.map((x) => ` ${x}`) // Re-indent
.join('\n')
)
.join('\n\n')

messages.push(
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
)
break
}
}

log.warn([
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
...messages,
`If this is content and not a class, replace it with \`${candidate
.replace('[', '[')
.replace(']', ']')}\` to silence this warning.`,
])
continue
}
}

matches = matches.map((list) => list.filter((match) => isParsableNode(match[1])))
Expand Down