tailwindcss/integrations/cli/plugins.test.ts
Philipp Spiess ab9e2b716b
Support complex addUtilities() configs (#15029)
This PR adds support for complex `addUtilities()` configuration objects
that use child combinators and other features.

For example, in v3 it was possible to add a utility that changes the
behavior of all children of the utility class node by doing something
like this:

```ts
addUtilities({
  '.red-children > *': {
    color: 'red',
  },
});
```

This is a pattern that was used by first-party plugins like
`@tailwindcss/aspect-ratio` but that we never made working in v4, since
it requires parsing the selector and properly extracting all utility
candidates.

While working on the codemod that can transform `@layer utilities`
scoped declarations like the above, we found out a pretty neat
heuristics on how to migrate these cases. We're basically finding all
class selectors and replace them with `&`. Then we create a nested CSS
structure like this:

```css
.red-children {
  & > * {
    color: red;
  }
}
```

Due to first party support for nesting, this works as expected in v4.

## Test Plan

We added unit tests to ensure the rewriting works in some edge cases.
Furthermore we added an integration test running the
`@tailwindcss/aspect-ratio` plugin. We've also installed the tarballs in
the Remix example from the
[playgrounds](https://github.com/philipp-spiess/tailwindcss-playgrounds)
and ensure we can use the `@tailwindcss/aspect-ratio` plugin just like
we could in v3:
 
<img width="2560" alt="Screenshot 2024-11-18 at 13 44 52"
src="https://github.com/user-attachments/assets/31889131-fad0-4c37-b574-cfac2b99f786">

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2024-11-19 15:52:06 +01:00

201 lines
5.1 KiB
TypeScript

import { candidate, css, html, json, test } from '../utils'
test(
'builds the `@tailwindcss/typography` plugin utilities',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/typography": "^0.5.14",
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.html': html`
<div className="prose">
<h1>Headline</h1>
<p>
Until now, trying to style an article, document, or blog post with Tailwind has been a
tedious task that required a keen eye for typography and a lot of complex custom CSS.
</p>
</div>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
`,
},
},
async ({ fs, exec }) => {
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
await fs.expectFileToContain('dist/out.css', [
candidate`prose`,
':where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *))',
':where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *))',
])
},
)
test(
'builds the `@tailwindcss/forms` plugin utilities',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/forms": "^0.5.7",
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.html': html`
<input type="text" class="form-input" />
<textarea class="form-textarea"></textarea>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin '@tailwindcss/forms';
`,
},
},
async ({ fs, exec }) => {
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
await fs.expectFileToContain('dist/out.css', [
//
candidate`form-input`,
candidate`form-textarea`,
])
await fs.expectFileNotToContain('dist/out.css', [
//
candidate`form-radio`,
])
},
)
test(
'builds the `@tailwindcss/forms` plugin utilities (with options)',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/forms": "^0.5.7",
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.html': html`
<input type="text" class="form-input" />
<textarea class="form-textarea"></textarea>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin '@tailwindcss/forms' {
strategy: base;
}
`,
},
},
async ({ fs, exec }) => {
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
await fs.expectFileToContain('dist/out.css', [
//
`::-webkit-date-and-time-value`,
`[type='checkbox']:indeterminate`,
])
// No classes are included even though they are used in the HTML
// because the `base` strategy is used
await fs.expectFileNotToContain('dist/out.css', [
//
candidate`form-input`,
candidate`form-textarea`,
candidate`form-radio`,
])
},
)
test(
'builds the `@tailwindcss/aspect-ratio` plugin utilities',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2",
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.html': html`
<div class="aspect-w-16 aspect-h-9">
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin '@tailwindcss/aspect-ratio';
`,
},
},
async ({ fs, exec }) => {
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
await fs.expectFileToContain('dist/out.css', [
//
candidate`aspect-w-16`,
candidate`aspect-h-9`,
])
},
)
test(
'builds the `tailwindcss-animate` plugin utilities',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss-animate": "^1.0.7",
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.html': html`
<div class="animate-in fade-in zoom-in duration-350"></div>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin 'tailwindcss-animate';
`,
},
},
async ({ fs, exec }) => {
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
await fs.expectFileToContain('dist/out.css', [
candidate`animate-in`,
candidate`fade-in`,
candidate`zoom-in`,
candidate`duration-350`,
'transition-duration: 350ms',
'animation-duration: 350ms',
'@keyframes enter {',
])
},
)