Have you ever been annoyed at a project where code is all over the place? Inconsistent naming, mix between spaces and tabs, some functions defining return types while others do not? Well, worry no more! Linters are here to save the day!

Working as a consultant I find myself working on all kinds of projects, of all ages and in all sorts of languages. Meeting projects developed by several different companies or developers can be a challenge. It is no secret that every developer has their style of coding, including everything from naming, spacing, and structure of code.

In this post, we will take a look at some useful tools to help you maintain a cleaner and more consistent code structure. Some of those being ESLint for code files, Prettier for formatting/auto fixing, and Stylelint for style files.

Getting started

As always, the code we will be making here is available on my GitHub. You can clone the repository, or follow along here.

What will we be doing?

In this post we will be making something simple, a card to guess a random number. Every time we click the card, the number changes.

Preview

Setting up our module

The first thing we need to do is to set up our module, let us create a directory, and initialize a new npm package with the following commands:

mkdir random-number-card
cd random-number-card
npm init -y
mkdir src

To keep our repository clean we will also add a .gitignore file to ensure unwanted files do not end up in our repository. For this, we will use the default GitHub file for node.js

# Add gitignore file
wget -O .gitignore https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore

# Install typescript and typings
npm i -D typescript @types/node @types/react

# Install react
npm i -S react

# Initialize typescript
tsc --init

The last command will generate a tsconfig.json file in your root directory, update this to contain the following:

{
  "compilerOptions": {
    "outDir": "build",
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "jsx": "react"
  }
}

Now, create the files so your project structure matches the following and update the contents of the files:

--- random-number-card
    --- src
        --- styles
            - main.css
        - RandomNumberCard.tsx
    - package.json
    - package-lock.json
    - .gitignore

src/RandomNumberCard.tsx

import * as React from "react";
import "./styles/main.css";

const RandomNumberCard = () => {
  const [rndNumb, setNum] = React.useState(0);

  return (
    <div
      className="RandomNumberCard"
      onClick={() => {
        const num = Math.floor(Math.random() * 100);
        setNum(num);
      }}
    >
      <h1 className="RandomNumberCard__title">Your random number was</h1>
      <p className="RandomNumberCard__number">{rndNumb}</p>
      <p>Can you guess the next one?</p>
    </div>
  );
};

export default RandomNumberCard;

src/styles/main.css

.RandomNumberCard {
  height: 300px;
  width: 300px;
  background-color: black;
  color: white;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 10px;
}
.RandomNumberCard:hover {
  cursor: pointer;
}
.RandomNumberCard__number {
  font-size: 60px;
}

.RandomNumberCard__title {
  font-size: 20px;
  font-weight: bold;
}

Setting up storybook

This might be a bit out of the scope of this post and not needed, but we will do it to save some time on other configurations. We will be setting up Storybook, a tool for developing UI components in isolation. Storybook will allow us to quickly visualize and test our components. If you do not want to do this section, you can skip as it is not necessary for the rest of the post.

Storybook is a quick setup. To set it up run the command npx sb init and the Storybook CLI will do most of the job. Now delete everything in the src/stories folder, and add a new file RandomNumberCard.stories.tsx with the following content:

import React from "react";
import { Story, Meta } from "@storybook/react/types-6-0";
import RandomNumberCard from "../RandomNumberCard";

export default {
  title: "RandomNumberCard",
  component: RandomNumberCard,
} as Meta;

const Template: Story = (args) => <RandomNumberCard {...args} />;

export const DefaultState = Template.bind({});
DefaultState.args = {};

Storybook should automatically have added a new script to your package.json file that you can use to start Storybook. If you want to run Storybook and test our component out, you can open a terminal window and run the command npm run storybook. Storybook will start up and you should see something like:

Storyboook

Setting up linting in our app

Now it is time to install the modules that will help us lint our module. Below you will find a table of the modules with a short description of their purpose.

Package Description
@typescript-eslint/eslint-plugin ESLint plugin that provides linting rules for TypeScript code
@typescript-eslint/parser ESLint parser that allows ESLint to work with TypeScript
eslint-config-prettier Plugin to disable conflicting or unnecessary prettier rules
eslint-plugin-prettier Allows running prettier as an ESLint rule
eslint-plugin-simple-import-sort Plugin to autofix import sorting
eslint-plugin-react React linting rules
eslint ESLint is a static code analysis tool to detect problems i JavaScript code
prettier Prettier is a code formatting tool
stylelint Linter for CSS files
stylelint-config-standard Standard config for stylelint
stylelint-config-recommended Turns on all stylelint rules
stylelint-config-recess-order Order/Sort CSS properties the Recess way

To install all these modules, run the command:

npm i -D prettier eslint eslint-plugin-simple-import-sort eslint-plugin-prettier eslint-config-prettier \
@typescript-eslint/parser @typescript-eslint/eslint-plugin stylelint stylelint-config-standard \
stylelint-config-recommended stylelint-config-recess-order

We have our modules installed, and now it is time to configure them to fit our needs. With that said, ESLint offers a large number of rules as well does Prettier. The rules we will be setting up is how I like to have it configured, but I highly suggest you do some searching of your own to figure out what works best for your project. I am not going to go into details about the configuration, because that makes this post to long and the details do not really matter that much. Most likely you will end up changing them to fit your project.

First, we will set up our .eslintrc.json file. This file contains all the rules for ESLint. Create this file with the following content:

