Table of Contents

Introduction

Recently, I’ve been working on a number of TypeScript projects. This includes both back-end Node JS APIs and front-end React user intefaces. I wanted to automate the code quality assessment as much as possible, but it turned out this is not trivial.

In this post, I’ll describe a set of tools for static analysis and automated audits. I’ll demonstrate how to configure and execute them locally so you can get quick feedback during development. The same scripts and commands can be used in any CI/CD tool.

Most of these tools and configurations are not specific to TypeScript and can be adapted to plain JavaScript. However, if you’re looking at automated code quality tooling for large code bases then TypeScript is your friend.

There are many code snippets in this post. At any point, you can refer to the GitHub repo which has all these tools and configurations ready to go. Just clone and play with it!

ESLint and Friends

ESLint has replaced TSLint as the go-to static analsys tool for TypeScript. ESLint has a plugin architecture, where additional rules and language support can be installed and configured. You can configure ESLint’s plugins and rules via a .eslintrc.js file in the root of your project.

We’ll need the following ESLint plugins and auxiliary packages:

Prettier is a popular code formatting tool. If you want to integrate it with ESLint, we’ll need the following.

If you’re using React, you’ll also need:

Let’s install all of these via npm or yarn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Core TypeScript ESLint support
npm install --save-dev @typescript-eslint/parser 
npm install --save-dev @typescript-eslint/eslint-plugin

# Extra code quality rules
npm install --save-dev eslint-plugin-import 
npm install --save-dev eslint-plugin-sonarjs

# Prettier and its ESLint integration
npm install --save-dev prettier 
npm install --save-dev eslint-config-prettier 
npm install --save-dev eslint-plugin-prettier 
npm install --save-dev prettier-eslint

# React rules for ESLint (Optional)
npm install --save-dev eslint-plugin-react eslint-plugin-react-hooks

Now let’s create the .eslintrc.js configuration file. Here is the full file, below is just a highlight of the important sections:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// `.eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser', // Parse TypeScript
  parserOptions: {
    project: './tsconfig.json',
    jsx: false // True for React
  },

  rules: { 
    /* disable or configure individual rules */

    /* Will need the following for React hooks: */
    // "react-hooks/rules-of-hooks": "error",
    // "react-hooks/exhaustive-deps": "warn"
  },

  // Use the rules from these plugins 
  extends: [
    'plugin:@typescript-eslint/recommended',
    'prettier/@typescript-eslint',
    'prettier',
    'plugin:prettier/recommended',
    'plugin:sonarjs/recommended',
    // 'plugin:react/recommended' // If we need React
  ]
};

You can configure the behaviour or Prettier with a .prettierrc and .prettierignore files in the root of the project.

Finally, let’s add two additional commands to our package.json. They’ll allow us to run ESLint and automatically fix formatting issues:

1
2
3
4
5
6
7
8
{
  ...
  "scripts": {
    ...
    "lint": "eslint './src/**/*.{tsx,ts}'",
    "lint-fix": "eslint './src/**/*.{tsx,ts}' --fix",
  }
}

Jest and Code Coverage

Jest has emerged as the most popular JavaScript testing framework. To make it work with TypeScript we’ll need a helper module called ts-jest. It dynamically compiles the TypeScript code. We also need jest to generate a test coverage report. We’ll feed this report into SonarQube later on for further analysis. There’s a handy module for this called jest-sonar-reporter.

We can install them and jest itself via npm or yarn:

1
2
npm install --save-dev jest @types/jest
npm install --save-dev ts-jest jest-sonar-reporter

