tailwindcss/integrations/cli/upgrade.test.ts
Robin Malfait d14249ddc2
Add CSS codemods for migrating @layer utilities (#14455)
This PR adds CSS codemods for migrating existing `@layer utilities` to
`@utility` directives.

This PR has the ability to migrate the following cases:

---

The most basic case is when you want to migrate a simple class to a
utility directive.

Input:
```css
@layer utilities {
  .foo {
    color: red;
  }

  .bar {
    color: blue;
  }
}
```

Output:
```css
@utility foo {
  color: red;
}

@utility bar {
  color: blue;
}
```

You'll notice that the class `foo` will be used as the utility name, the
declarations (and the rest of the body of the rule) will become the body
of the `@utility` definition.

---

In v3, every class in a selector will become a utility. To correctly
migrate this to `@utility` directives, we have to register each class in
the selector and generate `n` utilities.

We can use nesting syntax, and replace the current class with `&` to
ensure that the final result behaves the same.

Input:
```css
@layer utilities {
  .foo .bar .baz {
    color: red;
  }
}
```

Output:
```css
@utility foo {
  & .bar .baz {
    color: red;
  }
}

@utility bar {
  .foo & .baz {
    color: red;
  }
}

@utility .baz {
  .foo .bar & {
    color: red;
  }
}
```

In this case, it could be that you know that some of them will never be
used as a utility (e.g.: `hover:bar`), but then you can safely remove
them.

---

Even classes inside of `:has(…)` will become a utility. The only
exception to the rule is that we don't do it for `:not(…)`.

Input:
```css
@layer utilities {
  .foo .bar:not(.qux):has(.baz) {
    display: none;
  }
}
```

Output:
```css
@utility foo {
  & .bar:not(.qux):has(.baz) {
    display: none;
  }
}

@utility bar {
  .foo &:not(.qux):has(.baz) {
    display: none;
  }
}

@utility baz {
  .foo .bar:not(.qux):has(&) {
    display: none;
  }
}
```

Notice that there is no `@utility qux` because it was used inside of
`:not(…)`.

---

When classes are nested inside at-rules, then these classes will also
become utilities. However, the `@utility <name>` will be at the top and
the at-rules will live inside of it. If there are multiple classes
inside a shared at-rule, then the at-rule will be duplicated for each
class.

Let's look at an example to make it more clear:

Input:
```css
@layer utilities {
  @media (min-width: 640px) {
    .foo {
      color: red;
    }

    .bar {
      color: blue;
    }

    @media (min-width: 1024px) {
      .baz {
        color: green;
      }

      @media (min-width: 1280px) {
        .qux {
          color: yellow;
        }
      }
    }
  }
}
```

Output:
```css
@utility foo {
  @media (min-width: 640px) {
    color: red;
  }
}

@utility bar {
  @media (min-width: 640px) {
    color: blue;
  }
}

@utility baz {
  @media (min-width: 640px) {
    @media (min-width: 1024px) {
      color: green;
    }
  }
}

@utility qux {
  @media (min-width: 640px) {
    @media (min-width: 1024px) {
      @media (min-width: 1280px) {
        color: yellow;
      }
    }
  }
}
```

---

When classes result in multiple `@utility` directives with the same
name, then the definitions will be merged together.

Input:
```css
@layer utilities {
  .no-scrollbar::-webkit-scrollbar {
    display: none;
  }

  .no-scrollbar {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
}
```

Intermediate representation:
```css
@utility no-scrollbar {
  &::-webkit-scrollbar {
    display: none;
  }
}

@utility no-scrollbar {
  -ms-overflow-style: none;
  scrollbar-width: none;
}
```

Output:
```css
@utility no-scrollbar {
  &::-webkit-scrollbar {
    display: none;
  }
  -ms-overflow-style: none;
  scrollbar-width: none
}
```

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2024-09-24 18:17:09 +02:00

135 lines
2.5 KiB
TypeScript

import { css, json, test } from '../utils'
test(
'migrate @apply',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/index.css': css`
@import 'tailwindcss';
.a {
@apply flex;
}
.b {
@apply !flex;
}
.c {
@apply !flex flex-col! items-center !important;
}
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade')
await fs.expectFileToContain(
'src/index.css',
css`
.a {
@apply flex;
}
.b {
@apply flex!;
}
.c {
@apply flex! flex-col! items-center!;
}
`,
)
},
)
test(
'migrate `@tailwind` directives',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/index.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade')
await fs.expectFileToContain('src/index.css', css` @import 'tailwindcss'; `)
},
)
test(
'migrate `@layer utilities` and `@layer components`',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/index.css': css`
@import 'tailwindcss';
@layer components {
.btn {
@apply rounded-md px-2 py-1 bg-blue-500 text-white;
}
}
@layer utilities {
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
}
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade')
await fs.expectFileToContain(
'src/index.css',
css`
@utility btn {
@apply rounded-md px-2 py-1 bg-blue-500 text-white;
}
@utility no-scrollbar {
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
}
`,
)
},
)