Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 80 additions & 3 deletions src/lib/generateRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import parseObjectStyles from '../util/parseObjectStyles'
import isPlainObject from '../util/isPlainObject'
import prefixSelector from '../util/prefixSelector'
import { updateAllClasses } from '../util/pluginUtils'
import log from '../util/log'

let classNameParser = selectorParser((selectors) => {
return selectors.first.filter(({ type }) => type === 'class').pop().value
Expand Down Expand Up @@ -225,15 +226,19 @@ function* resolveMatches(candidate, context) {

for (let matchedPlugins of resolveMatchedPlugins(classCandidate, context)) {
let matches = []
let typesByMatches = new Map()

let [plugins, modifier] = matchedPlugins
let isOnlyPlugin = plugins.length === 1

for (let [sort, plugin] of plugins) {
let matchesPerPlugin = []

if (typeof plugin === 'function') {
for (let ruleSet of [].concat(plugin(modifier, { isOnlyPlugin }))) {
let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
for (let rule of rules) {
matches.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
}
}
}
Expand All @@ -242,12 +247,80 @@ function* resolveMatches(candidate, context) {
let ruleSet = plugin
let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
for (let rule of rules) {
matches.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
}
}

if (matchesPerPlugin.length > 0) {
typesByMatches.set(matchesPerPlugin, sort.options?.type)
matches.push(matchesPerPlugin)
}
}

matches = applyPrefix(matches, context)
// Only keep the result of the very first plugin if we are dealing with
// arbitrary values, to protect against ambiguity.
if (isArbitraryValue(modifier) && matches.length > 1) {
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)
}
}

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(
` - Replace "${candidate}" with "${candidate.replace(
'[',
`[${type}:`
)}" for:\n${rules}\n`
)
break
}
}

log.warn([
// TODO: Update URL
`The class "${candidate}" is ambiguous and matches multiple utilities. Use a type hint (https://tailwindcss.com/docs/just-in-time-mode#ambiguous-values) to fix this.`,
'',
...messages,
`If this is just part of your content and not a class, replace it with "${candidate
.replace('[', '[')
.replace(']', ']')}" to silence this warning.`,
])
continue
}

matches = applyPrefix(matches.flat(), context)

if (important) {
matches = applyImportant(matches, context)
Expand Down Expand Up @@ -317,4 +390,8 @@ function generateRules(candidates, context) {
})
}

function isArbitraryValue(input) {
return input.startsWith('[') && input.endsWith(']')
}

export { resolveMatches, generateRules }
12 changes: 12 additions & 0 deletions tests/arbitrary-values.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,15 @@ it('should not convert escaped underscores with spaces', () => {
`)
})
})

it('should warn and not generate if arbitrary values are ambigu', () => {
// If we don't protect against this, then `bg-[200px_100px]` would both
// generate the background-size as well as the background-position utilities.
let config = {
content: [{ raw: html`<div class="bg-[200px_100px]"></div>` }],
}

return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css``)
})
})