tailwindcss/packages/@tailwindcss-upgrade/src/codemods/template/migrate.test.ts
Robin Malfait 5131237d67
Fix migrating mt-[0px] to -mt-[0px] instead of the other way around (#18154)
I was testing to upgrade tool on various random projects just to see how
it behaves. Then I noticed an odd migration...

This PR fixes an issue where the upgrade tool accidentally migrated
classes such as `mt-[0px]` to `-mt-[0px]`. The reason for this is
because we are trying to find a replacement, and the computed signature
for both of them are exactly the same.

- `mt-[0px]` translates to:

   ```css
   .x {
     margin-top: 0px;
   }
   ```

- `-mt-[0px]` translates to:

   ```css
   .x {
     margin-top: calc(0px * -1);
   }
   ```

   Which in turn translates to

   ```css
   .x {
     margin-top: 0px;
   }
   ```

   Notice that this is `0px`, not `-0px`.

Internally we use the roots of functional utilities to find
replacements. For intellisense purposes we typically show negative
versions before positive versions. This then means that we will try
`-mt-*` before `mt-*`. Because of the signature above, the `mt-[0px]`
was translated into `-mt-[0px]`.

We could solve this in a few ways. The first thing we can try is to make
sure that the signature is not the same and that `-mt-[0px]` actually
translates into `-0px` not `0px`.

This would solve our problem of the accidental migration. However, if we
_just_ sort the functional utilities roots such that the positive
versions exist before negative version and rely on the fact that
`-mt-[0px]` has the same signature. Then it also means that by doing
that we can migrate `-mt-[0px]` into `mt-[0px]` which is even better
because it's the same result and shorter.

## Test plan

1. Added a test to verify that `mt-[0px]` does not get migrated to
`-mt-[0px]`.
2. Added a test to verify that `-mt-[0px]` does get migrated to
`mt-[0px]`.
2025-05-26 16:29:22 +02:00

119 lines
4.2 KiB
TypeScript

import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { describe, expect, test, vi } from 'vitest'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import * as versions from '../../utils/version'
import { migrateCandidate as migrate } from './migrate'
vi.spyOn(versions, 'isMajor').mockReturnValue(false)
const designSystems = new DefaultMap((base: string) => {
return new DefaultMap((input: string) => {
return __unstable__loadDesignSystem(input, { base })
})
})
const css = String.raw
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 named functional utlity
['[color:red]', 'text-red-500'],
// Promote data types to more specific utility if it exists
['bg-(position:--my-value)', 'bg-position-(--my-value)'],
// Promote inferred data type to more specific utility if it exists
['bg-[123px]', 'bg-position-[123px]'],
// Do not migrate bare values or arbitrary values to named values that are
// deprecated
['order-[0]', 'order-0'],
['order-0', 'order-0'],
// Migrate deprecated named values to bare values
['order-none', 'order-0'],
// `-0` should not be migrated to `0`.
//
// This used to be a bug that translate `mt-[0px]` into `-mt-[0px]` because
// `-mt-[0px]` translates to `margin-top: calc(0px * -1);` and therefore we
// handle the `0px * -1` case which translates to `0px` not `-0px`.
//
// This translation is actually fine, because now, we will prefer the
// non-negative version first so we can replace `-mt-[0px]` with `mt-[0px]`.
['mt-[0px]', 'mt-[0px]'],
['-mt-[0px]', 'mt-[0px]'],
])(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 = await migrate(designSystem, {}, candidate)
expect(migrated).toEqual(result)
})
test.each([
['order-[0]', 'order-0'],
['order-0', 'order-0'],
// Do not migrate `order-none` if defined as a custom utility as it is then
// not safe to migrate to `order-0`
['order-none', 'order-none'],
])(`${testName} with custom implementations`, 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 localInput = css`
${input}
@utility order-none {
order: none; /* imagine this exists */
}
`
let designSystem = await designSystems.get(__dirname).get(localInput)
let migrated = await migrate(designSystem, {}, candidate)
expect(migrated).toEqual(result)
})
})