Skip to content

Commit 49acd57

Browse files
committed
add fallback option to plugins
This will allow us to use the fallback plugin if there are 2 colliding plugins given a certain arbitrary value that's indistinguishable in either plugin. Normally we would then require a type cast, but now we can use the fallback. This is useful so that in the future adding more colliding plugins doesn't result in working code being broken because it requires a typehint all of a sudden. E.g.: ```js <!-- Before --> <div class="bg-[200px_100px]"></div><!-- Warning --> <!-- Fix: --> <div class="bg-[length:200px_100px]"></div> <div class="bg-[position:200px_100px]"></div> ``` ```js <!-- After (no fix required) --> <div class="bg-[200px_100px]"></div><!-- Generates styles for the background-size --> ```
1 parent e625252 commit 49acd57

File tree

4 files changed

+95
-51
lines changed

4 files changed

+95
-51
lines changed

src/corePlugins.js

+1
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,7 @@ export let corePlugins = {
14821482

14831483
backgroundSize: createUtilityPlugin('backgroundSize', [['bg', ['background-size']]], {
14841484
type: ['lookup', 'length', 'percentage'],
1485+
fallback: true,
14851486
}),
14861487

14871488
backgroundAttachment: ({ addUtilities }) => {

src/lib/generateRules.js

+64-46
Original file line numberDiff line numberDiff line change
@@ -541,62 +541,80 @@ function* resolveMatches(candidate, context, original = candidate) {
541541
}
542542

543543
if (isArbitraryValue(modifier)) {
544-
// When generated arbitrary values are ambiguous, we can't know
545-
// which to pick so don't generate any utilities for them
546544
if (matches.length > 1) {
547-
let typesPerPlugin = matches.map((match) => new Set([...(typesByMatches.get(match) ?? [])]))
548-
549-
// Remove duplicates, so that we can detect proper unique types for each plugin.
550-
for (let pluginTypes of typesPerPlugin) {
551-
for (let type of pluginTypes) {
552-
let removeFromOwnGroup = false
545+
let fallback = matches
546+
.slice()
547+
.reverse()
548+
.find((match) => {
549+
return match.every(([{ options }, node]) => {
550+
return options.fallback && isParsableNode(node)
551+
})
552+
})
553553

554-
for (let otherGroup of typesPerPlugin) {
555-
if (pluginTypes === otherGroup) continue
554+
if (fallback) {
555+
console.dir(fallback, { depth: null })
556+
matches = [fallback]
557+
}
556558

557-
if (otherGroup.has(type)) {
558-
otherGroup.delete(type)
559-
removeFromOwnGroup = true
559+
// When generated arbitrary values are ambiguous, we can't know
560+
// which to pick so don't generate any utilities for them
561+
else {
562+
let typesPerPlugin = matches.map(
563+
(match) => new Set([...(typesByMatches.get(match) ?? [])])
564+
)
565+
566+
// Remove duplicates, so that we can detect proper unique types for each plugin.
567+
for (let pluginTypes of typesPerPlugin) {
568+
for (let type of pluginTypes) {
569+
let removeFromOwnGroup = false
570+
571+
for (let otherGroup of typesPerPlugin) {
572+
if (pluginTypes === otherGroup) continue
573+
574+
if (otherGroup.has(type)) {
575+
otherGroup.delete(type)
576+
removeFromOwnGroup = true
577+
}
560578
}
561-
}
562579

563-
if (removeFromOwnGroup) pluginTypes.delete(type)
580+
if (removeFromOwnGroup) pluginTypes.delete(type)
581+
}
564582
}
565-
}
566583

567-
let messages = []
568-
569-
for (let [idx, group] of typesPerPlugin.entries()) {
570-
for (let type of group) {
571-
let rules = matches[idx]
572-
.map(([, rule]) => rule)
573-
.flat()
574-
.map((rule) =>
575-
rule
576-
.toString()
577-
.split('\n')
578-
.slice(1, -1) // Remove selector and closing '}'
579-
.map((line) => line.trim())
580-
.map((x) => ` ${x}`) // Re-indent
581-
.join('\n')
584+
let messages = []
585+
586+
for (let [idx, group] of typesPerPlugin.entries()) {
587+
for (let type of group) {
588+
let rules = matches[idx]
589+
.map(([, rule]) => rule)
590+
.flat()
591+
.map((rule) =>
592+
rule
593+
.toString()
594+
.split('\n')
595+
.slice(1, -1) // Remove selector and closing '}'
596+
.map((line) => line.trim())
597+
.map((x) => ` ${x}`) // Re-indent
598+
.join('\n')
599+
)
600+
.join('\n\n')
601+
602+
messages.push(
603+
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
582604
)
583-
.join('\n\n')
584-
585-
messages.push(
586-
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
587-
)
588-
break
605+
break
606+
}
589607
}
590-
}
591608

592-
log.warn([
593-
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
594-
...messages,
595-
`If this is content and not a class, replace it with \`${candidate
596-
.replace('[', '&lsqb;')
597-
.replace(']', '&rsqb;')}\` to silence this warning.`,
598-
])
599-
continue
609+
log.warn([
610+
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
611+
...messages,
612+
`If this is content and not a class, replace it with \`${candidate
613+
.replace('[', '&lsqb;')
614+
.replace(']', '&rsqb;')}\` to silence this warning.`,
615+
])
616+
continue
617+
}
600618
}
601619

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

src/util/createUtilityPlugin.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import transformThemeValue from './transformThemeValue'
33
export default function createUtilityPlugin(
44
themeKey,
55
utilityVariations = [[themeKey, [themeKey]]],
6-
{ filterDefault = false, ...options } = {}
6+
{ filterDefault = false, fallback = false, ...options } = {}
77
) {
88
let transformValue = transformThemeValue(themeKey)
99
return function ({ matchUtilities, theme }) {
@@ -24,6 +24,7 @@ export default function createUtilityPlugin(
2424
})
2525
}, {}),
2626
{
27+
fallback,
2728
...options,
2829
values: filterDefault
2930
? Object.fromEntries(

tests/arbitrary-values.test.js

+28-4
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,35 @@ it('should not convert escaped underscores with spaces', () => {
262262
})
263263
})
264264

265-
it('should warn and not generate if arbitrary values are ambiguous', () => {
266-
// If we don't protect against this, then `bg-[200px_100px]` would both
267-
// generate the background-size as well as the background-position utilities.
265+
it('should pick the fallback plugin when arbitrary values collide', () => {
268266
let config = {
269-
content: [{ raw: html`<div class="bg-[200px_100px]"></div>` }],
267+
content: [{ raw: html`<div class="bg-[200px_100px] md:bg-[100px_200px]"></div>` }],
268+
}
269+
270+
return run('@tailwind utilities', config).then((result) => {
271+
return expect(result.css).toMatchFormattedCss(css`
272+
.bg-\[200px_100px\] {
273+
background-size: 200px 100px;
274+
}
275+
276+
@media (min-width: 768px) {
277+
.md\:bg-\[100px_200px\] {
278+
background-size: 100px 200px;
279+
}
280+
}
281+
`)
282+
})
283+
})
284+
285+
it('should warn and not generate if arbitrary values are ambiguous (without fallback)', () => {
286+
let config = {
287+
content: [{ raw: html`<div class="foo-[200px_100px]"></div>` }],
288+
plugins: [
289+
function ({ matchUtilities }) {
290+
matchUtilities({ foo: (value) => ({ value }) }, { type: ['position'] })
291+
matchUtilities({ foo: (value) => ({ value }) }, { type: ['length'] })
292+
},
293+
],
270294
}
271295

272296
return run('@tailwind utilities', config).then((result) => {

0 commit comments

Comments
 (0)