Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions packages/tailwindcss/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 12 additions & 1 deletion packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Comment on lines +148 to +157
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t gate the at-rule collapse on isBlock.

isBlock only returns true when the raw variant string literally ends with }. As soon as someone writes a scoped variant with a trailing comment or other postfix (e.g. addVariant('scoped', '@scope (&) { @slot } /* note */')), the condition fails, we skip the body[0] return, and the compiler falls back to emitting .selector { @scope … }—the exact invalid structure this PR is meant to fix. Please decide purely from the parsed AST (e.g. if (body.length === 1 && body[0].kind === 'at-rule')) so single at-rule variants are collapsed regardless of formatting. This keeps the fix from regressing in common “annotated” plugin variants.

🤖 Prompt for AI Agents
In packages/tailwindcss/src/compat/plugin-api.ts around lines 148 to 157, the
code currently gates collapsing a single at-rule variant on the heuristic
isBlock (which checks whether the raw variant text ends with '}'), causing
variants with trailing comments or other postfixes to miss the collapse and
produce invalid output; change the logic to decide solely from the parsed AST by
removing the isBlock requirement and returning body[0] whenever body.length ===
1 and body[0].kind === 'at-rule' so single at-rule variants are collapsed
regardless of trailing formatting or comments.


r.nodes = body
},
{
compounds: compoundsForSelectors(typeof variant === 'string' ? [variant] : variant),
Expand Down
45 changes: 45 additions & 0 deletions packages/tailwindcss/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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) {
Expand Down
42 changes: 42 additions & 0 deletions packages/tailwindcss/src/variants.scope.test.ts
Original file line number Diff line number Diff line change
@@ -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')
Comment on lines +34 to +35
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug console.log.

The console.log statement on line 35 creates noisy test output and should be removed before merging.

Apply this diff:

-  // 👇 Move your debug log here
-  console.log('\n\n=== GENERATED CSS ===\n', result, '\n====================\n\n')
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 👇 Move your debug log here
console.log('\n\n=== GENERATED CSS ===\n', result, '\n====================\n\n')
🤖 Prompt for AI Agents
In packages/tailwindcss/src/variants.scope.test.ts around lines 34 to 35, there
is a debug console.log that prints generated CSS and creates noisy test output;
remove the console.log statement (and any surrounding leftover blank lines it
introduced) so tests no longer emit the debug output, keeping the rest of the
test intact.


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'))

})
2 changes: 1 addition & 1 deletion packages/tailwindcss/src/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const IS_VALID_VARIANT_NAME = /^@?[a-z0-9][a-zA-Z0-9_-]*(?<![_-])$/
type VariantFn<T extends Variant['kind']> = (
rule: Rule,
variant: Extract<Variant, { kind: T }>,
) => null | void
) => null | void | AstNode

type CompareFn = (a: Variant, z: Variant) => number

Expand Down