Migrating Tachyons to Tailwind CSS (II - the process)
Tags: tachyons, tailwindcss, css
Published: 2021-02-23 19:14:19

_This is the second part of the series about Tachyons to Tailwind migration. Having finished the [preparation works](https://dev.to/nejremeslnici/migrating-tachyons-to-tailwind-css-part-i-ich), we’ll show here how we made the transition itself and the tooling we built for it._

### Not all utility classes are equal (during migration)

So where do we begin? What utilities should we start with? We estimated to have a few hundred distinct Tachyons classes declared in around 20 SASS files… To not get overwhelmed by this mass, we needed a tool that would guide us a bit. So we built it.

**[`compare_classes.rb`](https://gist.github.com/borama/952edd2a0dc286a8b4591bb3ff19fd03)** is a small ruby script that **compares the names and declarations of all Tachyons utility classes to the Tailwind ones** and gives some helpful pointers for their migration. It works as follows:

- It takes two arguments on input: the compiled Tachyons CSS file and the Tailwind one.
- It parses both files using the nice **[css_parser gem](https://github.com/premailer/css_parser)** to extract the utility classes names and declarations.
- While doing so it filters out all non-essential stuff:
  - non-class selectors (e.g. declarations for the `html` element present in [normalize](https://github.com/tachyons-css/tachyons/blob/master/src/_normalize.css) / [preflight](https://tailwindcss.com/docs/preflight) styles)
  - responsive / media queries variants (e.g. `.something-ns` or `.lg:something`)
  - complex selectors involving cascade or pseudo-classes
  - and in our case also all color utilities as we treated them differently (this deserves a separate short post).
- Finally, the script compares the classes declarations and prints out the classes to migrate divided into four groups:<a name="classes-groups"/>
  1. **Identical classes** – the trivial case with utilities that have the same name as well as declaration. They should still be migrated though because their responsive variant names differ in the two systems.
  2. **Renamed classes** – the declarations are the same but the names differ.
  3. **Colliding classes** – the most problematic ones – utilities with the same name in both systems but different declarations (meanings). 
  4. **Missed classes** – the utility class is declared in Tachyons but a corresponding declaration has not been found in Tailwind.
- To allow estimating the relative importance of each class, its **usage count** all over the project is shown, too, calculated via a simple `grep` command. Note that the regex that matches the classes is quite dumb, on purpose, to keep it fast, so consider the numbers as approximate (usually over-estimated because of false positives).

Please read the instructions in the script carefully. The script gives best results when **the two CSS files are prepared in a certain way**:

- The Tachyons CSS file should be processed by PurgeCSS (as a manual step using the `npx purgecss` command) so that it doesn’t try to analyze utility classes that are not used in the project.
- The Tailwind CSS file should, on the contrary, be as complete as possible to give the script a bigger chance to match declarations. We only temporarily removed the base (preflight) styles as they added noise to the results.

The output of this script when run in the beginning of migrations may look similar to this:

```
$ ./compare_classes.rb /tmp/tachyons.css /tmp/tailwind.css

Identical classes (43):
  .absolute ⟶ .absolute (558)
  .bottom-0 ⟶ .bottom-0 (38)
  ...
Renamed classes (361):
  .bottom--1 ⟶ .-bottom-4 (1)
  .bottom--2 ⟶ .-bottom-8 (3)
  ...
  .w9 ⟶ .w-8 (106)
  .ws-normal ⟶ .whitespace-normal (28)
Colliding classes (36):
  .bottom-1 (4, tachyons: 'bottom: 1rem', tailwind: 'bottom: 0.25rem')
   ...
  .hidden (303, tachyons: 'visibility: hidden', tailwind: 'display: none')
   ...
  .w-80 (8, tachyons: 'width: 80%', tailwind: 'width: 20rem')
Missed classes (non-colours) (261):
  .arial (16)
  .aspect-ratio--16x9 (3)
  ...
  .z-max (2)
  .zoom (23)
Found 658 classes still to migrate.
```

All right, this might still sound overwhelming but we repeatedly used this summary throughout the whole migration process as a guide where to move on and how much work was still left (see our [rule #3](https://dev.to/nejremeslnici/migrating-tachyons-to-tailwind-css-part-i-ich#migration-rules)). We decided to start with the ”Renamed classes“ as they have known counterparts in Tailwind but without any risk of collisions. We guess it’s the time to present the migration script!

### The migration script

To be able to repeatedly migrate utility classes between the two systems, **we built a script named [`tachyons_to_tailwind.rb`](https://gist.github.com/borama/5b5d0892dfa21c951b7be98a146aca85)** which is accompanied by a YAML config file.

We used this script iteratively to convert a handful of classes at a time but it could as well be preconfigured to migrate all utilities in the whole project at once, if you feel brave enough.

Let’s briefly show how it works:<a name="migration-script"/>

- It reads the configuration from the YAML file.
- It expands the globs defined in the `TEMPLATE_GLOBS` constant to find all files to replace classes in.
- It loops over the files and opens them in the [in-place edit mode](https://ruby-doc.org/core-3.0.0/ARGF.html#method-i-inplace_mode-3D). This mode allows editing each file on the fly without having to create temporary files.
- For each line, it tries to find **all variants of all Tachyons utility classes and substitutes them with their Tailwind replacements**.
- It supports various exceptions defined in the config file.
- There are a few speed optimizations (utilizing the `grep` system command) in this procedure that try to make this script as fast as possible. **One migration run over the whole project took us about 2-3 minutes** on our machines, depending on the number of classes we were converting at a time.
- During the migration, the script **prints what classes were replaced and where**, giving an instant visual feedback about what it’s doing.
- When finished, the script creates a file with all migrated classes listed, one per line, called `migrated_classes.txt`. This file is useful for purging Tailwind CSS during the migration and we’ll describe this further below.

The core of the script is a **somewhat complex regular expression** around [line 85](https://gist.github.com/borama/5b5d0892dfa21c951b7be98a146aca85#file-tachyons_to_tailwind-rb-L84). It matches the utility classes in various contexts. For example, we use [Slim](http://slim-lang.com/) for our templates, so most of our classes look like `.this`. But they can also appear in a multitude of other situations, such as in Rails helpers or in JavaScript (e.g. `"this"` or `'another this'`). While not reliable absolutely, the regex seems to tackle all these contexts almost perfectly and we came across only very few places in our codebase that we had to convert by hand. 

### The migration config file

The conversion script would do nothing without proper configuration, which is expected in the same directory **in a YAML file called [`tachyons_to_tailwind.yml`](https://gist.github.com/borama/bcc250918bdbef56b65e79e5628bf9c9)**. The preceding gist includes a sample version of this config file. While it has been extracted from our actual config file during the migrations, it is very likely that your setup will be different and you’ll need to update the scales and / or add some other Tachyons classes, at the very least.

The file recognizes the following sections:

```yml
# media query: tachyons postfix (-mq) => tailwind prefix (mq:)
variants:
  ns: sm
  l: lg

# tachyons_class => tailwind_class
replacements:

  # paddings
  pa0: p-0
  pa1: p-0-5
  ...
  pa14: p-48
  pa15: p-64

  ...
  
  # display
  db: block
  di: inline
  ...
  inline-flex: inline-flex

# these (Tachyons) classes will be ignored during migrations
already_migrated:
  - absolute
  ...
  - z-max

  # components
  - horizontal-centering
  - vertical-centering
  ...
  - link-txt

  # colliding classes - do not remove from here!
  - bottom-1
  - bottom-2
  - bottom-5
  ...
  - top-20
  - top-40
  - zoom

# whole file / dir exceptions
glob_exceptions:
  - app/views/affiliate_widgets/*
  - app/views/vanity/*

# concrete class exceptions - file => tachyons_class => [line_numbers]
# if [line_numbers] is empty, ignore whole file for the given class
exceptions:
  app/javascript/src/controllers/kozel_listing_controller.js:
    h2: []
  app/views/shared/_search_tag_provider.html.slim:
    list: [161, 162, 165, 166, 180, 184, 185, 202]
```

- **`variants`** – mapping of **Tachyons responsive postfixes to the corresponding Tailwind prefixes**, e.g. `l: lg` means that the `-l` postfix should be converted to the `lg:` prefix.
- **`replacements`** – this is the main section listing **mappings of the particular Tachyons classes** to their Tailwind counterparts. Sometimes, a single Tachyons class should be converted to multiple Tailwind classes and in that case you can specify an array on the right-hand side (see ”transitons“ in the config for an example).
- **`already_migrated`** – a list of classes that should be **completely ignored during conversion**. This is useful for ”identical classes“ migration, as these should be migrated just once, otherwise they would get converted again and again during each script run (as they map to themselves). Also, ”colliding classes“ must be put here just after their migration as these classes must be converted just once (otherwise they would change meaning).
- **`glob_exceptions`** – globs of **files that should be completely skipped during the migrations**. Suitable e.g. for files that are in the view layer but you are absolutely sure they have no classes to covert.
- **`exceptions`** – list of **more specific exceptions**. List here each file that you want a given class to be ignored during migrations, either in the whole file (this is denoted by an empty array) or on particular lines only (denoted by an array of line numbers).

With these config options, **you can describe the complete transformation rules for all your utility classes and their variants, in your specific codebase**. If you set the config right, the script will migrate the whole project in a single run. Or, you can add a group of classes, one at a time, and migrate iteratively (same as we did).

### How we did it (the ”migration loop“)

To gradually migrate our Tachyons classes, we repeated the following pattern until we finished all of them:

- We ran the `compare_classes.rb` script and chose a group of utilities to convert in the iteration. 
- We carefully prepared a mapping for the classes in the `replacements` section of the config file.
- We commented out Tachyons classes that were never used in our project (to prevent anyone from using them for the first time during the migration which would leave the classes unconverted).
- We ran the `tachyons_to_tailwind.rb` script.
- We scanned through the updated files to check for potential issues with the conversion.
- We briefly tested a few pages on local environment.
- If the migrated classes were somehow special (identical or colliding classes), we copied them into the `already_migrated` section in the migration config file so that they are never migrated again.
- If all seemed good, we committed and pushed the update!
- Once the CI said the build was OK, we deployed to production and started over.

This procedure allowed us to **”refactor“ the site in a controlled fashion**: during each iteration, we changed only a small portion of the Tachyons classes and continuously tested everything locally, on the CI and on production. There were a few minor issues that slipped through but overall it worked great.

We did these migrations in a team of two, a colleague and myself, and we did them in parallel, independently. **It took us several days to migrate all of the ~650 utility classes on ~30,000 places in ~800 files.** 

The following chart shows **our progress with the migrations in time**. There were big jumps when we converted a group of related scales (involving many utilities, e.g. all paddings or margins). There were also ”plateaus“ as our progress slowed down while we tackled harder issues for the first time (we’ll discuss them below) and while we rested on the weekend 🙂. Once we had finished the big scales, we continued iterating over smaller groups of classes, in a faster pace, so the ”steps“ in the image are many and small. Finally, we dealt with the last tens of classes while doing other finishing tasks.

![Progress on the migration in time](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h0b9hfld8e67y7hwatki.png)

In general, we started by migrating the ”renamed classes“, then continued with the ”missing“ ones, then ”identical classes“ and finally the ”colliding“ ones. This way, we left the hardest bits for the final time, when we already had enough experience with the process and so that the rest of the team was affected for the shortest possible time.

_Good. In the final part, we will mention all more or less expected issues that we came across while migrating and how we tackled them.  As always, [we’ll tweet](https://twitter.com/boramacz)._