-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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 automatic var injection codemods (#14526)
In v4, we're [removing automatic var injection](#13657) (please refer to this PR for more detail as to why). Automatic var injection made it so that if you have a candidate like `bg-[--my-color]`, v3 would automatically wrap the content of the arbitrary section with a `var(…)`, resulting in the same as typing `bg-[var(--my-color)]`. This PR adds codemods that go over various arbitrary fields and does the `var(…)` injection for you. To be precise, we will add `var(…)` to: - Modifiers, e.g.: `bg-red-500/[var(--my-opacity)]` - Variants, e.g.: `supports-[var(--test)]:flex` - Arbitrary candidates, e.g.: `[color:var(--my-color)]` - Arbitrary values for functional candidates, e.g.: `bg-[var(--my-color)]` --------- Co-authored-by: Robin Malfait <[email protected]>
- Loading branch information
1 parent
6a50e6e
commit c0dd000
Showing
5 changed files
with
221 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
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
59 changes: 59 additions & 0 deletions
59
packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.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,59 @@ | ||
import { __unstable__loadDesignSystem } from '@tailwindcss/node' | ||
import { expect, test } from 'vitest' | ||
import { automaticVarInjection } from './automatic-var-injection' | ||
|
||
test.each([ | ||
// Arbitrary candidates | ||
['[color:--my-color]', '[color:var(--my-color)]'], | ||
['[--my-color:red]', '[--my-color:red]'], | ||
['[--my-color:--my-other-color]', '[--my-color:var(--my-other-color)]'], | ||
|
||
// Arbitrary values for functional candidates | ||
['bg-[--my-color]', 'bg-[var(--my-color)]'], | ||
['bg-[color:--my-color]', 'bg-[color:var(--my-color)]'], | ||
['border-[length:--my-length]', 'border-[length:var(--my-length)]'], | ||
['border-[line-width:--my-width]', 'border-[line-width:var(--my-width)]'], | ||
|
||
// Can clean up the workaround for opting out of automatic var injection | ||
['bg-[_--my-color]', 'bg-[--my-color]'], | ||
['bg-[color:_--my-color]', 'bg-[color:--my-color]'], | ||
['border-[length:_--my-length]', 'border-[length:--my-length]'], | ||
['border-[line-width:_--my-width]', 'border-[line-width:--my-width]'], | ||
|
||
// Modifiers | ||
['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/[var(--my-opacity)]'], | ||
['bg-red-500/[--my-opacity]', 'bg-red-500/[var(--my-opacity)]'], | ||
['bg-[--my-color]/[--my-opacity]', 'bg-[var(--my-color)]/[var(--my-opacity)]'], | ||
['bg-[color:--my-color]/[--my-opacity]', 'bg-[color:var(--my-color)]/[var(--my-opacity)]'], | ||
|
||
// Can clean up the workaround for opting out of automatic var injection | ||
['[color:--my-color]/[_--my-opacity]', '[color:var(--my-color)]/[--my-opacity]'], | ||
['bg-red-500/[_--my-opacity]', 'bg-red-500/[--my-opacity]'], | ||
['bg-[--my-color]/[_--my-opacity]', 'bg-[var(--my-color)]/[--my-opacity]'], | ||
['bg-[color:--my-color]/[_--my-opacity]', 'bg-[color:var(--my-color)]/[--my-opacity]'], | ||
|
||
// Variants | ||
['supports-[--test]:flex', 'supports-[var(--test)]:flex'], | ||
['supports-[_--test]:flex', 'supports-[--test]:flex'], | ||
|
||
// Some properties never had var() injection in v3. | ||
['[scroll-timeline-name:--myTimeline]', '[scroll-timeline-name:--myTimeline]'], | ||
['[timeline-scope:--myScope]', '[timeline-scope:--myScope]'], | ||
['[view-timeline-name:--myTimeline]', '[view-timeline-name:--myTimeline]'], | ||
['[font-palette:--myPalette]', '[font-palette:--myPalette]'], | ||
['[anchor-name:--myAnchor]', '[anchor-name:--myAnchor]'], | ||
['[anchor-scope:--myScope]', '[anchor-scope:--myScope]'], | ||
['[position-anchor:--myAnchor]', '[position-anchor:--myAnchor]'], | ||
['[position-try-options:--myAnchor]', '[position-try-options:--myAnchor]'], | ||
['[scroll-timeline:--myTimeline]', '[scroll-timeline:--myTimeline]'], | ||
['[animation-timeline:--myAnimation]', '[animation-timeline:--myAnimation]'], | ||
['[view-timeline:--myTimeline]', '[view-timeline:--myTimeline]'], | ||
['[position-try:--myAnchor]', '[position-try:--myAnchor]'], | ||
])('%s => %s', async (candidate, result) => { | ||
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { | ||
base: __dirname, | ||
}) | ||
|
||
let migrated = automaticVarInjection(designSystem, candidate) | ||
expect(migrated).toEqual(result) | ||
}) |
155 changes: 155 additions & 0 deletions
155
packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.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,155 @@ | ||
import { walk, WalkAction } from '../../../../tailwindcss/src/ast' | ||
import type { Candidate, Variant } from '../../../../tailwindcss/src/candidate' | ||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system' | ||
import { printCandidate } from '../candidates' | ||
|
||
export function automaticVarInjection(designSystem: DesignSystem, rawCandidate: string): string { | ||
for (let candidate of designSystem.parseCandidate(rawCandidate)) { | ||
let didChange = false | ||
|
||
// Add `var(…)` in modifier position, e.g.: | ||
// | ||
// `bg-red-500/[--my-opacity]` => `bg-red-500/[var(--my-opacity)]` | ||
if ( | ||
'modifier' in candidate && | ||
candidate.modifier?.kind === 'arbitrary' && | ||
!isAutomaticVarInjectionException(designSystem, candidate, candidate.modifier.value) | ||
) { | ||
let { value, didChange: modifierDidChange } = injectVar(candidate.modifier.value) | ||
candidate.modifier.value = value | ||
didChange ||= modifierDidChange | ||
} | ||
|
||
// Add `var(…)` to all variants, e.g.: | ||
// | ||
// `supports-[--test]:flex'` => `supports-[var(--test)]:flex` | ||
for (let variant of candidate.variants) { | ||
let didChangeVariant = injectVarIntoVariant(designSystem, variant) | ||
if (didChangeVariant) { | ||
didChange = true | ||
} | ||
} | ||
|
||
// Add `var(…)` to arbitrary candidates, e.g.: | ||
// | ||
// `[color:--my-color]` => `[color:var(--my-color)]` | ||
if ( | ||
candidate.kind === 'arbitrary' && | ||
!isAutomaticVarInjectionException(designSystem, candidate, candidate.value) | ||
) { | ||
let { value, didChange: valueDidChange } = injectVar(candidate.value) | ||
candidate.value = value | ||
didChange ||= valueDidChange | ||
} | ||
|
||
// Add `var(…)` to arbitrary values for functional candidates, e.g.: | ||
// | ||
// `bg-[--my-color]` => `bg-[var(--my-color)]` | ||
if ( | ||
candidate.kind === 'functional' && | ||
candidate.value && | ||
candidate.value.kind === 'arbitrary' && | ||
!isAutomaticVarInjectionException(designSystem, candidate, candidate.value.value) | ||
) { | ||
let { value, didChange: valueDidChange } = injectVar(candidate.value.value) | ||
candidate.value.value = value | ||
didChange ||= valueDidChange | ||
} | ||
|
||
if (didChange) { | ||
return printCandidate(candidate) | ||
} | ||
} | ||
return rawCandidate | ||
} | ||
|
||
function injectVar(value: string): { value: string; didChange: boolean } { | ||
let didChange = false | ||
if (value.startsWith('--')) { | ||
value = `var(${value})` | ||
didChange = true | ||
} else if (value.startsWith(' --')) { | ||
value = value.slice(1) | ||
didChange = true | ||
} | ||
return { value, didChange } | ||
} | ||
|
||
function injectVarIntoVariant(designSystem: DesignSystem, variant: Variant): boolean { | ||
let didChange = false | ||
if ( | ||
variant.kind === 'functional' && | ||
variant.value && | ||
variant.value.kind === 'arbitrary' && | ||
!isAutomaticVarInjectionException( | ||
designSystem, | ||
createEmptyCandidate(variant), | ||
variant.value.value, | ||
) | ||
) { | ||
let { value, didChange: valueDidChange } = injectVar(variant.value.value) | ||
variant.value.value = value | ||
didChange ||= valueDidChange | ||
} | ||
|
||
if (variant.kind === 'compound') { | ||
let compoundDidChange = injectVarIntoVariant(designSystem, variant.variant) | ||
if (compoundDidChange) { | ||
didChange = true | ||
} | ||
} | ||
|
||
return didChange | ||
} | ||
|
||
function createEmptyCandidate(variant: Variant) { | ||
return { | ||
kind: 'arbitrary' as const, | ||
property: 'color', | ||
value: 'red', | ||
modifier: null, | ||
variants: [variant], | ||
important: false, | ||
raw: 'candidate', | ||
} satisfies Candidate | ||
} | ||
|
||
const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([ | ||
// Concrete properties | ||
'scroll-timeline-name', | ||
'timeline-scope', | ||
'view-timeline-name', | ||
'font-palette', | ||
'anchor-name', | ||
'anchor-scope', | ||
'position-anchor', | ||
'position-try-options', | ||
|
||
// Shorthand properties | ||
'scroll-timeline', | ||
'animation-timeline', | ||
'view-timeline', | ||
'position-try', | ||
]) | ||
// Some properties never had var() injection in v3. We need to convert the candidate to CSS | ||
// so we can check the properties used by the utility. | ||
function isAutomaticVarInjectionException( | ||
designSystem: DesignSystem, | ||
candidate: Candidate, | ||
value: string, | ||
): boolean { | ||
let ast = designSystem.compileAstNodes(candidate).map((n) => n.node) | ||
|
||
let isException = false | ||
walk(ast, (node) => { | ||
if ( | ||
node.kind === 'declaration' && | ||
AUTO_VAR_INJECTION_EXCEPTIONS.has(node.property) && | ||
node.value == value | ||
) { | ||
isException = true | ||
return WalkAction.Stop | ||
} | ||
}) | ||
return isException | ||
} |
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