Skip to content

Commit

Permalink
Template migrations: Add automatic var injection codemods (#14526)
Browse files Browse the repository at this point in the history
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 <malfait.robin@gmail.com>
  • Loading branch information
philipp-spiess and RobinMalfait authored Oct 1, 2024
1 parent 6a50e6e commit c0dd000
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 5 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>
`,
)

Expand Down
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)
})
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
}
3 changes: 2 additions & 1 deletion packages/@tailwindcss-upgrade/src/template/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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)

Expand Down

0 comments on commit c0dd000

Please sign in to comment.