Skip to content

Ensure @tailwindcss/upgrade runs on Tailwind CSS v4 projects and is idempotent #17717

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Apr 22, 2025

Conversation

RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Apr 19, 2025

This PR ensures that the @tailwindcss/upgrade tool works on existing Tailwind CSS v4 projects. This PR also ensures that the upgrade tool is idempotent, meaning that it can be run multiple times and it should result in the same output.

One awesome feature this unlocks is that you can run the upgrade tool on your codebase at any time and upgrade classes if you still have some legacy syntaxes, such as bg-[var(--my-color)], in your muscle memory.

One small note: If something changed in the first run, re-running will not work immediately because your git repository will not be clean and the upgrade tool requires your git repo to be clean. But once you verified and committed your changes, the upgrade tool will be idempotent.

Idempotency is guaranteed by ensuring that some migrations are skipped by checking what version of Tailwind CSS you are on before the version is upgraded.

For the Tailwind CSS version: We will resolve tailwindcss itself to know the actual version that is installed (the one resolved from node_modules). Not the one available in your package.json. Your package.json could be out of sync if you reverted changes but didn't run npm install yet.

Back to Idempotency:

For example, we have migrations where we change the variant order of stacked variants. If we would run these migrations every time you run the upgrade tool then we would be flip-flopping the order every run.

See: https://tailwindcss.com/docs/upgrade-guide#variant-stacking-order

Another example is where we rename some utilities. For example, we rename:

Before After
shadow shadow-sm
shadow-sm shadow-xs

Notice how we have shadow-sm in both the before and after column.

If we would run the upgrade tool again, then we would eventually migrate your original shadow to shadow-sm (first run) and then to shadow-xs (second run). Which would result in the wrong shadow.

See: https://tailwindcss.com/docs/upgrade-guide#renamed-utilities


The order of upgrade steps changed a bit as well to make the internals are easier to work with and reason about.

  1. Find CSS files
  2. Link JS config files (if you are in a Tailwind CSS v3 project)
  3. Migrate the JS config files (if you are in a Tailwind CSS v3 project)
  4. Upgrade Tailwind CSS to v4 (or the latest version at that point)
  5. Migrate the stylesheets (we used to migrate the source files first)
  6. Migrate the source files

This is done so that step 5 and 6 will always operate on a Tailwind CSS v4 project and we don't need to check the version number again. This is also necessary because your CSS file will now very likely contain @import "tailwindcss"; which doesn't exist in Tailwind CSS v3.

This also means that we can rely on the same internals that Tailwind CSS actually uses for locating the source files. We will use @tailwindcss/oxide's scanner to find the source files (and it also keeps your custom @source directives into account).

This PR also introduces a few actual migrations related to recent features and changes we shipped.

  1. We migrate deprecated classes to their new names:

    Before After
    bg-left-top bg-top-left
    bg-left-bottom bg-bottom-left
    bg-right-top bg-top-right
    bg-right-bottom bg-bottom-right
    object-left-top object-top-left
    object-left-bottom object-bottom-left
    object-right-top object-top-right
    object-right-bottom object-bottom-right

    Introduced in:

  2. We migrate simple arbitrary variants to their dedicated variant:

    Before After
    [&:user-valid]:flex user-valid:flex
    [&:user-invalid]:flex user-invalid:flex

    Introduced in:

  3. We migrate @media variants to their dedicated variant:

    Before After
    [@media_print]:flex print:flex
    [@media(prefers-reduced-motion:no-preference)]:flex motion-safe:flex
    [@media(prefers-reduced-motion:reduce)]:flex motion-reduce:flex
    [@media(prefers-contrast:more)]:flex contrast-more:flex
    [@media(prefers-contrast:less)]:flex contrast-less:flex
    [@media(orientation:portrait)]:flex portrait:flex
    [@media(orientation:landscape)]:flex landscape:flex
    [@media(forced-colors:active)]:flex forced-colors:flex
    [@media(inverted-colors:inverted)]:flex inverted-colors:flex
    [@media(pointer:none)]:flex pointer-none:flex
    [@media(pointer:coarse)]:flex pointer-coarse:flex
    [@media(pointer:fine)]:flex pointer-fine:flex
    [@media(any-pointer:none)]:flex any-pointer-none:flex
    [@media(any-pointer:coarse)]:flex any-pointer-coarse:flex
    [@media(any-pointer:fine)]:flex any-pointer-fine:flex
    [@media(scripting:none)]:flex noscript:flex

    The new variants related to inverted-colors, pointer, any-pointer and scripting were introduced in:

    This also applies to the not case, e.g.:

    Before After
    [@media_not_print]:flex not-print:flex
    [@media_not(prefers-reduced-motion:no-preference)]:flex not-motion-safe:flex
    [@media_not(prefers-reduced-motion:reduce)]:flex not-motion-reduce:flex
    [@media_not(prefers-contrast:more)]:flex not-contrast-more:flex
    [@media_not(prefers-contrast:less)]:flex not-contrast-less:flex
    [@media_not(orientation:portrait)]:flex not-portrait:flex
    [@media_not(orientation:landscape)]:flex not-landscape:flex
    [@media_not(forced-colors:active)]:flex not-forced-colors:flex
    [@media_not(inverted-colors:inverted)]:flex not-inverted-colors:flex
    [@media_not(pointer:none)]:flex not-pointer-none:flex
    [@media_not(pointer:coarse)]:flex not-pointer-coarse:flex
    [@media_not(pointer:fine)]:flex not-pointer-fine:flex
    [@media_not(any-pointer:none)]:flex not-any-pointer-none:flex
    [@media_not(any-pointer:coarse)]:flex not-any-pointer-coarse:flex
    [@media_not(any-pointer:fine)]:flex not-any-pointer-fine:flex
    [@media_not(scripting:none)]:flex not-noscript:flex

