Skip to content

Commit f3157cd

Browse files
Ignore custom variants with :merge(…) selectors (#18020)
Closes #15617 ## Summary This PR ignores `addVariant(…)` legacy JS plugin calls for variants that are using the [`:merge(…)` selector](https://v3.tailwindcss.com/docs/plugins#parent-and-sibling-states) for parent and sibling states. We can ignore these now because in v4, `group-*` and `peer-*` variants _compound automatically_ and you don't have to define them anymore. ## Test plan Added a unit test to ensure that the `optional` variant example from the v3 docs work as expected.
1 parent e57a2f5 commit f3157cd

File tree

3 files changed

+104
-0
lines changed

3 files changed

+104
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Ensure that running the Standalone build does not leave temporary files behind ([#17981](https://github.com/tailwindlabs/tailwindcss/pull/17981))
1818
- Fix `-rotate-*` utilities with arbitrary values ([#18014](https://github.com/tailwindlabs/tailwindcss/pull/18014))
1919
- Upgrade: Change casing of utilities with named values to kebab-case to match updated theme variables ([#18017](https://github.com/tailwindlabs/tailwindcss/pull/18017))
20+
- Ignore custom variants using `:merge(…)` selectors in legacy JS plugins ([#18020](https://github.com/tailwindlabs/tailwindcss/pull/18020))
2021

2122
### Added
2223

packages/tailwindcss/src/compat/plugin-api.test.ts

+76
Original file line numberDiff line numberDiff line change
@@ -1830,6 +1830,44 @@ describe('addVariant', () => {
18301830
}"
18311831
`)
18321832
})
1833+
1834+
test('ignores variants that use :merge(…) and ensures `peer-*` and `group-*` rules work out of the box', async () => {
1835+
let { build } = await compile(
1836+
css`
1837+
@plugin "my-plugin";
1838+
@layer utilities {
1839+
@tailwind utilities;
1840+
}
1841+
`,
1842+
{
1843+
loadModule: async (id, base) => {
1844+
return {
1845+
path: '',
1846+
base,
1847+
module: ({ addVariant }: PluginAPI) => {
1848+
addVariant('optional', '&:optional')
1849+
addVariant('group-optional', { ':merge(.group):optional &': '@slot' })
1850+
addVariant('peer-optional', { '&': { ':merge(.peer):optional ~ &': '@slot' } })
1851+
},
1852+
}
1853+
},
1854+
},
1855+
)
1856+
let compiled = build([
1857+
'optional:flex',
1858+
'group-optional:flex',
1859+
'peer-optional:flex',
1860+
'group-optional/foo:flex',
1861+
])
1862+
1863+
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
1864+
"@layer utilities {
1865+
.group-optional\\:flex:is(:where(.group):optional *), .group-optional\\/foo\\:flex:is(:where(.group\\/foo):optional *), .peer-optional\\:flex:is(:where(.peer):optional ~ *), .optional\\:flex:optional {
1866+
display: flex;
1867+
}
1868+
}"
1869+
`)
1870+
})
18331871
})
18341872

18351873
describe('matchVariant', () => {
@@ -2702,6 +2740,44 @@ describe('matchVariant', () => {
27022740
}"
27032741
`)
27042742
})
2743+
2744+
test('ignores variants that use :merge(…)', async () => {
2745+
let { build } = await compile(
2746+
css`
2747+
@plugin "my-plugin";
2748+
@layer utilities {
2749+
@tailwind utilities;
2750+
}
2751+
`,
2752+
{
2753+
loadModule: async (id, base) => {
2754+
return {
2755+
path: '',
2756+
base,
2757+
module: ({ matchVariant }: PluginAPI) => {
2758+
matchVariant('optional', (flavor) => `&:optional:has(${flavor}) &`)
2759+
matchVariant('group-optional', (flavor) => `:merge(.group):optional:has(${flavor}) &`)
2760+
matchVariant('peer-optional', (flavor) => `:merge(.peer):optional:has(${flavor}) ~ &`)
2761+
},
2762+
}
2763+
},
2764+
},
2765+
)
2766+
let compiled = build([
2767+
'optional-[test]:flex',
2768+
'group-optional-[test]:flex',
2769+
'peer-optional-[test]:flex',
2770+
'group-optional-[test]/foo:flex',
2771+
])
2772+
2773+
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
2774+
"@layer utilities {
2775+
.group-optional-\\[test\\]\\:flex:is(:where(.group):optional:has(test) :where(.group) *), .group-optional-\\[test\\]\\/foo\\:flex:is(:where(.group\\/foo):optional:has(test) :where(.group\\/foo) *), .peer-optional-\\[test\\]\\:flex:is(:where(.peer):optional:has(test) :where(.peer) ~ *), .optional-\\[test\\]\\:flex:optional:has(test) .optional-\\[test\\]\\:flex {
2776+
display: flex;
2777+
}
2778+
}"
2779+
`)
2780+
})
27052781
})
27062782

27072783
describe('addUtilities()', () => {

packages/tailwindcss/src/compat/plugin-api.ts

+27
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,22 @@ export function buildPluginApi({
115115
)
116116
}
117117

118+
// Ignore variants emitting v3 `:merge(…)` rules. In v4, the `group-*` and `peer-*` variants
119+
// compound automatically.
120+
if (typeof variant === 'string') {
121+
if (variant.includes(':merge(')) return
122+
} else if (Array.isArray(variant)) {
123+
if (variant.some((v) => v.includes(':merge('))) return
124+
} else if (typeof variant === 'object') {
125+
function keyIncludes(object: Record<string, any>, search: string): boolean {
126+
return Object.entries(object).some(
127+
([key, value]) =>
128+
key.includes(search) || (typeof value === 'object' && keyIncludes(value, search)),
129+
)
130+
}
131+
if (keyIncludes(variant, ':merge(')) return
132+
}
133+
118134
// Single selector or multiple parallel selectors
119135
if (typeof variant === 'string' || Array.isArray(variant)) {
120136
designSystem.variants.static(
@@ -143,6 +159,17 @@ export function buildPluginApi({
143159
return parseVariantValue(resolved, nodes)
144160
}
145161

162+
try {
163+
// Sample variant value and ignore variants emitting v3 `:merge` rules. In
164+
// v4, the `group-*` and `peer-*` variants compound automatically.
165+
let sample = fn('a', { modifier: null })
166+
if (typeof sample === 'string' && sample.includes(':merge(')) {
167+
return
168+
} else if (Array.isArray(sample) && sample.some((r) => r.includes(':merge('))) {
169+
return
170+
}
171+
} catch {}
172+
146173
let defaultOptionKeys = Object.keys(options?.values ?? {})
147174
designSystem.variants.group(
148175
() => {

0 commit comments

Comments
 (0)