Robin Malfait 4f8ca556cf
CSS codemod: inject @import in a more expected location (#14536)
This PR inserts the `@import` in a more sensible location when running
codemods.

The idea is that we replace `@tailwind base; @tailwind components;
@tailwind utilities;` with the much simple `@import "tailwindcss";`. We
did this by adding the `@import` to the top of the file.

While this is correct, this means that the diff might not be as clear.
For example, if you have a situation where you have a license comment:
```css
/**! My license comment */
@tailwind base;
@tailwind components;
@tailwind utilities;
```

This resulted in:
```css
@import "tailwindcss";
/**! My license comment */
```

While it is not wrong, it feels weird that this behaves like this. In
this commit we make sure that it is injected in-place (the first
`@tailwind` at-rule we find) and fixup the position if we can't inject
it in-place.

The above example results in this:
```css
/**! My license comment */
@import "tailwindcss";
```

However, there are scenario's where you can't replace the `@tailwind`
directives directly. E.g.:
```css
/**! My license comment */
html {
  color: red;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
```

If we replace the `@tailwind` directives in-place, it would look like
this:
```css
/**! My license comment */
html {
  color: red;
}
@import "tailwindcss";
```

But this is invalid CSS, because you can't have CSS above an `@import`
at-rule. There are some exceptions like:
- `@charset`
- `@import`
- `@layer foo, bar;` (just the order, without a body)
- comments

In this scenario, we inject the import in the nearest place where it is
allowed to. In this case:

```css
/**! My license comment */
@import "tailwindcss";
@layer base {
  html {
     color: red;
  }
}
```

Additionally, we will wrap the existing CSS in an `@layer` of the first
Tailwind directive we saw. In this case an `@layer base`. This ensures
that utilities still win from the default styles.

Also note that the (license) comment is allowed to exist before the
`@import`, therefore we do not put the `@import` above it. This also
means that the diff doesn't touch the license header at all, which makes
the diffs cleaner and easier to reason about.

---------

Co-authored-by: Philipp Spiess <hello@philippspiess.com>
2024-09-30 13:32:30 +00:00

147 lines
2.6 KiB
TypeScript

import dedent from 'dedent'
import { expect, it } from 'vitest'
import { migrateContents } from './migrate'
const css = dedent
it('should print the input as-is', async () => {
expect(
await migrateContents(
css`
/* above */
.foo/* after */ {
/* above */
color: /* before */ red /* after */;
/* below */
}
`,
expect.getState().testPath,
),
).toMatchInlineSnapshot(`
"/* above */
.foo/* after */ {
/* above */
color: /* before */ red /* after */;
/* below */
}"
`)
})
it('should migrate a stylesheet', async () => {
expect(
await migrateContents(css`
@tailwind base;
html {
overflow: hidden;
}
@tailwind components;
.a {
z-index: 1;
}
@layer components {
.b {
z-index: 2;
}
}
.c {
z-index: 3;
}
@tailwind utilities;
.d {
z-index: 4;
}
@layer utilities {
.e {
z-index: 5;
}
}
`),
).toMatchInlineSnapshot(`
"@import 'tailwindcss';
@layer base {
html {
overflow: hidden;
}
}
@layer components {
.a {
z-index: 1;
}
}
@utility b {
z-index: 2;
}
@layer components {
.c {
z-index: 3;
}
}
@layer utilities {
.d {
z-index: 4;
}
}
@utility e {
z-index: 5;
}"
`)
})
it('should migrate a stylesheet (with imports)', async () => {
expect(
await migrateContents(css`
@import 'tailwindcss/base';
@import './my-base.css';
@import 'tailwindcss/components';
@import './my-components.css';
@import 'tailwindcss/utilities';
@import './my-utilities.css';
`),
).toMatchInlineSnapshot(`
"@import 'tailwindcss';
@import './my-base.css' layer(base);
@import './my-components.css' layer(components);
@import './my-utilities.css' layer(utilities);"
`)
})
it('should migrate a stylesheet (with preceding rules that should be wrapped in an `@layer`)', async () => {
expect(
await migrateContents(css`
@charset "UTF-8";
@layer foo, bar, baz;
/**! My license comment */
html {
color: red;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
`),
).toMatchInlineSnapshot(`
"@charset "UTF-8";
@layer foo, bar, baz;
/**! My license comment */
@import 'tailwindcss';
@layer base {
html {
color: red;
}
}"
`)
})