This PR lands a quick interface update for template migration with some
lessons learned form our existing migrations. Specifically, this version
attempts to:
- Allow migrations to access the raw candidate. This way we can migrate
candidates that _would not parse as valid in v4_. This will help us
migrate prefixes in candidates from v3 to v4.
- There is no more awkward "return null" if nothing has changed. The
return `null` was necessary because we relied on mutating the Variant
and since parsing/printing could remove some information, it was not
easy to find out wether a candidate needed to be migrated at all. With a
string though, we can do this cheaply by returning the `rawCandidate`.
- We previously asserted that if `parseCandidate` returns more than one
candidate, we only picked the first one. This behavior is now moved into
the migrations where we have more context. For now though, we still do
not need to worry about this since in all cases, these duplicate
candidates would serialize to the same `Candidate`. It is helpful if you
only want to run a migration on a specific type of candidate (e.g. if
there's a `static` one and a more generic `functional` one).
- We need access to the `DesignSystem` inside migrations now to be able
to `parseCandidate`s.
Opening this up as a separate PR since it can take some time to iron out
the edge cases for the individual codemod PRs and I don't want to be
rebasing all the time.
## Before
```ts
type Migration = (candidate: Candidate) => Candidate | null
```
## After
```ts
type Migration = (designSystem: DesignSystem, rawCandidate: string) => string
```
In v4, we're making room for more gradient primitives with the addition
of [gradient and conic gradient
utilities](https://github.com/tailwindlabs/tailwindcss/pull/14467). To
make this clearer, we are renaming all v3 `bg-gradient-*` utilities to
`bg-linear-*` to make it clear that these refer to linear gradients.
This PR adds a migration that will automatically update the class names
based on this migration.
This PR adds the initial setup and a first codemod for the template
migrations. These are a new set of migrations that operate on files
defined in the Tailwind v3 config as part of the `content` option (so
your HTML, JavaScript, TSX files etc.).
The migration for this is integrated in the new `@tailwindcss/upgrade`
package and will require pointing the migration to an input JavaScript
config file, like this:
```
npx @tailwindcss/upgrade --config tailwind.config.js
```
The idea of template migrations is to apply breaking changes from the v3
to v4 migration within your template files.
## Migrating !important syntax
The first migration that I’m adding with this PR is to ensure we use the
v4 important syntax that has the exclamation mark at the end of the
utility.
For example, this:
```html
<div class="!flex sm:!block"></div>
```
Will now turn into:
```html
<div class="flex! sm:block!"></div>
```
## Architecture considerations
Implementation wise, we make use of Oxide to scan the content files fast
and efficiently. By relying on the same scanner als Tailwind v4, we
guarantee that all candidates that are part of the v4 output will have
gone through a migration.
Migrations itself operate on the abstract `Candidate` type, similar to
the type we use in the v4 codebase. It will parse the candidate into its
parts so they can easily be introspected/modified. Migrations are typed
as:
```ts
type TemplateMigration = (candidate: Candidate) => Candidate | null
```
`null` should be returned if the `Candidate` does not need a migration.
We currently use the v4 `parseCandidate` function to get an abstract
definition of the candidate rule that we can operate on. _This will
likely need to change in the future as we need to fork `parseCandidate`
for v3 specific syntax_.
Additionally, we're inlining a `printCandidate` function that can
stringify the abstract `Candidate` type. It is not guaranteed that this
is an identity function since some information can be lost during the
parse step. This is not a problem though, because migrations will only
run selectively and if none of the selectors trigger, the candidates are
not updated. h/t to @RobinMalfait for providing the printer.
So the overall flow of a migration looks like this:
- Scan the config file for `content` files
- Use Oxide to extract a list of candidate and their positions from
these `content` files
- Run a few migrations that operate on the `Candidate` abstract type.
- Print the updated `Candidate` back into the original `content` file.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>