Skip to content

Commit a1ca4bc

Browse files
committed
use migrate arbitrary value to bare value using signatures
This will now make use of the signatures system so that we can verify that a migration is 100% successful.
1 parent 8ebf1ee commit a1ca4bc

2 files changed

Lines changed: 96 additions & 105 deletions

File tree

packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-value-to-bare-value.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ test.each([
6060
'data-[selected]:aria-[selected="true"]:aspect-[12/34]',
6161
'data-selected:aria-selected:aspect-12/34',
6262
],
63-
])('%s => %s', async (candidate, result) => {
63+
])('%s => %s (%#)', async (candidate, result) => {
6464
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
6565
base: __dirname,
6666
})
Lines changed: 95 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,45 @@
1-
import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
1+
import {
2+
parseCandidate,
3+
type Candidate,
4+
type NamedUtilityValue,
5+
type Variant,
6+
} from '../../../../tailwindcss/src/candidate'
27
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
38
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
4-
import { isPositiveInteger } from '../../../../tailwindcss/src/utils/infer-data-type'
9+
import {
10+
isPositiveInteger,
11+
isValidSpacingMultiplier,
12+
} from '../../../../tailwindcss/src/utils/infer-data-type'
513
import { segment } from '../../../../tailwindcss/src/utils/segment'
614
import { printCandidate } from './candidates'
15+
import { computeUtilitySignature } from './signatures'
716