Jest can be configured via a file called jest.config.js in the project root folder. We’ll use it to transform all test files matching the Jest naming convention with ts-jest and generate reports via jest-sonar-reporter. Here is the full file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module.exports = {
  coverageDirectory: './coverage',
  collectCoverageFrom: ['src/**/*.ts', 'src/**/*.tsx'],
  testEnvironment: 'node',
  modulePaths: ['<rootDir>/src', 'node_modules'],
  roots: ['src'],
  transform: {
    '^.+\\.tsx?$': 'ts-jest'
  },
  testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$',
  coverageReporters: ['json', 'lcov', 'text'],
  coveragePathIgnorePatterns: ['.*/src/.*\\.d\\.ts', '.*/src/testUtil/.*'],
  testResultsProcessor: 'jest-sonar-reporter'

  // Use the below to set coverage goals.
  // "npm test" will fail if these metrics are violated
  coverageThreshold: {
    global: {
      statements: 80,
      branches: 80,
      functions: 80,
      lines: 80
    }
  }
};

Now, we need to tell jest-sonar-reporter where to put the coverage output. Edit package.json and add the following at the end:

1
2
3
4
5
6
7
8
{
  ...
  "jestSonar": {
    "reportPath": "coverage",
    "reportFile": "test-reporter.xml",
    "indent": 4
  }
}

As a last step, we need to ensure we generate the coverage reports during testing. Add the following to your scripts in package.json:

1
"test": "jest --forceExit --detectOpenHandles --coverage"

If you run npm test you should see a new folder ./coverage with the code coverage reports.

Jest and Create React App (CRA)

Many React projects use the Create React App code generator. CRA uses Jest internally but hides many of its properties. Also, it doesn’t pick up the configuration in jest.config.js.

Fortunately, you can still configure the coverage output directly in the package.json script:

1
2
3
4
5
6
{
  ...
  "scripts": {
    "test": "CI=true react-scripts test --silent --env=jsdom --coverage --testResultsProcessor jest-sonar-reporter",
  }
}

You will still need to install jest-sonar-reporter and add the "jestSonar" configuration as in the previous section.

Dependency Audit / Supply Chain Analysis

A large JavaScript project can have hundreds of direct dependencies. Each of them can have many dependencies on its own. A security vulnerability in any of them can become a vulnerability for the entire project. This is known as a Supply Chain Attack.

To address this, both npm and yarn introduced a new command called audit. It checks whether your dependencies have known vulnerabilities and provides a report. Unfortunately, working directly with npm audit can be tricky. It provides limited options to filter threats by severity and to white list modules. It can generate a lot of noise.

There are a few auxiliary tools which “wrap” the audit command and give developers much more control. A popular option is audit-ci. It automatically detects whether you’re using npm or yarn and gives you plenty of configuration options. Let’s install it:

1
npm install --save-dev audit-ci

Now, let’s create a file audit-ci.json with its config:

1
2
3
4
5
6
7
8
9
{
  "high": true,
  "package-manager": "auto",
  "report-type": "summary",
  "path-whitelist": [
    "1217|sonarqube-scanner>download>decompress",
    "1217|sonarqube-verify>sonarqube-scanner>download>decompress"
  ]
}

In the above, we asked audit-ci to fail only if there’re threats classified as high or more critical. We also white listed a couple of threats from development dependencies so they will not cause failure.

Lastly, let’s configure a script for audit-ci in package.json:

1
2
3
4
5
6
{
  ...
  "scripts": {
    "audit-dependencies": "audit-ci --config audit-ci.json",
  }
}

Now, you should be able to run npm run audit-dependencies and analyze your supply chain for vulnerabilities.

Local SonarQube

SonarQube is a popular tool for static source code analysis. It supports many languages including TypeScript. Typically, a company would have a SonarQube instance which analyses all of its projects. The CI/CD pipeline would push your code to the SonarQube instance during each build.

I often find it convenient to run SonarQube locally so I can quickly analyse my code before integrating it. This is quite easy with a Docker container.

Let’s create a docker-compose.sonar.yml file:

1
2
3
4
5
6
7
8
version: '3'
services:
  sonarqube:
    container_name: sonarqube
    image: sonarqube:latest
    ports:
      - '9000:9000'
      - '9092:9092'

Then we can start/stop SonarQube with:

