Robin Malfait 4035ab0b76
Implement @variant (#15663)
This PR replaces `@variant` with `@custom-variant` for registering
custom variants via your CSS.

In addition, this PR introduces `@variant` that can be used in your CSS
to use a variant while writing custom CSS.

E.g.:

```css
.btn {
  background: white;

  @variant dark {
    background: black;
  }
}
```

Compiles to:

```css
.btn {
  background: white;
}

@media (prefers-color-scheme: dark) {
  .btn {
    background: black;
  }
}
```

For backwards compatibility, the `@variant` rules that don't have a body
and are
defined inline:

```css
@variant hocus (&:hover, &:focus);
```

And `@variant` rules that are defined with a body and a `@slot`:

```css
@variant hocus {
  &:hover, &:focus {
    @slot;
  }
}
```

Will automatically be upgraded to `@custom-variant` internally, so no
breaking changes are introduced with this PR.

---

TODO:
- [x] ~~Decide whether we want to allow multiple variants and if so,
what syntax should be used. If not, nesting `@variant <variant> {}` will
be the way to go.~~ Only a single `@variant <variant>` can be used, if
you want to use multiple, nesting should be used:

```css
.foo {
  @variant hover {
    @variant focus {
      color: red;
    }
  }
}
```
2025-01-21 10:20:35 -05:00

324 lines
6.6 KiB
TypeScript

import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import dedent from 'dedent'
import postcss from 'postcss'
import { expect, it } from 'vitest'
import { formatNodes } from './format-nodes'
import { migratePreflight } from './migrate-preflight'
import { sortBuckets } from './sort-buckets'
const css = dedent
async function migrate(input: string) {
let designSystem = await __unstable__loadDesignSystem(
css`
@import 'tailwindcss';
`,
{ base: __dirname },
)
return postcss()
.use(migratePreflight({ designSystem }))
.use(sortBuckets())
.use(formatNodes())
.process(input, { from: expect.getState().testPath })
.then((result) => result.css)
}
it("should add compatibility CSS after the `@import 'tailwindcss'`", async () => {
expect(
await migrate(css`
@import 'tailwindcss';
`),
).toMatchInlineSnapshot(`
"@import 'tailwindcss';
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}"
`)
})
it('should add the compatibility CSS after the last `@import`', async () => {
expect(
await migrate(css`
@import 'tailwindcss';
@import './foo.css';
@import './bar.css';
`),
).toMatchInlineSnapshot(`
"@import 'tailwindcss';
@import './foo.css';
@import './bar.css';
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}"
`)
})
it('should add the compatibility CSS after the last import, even if a body-less `@layer` exists', async () => {
expect(
await migrate(css`
@charset "UTF-8";
@layer foo, bar, baz, base;
/**!
* License header
*/
@import 'tailwindcss';
@import './foo.css';
@import './bar.css';
`),
).toMatchInlineSnapshot(`
"@charset "UTF-8";
@layer foo, bar, baz, base;
/**!
* License header
*/
@import 'tailwindcss';
@import './foo.css';
@import './bar.css';
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}"
`)
})
it('should add the compatibility CSS before the first `@layer base` (if the "tailwindcss" import exists)', async () => {
expect(
await migrate(css`
@import 'tailwindcss';
@custom-variant foo {
}
@utility bar {
}
@layer base {
}
@utility baz {
}
@layer base {
}
`),
).toMatchInlineSnapshot(`
"@import 'tailwindcss';
@custom-variant foo {
}
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@utility bar {
}
@utility baz {
}
@layer base {
}
@layer base {
}"
`)
})
it('should add the compatibility CSS before the first `@layer base` (if the "tailwindcss/preflight" import exists)', async () => {
expect(
await migrate(css`
@import 'tailwindcss/preflight';
@custom-variant foo {
}
@utility bar {
}
@layer base {
}
@utility baz {
}
@layer base {
}
`),
).toMatchInlineSnapshot(`
"@import 'tailwindcss/preflight';
@custom-variant foo {
}
/*
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@utility bar {
}
@utility baz {
}
@layer base {
}
@layer base {
}"
`)
})
it('should not add the backwards compatibility CSS when no `@import "tailwindcss"` or `@import "tailwindcss/preflight"` exists', async () => {
expect(
await migrate(css`
@custom-variant foo {
}
@utility bar {
}
@layer base {
}
@utility baz {
}
@layer base {
}
`),
).toMatchInlineSnapshot(`
"@custom-variant foo {
}
@utility bar {
}
@utility baz {
}
@layer base {
}
@layer base {
}"
`)
})
it('should not add the backwards compatibility CSS when another `@import "tailwindcss"` import exists such as theme or utilities', async () => {
expect(
await migrate(css`
@import 'tailwindcss/theme';
@custom-variant foo {
}
@utility bar {
}
@layer base {
}
@utility baz {
}
@layer base {
}
`),
).toMatchInlineSnapshot(`
"@import 'tailwindcss/theme';
@custom-variant foo {
}
@utility bar {
}
@utility baz {
}
@layer base {
}
@layer base {
}"
`)
})