817
export function migrateArbitraryValueToBareValue(
918
designSystem: DesignSystem,
1019
_userConfig: Config | null,
1120
rawCandidate: string,
1221
): string {
22+
let signatures = computeUtilitySignature.get(designSystem)
23+
1324
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
1425
let clone = structuredClone(candidate)
1526
let changed = false
1627

17-
// Convert [subgrid] to subgrid
18-
if (
19-
clone.kind === 'functional' &&
20-
clone.value?.kind === 'arbitrary' &&
21-
clone.value.value === 'subgrid' &&
22-
(clone.root === 'grid-cols' || clone.root == 'grid-rows')
23-
) {
24-
changed = true
25-
clone.value = {
26-
kind: 'named',
27-
value: 'subgrid',
28-
fraction: null,
29-
}
30-
}
31-
32-
// Convert utilities that accept bare values ending in %
33-
if (
34-
clone.kind === 'functional' &&
35-
clone.value?.kind === 'arbitrary' &&
36-
clone.value.dataType === null &&
37-
(clone.root === 'from' ||
38-
clone.root === 'via' ||
39-
clone.root === 'to' ||
40-
clone.root === 'font-stretch')
41-
) {
42-
if (clone.value.value.endsWith('%') && isPositiveInteger(clone.value.value.slice(0, -1))) {
43-
let percentage = parseInt(clone.value.value)
44-
if (
45-
clone.root === 'from' ||
46-
clone.root === 'via' ||
47-
clone.root === 'to' ||
48-
(clone.root === 'font-stretch' && percentage >= 50 && percentage <= 200)
49-
) {
50-
changed = true
51-
clone.value = {
52-
kind: 'named',
53-
value: clone.value.value,
54-
fraction: null,
28+
// Migrate arbitrary values to bare values
29+
if (clone.kind === 'functional' && clone.value?.kind === 'arbitrary') {
30+
let expectedSignature = signatures.get(rawCandidate)
31+
if (expectedSignature !== null) {
32+
for (let value of tryValueReplacements(clone)) {
33+
let newSignature = signatures.get(printCandidate(designSystem, { ...clone, value }))
34+
if (newSignature === expectedSignature) {
35+
changed = true
36+
clone.value = value
37+
break
5538
}
5639
}
5740
}
5841
}
5942

60-
// Convert arbitrary values with positive integers to bare values
61-
// Convert arbitrary values with fractions to bare values
62-
else if (
63-
clone.kind === 'functional' &&
64-
clone.value?.kind === 'arbitrary' &&
65-
clone.value.dataType === null
66-
) {
67-
if (clone.root === 'leading') {
68-
// leading-[1] -> leading-none
69-
if (clone.value.value === '1') {
70-
changed = true
71-
clone.value = {
72-
kind: 'named',
73-
value: 'none',
74-
fraction: null,
75-
}
76-
}
77-
78-
// Keep leading-[<number>] as leading-[<number>]
79-
else {
80-
continue
81-
}
82-
}
83-
84-
let parts = segment(clone.value.value, '/')
85-
if (parts.every((part) => isPositiveInteger(part))) {
86-
changed = true
87-
88-
let currentValue = clone.value
89-
let currentModifier = clone.modifier
90-
91-
// E.g.: `col-start-[12]`
92-
// ^^
93-
if (parts.length === 1) {
94-
clone.value = {
95-
kind: 'named',
96-
value: clone.value.value,
97-
fraction: null,
98-
}
99-
}
100-
101-
// E.g.: `aspect-[12/34]`
102-
// ^^ ^^
103-
else {
104-
clone.value = {
105-
kind: 'named',
106-
value: parts[0],
107-
fraction: clone.value.value,
108-
}
109-
clone.modifier = {
110-
kind: 'named',
111-
value: parts[1],
112-
}
113-
}
114-
115-
// Double check that the new value compiles correctly
116-
if (designSystem.compileAstNodes(clone).length === 0) {
117-
clone.value = currentValue
118-
clone.modifier = currentModifier
119-
changed = false
120-
}
121-
}
122-
}
123-
12443
for (let variant of variants(clone)) {
12544
// Convert `data-[selected]` to `data-selected`
12645
if (
@@ -201,3 +120,75 @@ function* variants(candidate: Candidate) {
201120
yield* inner(variant)
202121
}
203122
}
123+
124+
// Convert functional utilities with arbitrary values to bare values if we can.
125+
// We know that bare values can only be:
126+
//
127+
// 1. A number (with increments of .25)
128+
// 2. A percentage (with increments of .25 followed by a `%`)
129+
// 3. A ratio with whole numbers
130+
//
131+
// Not a bare value per se, but if we are dealing with a keyword, that could
132+
// potentially also look like a bare value (aka no `[` or `]`). E.g.:
133+
// ```diff
134+
// grid-cols-[subgrid]
135+
// grid-cols-subgrid
136+
// ```
137+
function* tryValueReplacements(
138+
candidate: Extract<Candidate, { kind: 'functional' }>,
139+
value: string = candidate.value?.value ?? '',
140+
seen: Set<string> = new Set(),
141+
): Generator<NamedUtilityValue> {
142+
if (seen.has(value)) return
143+
seen.add(value)
144+
145+
// 0. Just try to drop the square brackets and see if it works
146+
// 1. A number (with increments of .25)
147+
yield {
148+
kind: 'named',
149+
value,
150+
fraction: null,
151+
}
152+
153+
// 2. A percentage (with increments of .25 followed by a `%`)
154+
// Try to drop the `%` and see if it works
155+
if (value.endsWith('%') && isValidSpacingMultiplier(value.slice(0, -1))) {
156+
yield {
157+
kind: 'named',
158+
value: value.slice(0, -1),
159+
fraction: null,
160+
}
161+
}
162+
163+
// 3. A ratio with whole numbers
164+
if (value.includes('/')) {
165+
let [numerator, denominator] = value.split('/')
166+
if (isPositiveInteger(numerator) && isPositiveInteger(denominator)) {
167+
yield {
168+
kind: 'named',
169+
value: numerator,
170+
fraction: `${numerator}/${denominator}`,
171+
}
172+
}
173+
}
174+
175+
// It could also be that we have `20px`, we can try just `20` and see if it
176+
// results in the same signature.
177+
let allNumbersAndFractions = new Set<string>()
178+
179+
// Figure out all numbers and fractions in the value
180+
for (let match of value.matchAll(/(\d+\/\d+)|(\d+\.?\d+)/g)) {
181+
allNumbersAndFractions.add(match[0].trim())
182+
}
183+
184+
// Sort the numbers and fractions where the smallest length comes first. This
185+
// will result in the smallest replacement.
186+
let options = Array.from(allNumbersAndFractions).sort((a, z) => {
187+
return a.length - z.length
188+
})
189+
190+
// Try all the options
191+
for (let option of options) {
192+
yield* tryValueReplacements(candidate, option, seen)
193+
}
194+
}

0 commit comments

Comments
 (0)