Skip to content
Merged
Prev Previous commit
refactor: apply proper migrations on existing utilities in the system
Instead of creating an ad-hoc static utility for the candidate, we
simplify the candidate to it's base form, then perform the change and
then go back.

E.g.:

1. Incoming string of `hover:blur!`
2. Is turned into the candidate AST for `hover:blur!`
3. Is simplified such that variants and important is stripped
4. Is printed as a string `blur`
5. Find replacement `blur-sm`
6. Parse into candidate AST
7. Re-apply the variants and important
8. Stringified `hover:blur-sm!`
  • Loading branch information
RobinMalfait committed Nov 14, 2024
commit 69280d216a268e2c65c0af8483e30a987d92ba5b
187 changes: 110 additions & 77 deletions packages/@tailwindcss-upgrade/src/template/codemods/legacy-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,43 @@ import { isSafeMigration } from '../is-safe-migration'
const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

const LEGACY_CLASS_MAP = {
shadow: 'shadow-sm',
'shadow-sm': 'shadow-xs',
'shadow-xs': 'shadow-2xs',
const LEGACY_CLASS_MAP = new Map([
['shadow', 'shadow-sm'],
['shadow-sm', 'shadow-xs'],
['shadow-xs', 'shadow-2xs'],

'inset-shadow': 'inset-shadow-sm',
'inset-shadow-sm': 'inset-shadow-xs',
'inset-shadow-xs': 'inset-shadow-2xs',
['inset-shadow', 'inset-shadow-sm'],
['inset-shadow-sm', 'inset-shadow-xs'],
['inset-shadow-xs', 'inset-shadow-2xs'],

'drop-shadow': 'drop-shadow-sm',
'drop-shadow-sm': 'drop-shadow-xs',
['drop-shadow', 'drop-shadow-sm'],
['drop-shadow-sm', 'drop-shadow-xs'],

rounded: 'rounded-sm',
'rounded-sm': 'rounded-xs',
['rounded', 'rounded-sm'],
['rounded-sm', 'rounded-xs'],

blur: 'blur-sm',
'blur-sm': 'blur-xs',
}
['blur', 'blur-sm'],
['blur-sm', 'blur-xs'],
])

const THEME_KEYS = {
shadow: '--shadow',
'shadow-sm': '--shadow-sm',
'shadow-xs': '--shadow-xs',
'shadow-2xs': '--shadow-2xs',
const THEME_KEYS = new Map([
['shadow', '--shadow'],
['shadow-sm', '--shadow-sm'],
['shadow-xs', '--shadow-xs'],
['shadow-2xs', '--shadow-2xs'],

'drop-shadow': '--drop-shadow',
'drop-shadow-sm': '--drop-shadow-sm',
'drop-shadow-xs': '--drop-shadow-xs',
['drop-shadow', '--drop-shadow'],
['drop-shadow-sm', '--drop-shadow-sm'],
['drop-shadow-xs', '--drop-shadow-xs'],

rounded: '--radius',
'rounded-sm': '--radius-sm',
'rounded-xs': '--radius-xs',
['rounded', '--radius'],
['rounded-sm', '--radius-sm'],
['rounded-xs', '--radius-xs'],

blur: '--blur',
'blur-sm': '--blur-sm',
'blur-xs': '--blur-xs',
}
['blur', '--blur'],
['blur-sm', '--blur-sm'],
['blur-xs', '--blur-xs'],
])