For each candidate, we run a set of upgrade migrations. If at the end of the migrations the original candidate is still the same as the new candidate, then we will parse & print the candidate one more time to pretty print into consistent classes. Luckily parsing is cached so there is no real downside overhead.

Consistency (especially with arbitrary variants and values) will reduce your CSS file because there will be fewer "versions" of your class.

Concretely, the pretty printing will apply changes such as:

Before After
bg-[var(--my-color)] bg-(--my-color)
bg-[rgb(0,_0,_0)] bg-[rgb(0,0,0)]

Another big important reason for this change is that these classes on their own
would have been migrated if another migration was relevant for this candidate.
This means that there are were some inconsistencies. E.g.:

Before After Reason
!bg-[var(--my-color)] bg-(--my-color)! Because the ! is in the wrong spot
bg-[var(--my-color)] bg-[var(--my-color)] Because no migrations rand

As you can see, the way the --my-color variable is used, is different. This
changes will make sure it will now always be consistent:

Before After
!bg-[var(--my-color)] bg-(--my-color)!
bg-[var(--my-color)] bg-(--my-color)

Yay!

Of course, if you don't want these more cosmetic changes, you can always ignore the upgrade and revert these changes and only commit the changes you want.

Test plan

  • All existing tests still pass.
    • But I had to delete 1 test (we tested that Tailwind CSS v3 was required).
    • And had to mock the version.isMajor call to ensure we run the individual migration tests correctly.
  • Added new tests to test:
    1. Migrating Tailwind CSS v4 projects works
    2. Idempotency of the upgrade tool

[ci-all]

Once we are in v4, it could be that you have `@config` still, and maybe
we are able to migrate this stylesheet. But if no `@config` exists, then
there is no need to migrate the JS configuration file

This is just for printing the header.
This swaps the "migrate stylesheets" and "migrate source files" steps.
This will make things easier _if_ we don't even need to migrate JS
config files and can rely on the CSS file itself.

This will happen if you run the upgrade tool in Tailwind CSS v4
projects.
We will improve this in a future commit so we can still migrate `@apply`
for example in Tailwind CSS v4 -> v4 migrations.
We will be using the `Scanner` from `@tailwindcss/oxide` instead. Since
we already migrated the JS config files and linked the potential
diverging `content` array to `@source` directives it means we can rely
on the scanner.
The `DesignSystem` was already nullable (or well, optional) but that
wasn't well reflected in the types.

This would otherwise break this test:
- `tailwindcss/integrations/upgrade/index.test.ts`
  - `migrate utility files imported by multiple roots`
- `bg-{left,right}-{top,bottom}` in favor of `bg-{top,bottom}-{left,right}`
- `object-{left,right}-{top,bottom}` in favor of `object-{top,bottom}-{left,right}`
This will replace variants such as `[@media(pointer:fine)]:flex` to
`pointer-fine:flex`
When printing a candidate, we do some optimizations already, such as:

- `bg-[var(--foo)]` -> `bg-(--foo)`
- `bg-[rgb(0,_0,_0)]` -> `bg-[rgb(0,0,0)]`

Consistency in your project will reduce the file size.

We parse and reprint the candidate if nothing changed during migrations
because we don't have dedicated migrations for them.

So (a lot) of these classes weren't fully updated to the v4 flavor of
the classes.

Luckily re-parsing the candidate is fast because we are re-using the
design system which means that we have a cached version of the
candidate.
And actually applies changes where it can. It will also ignore unsafe
migrations such as variant order and `rounded` -> `rounded-sm`.
@RobinMalfait RobinMalfait force-pushed the feat/enable-codemods-on-v4-projects branch from 0c7682d to d20902f Compare April 22, 2025 09:19
@RobinMalfait RobinMalfait marked this pull request as ready for review April 22, 2025 09:45
@RobinMalfait RobinMalfait requested a review from a team as a code owner April 22, 2025 09:45
@RobinMalfait RobinMalfait enabled auto-merge (squash) April 22, 2025 15:02
@RobinMalfait RobinMalfait merged commit 8e826b1 into main Apr 22, 2025
21 checks passed
@RobinMalfait RobinMalfait deleted the feat/enable-codemods-on-v4-projects branch April 22, 2025 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants