Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ([#18020](https://github.com/tailwindlabs/tailwindcss/pull/18020))

### Added

Expand Down
76 changes: 76 additions & 0 deletions packages/tailwindcss/src/compat/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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()', () => {
Expand Down
27 changes: 27 additions & 0 deletions packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>, search: string): boolean {
return Object.entries(object).some(
([key, value]) =>
key.includes(search) || (typeof value === 'object' && keyIncludes(value, search)),
)
}
if (keyIncludes(variant, ':merge(')) return
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We should detect this in matchVariant as well.
  2. Should the object check happen recursively or no? I don't remember what v3 did there

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Ah yep forgot about matchVariant.
  2. Hmm yeah probably you're right.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thecrypticace The matchVariant(…) implementation is a bit annoying since we have to call the matcher function to sample the output. I tried throwing the variant out later but once we register it with the design system, the peer-* and group-* compounding doesn't work anymore.

The only other idea I have is to check for wether the variant starts with peer- or group-. What do you think?


// Single selector or multiple parallel selectors
if (typeof variant === 'string' || Array.isArray(variant)) {
designSystem.variants.static(
Expand Down Expand Up @@ -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(
() => {
Expand Down