From 9ceb6cef41020db96e5e95787f3f3785a41c9660 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 14 May 2025 15:51:04 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Ignore=20custom=20variants=20with=20`:merge?= =?UTF-8?q?(=E2=80=A6)`=20selectors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + .../tailwindcss/src/compat/plugin-api.test.ts | 76 +++++++++++++++++++ packages/tailwindcss/src/compat/plugin-api.ts | 27 +++++++ 3 files changed, 104 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6aada039ea0..bd478f4b138e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure that running the Standalone build does not leave temporary files behind ([#17981](https://github.com/tailwindlabs/tailwindcss/pull/17981)) - Fix `-rotate-*` utilities with arbitrary values ([#18014](https://github.com/tailwindlabs/tailwindcss/pull/18014)) - Upgrade: Change casing of utilities with named values to kebab-case to match updated theme variables ([#18017](https://github.com/tailwindlabs/tailwindcss/pull/18017)) +- Ignore custom variants using `:merge(…)` selectors in legacy JS plugins as they would create invalid CSS in v4 ([#18020](https://github.com/tailwindlabs/tailwindcss/pull/18020)) ### Added diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 465974679646..ea1ab6f0d4a4 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1830,6 +1830,44 @@ describe('addVariant', () => { }" `) }) + + test('ignores variants that use :merge(…) and ensures `peer-*` and `group-*` rules work out of the box', async () => { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('optional', '&:optional') + addVariant('group-optional', { ':merge(.group):optional &': '@slot' }) + addVariant('peer-optional', { '&': { ':merge(.peer):optional ~ &': '@slot' } }) + }, + } + }, + }, + ) + let compiled = build([ + 'optional:flex', + 'group-optional:flex', + 'peer-optional:flex', + 'group-optional/foo:flex', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .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 { + display: flex; + } + }" + `) + }) }) describe('matchVariant', () => { @@ -2702,6 +2740,44 @@ describe('matchVariant', () => { }" `) }) + + test('ignores variants that use :merge(…)', async () => { + let { build } = await compile( + css` + @plugin "my-plugin"; + @layer utilities { + @tailwind utilities; + } + `, + { + loadModule: async (id, base) => { + return { + path: '', + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('optional', (flavor) => `&:optional:has(${flavor}) &`) + matchVariant('group-optional', (flavor) => `:merge(.group):optional:has(${flavor}) &`) + matchVariant('peer-optional', (flavor) => `:merge(.peer):optional:has(${flavor}) ~ &`) + }, + } + }, + }, + ) + let compiled = build([ + 'optional-[test]:flex', + 'group-optional-[test]:flex', + 'peer-optional-[test]:flex', + 'group-optional-[test]/foo:flex', + ]) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .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 { + display: flex; + } + }" + `) + }) }) describe('addUtilities()', () => { diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index dc27993cbb86..938596e8ca4d 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -115,6 +115,22 @@ export function buildPluginApi({ ) } + // Ignore variants emitting v3 `:merge(…)` rules. In v4, the `group-*` and `peer-*` variants + // compound automatically. + if (typeof variant === 'string') { + if (variant.includes(':merge(')) return + } else if (Array.isArray(variant)) { + if (variant.some((v) => v.includes(':merge('))) return + } else if (typeof variant === 'object') { + function keyIncludes(object: Record, search: string): boolean { + return Object.entries(object).some( + ([key, value]) => + key.includes(search) || (typeof value === 'object' && keyIncludes(value, search)), + ) + } + if (keyIncludes(variant, ':merge(')) return + } + // Single selector or multiple parallel selectors if (typeof variant === 'string' || Array.isArray(variant)) { designSystem.variants.static( @@ -143,6 +159,17 @@ export function buildPluginApi({ return parseVariantValue(resolved, nodes) } + try { + // Sample variant value and ignore variants emitting v3 `:merge` rules. In + // v4, the `group-*` and `peer-*` variants compound automatically. + let sample = fn('a', { modifier: null }) + if (typeof sample === 'string' && sample.includes(':merge(')) { + return + } else if (Array.isArray(sample) && sample.some((r) => r.includes(':merge('))) { + return + } + } catch {} + let defaultOptionKeys = Object.keys(options?.values ?? {}) designSystem.variants.group( () => { From afb56d6b8e00c7860f15d64758d2425f8a940458 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 15 May 2025 12:59:33 +0200 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd478f4b138e..c161df30176b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure that running the Standalone build does not leave temporary files behind ([#17981](https://github.com/tailwindlabs/tailwindcss/pull/17981)) - Fix `-rotate-*` utilities with arbitrary values ([#18014](https://github.com/tailwindlabs/tailwindcss/pull/18014)) - Upgrade: Change casing of utilities with named values to kebab-case to match updated theme variables ([#18017](https://github.com/tailwindlabs/tailwindcss/pull/18017)) -- Ignore custom variants using `:merge(…)` selectors in legacy JS plugins as they would create invalid CSS in v4 ([#18020](https://github.com/tailwindlabs/tailwindcss/pull/18020)) +- Ignore custom variants using `:merge(…)` selectors in legacy JS plugins ([#18020](https://github.com/tailwindlabs/tailwindcss/pull/18020)) ### Added