From 381d336eb1d14b36edd0c994fb17ca87bb6a260f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 18 Jul 2024 12:22:26 -0400 Subject: [PATCH 1/4] wip --- packages/tailwindcss/src/ast.ts | 5 +- packages/tailwindcss/src/compile.ts | 9 ++++ packages/tailwindcss/src/variants.test.ts | 56 +++++++++++++++++++++++ packages/tailwindcss/src/variants.ts | 42 +++++++++-------- 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 66b37daf0..fb7d5b78c 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -79,16 +79,19 @@ export function walk( node: AstNode, utils: { parent: AstNode | null + ancestors: AstNode[] replaceWith(newNode: AstNode | AstNode[]): void }, ) => void | WalkAction, parent: AstNode | null = null, + ancestors: AstNode[] = [], ) { for (let i = 0; i < ast.length; i++) { let node = ast[i] let status = visit(node, { parent, + ancestors, replaceWith(newNode) { ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode])) // We want to visit the newly replaced node(s), which start at the @@ -105,7 +108,7 @@ export function walk( if (status === WalkAction.Skip) continue if (node.kind === 'rule') { - walk(node.nodes, visit, node) + walk(node.nodes, visit, node, [node, ...ancestors]) } } } diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index cac07451d..265630b8d 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -188,6 +188,14 @@ export function applyVariant(node: Rule, variant: Variant, variants: Variants): let result = applyVariant(isolatedNode, variant.variant, variants) if (result === null) return null + // Wrap in a single `&` rule so :not(…) can have the entire list of + // selectors / nodes from the "root" + // + // TODO: Rework this because it's not a great solution + if (isolatedNode.nodes.length > 1) { + isolatedNode.nodes = [rule('&', isolatedNode.nodes)] + } + for (let child of isolatedNode.nodes) { // Only some variants wrap children in rules. For example, the `force` // variant is a noop on the AST. And the `has` variant modifies the @@ -217,6 +225,7 @@ export function applyVariant(node: Rule, variant: Variant, variants: Variants): // All other variants let result = applyFn(node, variant) + if (result === null) return null } diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 5fb6b4f22..64be22c25 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1557,6 +1557,13 @@ test('not', () => { @slot; } } + @variant nested-selectors { + &:hover { + &:focus { + @slot; + } + } + } @tailwind utilities; `, [ @@ -1581,6 +1588,10 @@ test('not', () => { display: flex; } + .not-nested-selectors\:flex:not(:hover), .not-nested-selectors\:flex:not(:focus) { + display: flex; + } + .not-\\[\\:checked\\]\\:flex:not(:checked) { display: flex; } @@ -2518,3 +2529,48 @@ test('variant order', () => { }" `) }) + +test.only('not selector inversion creation thing', () => { + let input = css` + @variant omg { + &:hover { + &:focus { + &:active { + @slot; + } + + &[data-whatever] { + @slot; + } + } + + &[data-foo] { + @slot; + } + } + + &:visited { + @slot; + } + } + @tailwind utilities; + ` + + expect(compileCss(input, ['omg:flex', 'not-omg:flex'])).toMatchInlineSnapshot(` + ".not-omg\\:flex:is(:not(:active), :not(:focus), :not(:hover)):is(:not([data-whatever]), :not(:focus), :not(:hover)):is(:not([data-foo]), :not(:hover)):not(:visited) { + display: flex; + } + + .omg\\:flex:hover:focus:active, .omg\\:flex:hover:focus[data-whatever] { + display: flex; + } + + .omg\\:flex:hover[data-foo] { + display: flex; + } + + .omg\\:flex:visited { + display: flex; + }" + `) +}) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 909cfef22..4af361a26 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -206,39 +206,43 @@ export function createVariants(theme: Theme): Variants { variants.compound('not', (ruleNode, variant) => { if (variant.modifier) return null - let didApply = false + let groups: string[] = [] - walk([ruleNode], (node) => { + walk([ruleNode], (node, { ancestors }) => { if (node.kind !== 'rule') return WalkAction.Continue // Skip past at-rules, and continue traversing the children of the at-rule if (node.selector[0] === '@') return WalkAction.Continue - // Throw out any candidates with variants using nested selectors - if (didApply) { - walk([node], (childNode) => { - if (childNode.kind !== 'rule' || childNode.selector[0] === '@') return WalkAction.Continue + // 1. Walk the tree until we find `@slot` + if (node.nodes.length !== 0) return WalkAction.Continue - didApply = false - return WalkAction.Stop - }) + let selectors = [`:not(${node.selector.replaceAll('&', '*')})`] - return didApply ? WalkAction.Skip : WalkAction.Stop + // 2. Collect the selectors of the parents + for (let ancestor of ancestors) { + if (ancestor.kind !== 'rule') continue + + // Skip past at-rules, and continue traversing the children of the at-rule + if (ancestor.selector[0] === '@') continue + + // Skip over the "root" node + if (ancestor.selector === '&') continue + + selectors.push(`:not(${ancestor.selector.replaceAll('&', '*')})`) } - // Replace `&` in target variant with `*`, so variants like `&:hover` - // become `&:not(*:hover)`. The `*` will often be optimized away. - node.selector = `&:not(${node.selector.replaceAll('&', '*')})` - - // Track that the variant was actually applied - didApply = true + groups.push(`:is(${selectors.join(', ')})`) }) - // If the node wasn't modified, this variant is not compatible with - // `not-*` so discard the candidate. - if (!didApply) { + if (groups.length === 0) { return null } + + console.log({ groups }) + + ruleNode.selector = '&' + groups.join('') + ruleNode.nodes = [] }) variants.compound('group', (ruleNode, variant) => { From b86387e53227651e6ee31ba4808e5eecd97f41b7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 18 Jul 2024 15:07:55 -0400 Subject: [PATCH 2/4] Simplify code --- packages/tailwindcss/src/ast.ts | 9 +++++---- packages/tailwindcss/src/variants.ts | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index fb7d5b78c..4074ff1be 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -79,19 +79,20 @@ export function walk( node: AstNode, utils: { parent: AstNode | null - ancestors: AstNode[] + path: AstNode[] replaceWith(newNode: AstNode | AstNode[]): void }, ) => void | WalkAction, parent: AstNode | null = null, - ancestors: AstNode[] = [], + parentPath: AstNode[] = [], ) { for (let i = 0; i < ast.length; i++) { let node = ast[i] + let path = [node, ...parentPath] let status = visit(node, { parent, - ancestors, + path, replaceWith(newNode) { ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode])) // We want to visit the newly replaced node(s), which start at the @@ -108,7 +109,7 @@ export function walk( if (status === WalkAction.Skip) continue if (node.kind === 'rule') { - walk(node.nodes, visit, node, [node, ...ancestors]) + walk(node.nodes, visit, node, path) } } } diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 4af361a26..47e9ae16b 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -208,7 +208,7 @@ export function createVariants(theme: Theme): Variants { let groups: string[] = [] - walk([ruleNode], (node, { ancestors }) => { + walk([ruleNode], (node, { path }) => { if (node.kind !== 'rule') return WalkAction.Continue // Skip past at-rules, and continue traversing the children of the at-rule @@ -217,10 +217,10 @@ export function createVariants(theme: Theme): Variants { // 1. Walk the tree until we find `@slot` if (node.nodes.length !== 0) return WalkAction.Continue - let selectors = [`:not(${node.selector.replaceAll('&', '*')})`] + let selectors = [] // 2. Collect the selectors of the parents - for (let ancestor of ancestors) { + for (let ancestor of path) { if (ancestor.kind !== 'rule') continue // Skip past at-rules, and continue traversing the children of the at-rule From a609809ce03ee824cf70dab3d6fe866c318bdfee Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 18 Jul 2024 15:11:13 -0400 Subject: [PATCH 3/4] wip --- packages/tailwindcss/src/variants.test.ts | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 64be22c25..4fb91e8ed 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1569,6 +1569,7 @@ test('not', () => { [ 'not-[:checked]:flex', 'not-hocus:flex', + 'not-nested-selectors:flex', 'group-not-[:checked]:flex', 'group-not-[:checked]/parent-name:flex', @@ -1588,7 +1589,7 @@ test('not', () => { display: flex; } - .not-nested-selectors\:flex:not(:hover), .not-nested-selectors\:flex:not(:focus) { + .not-nested-selectors\\:flex:is(:not(:focus), :not(:hover)) { display: flex; } @@ -1641,21 +1642,9 @@ test('not', () => { compileCss( css` @variant custom-at-rule (@media foo); - @variant nested-selectors { - &:hover { - &:focus { - @slot; - } - } - } @tailwind utilities; `, - [ - 'not-[:checked]/foo:flex', - 'not-[@media_print]:flex', - 'not-custom-at-rule:flex', - 'not-nested-selectors:flex', - ], + ['not-[:checked]/foo:flex', 'not-[@media_print]:flex', 'not-custom-at-rule:flex'], ), ).toEqual('') }) @@ -2530,7 +2519,7 @@ test('variant order', () => { `) }) -test.only('not selector inversion creation thing', () => { +test('not selector inversion creation thing', () => { let input = css` @variant omg { &:hover { From 8023be68ce3e59d1dfecaf8262062965e7f8f748 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 19 Jul 2024 13:27:00 -0400 Subject: [PATCH 4/4] wip --- packages/tailwindcss/src/variants.test.ts | 34 +++++++----- packages/tailwindcss/src/variants.ts | 64 +++++++++++------------ 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 4fb91e8ed..9e4a7362d 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -705,23 +705,23 @@ test('group-[...]', () => { 'group-[&:hover]:group-[&_p]:flex', ]), ).toMatchInlineSnapshot(` - ".group-\\[\\&_p\\]\\:flex:is(:where(.group) p *) { + ".group-\\[\\&_p\\]\\:flex:is(:where(.group):is(* p) *) { display: flex; } - .group-\\[\\&\\:hover\\]\\:group-\\[\\&_p\\]\\:flex:is(:where(.group):hover *):is(:where(.group) p *) { + .group-\\[\\&\\:hover\\]\\:group-\\[\\&_p\\]\\:flex:is(:where(.group):hover *):is(:where(.group):is(* p) *) { display: flex; } - .group-\\[\\&_p\\]\\:hover\\:flex:is(:where(.group) p *):hover { + .group-\\[\\&_p\\]\\:hover\\:flex:is(:where(.group):is(* p) *):hover { display: flex; } - .hover\\:group-\\[\\&_p\\]\\:flex:hover:is(:where(.group) p *) { + .hover\\:group-\\[\\&_p\\]\\:flex:hover:is(:where(.group):is(* p) *) { display: flex; } - .hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover:is(:where(.group) p *):hover { + .hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover:is(:where(.group):is(* p) *):hover { display: flex; }" `) @@ -746,6 +746,13 @@ test('group-*', () => { @slot; } } + @variant nested-selectors { + &:hover { + &:focus { + @slot; + } + } + } @tailwind utilities; `, [ @@ -755,6 +762,8 @@ test('group-*', () => { 'group-hover:group-focus:flex', 'group-focus:group-hover:flex', + + 'group-nested-selectors:flex', ], ), ).toMatchInlineSnapshot(` @@ -774,7 +783,11 @@ test('group-*', () => { display: flex; } - .group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) { + .group-hocus\\:flex:is(:where(.group):is(:hover, :focus) *) { + display: flex; + } + + .group-nested-selectors\\:flex:is(:where(.group):focus:hover *) { display: flex; }" `) @@ -783,16 +796,9 @@ test('group-*', () => { compileCss( css` @variant custom-at-rule (@media foo); - @variant nested-selectors { - &:hover { - &:focus { - @slot; - } - } - } @tailwind utilities; `, - ['group-custom-at-rule:flex', 'group-nested-selectors:flex'], + ['group-custom-at-rule:flex'], ), ).toEqual('') }) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 47e9ae16b..22a59495c 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -239,9 +239,7 @@ export function createVariants(theme: Theme): Variants { return null } - console.log({ groups }) - - ruleNode.selector = '&' + groups.join('') + ruleNode.selector = `&:is(${groups.join('')})` ruleNode.nodes = [] }) @@ -252,50 +250,52 @@ export function createVariants(theme: Theme): Variants { ? `:where(.group\\/${variant.modifier.value})` : ':where(.group)' - let didApply = false + let groups: string[] = [] - walk([ruleNode], (node) => { + walk([ruleNode], (node, { path }) => { if (node.kind !== 'rule') return WalkAction.Continue // Skip past at-rules, and continue traversing the children of the at-rule if (node.selector[0] === '@') return WalkAction.Continue - // Throw out any candidates with variants using nested selectors - if (didApply) { - walk([node], (childNode) => { - if (childNode.kind !== 'rule' || childNode.selector[0] === '@') return WalkAction.Continue + // 1. Walk the tree until we find `@slot` + if (node.nodes.length !== 0) return WalkAction.Continue - didApply = false - return WalkAction.Stop - }) + let selectors = [] - return didApply ? WalkAction.Skip : WalkAction.Stop + // 2. Collect the selectors of the parents + for (let ancestor of path) { + if (ancestor.kind !== 'rule') continue + + // Skip past at-rules, and continue traversing the children of the at-rule + if (ancestor.selector[0] === '@') continue + + // Skip over the "root" node + if (ancestor.selector === '&') continue + + // For most variants we rely entirely on CSS nesting to build-up the final + // selector, but there is no way to use CSS nesting to make `&` refer to + // just the `.group` class the way we'd need to for these variants, so we + // need to replace it in the selector ourselves. + let selector = ancestor.selector.replaceAll('&', '*') + + // When the selector is a selector _list_ we need to wrap it in `:is` + // to make sure the matching behavior is consistent with the original + // variant / selector. + selectors.push(`:is(${selector})`) } - // For most variants we rely entirely on CSS nesting to build-up the final - // selector, but there is no way to use CSS nesting to make `&` refer to - // just the `.group` class the way we'd need to for these variants, so we - // need to replace it in the selector ourselves. - node.selector = node.selector.replaceAll('&', groupSelector) + if (selectors.length === 0) return - // When the selector is a selector _list_ we need to wrap it in `:is` - // to make sure the matching behavior is consistent with the original - // variant / selector. - if (segment(node.selector, ',').length > 1) { - node.selector = `:is(${node.selector})` - } - - node.selector = `&:is(${node.selector} *)` - - // Track that the variant was actually applied - didApply = true + groups.push(selectors.join('')) }) - // If the node wasn't modified, this variant is not compatible with - // `group-*` so discard the candidate. - if (!didApply) { + if (groups.length === 0) { return null } + + ruleNode.selector = `&:is(${groupSelector}:is(${groups.join(',')}) *)` + ruleNode.nodes = [] }) variants.suggest('group', () => {