Skip to content

Commit

Permalink
Implement fallback plugins when arbitrary values result in css from m…
Browse files Browse the repository at this point in the history
…ultiple plugins (#9376)

* use test with non-any type plugin

* choose backgroundSize over backgroundPosition

Ensure that `backgroundColor` can take any value

* add tests to verify fallback plugins

* implement fallback plugins

Whenever an arbitrary value results in css from multiple plugins we
first try to resolve a falback plugin.

The fallback mechanism works like this:

- If A has type `any` and B has type `color`, then B should win.

  > This is because `A` will match *anything*, but the more precise type
    should win instead. E.g.: `backgroundColor` has the type `any` so
    `bg-[100px_200px]` would match both the `backgroundColor` and
    `backgroundSize` but `backgroundSize` matched because of a specific
    type and not because of the `any` type.
- If A has type `length` and B has type `[length, { disambiguate: true }]`, then B should win.
  > This is because `B` marked the `length` as the plugin that should
    win in case a clash happens.

* Add any type to a handful of plugins

Needs tests tho

* Add any type to `border-{x,y,t,r,b,l}` plugins

* Add test for any type

* Split on multiple lines

* fixup

* add tests for implicit `any` types

* rename `disambiguate` to `preferOnConflict`

* update tests to reflect `any` types a bit better

* update changelog

* annotate any-type test with a bit more information

Just for future debugging reasons!

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
  • Loading branch information
RobinMalfait and thecrypticace authored Sep 29, 2022
1 parent 52ab315 commit 94d6e72
Show file tree
Hide file tree
Showing 10 changed files with 997 additions and 88 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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('[', '&lsqb;')
.replace(']', '&rsqb;')}\` 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('[', '&lsqb;')
.replace(']', '&rsqb;')}\` to silence this warning.`,
])
continue
}
}

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

0 comments on commit 94d6e72

Please sign in to comment.