diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 4d53d80c1..810232e55 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -93,30 +93,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', } async function expectCanonicalization( - input: string, - candidate: string, - expected: string, - options: CanonicalizeOptions = DEFAULT_CANONICALIZATION_OPTIONS, - ) { - candidate = prepare(candidate) - expected = prepare(expected) - - if (strategy === 'prefix') { - input = input.replace("@import 'tailwindcss';", "@import 'tailwindcss' prefix(tw);") - } - - let designSystem = await designSystems.get(__dirname).get(input) - let [actual] = designSystem.canonicalizeCandidates([candidate], options) - - try { - expect(actual).toBe(expected) - } catch (err) { - if (err instanceof Error) Error.captureStackTrace(err, expectCanonicalization) - throw err - } - } - - async function expectCombinedCanonicalization( input: string, candidates: string, expected: string, @@ -135,179 +111,190 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', try { expect(actual).toEqual(preparedExpected) } catch (err) { - if (err instanceof Error) Error.captureStackTrace(err, expectCombinedCanonicalization) + if (err instanceof Error) Error.captureStackTrace(err, expectCanonicalization) throw err } } /// ---------------------------------- - test.each([ - /// Legacy bg-gradient-* → bg-linear-* - ['bg-gradient-to-t', 'bg-linear-to-t'], - ['bg-gradient-to-tr', 'bg-linear-to-tr'], - ['bg-gradient-to-r', 'bg-linear-to-r'], - ['bg-gradient-to-br', 'bg-linear-to-br'], - ['bg-gradient-to-b', 'bg-linear-to-b'], - ['bg-gradient-to-bl', 'bg-linear-to-bl'], - ['bg-gradient-to-l', 'bg-linear-to-l'], - ['bg-gradient-to-tl', 'bg-linear-to-tl'], - - /// theme(…) to `var(…)` - // Keep candidates that don't contain `theme(…)` or `theme(…, …)` - ['[color:red]', 'text-[red]'], - - // Handle special cases around `.1` in the `theme(…)` - ['[--value:theme(spacing.1)]', '[--value:--spacing(1)]'], - ['[--value:theme(fontSize.xs.1.lineHeight)]', '[--value:var(--text-xs--line-height)]'], - ['[--value:theme(spacing[1.25])]', '[--value:--spacing(1.25)]'], - - // Should not convert invalid spacing values to calc - ['[--value:theme(spacing[1.1])]', '[--value:theme(spacing[1.1])]'], - - // Convert to `var(…)` if we can resolve the path - ['[color:theme(colors.red.500)]', 'text-red-500'], // Arbitrary property - ['[color:theme(colors.red.500)]/50', 'text-red-500/50'], // Arbitrary property + modifier - ['bg-[theme(colors.red.500)]', 'bg-red-500'], // Arbitrary value - ['bg-[size:theme(spacing.4)]', 'bg-size-[--spacing(4)]'], // Arbitrary value + data type hint - - // Pretty print CSS functions preceded by an operator to prevent consecutive - // operator characters. - ['w-[calc(100dvh-theme(spacing.2))]', 'w-[calc(100dvh-(--spacing(2)))]'], - ['w-[calc(100dvh+theme(spacing.2))]', 'w-[calc(100dvh+(--spacing(2)))]'], - ['w-[calc(100dvh/theme(spacing.2))]', 'w-[calc(100dvh/(--spacing(2)))]'], - ['w-[calc(100dvh*theme(spacing.2))]', 'w-[calc(100dvh*(--spacing(2)))]'], - - // Convert to `var(…)` if we can resolve the path, but keep fallback values - ['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'], - - // Keep `theme(…)` if we can't resolve the path - ['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'], - - // Keep `theme(…)` if we can't resolve the path, but still try to convert the - // fallback value. - ['bg-[theme(colors.foo.1000,theme(colors.red.500))]', 'bg-red-500'], - - // Use `theme(…)` (deeply nested) inside of a `calc(…)` function - ['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--text-xs)*2)]'], - - // Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)` - // - Can't convert to `var(…)` because that would lose the modifier. - // - Can't convert to a candidate modifier because there are multiple - // `theme(…)` calls. - // - // If we really want to, we can make a fancy migration that tries to move it - // to a candidate modifier _if_ all `theme(…)` calls use the same modifier. - [ - '[color:theme(colors.red.500/50,theme(colors.blue.500/50))]', - 'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]', - ], - [ - '[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50', - 'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]/50', - ], - - // Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`), - // to a candidate modifier. - // Arbitrary property, with simple percentage modifier - ['[color:theme(colors.red.500/75%)]', 'text-red-500/75'], - - // Arbitrary property, with numbers (0-1) without a unit - ['[color:theme(colors.red.500/.12)]', 'text-red-500/12'], - ['[color:theme(colors.red.500/0.12)]', 'text-red-500/12'], - - // Arbitrary property, with more complex modifier (we only allow whole numbers - // as bare modifiers). Convert the complex numbers to arbitrary values instead. - ['[color:theme(colors.red.500/12.34%)]', 'text-red-500/[12.34%]'], - ['[color:theme(colors.red.500/var(--opacity))]', 'text-red-500/(--opacity)'], - ['[color:theme(colors.red.500/.12345)]', 'text-red-500/1234.5'], - ['[color:theme(colors.red.500/50.25%)]', 'text-red-500/50.25'], - - // Arbitrary value - ['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'], - ['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'], - - // Arbitrary property that already contains a modifier - ['[color:theme(colors.red.500/50%)]/50', 'text-[--theme(--color-red-500/50%)]/50'], - - // Values that don't contain only `theme(…)` calls should not be converted to - // use a modifier since the color is not the whole value. - [ - 'shadow-[shadow:inset_0px_1px_theme(colors.white/15%)]', - 'shadow-[inset_0px_1px_--theme(--color-white/15%)]', - ], - - // Arbitrary value, where the candidate already contains a modifier - // This should still migrate the `theme(…)` syntax to the modern syntax. - ['bg-[theme(colors.red.500/50%)]/50', 'bg-[--theme(--color-red-500/50%)]/50'], - - // Variants, we can't use `var(…)` especially inside of `@media(…)`. We can - // still upgrade the `theme(…)` to the modern syntax. - ['max-[theme(screens.lg)]:flex', 'max-[--theme(--breakpoint-lg)]:flex'], - // There are no variables for `--spacing` multiples, so we can't convert this - ['max-[theme(spacing.4)]:flex', 'max-[theme(spacing.4)]:flex'], - - // This test in itself doesn't make much sense. But we need to make sure - // that this doesn't end up as the modifier in the candidate itself. - ['max-[theme(spacing.4/50)]:flex', 'max-[theme(spacing.4/50)]:flex'], - - // `theme(…)` calls in another CSS function is replaced correctly. - // Additionally we remove unnecessary whitespace. - ['grid-cols-[min(50%_,_theme(spacing.80))_auto]', 'grid-cols-[min(50%,--spacing(80))_auto]'], - - // `calc(var(--spacing)*…)` to `--spacing(…)` - ['pt-[min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'], - ['pt-[min(20%,calc(var(--spacing)*var(--other)))]', 'pt-[min(20%,--spacing(var(--other)))]'], - ['pt-[calc(var(--spacing)*8)]', 'pt-8'], - ['pt-[calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'], - - ['[padding-top:min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'], - [ - '[padding-top:min(20%,calc(var(--spacing)*var(--other)))]', - 'pt-[min(20%,--spacing(var(--other)))]', - ], - ['[padding-top:calc(var(--spacing)*8)]', 'pt-8'], - ['[padding-top:calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'], - - // `theme(…)` calls valid in v3, but not in v4 should still be converted. - ['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'], - - // Renamed theme keys - ['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'], - ['w-[theme(maxWidth.md)]', 'w-md'], - - // Invalid cases - ['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'], - ['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'], - - // Partially invalid cases - [ - '[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]', - '[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]', - ], - [ - '[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50', - '[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50', - ], - - // If a utility sets `property` and `--tw-{property}` with the same value, - // we can ignore the `--tw-{property}`. This is just here for composition. - // This means that we should be able to upgrade the one _without_ to the one - // _with_ the variable - ['[font-weight:400]', 'font-normal'], - ['[line-height:0]', 'leading-0'], - ['[border-style:solid]', 'border-solid'], - ])(testName, { timeout }, async (candidate, expected) => { - await expectCanonicalization( - css` + describe('deprecated utilities', () => { + test.each([ + /// Legacy bg-gradient-* → bg-linear-* + ['bg-gradient-to-t', 'bg-linear-to-t'], + ['bg-gradient-to-tr', 'bg-linear-to-tr'], + ['bg-gradient-to-r', 'bg-linear-to-r'], + ['bg-gradient-to-br', 'bg-linear-to-br'], + ['bg-gradient-to-b', 'bg-linear-to-b'], + ['bg-gradient-to-bl', 'bg-linear-to-bl'], + ['bg-gradient-to-l', 'bg-linear-to-l'], + ['bg-gradient-to-tl', 'bg-linear-to-tl'], + ])(testName, { timeout }, async (candidate, expected) => { + let input = css` @import 'tailwindcss'; - `, - candidate, - expected, - ) + ` + + await expectCanonicalization(input, candidate, expected) + }) + + let deprecated: [string, string][] = [ + ['order-none', 'order-0'], + ['break-words', 'wrap-break-word'], + ['overflow-ellipsis', 'text-ellipsis'], + + ['start-full', 'inset-s-full'], + ['-start-full', '-inset-s-full'], + ['start-auto', 'inset-s-auto'], + ['start-px', 'inset-s-px'], + ['-start-px', '-inset-s-px'], + ['start-8', 'inset-s-8'], // Within default spacing scale + ['-start-8', '-inset-s-8'], // Within default spacing scale + ['start-123', 'inset-s-123'], // Outside of default spacing scale + ['-start-123', '-inset-s-123'], // Outside of default spacing scale + + ['end-full', 'inset-e-full'], + ['-end-full', '-inset-e-full'], + ['end-auto', 'inset-e-auto'], + ['end-px', 'inset-e-px'], + ['-end-px', '-inset-e-px'], + ['end-8', 'inset-e-8'], // Within default spacing scale + ['-end-8', '-inset-e-8'], // Within default spacing scale + ['end-123', 'inset-e-123'], // Outside of default spacing scale + ['-end-123', '-inset-e-123'], // Outside of default spacing scale + ] + + test.each(deprecated)(testName, { timeout }, async (candidate, expected) => { + let input = css` + @import 'tailwindcss'; + ` + + await expectCanonicalization(input, candidate, expected) + }) + + describe('With custom implementation', () => { + // Creating a shared CSS file such that we can re-use the same design + // system for all of these. + let customImplementations = deprecated + .map( + ([candidate]) => css` + @utility ${candidate} { + --custom-${randomUUID()}: implementation; + } + `, + ) + .join('\n') + + // Keep the current utility because of the custom implementation + test.each(deprecated.map(([candidate]) => [candidate, candidate]))( + testName, + { timeout }, + async (candidate, expected) => { + let input = css` + @import 'tailwindcss'; + + ${customImplementations} + ` + + await expectCanonicalization(input, candidate, expected) + }, + ) + }) }) - describe('arbitrary utilities', () => { + describe('arbitrary properties', () => { + test.each([ + /// theme(…) to `var(…)` + // Keep candidates that don't contain `theme(…)` or `theme(…, …)` + ['[color:red]', 'text-[red]'], + + // Handle special cases around `.1` in the `theme(…)` + ['[--value:theme(spacing.1)]', '[--value:--spacing(1)]'], + ['[--value:theme(fontSize.xs.1.lineHeight)]', '[--value:var(--text-xs--line-height)]'], + ['[--value:theme(spacing[1.25])]', '[--value:--spacing(1.25)]'], + + // Should not convert invalid spacing values to calc + ['[--value:theme(spacing[1.1])]', '[--value:theme(spacing[1.1])]'], + + // Convert to `var(…)` if we can resolve the path + ['[color:theme(colors.red.500)]', 'text-red-500'], // Arbitrary property + ['[color:theme(colors.red.500)]/50', 'text-red-500/50'], // Arbitrary property + modifier + + // Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)` + // - Can't convert to `var(…)` because that would lose the modifier. + // - Can't convert to a candidate modifier because there are multiple + // `theme(…)` calls. + // + // If we really want to, we can make a fancy migration that tries to move it + // to a candidate modifier _if_ all `theme(…)` calls use the same modifier. + [ + '[color:theme(colors.red.500/50,theme(colors.blue.500/50))]', + 'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]', + ], + [ + '[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50', + 'text-[--theme(--color-red-500/50,--theme(--color-blue-500/50))]/50', + ], + + // Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`), + // to a candidate modifier. + // Arbitrary property, with simple percentage modifier + ['[color:theme(colors.red.500/75%)]', 'text-red-500/75'], + + // Arbitrary property, with numbers (0-1) without a unit + ['[color:theme(colors.red.500/.12)]', 'text-red-500/12'], + ['[color:theme(colors.red.500/0.12)]', 'text-red-500/12'], + + // Arbitrary property, with more complex modifier (we only allow whole numbers + // as bare modifiers). Convert the complex numbers to arbitrary values instead. + ['[color:theme(colors.red.500/12.34%)]', 'text-red-500/[12.34%]'], + ['[color:theme(colors.red.500/var(--opacity))]', 'text-red-500/(--opacity)'], + ['[color:theme(colors.red.500/.12345)]', 'text-red-500/1234.5'], + ['[color:theme(colors.red.500/50.25%)]', 'text-red-500/50.25'], + + // Arbitrary property that already contains a modifier + ['[color:theme(colors.red.500/50%)]/50', 'text-[--theme(--color-red-500/50%)]/50'], + + // `calc(var(--spacing)*…)` to `--spacing(…)` + ['[padding-top:min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'], + [ + '[padding-top:min(20%,calc(var(--spacing)*var(--other)))]', + 'pt-[min(20%,--spacing(var(--other)))]', + ], + ['[padding-top:calc(var(--spacing)*8)]', 'pt-8'], + ['[padding-top:calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'], + + // `theme(…)` calls valid in v3, but not in v4 should still be converted. + ['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'], + + // Invalid cases + ['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'], + ['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'], + + // Partially invalid cases + [ + '[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]', + '[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]', + ], + [ + '[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50', + '[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50', + ], + + // If a utility sets `property` and `--tw-{property}` with the same value, + // we can ignore the `--tw-{property}`. This is just here for composition. + // This means that we should be able to upgrade the one _without_ to the one + // _with_ the variable + ['[font-weight:400]', 'font-normal'], + ['[line-height:0]', 'leading-0'], + ['[border-style:solid]', 'border-solid'], + ])(testName, { timeout }, async (candidate, expected) => { + let input = css` + @import 'tailwindcss'; + ` + + await expectCanonicalization(input, candidate, expected) + }) + test.each([ // Arbitrary property to static utility ['[text-wrap:balance]', 'text-balance'], @@ -318,10 +305,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['[display:_flex]', 'flex'], ['[display:flex_]', 'flex'], - // Arbitrary property to static utility - // Map number to keyword-like value - ['leading-[1]', 'leading-none'], - // Arbitrary property to named functional utility ['[color:var(--color-red-500)]', 'text-red-500'], ['[background-color:var(--color-red-500)]', 'bg-red-500'], @@ -349,20 +332,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['[grid-column:2]', 'col-2'], ['[grid-column:1234]', 'col-1234'], - // Arbitrary value to bare value - ['border-[2px]', 'border-2'], - ['border-[1234px]', 'border-1234'], - - // Arbitrary value with data type, to more specific arbitrary value - ['bg-[position:123px]', 'bg-position-[123px]'], - ['bg-[size:123px]', 'bg-size-[123px]'], - - // Arbitrary value with inferred data type, to more specific arbitrary value - ['bg-[123px]', 'bg-position-[123px]'], - - // Arbitrary value with spacing mul - ['w-[64rem]', 'w-256'], - // Complex arbitrary property to arbitrary value [ '[grid-template-columns:repeat(2,minmax(100px,1fr))]', @@ -370,13 +339,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ], // 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, { timeout }, async (candidate, expected) => { let input = css` @import 'tailwindcss'; @@ -393,7 +355,107 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', await expectCanonicalization(input, candidate, expected) }) + }) + describe('arbitrary values', () => { + test.each([ + // Convert to `var(…)` if we can resolve the path + ['bg-[theme(colors.red.500)]', 'bg-red-500'], // Arbitrary value + ['bg-[size:theme(spacing.4)]', 'bg-size-[--spacing(4)]'], // Arbitrary value + data type hint + + // Pretty print CSS functions preceded by an operator to prevent consecutive + // operator characters. + ['w-[calc(100dvh-theme(spacing.2))]', 'w-[calc(100dvh-(--spacing(2)))]'], + ['w-[calc(100dvh+theme(spacing.2))]', 'w-[calc(100dvh+(--spacing(2)))]'], + ['w-[calc(100dvh/theme(spacing.2))]', 'w-[calc(100dvh/(--spacing(2)))]'], + ['w-[calc(100dvh*theme(spacing.2))]', 'w-[calc(100dvh*(--spacing(2)))]'], + + // Convert to `var(…)` if we can resolve the path, but keep fallback values + ['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'], + + // Keep `theme(…)` if we can't resolve the path + ['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'], + + // Keep `theme(…)` if we can't resolve the path, but still try to convert the + // fallback value. + ['bg-[theme(colors.foo.1000,theme(colors.red.500))]', 'bg-red-500'], + + // Use `theme(…)` (deeply nested) inside of a `calc(…)` function + ['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--text-xs)*2)]'], + + // Arbitrary value + ['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'], + ['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'], + + // Values that don't contain only `theme(…)` calls should not be converted to + // use a modifier since the color is not the whole value. + [ + 'shadow-[shadow:inset_0px_1px_theme(colors.white/15%)]', + 'shadow-[inset_0px_1px_--theme(--color-white/15%)]', + ], + + // Arbitrary value, where the candidate already contains a modifier + // This should still migrate the `theme(…)` syntax to the modern syntax. + ['bg-[theme(colors.red.500/50%)]/50', 'bg-[--theme(--color-red-500/50%)]/50'], + + // Variants, we can't use `var(…)` especially inside of `@media(…)`. We can + // still upgrade the `theme(…)` to the modern syntax. + ['max-[theme(screens.lg)]:flex', 'max-[--theme(--breakpoint-lg)]:flex'], + // There are no variables for `--spacing` multiples, so we can't convert this + ['max-[theme(spacing.4)]:flex', 'max-[theme(spacing.4)]:flex'], + + // This test in itself doesn't make much sense. But we need to make sure + // that this doesn't end up as the modifier in the candidate itself. + ['max-[theme(spacing.4/50)]:flex', 'max-[theme(spacing.4/50)]:flex'], + + // `theme(…)` calls in another CSS function is replaced correctly. + // Additionally we remove unnecessary whitespace. + ['grid-cols-[min(50%_,_theme(spacing.80))_auto]', 'grid-cols-[min(50%,--spacing(80))_auto]'], + + // `calc(var(--spacing)*…)` to `--spacing(…)` + ['pt-[min(20%,calc(var(--spacing)*8))]', 'pt-[min(20%,--spacing(8))]'], + ['pt-[min(20%,calc(var(--spacing)*var(--other)))]', 'pt-[min(20%,--spacing(var(--other)))]'], + ['pt-[calc(var(--spacing)*8)]', 'pt-8'], + ['pt-[calc(var(--spacing)*var(--other))]', 'pt-[--spacing(var(--other))]'], + + // Renamed theme keys + ['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'], + ['w-[theme(maxWidth.md)]', 'w-md'], + + // Arbitrary property to static utility + // Map number to keyword-like value + ['leading-[1]', 'leading-none'], + + // Arbitrary value to bare value + ['border-[2px]', 'border-2'], + ['border-[1234px]', 'border-1234'], + + // Arbitrary value with data type, to more specific arbitrary value + ['bg-[position:123px]', 'bg-position-[123px]'], + ['bg-[size:123px]', 'bg-size-[123px]'], + + // Arbitrary value with inferred data type, to more specific arbitrary value + ['bg-[123px]', 'bg-position-[123px]'], + + // Arbitrary value with spacing mul + ['w-[64rem]', 'w-256'], + + // 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, { timeout }, async (candidate, expected) => { + let input = css` + @import 'tailwindcss'; + ` + + await expectCanonicalization(input, candidate, expected) + }) + }) + + describe('arbitrary utilities', () => { test('migrate with custom static utility `@utility custom {…}`', { timeout }, async () => { let candidate = '[--key:value]' let expected = 'custom' @@ -611,71 +673,6 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', }) }) - describe('deprecated utilities', () => { - let deprecated: [string, string][] = [ - ['order-none', 'order-0'], - ['break-words', 'wrap-break-word'], - ['overflow-ellipsis', 'text-ellipsis'], - - ['start-full', 'inset-s-full'], - ['-start-full', '-inset-s-full'], - ['start-auto', 'inset-s-auto'], - ['start-px', 'inset-s-px'], - ['-start-px', '-inset-s-px'], - ['start-8', 'inset-s-8'], // Within default spacing scale - ['-start-8', '-inset-s-8'], // Within default spacing scale - ['start-123', 'inset-s-123'], // Outside of default spacing scale - ['-start-123', '-inset-s-123'], // Outside of default spacing scale - - ['end-full', 'inset-e-full'], - ['-end-full', '-inset-e-full'], - ['end-auto', 'inset-e-auto'], - ['end-px', 'inset-e-px'], - ['-end-px', '-inset-e-px'], - ['end-8', 'inset-e-8'], // Within default spacing scale - ['-end-8', '-inset-e-8'], // Within default spacing scale - ['end-123', 'inset-e-123'], // Outside of default spacing scale - ['-end-123', '-inset-e-123'], // Outside of default spacing scale - ] - - test.each(deprecated)(testName, { timeout }, async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - ` - - await expectCanonicalization(input, candidate, expected) - }) - - describe('With custom implementation', () => { - // Creating a shared CSS file such that we can re-use the same design - // system for all of these. - let customImplementations = deprecated - .map( - ([candidate]) => css` - @utility ${candidate} { - --custom-${randomUUID()}: implementation; - } - `, - ) - .join('\n') - - // Keep the current utility because of the custom implementation - test.each(deprecated.map(([candidate]) => [candidate, candidate]))( - testName, - { timeout }, - async (candidate, expected) => { - let input = css` - @import 'tailwindcss'; - - ${customImplementations} - ` - - await expectCanonicalization(input, candidate, expected) - }, - ) - }) - }) - describe('arbitrary variants', () => { let input = css` @import 'tailwindcss'; @@ -1096,7 +1093,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', let input = css` @import 'tailwindcss'; ` - await expectCombinedCanonicalization(input, candidates, expected) + await expectCanonicalization(input, candidates, expected) }) }) @@ -1130,7 +1127,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', let input = css` @import 'tailwindcss'; ` - await expectCombinedCanonicalization(input, candidates.trim(), expected) + await expectCanonicalization(input, candidates.trim(), expected) }) }) @@ -1242,115 +1239,120 @@ describe('options', () => { }) }) -// https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/321 -test('a subset of classes should be canonicalizable', { timeout }, async () => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - `) +describe('regressions', () => { + // https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/321 + { + test('a subset of classes should be canonicalizable', { timeout }, async () => { + let designSystem = await designSystems.get(__dirname).get(css` + @import 'tailwindcss'; + `) - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, + let options: CanonicalizeOptions = { + collapse: true, + logicalToPhysical: true, + rem: 16, + } + + expect( + designSystem.canonicalizeCandidates(['underline', 'h-4', 'w-4', 'text-sm'], options), + ).toEqual(['underline', 'text-sm', 'size-4']) + }) + + test('collapse canonicalization is not affected by previous calls', { timeout }, async () => { + let designSystem = await designSystems.get(__dirname).get(css` + @import 'tailwindcss'; + `) + + let options: CanonicalizeOptions = { + collapse: true, + logicalToPhysical: true, + rem: 16, + } + + let target = ['underline', 'h-4', 'w-4'] + + expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4']) + + designSystem.canonicalizeCandidates(['mb-4', 'text-sm'], options) + designSystem.canonicalizeCandidates(['underline', 'mb-4'], options) + + expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4']) + expect(designSystem.canonicalizeCandidates(target.concat('text-sm'), options)).toEqual([ + 'underline', + 'text-sm', + 'size-4', + ]) + }) } - expect( - designSystem.canonicalizeCandidates(['underline', 'h-4', 'w-4', 'text-sm'], options), - ).toEqual(['underline', 'text-sm', 'size-4']) + // https://github.com/tailwindlabs/tailwindcss/pull/19727 + test( + 'collapse does not crash when utilities with no standard properties are present', + { timeout }, + async () => { + let designSystem = await designSystems.get(__dirname).get(css` + @import 'tailwindcss'; + `) + + let options: CanonicalizeOptions = { + collapse: true, + logicalToPhysical: true, + rem: 16, + } + + // Shadow utilities use CSS custom properties and @property rules but may + // produce empty property maps in the collapse algorithm. This should not + // crash with "Cannot read properties of null" or "X is not iterable". + expect(() => + designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options), + ).not.toThrow() + + expect(() => designSystem.canonicalizeCandidates(['shadow-md', 'p-4'], options)).not.toThrow() + + expect(() => + designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options), + ).not.toThrow() + + // Verify the candidates are returned (not collapsed, since shadows can't + // meaningfully collapse with unrelated utilities) + expect(designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options)).toEqual( + expect.arrayContaining(['shadow-sm', 'border']), + ) + + expect(designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options)).toEqual( + expect.arrayContaining(['shadow-sm', 'shadow-md']), + ) + }, + ) + + // https://github.com/tailwindlabs/tailwindcss/issues/19835 + test.each([ + // Arbitrary values should be collapsed to another arbitrary value + [ + ['px-[1.2rem]', 'py-[1.2rem]', 'text-left'], + ['text-left', 'p-[1.2rem]'], + ], + + // Arbitrary values could also be collapsed into a bare value + [ + ['px-[30.75rem]', 'py-[30.75rem]', 'text-left'], + ['text-left', 'p-123'], + ], + ])( + 'collapse canonicalization works for arbitrary values', + { timeout }, + async (candidates, expected) => { + let designSystem = await designSystems.get(__dirname).get(css` + @import 'tailwindcss'; + `) + + let options: CanonicalizeOptions = { + collapse: true, + logicalToPhysical: true, + rem: 16, + } + + expect(designSystem.canonicalizeCandidates(candidates, options)).toEqual(expected) + }, + ) }) - -test('collapse canonicalization is not affected by previous calls', { timeout }, async () => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - `) - - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, - } - - let target = ['underline', 'h-4', 'w-4'] - - expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4']) - - designSystem.canonicalizeCandidates(['mb-4', 'text-sm'], options) - designSystem.canonicalizeCandidates(['underline', 'mb-4'], options) - - expect(designSystem.canonicalizeCandidates(target, options)).toEqual(['underline', 'size-4']) - expect(designSystem.canonicalizeCandidates(target.concat('text-sm'), options)).toEqual([ - 'underline', - 'text-sm', - 'size-4', - ]) -}) - -test( - 'collapse does not crash when utilities with no standard properties are present', - { timeout }, - async () => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - `) - - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, - } - - // Shadow utilities use CSS custom properties and @property rules but may - // produce empty property maps in the collapse algorithm. This should not - // crash with "Cannot read properties of null" or "X is not iterable". - expect(() => - designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options), - ).not.toThrow() - - expect(() => designSystem.canonicalizeCandidates(['shadow-md', 'p-4'], options)).not.toThrow() - - expect(() => - designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options), - ).not.toThrow() - - // Verify the candidates are returned (not collapsed, since shadows can't - // meaningfully collapse with unrelated utilities) - expect(designSystem.canonicalizeCandidates(['shadow-sm', 'border'], options)).toEqual( - expect.arrayContaining(['shadow-sm', 'border']), - ) - - expect(designSystem.canonicalizeCandidates(['shadow-sm', 'shadow-md'], options)).toEqual( - expect.arrayContaining(['shadow-sm', 'shadow-md']), - ) - }, -) - -// https://github.com/tailwindlabs/tailwindcss/issues/19835 -test.each([ - // Arbitrary values should be collapsed to another arbitrary value - [ - ['px-[1.2rem]', 'py-[1.2rem]', 'text-left'], - ['text-left', 'p-[1.2rem]'], - ], - - // Arbitrary values could also be collapsed into a bare value - [ - ['px-[30.75rem]', 'py-[30.75rem]', 'text-left'], - ['text-left', 'p-123'], - ], -])( - 'collapse canonicalization works for arbitrary values', - { timeout }, - async (candidates, expected) => { - let designSystem = await designSystems.get(__dirname).get(css` - @import 'tailwindcss'; - `) - - let options: CanonicalizeOptions = { - collapse: true, - logicalToPhysical: true, - rem: 16, - } - - expect(designSystem.canonicalizeCandidates(candidates, options)).toEqual(expected) - }, -)