Template migrations: Add automatic var injection codemods (#14526)
In v4, we're [removing automatic var injection](https://github.com/tailwindlabs/tailwindcss/pull/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 <malfait.robin@gmail.com>
This commit is contained in:
parent
6a50e6e160
commit
c0dd000c3c
5 changed files with 221 additions and 5 deletions
|
|
@ -12,8 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501))
|
||||
- Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553))
|
||||
- Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556))
|
||||
- _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537]))
|
||||
- _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514))
|
||||
- _Experimental_: Add template codemods for removal of automatic `var(…)` injection ([#14526](https://github.com/tailwindlabs/tailwindcss/pull/14526))
|
||||
- _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537]))
|
||||
- _Experimental_: Add template codemods for migrating important utilities (e.g. `!flex` to `flex!`) ([#14502](https://github.com/tailwindlabs/tailwindcss/pull/14502))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
@ -39,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- _Experimental_: Add CSS codemods for `@apply` ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14434))
|
||||
- _Experimental_: Add CSS codemods for migrating `@tailwind` directives ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14411), [#14504](https://github.com/tailwindlabs/tailwindcss/pull/14504))
|
||||
- _Experimental_: Add CSS codemods for migrating `@layer utilities` and `@layer components` ([#14455](https://github.com/tailwindlabs/tailwindcss/pull/14455))
|
||||
- _Experimental_: Add template codemods for migrating important utilities (e.g. `!flex` to `flex!`) ([#14502](https://github.com/tailwindlabs/tailwindcss/pull/14502))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ test(
|
|||
`,
|
||||
'src/index.html': html`
|
||||
<h1>🤠👋</h1>
|
||||
<div class="!flex sm:!block bg-gradient-to-t"></div>
|
||||
<div class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"></div>
|
||||
`,
|
||||
'src/input.css': css`
|
||||
@tailwind base;
|
||||
|
|
@ -35,7 +35,7 @@ test(
|
|||
'src/index.html',
|
||||
html`
|
||||
<h1>🤠👋</h1>
|
||||
<div class="flex! sm:block! bg-linear-to-t"></div>
|
||||
<div class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"></div>
|
||||
`,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
|
|||
import path from 'node:path'
|
||||
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
|
||||
import { extractRawCandidates, replaceCandidateInContent } from './candidates'
|
||||
import { automaticVarInjection } from './codemods/automatic-var-injection'
|
||||
import { bgGradient } from './codemods/bg-gradient'
|
||||
import { important } from './codemods/important'
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ export type Migration = (designSystem: DesignSystem, rawCandidate: string) => st
|
|||
export default async function migrateContents(
|
||||
designSystem: DesignSystem,
|
||||
contents: string,
|
||||
migrations: Migration[] = [important, bgGradient],
|
||||
migrations: Migration[] = [important, automaticVarInjection, bgGradient],
|
||||
): Promise<string> {
|
||||
let candidates = await extractRawCandidates(contents)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue