Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
815a83c
add `Writable` types helper
RobinMalfait Apr 30, 2025
40fa9d7
add `memcpy` util
RobinMalfait Apr 30, 2025
551bbed
add signatures
RobinMalfait Apr 30, 2025
1d32882
add optimize modifier migration
RobinMalfait Apr 30, 2025
7838f27
add drop unnecessary data types migration
RobinMalfait Apr 30, 2025
48d449d
add arbitrary variants migration
RobinMalfait Apr 30, 2025
e064185
add arbitrary utilities migration
RobinMalfait Apr 30, 2025
b6a2a55
export candidate types
RobinMalfait Apr 30, 2025
56303f7
use new migrations
RobinMalfait Apr 30, 2025
24de47d
use memcpy
RobinMalfait Apr 30, 2025
7f23841
use migrate arbitrary value to bare value using signatures
RobinMalfait Apr 30, 2025
a2585e9
improve printing of candidates
RobinMalfait Apr 30, 2025
52de5ed
update apply migration test
RobinMalfait Apr 30, 2025
271f803
move test
RobinMalfait Apr 30, 2025
3705e1a
add walk variants util
RobinMalfait Apr 30, 2025
49da4f0
remove hardcoded list of variants
RobinMalfait Apr 30, 2025
1f69c0f
ensure incomputable signatures result in unique value
RobinMalfait Apr 30, 2025
5af9fa5
`--tw-sort` is the property, not the value
RobinMalfait Apr 30, 2025
fe5d717
prevent infinitely parsing the same value
RobinMalfait Apr 30, 2025
457e9b5
improve signature generation for variants
RobinMalfait Apr 30, 2025
100f524
update integration tests
RobinMalfait Apr 30, 2025
0f4c724
add tests to migrate to more specific utilities
RobinMalfait Apr 30, 2025
6aeecd6
try to migrate both arbitrary properties and arbitrary values
RobinMalfait Apr 30, 2025
fa15d91
handle negative in arbitrary scale
RobinMalfait Apr 30, 2025
f88fec3
update changelog
RobinMalfait Apr 30, 2025
892a417
prefer bare values over arbitrary values
RobinMalfait Apr 30, 2025
ba360ab
convert arbitrary rem value to bare value
RobinMalfait May 1, 2025
ccd8053
abstract parsing dimensions
RobinMalfait May 1, 2025
5d8b626
Update packages/@tailwindcss-upgrade/src/utils/dimension.ts
RobinMalfait May 1, 2025
34565f0
rename variables / comments
RobinMalfait May 1, 2025
42b621b
use existing `getClassList`
RobinMalfait May 1, 2025
1e9ce41
move printing candidate to core
RobinMalfait May 1, 2025
013f0b5
Update CHANGELOG.md
RobinMalfait May 2, 2025
5573c88
use try/finally for extra safety
RobinMalfait May 2, 2025
d5ac5cd
update changelog
RobinMalfait May 2, 2025
f2653ac
drop level of nesting
RobinMalfait May 2, 2025
cd92ff1
use `printModifier` helper
RobinMalfait May 2, 2025
987b9e7
add basic tests for _all_ migrations
RobinMalfait May 2, 2025
fbabe79
rename `memcpy` to `replaceObject`
RobinMalfait May 2, 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
add arbitrary utilities migration
  • Loading branch information
RobinMalfait committed Apr 30, 2025
commit e064185e68d03fb5c8fa70ac6dc52d4145784cf9
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
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 { migrateArbitraryUtilities } from './migrate-arbitrary-utilities'
import { migrateArbitraryValueToBareValue } from './migrate-arbitrary-value-to-bare-value'
import { migrateOptimizeModifier } from './migrate-optimize-modifier'

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 [
migrateArbitraryUtilities,
migrateArbitraryValueToBareValue,
migrateOptimizeModifier,
]) {
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;
--color-red-500: red;

/* Equivalent of blue-500/50 */
--color-primary: color-mix(in oklab, oklch(62.3% 0.214 259.815) 50%, transparent);
}
`

test.each([
// Arbitrary property to static utility
['[text-wrap:balance]', 'text-balance'],

// Arbitrary property to static utility with slight differences in
// whitespace. This will require some canonicalization.
['[display:_flex_]', 'flex'],
['[display:_flex]', 'flex'],
['[display:flex_]', 'flex'],

// Arbitrary property to named functional utility
['[color:var(--color-red-500)]', 'text-red-500'],
['[background-color:var(--color-red-500)]', 'bg-red-500'],

// Arbitrary property with modifier to named functional utility with modifier
['[color:var(--color-red-500)]/25', 'text-red-500/25'],

// Arbitrary property with arbitrary modifier to named functional utility with
// arbitrary modifier
['[color:var(--color-red-500)]/[25%]', 'text-red-500/25'],
['[color:var(--color-red-500)]/[100%]', 'text-red-500'],
['[color:var(--color-red-500)]/100', 'text-red-500'],
// No need for `/50` because that's already encoded in the `--color-primary`
// value
['[color:oklch(62.3%_0.214_259.815)]/50', 'text-primary'],

// Arbitrary property to arbitrary value
['[max-height:20px]', 'max-h-[20px]'],

// Arbitrary property to bare value
['[grid-column:2]', 'col-2'],
['[grid-column:1234]', 'col-1234'],

// Arbitrary value to bare value
['border-[2px]', 'border-2'],
['border-[1234px]', 'border-1234'],

// Complex arbitrary property to arbitrary value
[
'[grid-template-columns:repeat(2,minmax(100px,1fr))]',
'grid-cols-[repeat(2,minmax(100px,1fr))]',
],
// Complex arbitrary property to bare value
['[grid-template-columns:repeat(2,minmax(0,1fr))]', 'grid-cols-2'],

// Arbitrary value to bare value with percentage
['from-[25%]', 'from-25%'],

// Arbitrary percentage value must be a whole number. Should not migrate to
// a bare value.
['from-[2.5%]', 'from-[2.5%]'],
])(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)
})
})

const css = String.raw
test('migrate with custom static utility `@utility custom {…}`', async () => {
let candidate = '[--key:value]'
let result = 'custom'

let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
}
@utility custom {
--key: value;
}
`
let designSystem = await __unstable__loadDesignSystem(input, {
base: __dirname,
})

let migrated = migrate(designSystem, {}, candidate)
expect(migrated).toEqual(result)
})

test('migrate with custom functional utility `@utility custom-* {…}`', async () => {
let candidate = '[--key:value]'
let result = 'custom-value'

let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
}
@utility custom-* {
--key: --value('value');
}
`
let designSystem = await __unstable__loadDesignSystem(input, {
base: __dirname,
})

let migrated = migrate(designSystem, {}, candidate)
expect(migrated).toEqual(result)
})

test('migrate with custom functional utility `@utility custom-* {…}` that supports bare values', async () => {
let candidate = '[tab-size:4]'
let result = 'tab-4'

let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
}
@utility tab-* {
tab-size: --value(integer);
}
`
let designSystem = await __unstable__loadDesignSystem(input, {
base: __dirname,
})

let migrated = migrate(designSystem, {}, candidate)
expect(migrated).toEqual(result)
})

test.each([
['[tab-size:0]', 'tab-0'],
['[tab-size:4]', 'tab-4'],
['[tab-size:8]', 'tab-github'],
['tab-[0]', 'tab-0'],
['tab-[4]', 'tab-4'],
['tab-[8]', 'tab-github'],
])(
'migrate custom @utility from arbitrary values to bare values and named values (based on theme)',
async (candidate, expected) => {
let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
--tab-size-github: 8;
}

@utility tab-* {
tab-size: --value(--tab-size, integer, [integer]);
}
`
let designSystem = await __unstable__loadDesignSystem(input, {
base: __dirname,
})

let migrated = migrate(designSystem, {}, candidate)
expect(migrated).toEqual(expected)
},
)

describe.each([['@theme'], ['@theme inline']])('%s', (theme) => {
test.each([
['[color:CanvasText]', 'text-canvas'],
['text-[CanvasText]', 'text-canvas'],
])('migrate arbitrary value to theme value %s => %s', async (candidate, result) => {
let input = css`
@import 'tailwindcss';
${theme} {
--*: initial;
--color-canvas: CanvasText;
}
`
let designSystem = await __unstable__loadDesignSystem(input, {
base: __dirname,
})

let migrated = migrate(designSystem, {}, candidate)
expect(migrated).toEqual(result)
})

// Some utilities read from specific namespaces, in this case we do not want
// to migrate to a value in that namespace if we reference a variable that
// results in the same value, but comes from a different namespace.
//
// E.g.: `max-w` reads from: ['--max-width', '--spacing', '--container']
test.each([
// `max-w` does not read from `--breakpoint-md`, but `--breakpoint-md` and
// `--container-3xl` happen to result in the same value. The difference is
// the semantics of the value.
['max-w-(--breakpoint-md)', 'max-w-(--breakpoint-md)'],
['max-w-(--container-3xl)', 'max-w-3xl'],
])('migrate arbitrary value to theme value %s => %s', async (candidate, result) => {
let input = css`
@import 'tailwindcss';
${theme} {
--*: initial;
--breakpoint-md: 48rem;
--container-3xl: 48rem;
}
`
let designSystem = await __unstable__loadDesignSystem(input, {
base: __dirname,
})

let migrated = migrate(designSystem, {}, candidate)
expect(migrated).toEqual(result)
})
})

test('migrate a arbitrary property without spaces, to a theme value with spaces (canonicalization)', async () => {
let candidate = 'font-[foo,bar,baz]'
let expected = 'font-example'
let input = css`
@import 'tailwindcss';
@theme {
--*: initial;
--font-example: foo, bar, baz;
}
`
let designSystem = await __unstable__loadDesignSystem(input, {
base: __dirname,
})

let migrated = migrate(designSystem, {}, candidate)
expect(migrated).toEqual(expected)
})
Loading