mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Migrate bare values to named values (#18000)
This PR improves the upgrade tool by also migrating bare values to named values defined in the `@theme`. Recently we shipped some updates dat allowed us to migrate arbitrary values (with square brackets), but we didn't migrate bare values yet. That means that in this example: ```html <div class="aspect-[16/9]"></div> <div class="aspect-16/9"></div> ``` We migrated this to: ```html <div class="aspect-video"></div> <div class="aspect-16/9"></div> ``` With this change, we will also try and migrate the bare value to a named value. So this example: ```html <div class="aspect-[16/9]"></div> <div class="aspect-16/9"></div> ``` Now becomes: ```html <div class="aspect-video"></div> <div class="aspect-video"></div> ``` ## Test plan 1. Added unit tests for the new functionality. 2. Ran this on a local project Before: <img width="432" alt="image" src="https://github.com/user-attachments/assets/ce1adfbd-7be1-4062-bea5-66368f748e44" /> After: <img width="382" alt="image" src="https://github.com/user-attachments/assets/a385c94c-4e4c-4e1c-ac73-680c56ac4081" />
This commit is contained in:
parent
ef2e6c71fe
commit
498f9ff003
@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `lightningcss` now statically links Visual Studio redistributables ([#17979](https://github.com/tailwindlabs/tailwindcss/pull/17979))
|
||||
- Ensure that running the Standalone build does not leave temporary files behind ([#17981](https://github.com/tailwindlabs/tailwindcss/pull/17981))
|
||||
|
||||
### Added
|
||||
|
||||
- Upgrade: Migrate bare values to named values ([#18000](https://github.com/tailwindlabs/tailwindcss/pull/18000))
|
||||
|
||||
## [4.1.6] - 2025-05-09
|
||||
|
||||
### Added
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { Scanner } from '@tailwindcss/oxide'
|
||||
import type { Candidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
|
||||
export async function extractRawCandidates(
|
||||
content: string,
|
||||
@ -13,3 +15,21 @@ export async function extractRawCandidates(
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
// Create a basic stripped candidate without variants or important flag
|
||||
export function baseCandidate<T extends Candidate>(candidate: T) {
|
||||
let base = structuredClone(candidate)
|
||||
|
||||
base.important = false
|
||||
base.variants = []
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
export function parseCandidate(designSystem: DesignSystem, input: string) {
|
||||
return designSystem.parseCandidate(
|
||||
designSystem.theme.prefix && !input.startsWith(`${designSystem.theme.prefix}:`)
|
||||
? `${designSystem.theme.prefix}:${input}`
|
||||
: input,
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,45 +2,11 @@ import { printModifier, type Candidate } from '../../../../tailwindcss/src/candi
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
import { isValidSpacingMultiplier } from '../../../../tailwindcss/src/utils/infer-data-type'
|
||||
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
|
||||
import { dimensions } from '../../utils/dimension'
|
||||
import type { Writable } from '../../utils/types'
|
||||
import { computeUtilitySignature } from './signatures'
|
||||
|
||||
// For all static utilities in the system, compute a lookup table that maps the
|
||||
// utility signature to the utility name. This is used to find the utility name
|
||||
// for a given utility signature.
|
||||
//
|
||||
// For all functional utilities, we can compute static-like utilities by
|
||||
// essentially pre-computing the values and modifiers. This is a bit slow, but
|
||||
// also only has to happen once per design system.
|
||||
const preComputedUtilities = new DefaultMap<DesignSystem, DefaultMap<string, string[]>>((ds) => {
|
||||
let signatures = computeUtilitySignature.get(ds)
|
||||
let lookup = new DefaultMap<string, string[]>(() => [])
|
||||
|
||||
for (let [className, meta] of ds.getClassList()) {
|
||||
let signature = signatures.get(className)
|
||||
if (typeof signature !== 'string') continue
|
||||
lookup.get(signature).push(className)
|
||||
|
||||
for (let modifier of meta.modifiers) {
|
||||
// Modifiers representing numbers can be computed and don't need to be
|
||||
// pre-computed. Doing the math and at the time of writing this, this
|
||||
// would save you 250k additionally pre-computed utilities...
|
||||
if (isValidSpacingMultiplier(modifier)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let classNameWithModifier = `${className}/${modifier}`
|
||||
let signature = signatures.get(classNameWithModifier)
|
||||
if (typeof signature !== 'string') continue
|
||||
lookup.get(signature).push(classNameWithModifier)
|
||||
}
|
||||
}
|
||||
|
||||
return lookup
|
||||
})
|
||||
import { baseCandidate, parseCandidate } from './candidates'
|
||||
import { computeUtilitySignature, preComputedUtilities } from './signatures'
|
||||
|
||||
const baseReplacementsCache = new DefaultMap<DesignSystem, Map<string, Candidate>>(
|
||||
() => new Map<string, Candidate>(),
|
||||
@ -114,9 +80,7 @@ export function migrateArbitraryUtilities(
|
||||
// will re-add those later but they are irrelevant for what we are trying to
|
||||
// do here (and will increase cache hits because we only have to deal with
|
||||
// the base utility, nothing more).
|
||||
let targetCandidate = structuredClone(candidate)
|
||||
targetCandidate.important = false
|
||||
targetCandidate.variants = []
|
||||
let targetCandidate = baseCandidate(candidate)
|
||||
|
||||
let targetCandidateString = designSystem.printCandidate(targetCandidate)
|
||||
if (baseReplacementsCache.get(designSystem).has(targetCandidateString)) {
|
||||
@ -275,14 +239,6 @@ export function migrateArbitraryUtilities(
|
||||
}
|
||||
}
|
||||
|
||||
function parseCandidate(designSystem: DesignSystem, input: string) {
|
||||
return designSystem.parseCandidate(
|
||||
designSystem.theme.prefix && !input.startsWith(`${designSystem.theme.prefix}:`)
|
||||
? `${designSystem.theme.prefix}:${input}`
|
||||
: input,
|
||||
)
|
||||
}
|
||||
|
||||
// Let's make sure that all variables used in the value are also all used in the
|
||||
// found replacement. If not, then we are dealing with a different namespace or
|
||||
// we could lose functionality in case the variable was changed higher up in the
|
||||
|
||||
@ -1,28 +1,9 @@
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
import { replaceObject } from '../../utils/replace-object'
|
||||
import type { Writable } from '../../utils/types'
|
||||
import { walkVariants } from '../../utils/walk-variants'
|
||||
import { computeVariantSignature } from './signatures'
|
||||
|
||||
const variantsLookup = new DefaultMap<DesignSystem, DefaultMap<string, string[]>>(
|
||||
(designSystem) => {
|
||||
let signatures = computeVariantSignature.get(designSystem)
|
||||
let lookup = new DefaultMap<string, string[]>(() => [])
|
||||
|
||||
// Actual static variants
|
||||
for (let [root, variant] of designSystem.variants.entries()) {
|
||||
if (variant.kind === 'static') {
|
||||
let signature = signatures.get(root)
|
||||
if (typeof signature !== 'string') continue
|
||||
lookup.get(signature).push(root)
|
||||
}
|
||||
}
|
||||
|
||||
return lookup
|
||||
},
|
||||
)
|
||||
import { computeVariantSignature, preComputedVariants } from './signatures'
|
||||
|
||||
export function migrateArbitraryVariants(
|
||||
designSystem: DesignSystem,
|
||||
@ -30,7 +11,7 @@ export function migrateArbitraryVariants(
|
||||
rawCandidate: string,
|
||||
): string {
|
||||
let signatures = computeVariantSignature.get(designSystem)
|
||||
let variants = variantsLookup.get(designSystem)
|
||||
let variants = preComputedVariants.get(designSystem)
|
||||
|
||||
for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) {
|
||||
// We are only interested in the variants
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import type { UserConfig } from '../../../../tailwindcss/src/compat/config/types'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
import { migrateBareValueUtilities } from './migrate-bare-utilities'
|
||||
|
||||
const css = String.raw
|
||||
|
||||
const designSystems = new DefaultMap((base: string) => {
|
||||
return new DefaultMap((input: string) => {
|
||||
return __unstable__loadDesignSystem(input, { base })
|
||||
})
|
||||
})
|
||||
|
||||
function migrate(designSystem: DesignSystem, userConfig: UserConfig | null, rawCandidate: string) {
|
||||
for (let migration of [migrateBareValueUtilities]) {
|
||||
rawCandidate = migration(designSystem, userConfig, rawCandidate)
|
||||
}
|
||||
return rawCandidate
|
||||
}
|
||||
|
||||
describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', (strategy) => {
|
||||
let testName = '%s => %s (%#)'
|
||||
if (strategy === 'with-variant') {
|
||||
testName = testName.replaceAll('%s', 'focus:%s')
|
||||
} else if (strategy === 'important') {
|
||||
testName = testName.replaceAll('%s', '%s!')
|
||||
} else if (strategy === 'prefix') {
|
||||
testName = testName.replaceAll('%s', 'tw:%s')
|
||||
}
|
||||
|
||||
// Basic input with minimal design system to keep the tests fast
|
||||
let input = css`
|
||||
@import 'tailwindcss' ${strategy === 'prefix' ? 'prefix(tw)' : ''};
|
||||
@theme {
|
||||
--*: initial;
|
||||
--spacing: 0.25rem;
|
||||
--aspect-video: 16 / 9;
|
||||
--tab-size-github: 8;
|
||||
}
|
||||
|
||||
@utility tab-* {
|
||||
tab-size: --value(--tab-size, integer);
|
||||
}
|
||||
`
|
||||
|
||||
test.each([
|
||||
// Built-in utility with bare value fraction
|
||||
['aspect-16/9', 'aspect-video'],
|
||||
|
||||
// Custom utility with bare value integer
|
||||
['tab-8', 'tab-github'],
|
||||
])(testName, async (candidate, result) => {
|
||||
if (strategy === 'with-variant') {
|
||||
candidate = `focus:${candidate}`
|
||||
result = `focus:${result}`
|
||||
} else if (strategy === 'important') {
|
||||
candidate = `${candidate}!`
|
||||
result = `${result}!`
|
||||
} else if (strategy === 'prefix') {
|
||||
// Not only do we need to prefix the candidate, we also have to make
|
||||
// sure that we prefix all CSS variables.
|
||||
candidate = `tw:${candidate.replaceAll('var(--', 'var(--tw-')}`
|
||||
result = `tw:${result.replaceAll('var(--', 'var(--tw-')}`
|
||||
}
|
||||
|
||||
let designSystem = await designSystems.get(__dirname).get(input)
|
||||
let migrated = migrate(designSystem, {}, candidate)
|
||||
expect(migrated).toEqual(result)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,122 @@
|
||||
import { type Candidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
import type { Writable } from '../../utils/types'
|
||||
import { baseCandidate, parseCandidate } from './candidates'
|
||||
import { computeUtilitySignature, preComputedUtilities } from './signatures'
|
||||
|
||||
const baseReplacementsCache = new DefaultMap<DesignSystem, Map<string, Candidate>>(
|
||||
() => new Map<string, Candidate>(),
|
||||
)
|
||||
|
||||
export function migrateBareValueUtilities(
|
||||
designSystem: DesignSystem,
|
||||
_userConfig: Config | null,
|
||||
rawCandidate: string,
|
||||
): string {
|
||||
let utilities = preComputedUtilities.get(designSystem)
|
||||
let signatures = computeUtilitySignature.get(designSystem)
|
||||
|
||||
for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) {
|
||||
// We are only interested in bare value utilities
|
||||
if (readonlyCandidate.kind !== 'functional' || readonlyCandidate.value?.kind !== 'named') {
|
||||
continue
|
||||
}
|
||||
|
||||
// The below logic makes use of mutation. Since candidates in the
|
||||
// DesignSystem are cached, we can't mutate them directly.
|
||||
let candidate = structuredClone(readonlyCandidate) as Writable<typeof readonlyCandidate>
|
||||
|
||||
// Create a basic stripped candidate without variants or important flag. We
|
||||
// will re-add those later but they are irrelevant for what we are trying to
|
||||
// do here (and will increase cache hits because we only have to deal with
|
||||
// the base utility, nothing more).
|
||||
let targetCandidate = baseCandidate(candidate)
|
||||
|
||||
let targetCandidateString = designSystem.printCandidate(targetCandidate)
|
||||
if (baseReplacementsCache.get(designSystem).has(targetCandidateString)) {
|
||||
let target = structuredClone(
|
||||
baseReplacementsCache.get(designSystem).get(targetCandidateString)!,
|
||||
)
|
||||
// Re-add the variants and important flag from the original candidate
|
||||
target.variants = candidate.variants
|
||||
target.important = candidate.important
|
||||
|
||||
return designSystem.printCandidate(target)
|
||||
}
|
||||
|
||||
// Compute the signature for the target candidate
|
||||
let targetSignature = signatures.get(targetCandidateString)
|
||||
if (typeof targetSignature !== 'string') continue
|
||||
|
||||
// Try a few options to find a suitable replacement utility
|
||||
for (let replacementCandidate of tryReplacements(targetSignature, targetCandidate)) {
|
||||
let replacementString = designSystem.printCandidate(replacementCandidate)
|
||||
let replacementSignature = signatures.get(replacementString)
|
||||
if (replacementSignature !== targetSignature) {
|
||||
continue
|
||||
}
|
||||
|
||||
replacementCandidate = structuredClone(replacementCandidate)
|
||||
|
||||
// Cache the result so we can re-use this work later
|
||||
baseReplacementsCache.get(designSystem).set(targetCandidateString, replacementCandidate)
|
||||
|
||||
// Re-add the variants and important flag from the original candidate
|
||||
replacementCandidate.variants = candidate.variants
|
||||
replacementCandidate.important = candidate.important
|
||||
|
||||
// Update the candidate with the new value
|
||||
Object.assign(candidate, replacementCandidate)
|
||||
|
||||
// We will re-print the candidate to get the migrated candidate out
|
||||
return designSystem.printCandidate(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
return rawCandidate
|
||||
|
||||
function* tryReplacements(
|
||||
targetSignature: string,
|
||||
candidate: Extract<Candidate, { kind: 'functional' }>,
|
||||
): Generator<Candidate> {
|
||||
// Find a corresponding utility for the same signature
|
||||
let replacements = utilities.get(targetSignature)
|
||||
|
||||
// Multiple utilities can map to the same signature. Not sure how to migrate
|
||||
// this one so let's just skip it for now.
|
||||
//
|
||||
// TODO: Do we just migrate to the first one?
|
||||
if (replacements.length > 1) return
|
||||
|
||||
// If we didn't find any replacement utilities, let's try to strip the
|
||||
// modifier and find a replacement then. If we do, we can try to re-add the
|
||||
// modifier later and verify if we have a valid migration.
|
||||
//
|
||||
// This is necessary because `text-red-500/50` will not be pre-computed,
|
||||
// only `text-red-500` will.
|
||||
if (replacements.length === 0 && candidate.modifier) {
|
||||
let candidateWithoutModifier = { ...candidate, modifier: null }
|
||||
let targetSignatureWithoutModifier = signatures.get(
|
||||
designSystem.printCandidate(candidateWithoutModifier),
|
||||
)
|
||||
if (typeof targetSignatureWithoutModifier === 'string') {
|
||||
for (let replacementCandidate of tryReplacements(
|
||||
targetSignatureWithoutModifier,
|
||||
candidateWithoutModifier,
|
||||
)) {
|
||||
yield Object.assign({}, replacementCandidate, { modifier: candidate.modifier })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If only a single utility maps to the signature, we can use that as the
|
||||
// replacement.
|
||||
if (replacements.length === 1) {
|
||||
for (let replacementCandidate of parseCandidate(designSystem, replacements[0])) {
|
||||
yield replacementCandidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
import * as version from '../../utils/version'
|
||||
import { baseCandidate } from './candidates'
|
||||
import { isSafeMigration } from './is-safe-migration'
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url)
|
||||
@ -92,10 +93,8 @@ export async function migrateLegacyClasses(
|
||||
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 = designSystem.printCandidate(baseCandidate)
|
||||
let base = baseCandidate(candidate)
|
||||
let baseCandidateString = designSystem.printCandidate(base)
|
||||
|
||||
// Find the new base candidate string. `blur` -> `blur-sm`
|
||||
let newBaseCandidateString = LEGACY_CLASS_MAP.get(baseCandidateString)
|
||||
|
||||
@ -9,6 +9,7 @@ import { migrateArbitraryUtilities } from './migrate-arbitrary-utilities'
|
||||
import { migrateArbitraryValueToBareValue } from './migrate-arbitrary-value-to-bare-value'
|
||||
import { migrateArbitraryVariants } from './migrate-arbitrary-variants'
|
||||
import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection'
|
||||
import { migrateBareValueUtilities } from './migrate-bare-utilities'
|
||||
import { migrateBgGradient } from './migrate-bg-gradient'
|
||||
import { migrateDropUnnecessaryDataTypes } from './migrate-drop-unnecessary-data-types'
|
||||
import { migrateEmptyArbitraryValues } from './migrate-handle-empty-arbitrary-values'
|
||||
@ -47,6 +48,7 @@ export const DEFAULT_MIGRATIONS: Migration[] = [
|
||||
migrateAutomaticVarInjection,
|
||||
migrateLegacyArbitraryValues,
|
||||
migrateArbitraryUtilities,
|
||||
migrateBareValueUtilities,
|
||||
migrateModernizeArbitraryValues,
|
||||
migrateArbitraryVariants,
|
||||
migrateDropUnnecessaryDataTypes,
|
||||
|
||||
@ -5,6 +5,7 @@ import * as SelectorParser from '../../../../tailwindcss/src/compat/selector-par
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { ThemeOptions } from '../../../../tailwindcss/src/theme'
|
||||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
import { isValidSpacingMultiplier } from '../../../../tailwindcss/src/utils/infer-data-type'
|
||||
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
|
||||
import { dimensions } from '../../utils/dimension'
|
||||
|
||||
@ -236,6 +237,42 @@ export const computeUtilitySignature = new DefaultMap<
|
||||
})
|
||||
})
|
||||
|
||||
// For all static utilities in the system, compute a lookup table that maps the
|
||||
// utility signature to the utility name. This is used to find the utility name
|
||||
// for a given utility signature.
|
||||
//
|
||||
// For all functional utilities, we can compute static-like utilities by
|
||||
// essentially pre-computing the values and modifiers. This is a bit slow, but
|
||||
// also only has to happen once per design system.
|
||||
export const preComputedUtilities = new DefaultMap<DesignSystem, DefaultMap<string, string[]>>(
|
||||
(ds) => {
|
||||
let signatures = computeUtilitySignature.get(ds)
|
||||
let lookup = new DefaultMap<string, string[]>(() => [])
|
||||
|
||||
for (let [className, meta] of ds.getClassList()) {
|
||||
let signature = signatures.get(className)
|
||||
if (typeof signature !== 'string') continue
|
||||
lookup.get(signature).push(className)
|
||||
|
||||
for (let modifier of meta.modifiers) {
|
||||
// Modifiers representing numbers can be computed and don't need to be
|
||||
// pre-computed. Doing the math and at the time of writing this, this
|
||||
// would save you 250k additionally pre-computed utilities...
|
||||
if (isValidSpacingMultiplier(modifier)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let classNameWithModifier = `${className}/${modifier}`
|
||||
let signature = signatures.get(classNameWithModifier)
|
||||
if (typeof signature !== 'string') continue
|
||||
lookup.get(signature).push(classNameWithModifier)
|
||||
}
|
||||
}
|
||||
|
||||
return lookup
|
||||
},
|
||||
)
|
||||
|
||||
// Given a variant, compute a signature that represents the variant. The
|
||||
// signature will be a normalised form of the generated CSS for the variant, or
|
||||
// a unique symbol if the variant is not valid. The class in the selector will
|
||||
@ -342,6 +379,24 @@ export const computeVariantSignature = new DefaultMap<
|
||||
})
|
||||
})
|
||||
|
||||
export const preComputedVariants = new DefaultMap<DesignSystem, DefaultMap<string, string[]>>(
|
||||
(designSystem) => {
|
||||
let signatures = computeVariantSignature.get(designSystem)
|
||||
let lookup = new DefaultMap<string, string[]>(() => [])
|
||||
|
||||
// Actual static variants
|
||||
for (let [root, variant] of designSystem.variants.entries()) {
|
||||
if (variant.kind === 'static') {
|
||||
let signature = signatures.get(root)
|
||||
if (typeof signature !== 'string') continue
|
||||
lookup.get(signature).push(root)
|
||||
}
|
||||
}
|
||||
|
||||
return lookup
|
||||
},
|
||||
)
|
||||
|
||||
function temporarilyDisableThemeInline<T>(designSystem: DesignSystem, cb: () => T): T {
|
||||
// Turn off `@theme inline` feature such that `@theme` and `@theme inline` are
|
||||
// considered the same. The biggest motivation for this is referencing
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user