Discard matchVariant matches with unknown named values (#18799)

This PR fixes two issues:
- When a variant is defined by `matchVariant` it could match unknown
values but not apply the variant (because it's unknown). This would
result in a utility being output that is the _same_ as a bare utility
without variants but a longer name. These were intended to be discarded
but weren't done so correctly.
- Similarly, when we encounter a known value but its not a string the
same thing would happen where we'd output a utility without applying the
variant. This was also intended to be discarded.

Basically given this code:
```js
matchVariant(
  "foo",
  (value) => `&:is([data-foo='${value}'])`,
  {
    values: {
      DEFAULT: "",
      bar: "bar",
      obj: { some: "object" },
    },
  }
)
```

And this HTML:
```html
<div class="foo-bar:bg-none foo-[baz]:bg-none foo-baz:bg-none foo-obj:bg-none"></div>
```

This CSS would be produced:
```css
@layer utilities {
  .foo-bar\:bg-none {
    &:is([data-foo='bar']) {
      background-image: none;
    }
  }
  /* this one shouldn't be here */
  .foo-baz\:bg-none {
    background-image: none;
  }
  /* this one shouldn't be here */
  .foo-obj\:bg-none {
    background-image: none;
  }
  .foo-\[baz\]\:bg-none {
    &:is([data-foo='baz']) {
      background-image: none;
    }
  }
}
```
This commit is contained in:
Jordan Pittman 2025-08-26 10:21:15 -04:00 • committed by GitHub
parent ce9b290b6b
commit ee987e3f6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 78 additions and 1 deletions

View file

@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Don't transition `visibility` when using `transition` ([#18795](https://github.com/tailwindlabs/tailwindcss/pull/18795))
- Discard matched variants with unknown named values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
- Discard matched variants with non-string values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
## [4.1.12] - 2025-08-13

View file

@ -2778,6 +2778,79 @@ describe('matchVariant', () => {
}"
`)
})
test('ignores variants that use unknown values', async () => {
let { build } = await compile(
css`
@plugin "my-plugin";
@layer utilities {
@tailwind utilities;
}
`,
{
loadModule: async (id, base) => {
return {
path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('foo', (flavor) => `&:is(${flavor})`, {
values: {
known: 'known',
},
})
},
}
},
},
)
let compiled = build(['foo-[test]:flex', 'foo-known:flex', 'foo-unknown:flex'])
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
"@layer utilities {
.foo-known\\:flex:is(known), .foo-\\[test\\]\\:flex:is(test) {
display: flex;
}
}"
`)
})
test('ignores variants that produce non-string values', async () => {
let { build } = await compile(
css`
@plugin "my-plugin";
@layer utilities {
@tailwind utilities;
}
`,
{
loadModule: async (id, base) => {
return {
path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('foo', (flavor) => `&:is(${flavor})`, {
values: {
string: 'some string',
object: { some: 'object' },
},
})
},
}
},
},
)
let compiled = build(['foo-[test]:flex', 'foo-string:flex', 'foo-object:flex'])
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
"@layer utilities {
.foo-string\\:flex:is(some string), .foo-\\[test\\]\\:flex:is(test) {
display: flex;
}
}"
`)
})
})
describe('addUtilities()', () => {

View file

@ -203,10 +203,12 @@ export function buildPluginApi({
} else if (variant.value.kind === 'named' && options?.values) {
let defaultValue = options.values[variant.value.value]
if (typeof defaultValue !== 'string') {
return
return null
}
ruleNodes.nodes = resolveVariantValue(defaultValue, variant.modifier, ruleNodes.nodes)
} else {
return null
}
})
},