const DESIGN_SYSTEMS = new DefaultMap((base) => {
return __unstable__loadDesignSystem('@import "tailwindcss";', { base })
Expand All @@ -65,58 +65,91 @@ export async function legacyClasses(
): Promise<string> {
let defaultDesignSystem = await DESIGN_SYSTEMS.get(__dirname)

for (let candidate of designSystem.parseCandidate(rawCandidate)) {
if (candidate.kind === 'functional') {
let parts = [candidate.root]
if (candidate.value?.kind === 'named') {
parts.push(candidate.value.value)
function* migrate(rawCandidate: string) {
for (let candidate of designSystem.parseCandidate(rawCandidate)) {
// Create a base candidate string from the candidate.
// E.g.: `hover:blur!` -> `blur`
let baseCandidate = structuredClone(candidate) as Candidate
baseCandidate.variants = []
baseCandidate.important = false
let baseCandidateString = printCandidate(designSystem, baseCandidate)

// Find the new base candidate string. `blur` -> `blur-sm`
let newBaseCandidateString = LEGACY_CLASS_MAP.get(baseCandidateString)
if (!newBaseCandidateString) continue

// Parse the new base candidate string into an actual candidate AST.
let [newBaseCandidate] = designSystem.parseCandidate(newBaseCandidateString)
if (!newBaseCandidate) continue

// Re-apply the variants and important flag from the original candidate.
// E.g.: `hover:blur!` -> `blur` -> `blur-sm` -> `hover:blur-sm!`
let newCandidate = structuredClone(newBaseCandidate) as Candidate
newCandidate.variants = candidate.variants
newCandidate.important = candidate.important

yield [
candidate,
newCandidate,
THEME_KEYS.get(baseCandidateString),
THEME_KEYS.get(newBaseCandidateString),
] as const
}
}

for (let [fromCandidate, toCandidate, fromThemeKey, toThemeKey] of migrate(rawCandidate)) {
// Every utility that has a simple representation (e.g.: `blur`, `radius`,
// etc.`) without variants or special characters _could_ be a potential
// problem during the migration.
let isPotentialProblematicClass = (() => {
if (fromCandidate.variants.length > 0) {
return false
}

let root = parts.join('-')
if (Object.hasOwn(LEGACY_CLASS_MAP, root)) {
let newRoot = LEGACY_CLASS_MAP[root as keyof typeof LEGACY_CLASS_MAP]

if (location && !root.includes('-') && !isSafeMigration(location)) {
continue
}

let fromThemeKey = THEME_KEYS[root as keyof typeof THEME_KEYS]
let toThemeKey = THEME_KEYS[newRoot as keyof typeof THEME_KEYS]

if (fromThemeKey && toThemeKey) {
// Migrating something that resolves to a value in the theme.
let customFrom = designSystem.resolveThemeValue(fromThemeKey)
let defaultFrom = defaultDesignSystem.resolveThemeValue(fromThemeKey)
let customTo = designSystem.resolveThemeValue(toThemeKey)
let defaultTo = defaultDesignSystem.resolveThemeValue(toThemeKey)

// The new theme value is not defined, which means we can't safely
// migrate the utility.
if (customTo === undefined) {
continue
}

// The "from" theme value changed compared to the default theme value.
if (customFrom !== defaultFrom) {
continue
}

// The "to" theme value changed compared to the default theme value.
if (customTo !== defaultTo) {
continue
}
}

for (let newCandidate of designSystem.parseCandidate(newRoot)) {
let clone = structuredClone(newCandidate) as Candidate

clone.important = candidate.important
clone.variants = candidate.variants

return printCandidate(designSystem, clone)
}
if (fromCandidate.kind === 'arbitrary') {
return false
}

if (fromCandidate.kind === 'static') {
return !fromCandidate.root.includes('-')
}

if (fromCandidate.kind === 'functional') {
return fromCandidate.value === null || !fromCandidate.root.includes('-')
}

return false
})()

if (location && isPotentialProblematicClass && !isSafeMigration(location)) {
continue
}

if (fromThemeKey && toThemeKey) {
// Migrating something that resolves to a value in the theme.
let customFrom = designSystem.resolveThemeValue(fromThemeKey)
let defaultFrom = defaultDesignSystem.resolveThemeValue(fromThemeKey)
let customTo = designSystem.resolveThemeValue(toThemeKey)
let defaultTo = defaultDesignSystem.resolveThemeValue(toThemeKey)

// The new theme value is not defined, which means we can't safely
// migrate the utility.
if (customTo === undefined) {
continue
}

// The "from" theme value changed compared to the default theme value.
if (customFrom !== defaultFrom) {
continue
}

// The "to" theme value changed compared to the default theme value.
if (customTo !== defaultTo) {
continue
}
}

return printCandidate(designSystem, toCandidate)
}

return rawCandidate
Expand Down