-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Template migrations: Add variant order codemods
- Loading branch information
1 parent
732147a
commit a754e06
Showing
4 changed files
with
158 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
packages/@tailwindcss-upgrade/src/template/codemods/variant-order.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { __unstable__loadDesignSystem } from '@tailwindcss/node' | ||
import { expect, test } from 'vitest' | ||
import { printCandidate } from '../candidates' | ||
import { variantOrder } from './variant-order' | ||
|
||
test.each([ | ||
// Does nothing unless there are at least two variants | ||
['flex', null], | ||
['hover:flex', null], | ||
['[color:red]', null], | ||
['[&:focus]:[color:red]', null], | ||
|
||
// // Reorders simple variants | ||
['data-[invalid]:data-[hover]:flex', 'data-[hover]:data-[invalid]:flex'], | ||
|
||
// // Does not reorder some known combinations where the order does not matter | ||
['hover:focus:flex', null], | ||
['focus:hover:flex', null], | ||
['[&:hover]:[&:focus]:flex', null], | ||
['[&:focus]:[&:hover]:flex', null], | ||
|
||
// Handles pseudo-elements that cannot have anything after them | ||
// c.f. https://github.com/tailwindlabs/tailwindcss/pull/13478/files#diff-7779a0eebf6b980dd3abd63b39729b3023cf9a31c91594f5a25ea020b066e1c0 | ||
['dark:before:flex', 'dark:before:flex'], | ||
['before:dark:flex', 'dark:before:flex'], | ||
|
||
// Puts some pseudo-elements that must appear at the end of the selector at | ||
// the end of the candidate | ||
['dark:*:before:after:flex', 'dark:*:after:before:flex'], | ||
['dark:before:after:*:flex', 'dark:*:after:before:flex'], | ||
|
||
// Some pseudo-elements are treated as regular variants | ||
['dark:*:hover:file:focus:underline', 'dark:focus:file:hover:*:underline'], | ||
|
||
// Keeps @media-variants and the dark variant in the beginning and keeps their | ||
// order | ||
['sm:dark:hover:flex', 'sm:dark:hover:flex'], | ||
['[@media(print)]:group-hover:flex', '[@media(print)]:group-hover:flex'], | ||
['sm:max-xl:data-[a]:data-[b]:dark:hover:flex', 'sm:max-xl:dark:hover:data-[b]:data-[a]:flex'], | ||
])('%s => %s', async (candidate, result) => { | ||
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { | ||
base: __dirname, | ||
}) | ||
|
||
let migrated = variantOrder(designSystem, designSystem.parseCandidate(candidate)[0]!) | ||
expect(migrated ? printCandidate(migrated) : migrated).toEqual(result) | ||
}) |
102 changes: 102 additions & 0 deletions
102
packages/@tailwindcss-upgrade/src/template/codemods/variant-order.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { walk, type AstNode } from '../../../../tailwindcss/src/ast' | ||
import type { Candidate, Variant } from '../../../../tailwindcss/src/candidate' | ||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system' | ||
|
||
export function variantOrder(designSystem: DesignSystem, candidate: Candidate): Candidate | null { | ||
if (candidate.variants.length > 1) { | ||
// If the variant stack is made of only variants where the order does not | ||
// matter, we can skip the reordering | ||
if (candidate.variants.every((v) => isOrderIndependentVariant(designSystem, v))) { | ||
return null | ||
} | ||
|
||
let mediaVariants = [] | ||
let regularVariants = [] | ||
let pseudoElementVariants = [] | ||
|
||
for (let variant of candidate.variants) { | ||
if (isMediaVariant(designSystem, variant)) { | ||
mediaVariants.push(variant) | ||
} else if (isEndOfSelectorPseudoElement(variant)) { | ||
pseudoElementVariants.push(variant) | ||
} else { | ||
regularVariants.push(variant) | ||
} | ||
} | ||
|
||
// The candidate list in the AST need to be in reverse order | ||
candidate.variants = [ | ||
...pseudoElementVariants.reverse(), | ||
...regularVariants.reverse(), | ||
...mediaVariants, | ||
] | ||
return candidate | ||
} | ||
return null | ||
} | ||
|
||
function isOrderIndependentVariant(designSystem: DesignSystem, variant: Variant) { | ||
let stack = getAppliedNodeStack(designSystem, variant) | ||
// Remove media variants from the stack, these are hoisted and don't affect the order of the | ||
// below pseudos | ||
.filter((node) => !(node.kind === 'rule' && node.selector.startsWith('@media (hover:'))) | ||
return stack.every( | ||
(node) => node.kind === 'rule' && (node.selector === '&:hover' || node.selector === '&:focus'), | ||
) | ||
} | ||
|
||
function isMediaVariant(designSystem: DesignSystem, variant: Variant) { | ||
// Handle the dark variant as a media variant | ||
if (variant.kind === 'static' && variant.root === 'dark') { | ||
return true | ||
} | ||
let stack = getAppliedNodeStack(designSystem, variant) | ||
return stack.every((node) => node.kind === 'rule' && node.selector.startsWith('@media')) | ||
} | ||
|
||
function isEndOfSelectorPseudoElement(variant: Variant) { | ||
if (variant.kind !== 'static') { | ||
return false | ||
} | ||
switch (variant.root) { | ||
case 'after': | ||
case 'backdrop': | ||
case 'before': | ||
case 'first-letter': | ||
case 'first-line': | ||
case 'marker': | ||
case 'placeholder': | ||
case 'selection': | ||
return true | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
function getAppliedNodeStack(designSystem: DesignSystem, variant: Variant): AstNode[] { | ||
let stack: AstNode[] = [] | ||
let ast = designSystem | ||
.compileAstNodes({ | ||
kind: 'arbitrary', | ||
property: 'color', | ||
value: 'red', | ||
modifier: null, | ||
variants: [variant], | ||
important: false, | ||
raw: 'candidate', | ||
}) | ||
.map((c) => c.node) | ||
|
||
walk(ast, (node) => { | ||
// Ignore the variant root class | ||
if (node.kind === 'rule' && node.selector === '.candidate') { | ||
return | ||
} | ||
// Ignore the dummy declaration | ||
if (node.kind === 'declaration' && node.property === 'color' && node.value === 'red') { | ||
return | ||
} | ||
stack.push(node) | ||
}) | ||
return stack | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters