Skip to content

Commit

Permalink
add fallback option to plugins
Browse files Browse the repository at this point in the history
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 -->
```
  • Loading branch information
RobinMalfait committed Sep 21, 2022
1 parent e625252 commit 49acd57
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 51 deletions.
1 change: 1 addition & 0 deletions src/corePlugins.js
Expand Up @@ -1482,6 +1482,7 @@ export let corePlugins = {

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

backgroundAttachment: ({ addUtilities }) => {
Expand Down
110 changes: 64 additions & 46 deletions src/lib/generateRules.js
Expand Up @@ -541,62 +541,80 @@ function* resolveMatches(candidate, context, original = candidate) {
}

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) ?? [])]))

// 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
let fallback = matches
.slice()
.reverse()
.find((match) => {
return match.every(([{ options }, node]) => {
return options.fallback && isParsableNode(node)
})
})

for (let otherGroup of typesPerPlugin) {
if (pluginTypes === otherGroup) continue
if (fallback) {
console.dir(fallback, { depth: null })
matches = [fallback]
}

if (otherGroup.has(type)) {
otherGroup.delete(type)
removeFromOwnGroup = true
// When generated arbitrary values are ambiguous, we can't know
// which to pick so don't generate any utilities for them
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)
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')
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()}\``
)
.join('\n\n')

messages.push(
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
)
break
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
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
3 changes: 2 additions & 1 deletion src/util/createUtilityPlugin.js
Expand Up @@ -3,7 +3,7 @@ import transformThemeValue from './transformThemeValue'
export default function createUtilityPlugin(
themeKey,
utilityVariations = [[themeKey, [themeKey]]],
{ filterDefault = false, ...options } = {}
{ filterDefault = false, fallback = false, ...options } = {}
) {
let transformValue = transformThemeValue(themeKey)
return function ({ matchUtilities, theme }) {
Expand All @@ -24,6 +24,7 @@ export default function createUtilityPlugin(
})
}, {}),
{
fallback,
...options,
values: filterDefault
? Object.fromEntries(
Expand Down
32 changes: 28 additions & 4 deletions tests/arbitrary-values.test.js
Expand Up @@ -262,11 +262,35 @@ it('should not convert escaped underscores with spaces', () => {
})
})

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

return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.bg-\[200px_100px\] {
background-size: 200px 100px;
}
@media (min-width: 768px) {
.md\:bg-\[100px_200px\] {
background-size: 100px 200px;
}
}
`)
})
})

it('should warn and not generate if arbitrary values are ambiguous (without fallback)', () => {
let config = {
content: [{ raw: html`<div class="foo-[200px_100px]"></div>` }],
plugins: [
function ({ matchUtilities }) {
matchUtilities({ foo: (value) => ({ value }) }, { type: ['position'] })
matchUtilities({ foo: (value) => ({ value }) }, { type: ['length'] })
},
],
}

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

0 comments on commit 49acd57

Please sign in to comment.