Skip to content
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

Add CSS codemods for migrating @layer utilities #14455

Merged
merged 17 commits into from
Sep 24, 2024

Conversation

RobinMalfait
Copy link
Member

This PR adds CSS codemods for migrating existing @layer utilities to @utility directives.

This PR has the ability to migrate the following cases:


The most basic case is when you want to migrate a simple class to a utility directive.

Input:

@layer utilities {
  .foo {
    color: red;
  }

  .bar {
    color: blue;
  }
}

Output:

@utility foo {
  color: red;
}

@utility bar {
  color: blue;
}

You'll notice that the class foo will be used as the utility name, the declarations (and the rest of the body of the rule) will become the body of the @utility definition.


In v3, every class in a selector will become a utility. To correctly migrate this to @utility directives, we have to register each class in the selector and generate n utilities.

We can use nesting syntax, and replace the current class with & to ensure that the final result behaves the same.

Input:

@layer utilities {
  .foo .bar .baz {
    color: red;
  }
}

Output:

@utility foo {
  & .bar .baz {
    color: red;
  }
}

@utility bar {
  .foo & .baz {
    color: red;
  }
}

@utility .baz {
  .foo .bar & {
    color: red;
  }
}

In this case, it could be that you know that some of them will never be used as a utility (e.g.: hover:bar), but then you can safely remove them.


Even classes inside of :has(…) will become a utility. The only exception to the rule is that we don't do it for :not(…).

Input:

@layer utilities {
  .foo .bar:not(.qux):has(.baz) {
    display: none;
  }
}

Output:

@utility foo {
  & .bar:not(.qux):has(.baz) {
    display: none;
  }
}

@utility bar {
  .foo &:not(.qux):has(.baz) {
    display: none;
  }
}

@utility baz {
  .foo .bar:not(.qux):has(&) {
    display: none;
  }
}

Notice that there is no @utility qux because it was used inside of :not(…).


When classes are nested inside at-rules, then these classes will also become utilities. However, the @utility <name> will be at the top and the at-rules will live inside of it. If there are multiple classes inside a shared at-rule, then the at-rule will be duplicated for each class.

Let's look at an example to make it more clear:

Input:

@layer utilities {
  @media (min-width: 640px) {
    .foo {
      color: red;
    }

    .bar {
      color: blue;
    }

    @media (min-width: 1024px) {
      .baz {
        color: green;
      }

      @media (min-width: 1280px) {
        .qux {
          color: yellow;
        }
      }
    }
  }
}

Output:

@utility foo {
  @media (min-width: 640px) {
    color: red;
  }
}

@utility bar {
  @media (min-width: 640px) {
    color: blue;
  }
}

@utility baz {
  @media (min-width: 640px) {
    @media (min-width: 1024px) {
      color: green;
    }
  }
}

@utility qux {
  @media (min-width: 640px) {
    @media (min-width: 1024px) {
      @media (min-width: 1280px) {
        color: yellow;
      }
    }
  }
}

When classes result in multiple @utility directives with the same name, then the definitions will be merged together.

Input:

@layer utilities {
  .no-scrollbar::-webkit-scrollbar {
    display: none;
  }

  .no-scrollbar {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
}

Intermediate representation:

@utility no-scrollbar {
  &::-webkit-scrollbar {
    display: none;
  }
}

@utility no-scrollbar {
  -ms-overflow-style: none;
  scrollbar-width: none;
}

Output:

@utility no-scrollbar {
  &::-webkit-scrollbar {
    display: none;
  }
  -ms-overflow-style: none;
  scrollbar-width: none
}

@RobinMalfait RobinMalfait force-pushed the feat/add-codemod-tooling branch from bc9b0a7 to 330dbff Compare September 18, 2024 13:49
@RobinMalfait RobinMalfait force-pushed the feat/css-codemods-at-layer-utilities branch from c0636e0 to ebf5a17 Compare September 18, 2024 13:51
@RobinMalfait RobinMalfait changed the title Add codemods for @layer utilities Add CSS codemods for migrating @layer utilities Sep 18, 2024
Base automatically changed from feat/add-codemod-tooling to next September 18, 2024 14:45
@RobinMalfait RobinMalfait force-pushed the feat/css-codemods-at-layer-utilities branch 7 times, most recently from 161b22f to e361319 Compare September 23, 2024 20:24
RobinMalfait and others added 16 commits September 24, 2024 17:59
This was unnecessary. Not related to this PR, but also not worth a
separate PR. Can probably move this commit directly to the main branch.
We already sort components before utilities if they contain more
properties. If the user _wants_ to have the components in a separate
layer, then can still do that:

```css
@Utility btn {
  @layer components {
    …
  }
}
```
A bit of a vague commit message, but this does a lot of things. I could
split it up, but not sure if it's worth it. Instead, let's talk about
it.

While working on keeping track of comment locations I was running into
some issues. Not the end of the world, but we could make things better.

Paired with Jordan on this to rework the algorithm. The idea is that we
now do multiple passes which is technically slower, but now we can work
on separate units of work.

- Step #1 is to prepare the at-rule. This means that rules with multiple
  selectors will be split in multiple nodes with the their own single
  selector.
- Step #2 is to collect all the classes we want to create an `@utility`
  for.
- Step #3 is to create a clone of the main `@layer utilities` for all
  the non-`@utility` leftover nodes (E.g.: rules with element and ID
  selectors).
- Step #4 is to create a clone of the main `@layer utilities` node for
  every single `@utility <name>` we want to create.
- Step #5 is to go over every clone, and eliminate everything that is
  not part of the `@utility` in question. So we can remove siblings
  (except for comments near it) and go up the chain.
- Step #6 is now to go over the initial `@layer utilities` clone we set
  aside, and remove everything that's not part of any of the clones.
- Step #7 is cleanup work, where empty nodes are removed, and rules with
  a selector of `&` are replaced by its children. This is done in a
  depth-first traversal instead of breadth first.

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
E.g.:
```css
.foo {
  .bar {
    color: red;
  }
}
```

If this becomes:
```css
.foo {
  & {
    color: red;
  }
}
```

Then this essentially means `.foo & {}`, not `.foo {}`
@RobinMalfait RobinMalfait force-pushed the feat/css-codemods-at-layer-utilities branch from e361319 to 7abb3fd Compare September 24, 2024 15:59
Copy link
Member

@philipp-spiess philipp-spiess left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the test cases!

@RobinMalfait RobinMalfait merged commit d14249d into next Sep 24, 2024
3 checks passed
@RobinMalfait RobinMalfait deleted the feat/css-codemods-at-layer-utilities branch September 24, 2024 16:17
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