{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json",
    "ecmaFeatures": {
      "jsx": true
    },
    "ecmaVersion": 2018,
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint", "simple-import-sort"],
  "extends": [
    "prettier",
    "prettier/react",
    "eslint:recommended",
    "plugin:react/recommended",
    "prettier/@typescript-eslint",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {
    "@typescript-eslint/explicit-member-accessibility": "off",
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/no-unused-vars": "off",
    "@typescript-eslint/no-submodule-imports": "off",
    "@typescript-eslint/no-unused-expressions": "warn",
    "@typescript-eslint/no-use-before-define": "off",
    "@typescript-eslint/jsx-no-lambda": "off",
    "@typescript-eslint/prefer-interface": "off",
    "@typescript-eslint/interface-name-prefix": "off",
    "@typescript-eslint/camelcase": "off",
    "simple-import-sort/sort": "warn",
    "no-console": "off",
    "sort-keys": "off"
  },
  "env": {
    "browser": true,
    "es6": true
  }
}

:information_source: You could also include a .eslintignore or .stylelintignore file for any files you would like ESLint/Stylelint to ignore.

and our .prettierrc

{
  "printWidth": 100,
  "singleQuote": true,
  "semi": true,
  "tabWidth": 2,
  "trailingComma": "none",
  "useTabs": false,
  "arrowParens": "avoid"
}

lastly, I am going to create our .stylelintrc file

{
  "extends": ["stylelint-config-recommended", "stylelint-config-recess-order"]
}

:information_source: If you are using Visual Studio Code as your editor, I highly suggest you install the ESLint, Stylelint, and Prettier extensions to get these warnings and functionality directly in the ‘Problems’ pane of VSCode.

The configuration is now in place and well add some new scripts to our package.json file to run them. Add these five lines to your file under the scripts section. The first two will only display the issues found, the next two will attempt to auto fix any auto fixable issues while the last one will run both our linters at the same time.

  "lint:styles": "npx stylelint '**/**.css'",
  "lint:ts": "eslint src/**/*.tsx src/*.tsx",
  "lint:styles:fix": "npx stylelint '**/**.css' --fix",
  "lint:ts:fix": "eslint src/**/*.tsx src/*.tsx --fix",
  "lint": "npm run lint:styles && npm run lint:ts"

Let us see how the code we added earlier is doing. First run npm run lint:ts and then npm run lint:styles (alternatively just run npm run lint). You should see an output something like this:


> eslint src/**/*.tsx src/*.tsx


src/RandomNumberCard.tsx
  1:1   warning  Run autofix to sort these imports!  simple-import-sort/sort
  4:26  warning  Missing return type on function     @typescript-eslint/explicit-module-boundary-types

✖ 2 problems (0 errors, 2 warnings)
  0 errors and 1 warning potentially fixable with the `--fix` option.


> npx stylelint '**/**.css'


src/styles/main.css
 3:3    Expected "width" to come before "height"                  order/properties-order
 5:3    Expected "color" to come before "background-color"        order/properties-order
 6:3    Expected "display" to come before "color"                 order/properties-order
 9:3    Expected "align-items" to come before "justify-content"   order/properties-order


Here we can see that it found two issues linting our React files and four issues in our CSS file. We can try to auto fix these issues running npm run lint:ts:fix and npm run lint:styles:fix. If we then run the same commands again we should see it was able to fix five out of six issues:


> eslint src/**/*.tsx src/*.tsx


src/RandomNumberCard.tsx
  3:33  warning  Missing return type on function  @typescript-eslint/explicit-module-boundary-types

✖ 1 problem (0 errors, 1 warning)

This was one issue it was unable to fix automatically, so let us fix this ourselves. We will add the return type to the component in the RandomNumberCard.tsx like this:

export const RandomNumberCard = () : React.ReactElement => {

If we now run the command npm run ts:lint for a third time we can see that no issues are found! Our code is now following the rules set by us in our configuration and we can proceed.

Prettier

I am using prettier as VSCode plugin. You can also auto format all your files using the Prettier CLI with the command prettier --write "src/**/*.tsx" "src/*.tsx" "**/**.css" More on the prettier CLI can be found here

Run linters in our pipeline

Having linting enabled in our app will do no good if we do not enforce it. There are several ways we can enforce it:

Git hooks

Adding the linting to our repositories git commit hook would run it and abort the commit if our rules are not followed. Although it is an unlikely situation, the hook could be removed from the local git directory by a developer, or fail to run for various reasons.

:information_source: Hooks are programs you can place in a hooks directory to trigger actions at certain points in git’s execution - githooks Documentation

Pipeline

Running the linting in our pipeline would ensure everything happens server-side and there are fewer opportunities for failures. We could set the pipeline to fail if our linting fail, and restrict merging to required successful builds. The following pipeline configuration gives one example of a validation only pipeline and illustrates the few steps needed to enforce our linting rules:

trigger:
  - master

pool:
  vmImage: "ubuntu-latest"

steps:
  - task: NodeTool@0
    inputs:
      versionSpec: "10.x"
    displayName: "Install Node.js"
  - task: Npm@1
    displayName: "npm install"
    inputs:
      command: "install"
  - task: Npm@1
    displayName: "Lint files"
    inputs:
      command: "custom"
      customCommand: "run lint"

The main job here is done by our Lint files step, this will automatically break the build if our files are not up to code. I reverted some of the changes we had done before running the pipeline, and this was the result: lint-pipeline

Reflection

So, in this post we create a custom React module, we ensure our code is kept clean using linters and we ensured that this was enforced during the build. As mentioned previously, ESLint and Stylelint comes with a vast majority of rules allowing you full control over your code. It will be up to you to configure it correctly for your project. In one of the next posts, we will extend to implementing some simple tests and ensuring code coverage is kept at an acceptable level.

See you next time!

// Joachim

Changes after publish

2020-10-02: Clearified some usages of prettier