1
2
3
4
5
# Start it ...
docker-compose -f docker-compose.sonar.yml up -d

# Stop it ...
docker-compose -f docker-compose.sonar.yml down

After starting it, wait for Sonar to load on http://localhost:9000 (it can take 1-2mins). Navigate to http://localhost:9000/dashboard and use admin/admin to login.

Note that the local SonarQube instance uses in-memory data storage. Any changes you make will be wiped out on restart.

Run SonarQube Analysis

Before you analyse a project in SonarQube, you need to create a config file called sonar-project.properties in the root folder:

1
2
3
4
5
6
7
8
9
10
11
sonar.projectKey=secure-typescript-boilerplate
sonar.projectName=secure-typescript-boilerplate
sonar.projectVersion=1.0
sonar.language=ts
sonar.sources=src
sonar.sourceEncoding=UTF-8
sonar.exclusions=src/**/*.test.ts
sonar.test.inclusions=src/**/*.test.ts
sonar.coverage.exclusions=src/**/*.test.ts,src/**/*.mock.ts,node_modules/*,coverage/lcov-report/*
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.testExecutionReportPaths=coverage/test-reporter.xml

This file defines the project name, key, and version. It also specifies the programming language, code location, and the code coverage report.

To run the SonarQube analysis we will need an auxiliary module called sonarqube-scanner:

1
npm install --save-dev sonarqube-scanner

The module expects to find a file called sonar-project.js in the project root. Let’s create it:

1
2
3
4
5
6
7
8
9
10
11
12
const sonarqubeScanner = require('sonarqube-scanner');

sonarqubeScanner(
  {
    serverUrl: process.env.SONAR_SERVER || 'http://localhost:9000',
    token: process.env.SONAR_TOKEN || '',
    options: {}
  },
  () => {
    console.log('>> Sonar analysis is done!');
  }
);

Now we can run sonarqube-scanner with node sonar-project.js and this will submit our code sonar server. By default, the local server is used, but this can be overriden via environment variables. Go on and test it to make sure it works.

The sonarqube-scanner module has one shortcoming. It starts the SonarQube analysis asynchronously and it doesn’t wait for it to complete. This is problematic when you want to integrate the code scan into a CI/CD pipeline. You may need to wait for the analysis to complete and either fail/proceed based on the result.

Thus, we’ll use another module called sonarqube-verify:

1
npm install --save-dev sonarqube-verify

The sonarqube-verify library is a wrapper of sonarqube-scanner. It starts the code analsys and then checks its progress every few seconds. On completion, it either suceeds or fails based on the analysis result.

Let’s create the script to run it:

1
2
3
4
5
6
{
  ...
  "scripts": {
    "sonar": "sonarqube-verify"
  }
}

Now we can run the analysis:

1
2
3
4
5
6
7
8
# Generate the test coverage report
npm test

# Run the analysis (remember to start SonarQube)
npm run sonar

# Visit http://localhost:9000/projects to see result 
# login with admin/admin

Check out the documentation of sonarqube-verify to see how to push to a remote server via env variables.

Git Hooks with Husky

Git hooks are a great way to ensure code quality before your code hits the CI/CD pipeline. They can significantly speed up the developer feedback loop and can enforce discipline in a team. Husky is a popular Node JS library for custom hooks. Let’s install it:

1
npm install --save-dev husky

Now we need to create a .huskyrc file which defines the hook:

1
2
3
"hooks": {
  "pre-push": "npm build && npm test && npm run lint && npm run audit-dependencies"
}

This ensures that the code builds, the tests pass, no ESLint errors are present, and there’re no known vulnerabilities in the dependencies. Depending on the project size these commands can take a while. I prefer to use pre-push instead of pre-commit so that local commits are quick, but everyone is forced to clean up their code before they integrate. Every team has its own philosophy - use whatever works for you!

From now on, on every git push the above command will run. If it fails, no code will be pushed to the remote repository.