Philipp Spiess e4308da604
Template migrations: Add variant order codemods (#14524)
One of the breaking changes of v4 is the [inversion of variant order
application](https://github.com/tailwindlabs/tailwindcss/pull/13478). In
v3, variants are applied "inside-out".


For example a candidate like `*:first:underline` would produce the
following CSS in v3:

```css
.\*\:first\:underline:first-child > * {
  text-decoration-line: underline;
}
```

To get the same behavior in v4, you would need to invert the candidate
order to `first:*:underline`. This would generate the following CSS in
v4:

```css
:where(.first\:\*\:underline:first-child > *) {
  text-decoration-line: underline;
}
```

## The Migration

The most naive approach would be to invert the variants for every
candidate with at least two variants. This, however, runs into one issue
and some unexpected inconsistencies. I have identified the following
areas:

1. Some pseudo class variants _must appear at the end of the selector_.
v3 was patching over this by doing some manual reordering in for these
variants. For example, in v3, both of these variants create the same
output CSS: `hover:before:underline` and `before:hover:underline`. In v4
we simplified this system though and no longer generate the same output
in both cases. Instead, you'd always want to write
`hover:before:underline`, ensuring that these variants will appear at
the end.
   
For an exact list of which variants these affect, take a look [at this
diff](https://github.com/tailwindlabs/tailwindcss/pull/13478/files#diff-7779a0eebf6b980dd3abd63b39729b3023cf9a31c91594f5a25ea020b066e1c0L228-L246).

2. The `dark` variant and other at-rule variants are usually written
before other variants. This is more of a recommendation to make it
easier to read candidates rather than a difference in behavior as
`@media` queries are hoisted by the engine. For this reason, both of
these variants are _correct_ yet in real applications we prefer the
first one: `lg:hover:underline`, `hover:lg:underline`.
   
To avoid shuffling these rules across all candidates during the
migration, we bucket `dark` and other at-rule variants into a special
bucket that will not have their order changed (since people wrote stacks
like `sm:max-lg:` before and we want to keep them as-is) and appear
before all other variants.

3. For some variant stacks, the order does not matter. E.g.:
`focus:hover:underline` and `hover:focus:underline` will be the same. We
don't want to needlessly shuffle their order if we have to.

With these considerations, the migration now works as follows:

- If there is less then two variants, we do not need to migrate the
candidate
- If _every_ variant in the stack is an order-independent variant, we do
not need to migrate the candidate
- _Note that this is currently hardcoded to only support `&:hover` and
`&:focus`._
- Otherwise, we loop over the candidates and put them into three
buckets:
- `mediaVariants` hold variants that only contribute `@media` rules
_and_ the `dark` variant.
- `pseudoElementVariants` hold variants that _must appear at the end of
the selector_. This is based on the allow list from v3/early v4.
  - `regularVariants` contains the rest.
- We now compute if any of the variants inside `regularVariants` is
order dependent.
- With this list of variants, we now construct the new order of variants
as:
   
   ```ts
   [
     ...atRuleVariants,
...(anyRegularVariantOrderDependent ? regularVariants.reverse() :
regularVariants),
     ...pseudoElementVariants,
   ]
   ```

---------

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-10-03 09:15:19 -04:00
..
2024-03-05 14:29:15 +01:00