Skip to content

While JavaScript ensures that a web app acts the part, CSS makes sure that a web apps looks the part. In recent years, CSS gained important capabilities like CSS custom properties and CSS Grid. Still, there are some things that CSS is still lacking. Here's where a CSS preprocessor comes into play.

CSS with superpowers #

A CSS preprocessor lets you generate valid CSS from the preprocessor's own unique syntax. There are a few popular CSS preprocessors like SCSS and Stylus which offer rather similar features. In a mature Angular monorepo we were using Stylus as CSS preprocessor. As Stylus has been deprecated, the Angular team will discontinue supporting Stylus as well. This made us look for alternatives so that we are not making future Angular updates more difficult.

As SCSS is the most widely used CSS preprocessor we quickly decided to go forward with SCSS. We had already used SCSS in some smaller web projects (e.g. the Self-Service Portal) but this application has more than 1000 components and multiple teams working on it. Some of them are rather basic while others use advanced Stylus features. Therefore, we didn't want to fix all of them manually. Hence, we tried to automate this as much as possible.

Since SCSS is a superset of CSS, valid CSS code is also valid SCSS code. Unfortunately, it's not that easy with Stylus as the syntax may differ (e.g. it is possible to not use braces in Stylus).

Here's how a Stylus stylesheet of an Angular component looked like:

@require('lib/constants')
$buttonTextSize = 80%

button
min-width: $buttonTextSize
background-color: $colorPrimary

With SCSS, the same code looks like this:

@import 'lib/constants';
$buttonTextSize: 16px;

button {
min-width: $buttonTextSize;
background-color: $colorPrimary;
}

Both these examples will be compiled into the following CSS code:

button {
min-width: 16px;
background-color: #1666ee;
}

As you can see, the differences are not huge. However, given the size of the repository we tried to tackle this topic step by step.

Baby steps are still steps #

Big bang approaches rarely happen without issues. At LeanIX we prefer to do small and quick iterations in order to ship value as early as possible with the least amount of changes. This also makes it easier to revert changes that did not work as expected. To tackle the migration, we did multiple things one after another:

  1. The Angular schematics specified in the angular.json file were updated so that newly created components would use SCSS by default.
  2. Using zx we wrote a basic script to migrate all empty Stylus files to SCSS. There is hardly any danger in converting empty stylesheets 😉
  3. We found an open-source library to convert Stylus to SCSS. This library served most of our purposes even though we still had to double-check the results (e.g. ensuring selectors are still valid).
    a. First, we migrated Stylus files which did not use advanced Stylus features.
    b. Some Stylus files made use of advanced Stylus features which we could not easily migrate with the library out-of-the-box. One example: the usage of @require() statements to use Stylus constants defined in another Stylus file. We had to adjust our custom script to create the correct @import statements.
  4. Finally, we shared our findings and results with the engineering department. As we did not want to migrate everything by ourselves, we gathered the remaining files and distributed them to the corresponding teams.
    • In general, we like to follow the person scout rule “Always leave the campground cleaner than you found it.”. In this case it means: whenever you work on code that still used Stylus then make the effort to convert this code to SCSS.
Click here to see a simplified version of the script we came up with.
import { $, chalk, fs, globby, question } from 'zx';

const targetDirectory = await question('Enter the path for the repository: '});
const regex = await question('Enter regex pattern (e.g. apps/**/*.component.styl): ');
const stylePaths = await globby([regex], { cwd: targetDirectory, ignore: ['node_modules'] });

console.log(chalk.cyan('Total files:', stylePaths.length));

for (const filePath of stylePaths) {
const fullFilePath = `${targetDirectory}/${filePath}`;
const file = await fs.readFile(fullFilePath, { encoding: 'utf-8' });
console.log(chalk.cyan('📄 Migrating stylesheet', filePath));
if (!file.trim()) {
await renameStylusFileToScss(fullFilePath);
await updateStylePathInComponent(filePath);
} else {
await convertStylusToScss(filePath);
await updateStylePathInComponent(fullFilePath);
await copyFileContentAndRename(fullFilePath);
}
}

console.log(chalk.green(`\n🚀 Your Stylus files should have been migrated to SCSS! \n`));

await formatFilesWithPrettier();

async function formatFilesWithPrettier() {
await $`npx prettier --write ${regex} --loglevel silent`;
}

async function convertStylusToScss(filePath) {
await $`npx stylus-conver -i ${filePath} -o ${filePath.replace('.styl', '.scss')}`;
}

async function renameStylusFileToScss(filePath) {
const newFilePath = filePath.replace('.styl', '.scss');
await $`git mv ${filePath} ${newFilePath}`;
}

async function updateStylePathInComponent(filePath) {
const componentTsPath = filePath.replace('.styl', '.ts');
const fileWithChanges = await fs.readFile(componentTsPath, { encoding: 'utf-8' }).replace('component.styl', 'component.scss');
await fs.writeFile(componentTsPath, fileWithChanges, { encoding: 'utf-8' });
}

async function copyFileContentAndRename(stylusFilePath) {
const scssFilePath = stylusFilePath.replace('.styl', '.scss');
const tempFile = await fs.readFile(scssFilePath, { encoding: 'utf-8' }).replace('component.styl', 'component.scss');
await fs.remove(scssFilePath);
await renameStylusFileToScss(stylusFilePath);
await fs.writeFile(scssFilePath, tempFile, { encoding: 'utf-8' });
}

The outcomes #

  • In the end, we were able to migrate ~70% of the Stylus usage to SCSS in less than a week without causing any noticeable problems for our customers. Instead of doing everything in one pull request, we divided the problem into more manageable chunks that could be shipped on their own.
  • In the process of migrating to SCSS, we improved how we work with stylesheets (e.g. sharing color constants across multiple applications).
  • Since SCSS is widely supported by libraries and frameworks (e.g. Angular supports SCSS out-of-the-box) we could remove some code that was necessary for using Stylus. Who does not like removing code?

Ensuring that we do not ship broken code #

There are a couple of things we can do to ship code that works as intended.

  • At LeanIX we automate the development workflow as much as possible. We use GitHub Actions heavily to build, test, lint and deploy our applications to Microsoft Azure.
  • No pull request can be merged without a reviewer approving the pull request. Code review is a good opportunity to share knowledge and to find issues before they are present in production.
  • Ideally, a pull request is as small as possible. Nobody likes to review 493 file changes in one pull request. And reverting large pull requests can be a pain as well. Therefore, we created multiple pull requests that could be merged independently.
  • We have a collection of end-to-end tests which are executed against a real web browser. Having some end-to-end tests which use the UI like a real user helps to ensure that certain user flows (e.g. login, creating a Fact Sheet) still work.

Conclusion #

As always, it pays off to be in the loop how the ecosystem evolves. In the ever-changing world of software engineering, it is easy to jump into hypes without knowing if it will pay off in the future. On the other hand, if you keep the status quo too long then you might risk getting stuck with outdated tools and frameworks. Therefore, we aim to continuously reevaluate our technology choices and act when necessary.

Image of the author

Published by Ali Kamalizade

Visit author page