How we migrated entirely to CSS Modules using codemods and Sourcegraph Code Insights
In Spring 2021, the Sourcegraph team overhauled the design system and UI of our web application. It was an ambitious project because, along with improving the UX, we strove to make web interfaces more consistent, which required the collaboration of most product teams. The task was also challenging from a technical perspective because the application styles were implemented using global CSS rules with Bootstrap as a foundation. Our UI components were typically styled in three different ways:
- Using a combination of Bootstrap utility classes.
- Using a custom class specific to the element.
- Using global styles inherited from the global scope.
It was hard to change individual UI components because engineers needed to keep in mind the whole stylesheet to avoid colliding styles in the global scope:
- When adding a new class name to the stylesheet, engineers needed to ensure no other component already used it. The BEM methodology helped with this a lot, but we still bumped into issues from time to time. These issues were incredibly difficult to debug as global styles can be used in so many different ways.
- Global CSS rules depend on the order of the code, and it's difficult to ensure that the order is preserved when implementing global changes like the application redesign. Moving files around could result in a slightly different import order that affects a tiny bit of UI. Debugging such small changes can significantly slow teams down and makes DX a nightmare.
- There was no robust and configured way to identify unused CSS code not linked to any HTML elements. In the process of the redesign, we occasionally uncovered such styles. These styles added no value to our codebase or our product.
To overcome these issues, we decided that, before diving into the redesign, we should find a way to embrace scoped styles instead of adding new styles to the global scope. At the same time, we needed to ensure that this solution could be integrated with the current approach effectively. The definition of success was a combination of the following outcomes:
- Modular CSS is enforced for new design system components.
- It works well with existing global styles and UI elements.
- We have a clear migration path that doesn’t interfere with feature development.
CSS Modules solution
We researched popular solutions available in the open source universe: trade-offs between CSS in JS, regular CSS, and hybrid solutions. Some offered more powerful features, but we decided to use a solution that we knew we could easily adopt—CSS modules.
What are CSS Modules?
CSS modules are great because they help you write reusable components with isolated styles. According to the repo, CSS modules are:
“CSS files in which all class names and animation names are scoped locally by default.â€
With CSS modules, CSS classes should be referenced in the JavaScript file via explicit binding to the styles file:
```jsx import styles from './styles.css'
const Title = () =>
Heading!
```
The compiler would update the CSS file during the build step by replacing the CSS selector class referenced in the markup with a unique character set. And the JavaScript file would be updated by replacing the CSS class with the new inlined string. The final HTML markup might look like this:
```html
Heading!
```
This approach is designed to fix the problem of the global scope in CSS. Engineers can happily name their CSS selectors whatever they want, without worrying about unintended consequences in other areas of the code. Creating a CSS module is ultimately very similar to creating a typical CSS file. Maintaining this flow ensured we could easily start adopting this approach without interfering with our developer experience too much. After we updated documentation on how to use CSS modules, teams adopted this approach for new features immediately.
Migration tracking challenges
The Frontend Platform team started the migration by manually converting global styles to CSS modules for a single, recently developed feature. We quickly noticed that despite some initial progress, we had some questions that slowed us down:
- How can we know whether CSS Modules are continuously used for new features added to the project?
- What’s our current progress? What percentage of global styles have already been converted to CSS Modules?
- Given the current rate of changes, how much time will it take to migrate all remaining styles to CSS Modules?
We knew we could search the codebase manually for relevant files and make conclusions based on that, or we could write a script to do it for us. But an ideal solution would give a clear, at-a-glance indication of where we were at the moment, and be easy for us to maintain. Thankfully, another product team at Sourcegraph had developed the exact solution to our problem!
“Code Insights reveals high-level information about your codebase, based on both how your code changes over time and its current state.†– source.
Code Insights entered Beta in August 2021, and we happily started using it to track the migration progress. As of today, Code Insights is now Generally Available.
We used a few simple search queries to create the code insight, and immediately got answers to all the questions important for migration tracking in one picture. It was a crucial tool in communicating where we were with the migration progress to the engineering organization going forward. Code Insights can be especially helpful for platform teams like ours, which do a lot of invisible work and can struggle to make the case for dedicating time to initiatives like tackling tech debt. Being able to communicate progress visually to stakeholders outside engineering or in leadership is really persuasive. Teams that typically manage large parts of a codebase can find it difficult to get insight into what is happening, and Code Insights makes that really easy.
The migration that we started working on didn’t have any hard deadlines, so getting sidetracked with new shiny initiatives was easy. Also, multiple modules were a bit harder to rewrite, and we subconsciously delayed refactoring them. These factors contributed to slowing down, but having a code insight as a map of planned work with a clear destination was motivating to push the migration to 100% completion. It kept reminding us that we were close to our goal and helped us close this chapter without any leftover work.
Migration execution challenges
Global migrations are challenging for platform teams. After spending time to migrate another feature to CSS Modules, we discovered some execution obstacles in our way:
- The migration work is quite repetitive, and we had to do a ton of it because the size of the codebase is pretty significant: ~400 files to migrate.
- We wanted to make the migration as smooth as possible for product teams. Asking them to allocate a substantial amount of their time to work on the migration was an option but not ideal.
- It's not very exciting to work on for an extended period of time, but dragging out the migration would mean leaving the codebase inconsistent for too long. New engineers might be confused about which approach to use, even if we document it.
Codemods to the rescue
A codemod is an automated change to source code, which helps platform teams execute global refactorings faster and with higher confidence. Because codemods are usually written in the same language as the project, they can understand its subtleties and complexities. It makes them perfect for executing large-scale migration where find-and-replace functionality is insufficient. A codemod generally includes the following steps:
- Generate an Abstract Syntax Tree (AST) from a source file.
- Traverse the AST looking for nodes to transform.
- Make changes to the AST where appropriate.
- Regenerate the source file based on the new AST.