Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
80a90e0
hoist regex
RobinMalfait Sep 26, 2025
33dd0f8
remove async
RobinMalfait Sep 30, 2025
a62d857
tmp: annotate migrations
RobinMalfait Sep 30, 2025
c82bc30
move `printCandidate` normalization tests to core
RobinMalfait Sep 30, 2025
8c6fe35
expose `canonicalizeCandidates` on the design system
RobinMalfait Sep 26, 2025
5d6ea74
canonicalize candidates at the end of upgrading your project
RobinMalfait Sep 30, 2025
44352e8
move `migrate-bg-gradient` to core
RobinMalfait Sep 30, 2025
afa3216
move `migrate-theme-to-var` to core
RobinMalfait Sep 30, 2025
852e288
cache the converter
RobinMalfait Sep 30, 2025
4c722c0
only prefix variable that are not in `--theme(…)`
RobinMalfait Sep 30, 2025
4bf93bf
move types to core
RobinMalfait Sep 30, 2025
5de55bd
move dimensions to core
RobinMalfait Sep 30, 2025
2c9a2c7
move signatures to core
RobinMalfait Sep 30, 2025
34cb8b8
ensure `canonicalizeCandidates` returns a unique list
RobinMalfait Sep 30, 2025
0c3d48e
move `migrate-arbitrary-utilities` to core
RobinMalfait Sep 30, 2025
f088459
move `migrate-bare-utilities` to core
RobinMalfait Sep 30, 2025
ae5909c
move `migrate-deprecated-utilities` to core
RobinMalfait Sep 30, 2025
e5352d3
move `replaceObject` to core
RobinMalfait Sep 30, 2025
2863c60
move `migrate-arbitrary-variants` to core
RobinMalfait Sep 30, 2025
0eaf373
move `migrate-drop-unnecessary-data-types` to core
RobinMalfait Sep 30, 2025
683ffc5
move `migrate-arbitrary-value-to-bare-value` to core
RobinMalfait Sep 30, 2025
1c02690
move `migrate-optimize-modifier` to core
RobinMalfait Sep 30, 2025
760aef8
remove `!` from test case
RobinMalfait Oct 2, 2025
d5ba933
handle `&` and `*` in selector parser as standalone selector
RobinMalfait Oct 3, 2025
80029cd
move parts of `migrate-modernize-arbitrary-values` to core
RobinMalfait Oct 3, 2025
02ba94f
ensure we canonicalize at the end
RobinMalfait Oct 3, 2025
c956b08
big refactor
RobinMalfait Oct 3, 2025
c281426
drop unnecessary calls
RobinMalfait Oct 4, 2025
368eb80
only stringify the value when something changed
RobinMalfait Oct 4, 2025
55cbbaf
remove migration annotations
RobinMalfait Oct 4, 2025
e7ce1ad
move `selector-parser` from `./src/compat` to just `./src`
RobinMalfait Oct 5, 2025
705476c
add attribute selector parser
RobinMalfait Oct 5, 2025
226ae47
use dedicated `AttributeSelectorParser`
RobinMalfait Oct 5, 2025
4441c44
remove intellisense features from `@tailwindcss/browser`
RobinMalfait Oct 6, 2025
5b7fa87
immediately return replacement candidates
RobinMalfait Oct 6, 2025
93b97b1
remove `@property` when computing a signature
RobinMalfait Oct 6, 2025
ce08cba
add note when parsing the attribute selector as a whole
RobinMalfait Oct 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
move migrate-bare-utilities to core
  • Loading branch information
RobinMalfait committed Oct 6, 2025
commit f0884592296e401254b62a212762bf3183537b78

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { isSafeMigration } from './is-safe-migration'
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 { migrateCamelcaseInNamedValue } from './migrate-camelcase-in-named-value'
import { migrateCanonicalizeCandidate } from './migrate-canonicalize-candidate'
import { migrateDeprecatedUtilities } from './migrate-deprecated-utilities'
Expand Down Expand Up @@ -42,7 +41,6 @@ export const DEFAULT_MIGRATIONS: Migration[] = [
migrateVariantOrder, // sync, v3 → v4, Has to happen before migrations that modify variants
migrateAutomaticVarInjection, // sync, v3 → v4
migrateLegacyArbitraryValues, // sync, v3 → v4 (could also consider it a v4 optimization)
migrateBareValueUtilities, // sync, v4
migrateDeprecatedUtilities, // sync, v4 (deprecation map, order-none → order-0)
migrateModernizeArbitraryValues, // sync, v3 and v4 optimizations, split up?
migrateArbitraryVariants, // sync, v4
Expand Down
26 changes: 26 additions & 0 deletions packages/tailwindcss/src/canonicalize-candidates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,32 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
})

describe('bare values', () => {
let input = css`
@import 'tailwindcss';
@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, expected) => {
await expectCanonicalization(input, candidate, expected)
})
})
})

describe('theme to var', () => {
Expand Down
117 changes: 116 additions & 1 deletion packages/tailwindcss/src/canonicalize-candidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ const canonicalizeCandidateCache = new DefaultMap((ds: DesignSystem) => {
})
})

const CANONICALIZATIONS = [bgGradientToLinear, themeToVar, arbitraryUtilities, print]
const CANONICALIZATIONS = [
bgGradientToLinear,
themeToVar,
arbitraryUtilities,
bareValueUtilities,
print,
]

function print(designSystem: DesignSystem, rawCandidate: string): string {
for (let candidate of designSystem.parseCandidate(rawCandidate)) {
Expand Down Expand Up @@ -696,3 +702,112 @@ function allVariablesAreUsed(

return isSafeMigration
}

// ----

function bareValueUtilities(designSystem: DesignSystem, 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
}
}
}
}