diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 4069268c1ae0..6c3056f27e75 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -323,6 +323,8 @@ export function optimizeAst( if (nodes.length === 0) return + + // Rules with `&` as the selector should be flattened if (node.selector === '&') { parent.push(...nodes) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 5954040017d2..a988ef25149e 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -145,7 +145,18 @@ export function buildPluginApi({ designSystem.variants.static( name, (r) => { - r.nodes = parseVariantValue(variant, r.nodes) + let body = parseVariantValue(variant, r.nodes) + + const isBlock = + typeof variant === 'string' + ? variant.trim().endsWith('}') + : variant.some((v) => v.trim().endsWith('}')) + + if (isBlock && body.length === 1 && body[0].kind === 'at-rule') { + return body[0] + } + + r.nodes = body }, { compounds: compoundsForSelectors(typeof variant === 'string' ? [variant] : variant), diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 82ac77eb1d6e..40fe8542a8af 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -197,6 +197,10 @@ export function applyVariant( // not hitting this code path. let { applyFn } = variants.get(variant.root)! + + + let originalSelector = node.kind === 'rule' ? (node as StyleRule).selector : undefined + if (variant.kind === 'compound') { // Some variants traverse the AST to mutate the nodes. E.g.: `group-*` wants // to prefix every selector of the variant it's compounding with `.group`. @@ -255,6 +259,47 @@ export function applyVariant( // All other variants let result = applyFn(node, variant) if (result === null) return null + + + + + + + if (result && typeof result === 'object' && 'kind' in (result as any)) { + const newNode = result as AstNode + if (newNode.kind === 'at-rule' && originalSelector) { + let replaced = false + walk(newNode.nodes, (child) => { + if (child.kind === 'rule' && child.selector === '&') { + child.selector = originalSelector! + replaced = true + } + }) + + if (!replaced) { + newNode.nodes = [rule(originalSelector!, newNode.nodes)] + } + } + + + if (newNode.kind === 'at-rule') { + ;(node as any).kind = 'at-rule' + ;(node as any).name = newNode.name + ;(node as any).params = newNode.params + ;(node as any).nodes = newNode.nodes + + delete (node as any).selector + } else if (newNode.kind === 'rule') { + ;(node as any).kind = 'rule' + ;(node as any).selector = newNode.selector + ;(node as any).nodes = newNode.nodes + delete (node as any).name + delete (node as any).params + } else { + + ;(node as any).nodes = (newNode as any).nodes ?? [] + } + } } function isFallbackUtility(utility: Utility) { diff --git a/packages/tailwindcss/src/variants.scope.test.ts b/packages/tailwindcss/src/variants.scope.test.ts new file mode 100644 index 000000000000..17c0caa62f19 --- /dev/null +++ b/packages/tailwindcss/src/variants.scope.test.ts @@ -0,0 +1,42 @@ +import { test, expect } from 'vitest' +import { compile } from '.' +import type { PluginAPI } from './compat/plugin-api' + +const css = String.raw + +test('custom variants using @scope should wrap correctly', async () => { + let compiler = await compile( + css` + @theme { + --color-red-500: #ef4444; + } + @tailwind utilities; + @plugin 'my-plugin'; + `, + { + loadModule: async (id) => { + if (id === 'my-plugin') { + return { + path: '', + base: '', + module: ({ addVariant }: PluginAPI) => { + addVariant('scoped', '@scope (.theme) { & }') + }, + } + } + return { path: '', base: '', module: () => {} } + }, + }, + ) + + let result = compiler.build(['scoped:bg-red-500']) + + // 👇 Move your debug log here + console.log('\n\n=== GENERATED CSS ===\n', result, '\n====================\n\n') + + expect(result).toContain('@scope (.theme)') + expect(result).toContain('.scoped\\:bg-red-500') + // The @scope at-rule should wrap the selector: @scope { .selector { ... } } + expect(result.indexOf('@scope')).toBeLessThan(result.indexOf('.scoped\\:bg-red-500')) + +}) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 82a2b8592cfe..c432f9754104 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -26,7 +26,7 @@ export const IS_VALID_VARIANT_NAME = /^@?[a-z0-9][a-zA-Z0-9_-]*(? = ( rule: Rule, variant: Extract, -) => null | void +) => null | void | AstNode type CompareFn = (a: Variant, z: Variant) => number