In this blog post I want to describe how Cypress and ESLint can be added to a new Angular project.

Angular currently comes without any linting tool and without an end to end testing tool as protractor is not included in Angular anymore and TSLint is marked as deprecated.

As a replacement Cypress and ESLint jump into place to fill this gap. Cypress is the go to tool for end to end testing here and ESLint can replace the deprecated TSLint.

In this article we will cover how we can add Cypress and ESLint to a new Angular Project. We will also briefly cover a migration from TSLint to ESLint.

TOC

Creating a new project

With this tweet from Stephen Fluin I learned that if you always want to have the latest version of Angular when starting a new project, you can use

npx @angular/cli new <my-project>

instead of the standard

ng new <my-project>

Doing this brings us the following folder structure with the latest versios:

├── src
│   └── ...
├── .browserslistrc
├── .editorconfig
├── .gitignore
├── angular.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json

At the time of writing the Angular Version is 12.2.0. So we will ride with that for this post.

Adding ESLint to an Angular Project

Adding ESLint is done by using the schematics James Henry is maintaining on GitHub: https://github.com/angular-eslint/angular-eslint

Also this video helps a lot when migrating or adding ESLint to your Angular project.

You can add ESLint by using the schematics with

ng add @angular-eslint/schematics

Screenshot of console adding eslint initially

After you have done this your package.json is showing those changes. A lint script and the dependencies have been added.

{
  "name": "angular-eslint-cypress",
  "version": "0.0.0",
  "scripts": {
    // ...
    "lint": "ng lint"
  },
  "private": true,
  "dependencies": {
    // ...
  },
  "devDependencies": {
    // ...
    "@angular-eslint/builder": "12.5.0",
    "@angular-eslint/eslint-plugin": "12.5.0",
    "@angular-eslint/eslint-plugin-template": "12.5.0",
    "@angular-eslint/schematics": "12.5.0",
    "@angular-eslint/template-parser": "12.5.0",
    // ...
    "@typescript-eslint/eslint-plugin": "4.28.2",
    "@typescript-eslint/parser": "4.28.2",
    "eslint": "^7.26.0"
    // ...
  }
}

(I pointed out only the changes here).

The angular.json has changes as well. A lint property was added with the appropriate builders.

{
  // ...
  "cli": {
    "defaultCollection": "@angular-eslint/schematics"
  },
  // ...
  "projects": {
    "angular-eslint-cypress": {
      // ...
      "architect": {
        "build": {
          // ...
        },
        "serve": {
          // ...
        },
        "extract-i18n": {
          // ...
        },
        "test": {
          // ...
        },
        "lint": {
          "builder": "@angular-eslint/builder:lint",
          "options": {
            "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
          }
        }
      }
    }
  }
}

In the end a new .eslintrc.json was added as well containing all the rules.

.
├── src
│   └── ...
├── .browserslistrc
├── .editorconfig
├── .eslintrc.json  // <-- Was added!
├── .gitignore
├── angular.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json

You can now apply rules from @angular-eslint/, @typescript-eslint/... or the standard rules from ESLint.

{
  // ...
    "rules": {
        "@angular-eslint/...": [...],
        "@typescript-eslint/...": [...],
        "newline-before-return": "error",
        "max-len": "off",
        "no-useless-constructor": "off",
    }

  // ...
}

If we now run the npm run lint command we can see that ESLint is being run.

Screenshot of console executing eslint

Speeding up the process

In a project I am working on I found the speed of ESLint pretty slow so I searched around and found the recommendation to introduce a separate tsconfig.eslint.json extending the normal tsconfig.json and only including the ts files.

tsconfig.eslint.json

{
  "extends": "./tsconfig.json",
  "include": ["src/**/*.ts"]
}

In the .eslintrc.json you can now use this file instead of the current one:

{
  "root": true,
  "ignorePatterns": ["projects/**/*"],
  "overrides": [
    {
      "files": ["*.ts"],
      "parserOptions": {
        "project": ["tsconfig.eslint.json"],
        "createDefaultProgram": true
      }
      // ...
    }
    // ...
  ]
}

Removing TSLint

If you have TSLint in your project you can remove or migrate as mentioned in the video above. This is the cmd to do it. The specific parameters are explained on the GitHub Repo

ng g @angular-eslint/schematics:convert-tslint-to-eslint --remove-tslint-if-no-more-tslint-targets --ignore-existing-tslint-config

Adding Cypress to a project

So there is the end to end testing left. For this we will introduce and use Cypress to rely on the latest toolset and best integration.

We can find the schematic to add Cypress on npm and/or GitHub

We add cypress by executing

ng add @cypress/schematic

Screenshot of console adding cypress

After having done this we find the following changes in our repo:

The package.json has been updated in the scripts and dependencies sections.

package.json

{
  "name": "angular-eslint-cypress",
  "version": "0.0.0",
  "scripts": {
    // ...
    "e2e": "ng e2e",
    "cypress:open": "cypress open",
    "cypress:run": "cypress run"
  },
  "private": true,
  "dependencies": {
    // ...
  },
  "devDependencies": {
    // ...
    "@cypress/schematic": "^1.5.1",
    // ...
    "typescript": "~4.3.5",
    "cypress": "8.5.0"
  }
}

In the angular.json the cypress-run, cypress-open and e2e properties have been added as well.

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "cli": {
    // ...
  },
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-eslint-cypress": {
      // ...
      "architect": {
        "build": {
          // ...
        },
        "serve": {
          // ...
        },
        "extract-i18n": {
          // ...
        },
        "test": {
          // ...
        },
        "lint": {
          // ...
        },
        "cypress-run": {
          "builder": "@cypress/schematic:cypress",
          "options": {
            "devServerTarget": "angular-eslint-cypress:serve"
          },
          "configurations": {
            "production": {
              "devServerTarget": "angular-eslint-cypress:serve:production"
            }
          }
        },
        "cypress-open": {
          "builder": "@cypress/schematic:cypress",
          "options": {
            "watch": true,
            "headless": false
          }
        },
        "e2e": {
          "builder": "@cypress/schematic:cypress",
          "options": {
            "devServerTarget": "angular-eslint-cypress:serve",
            "watch": true,
            "headless": false
          },
          "configurations": {
            "production": {
              "devServerTarget": "angular-eslint-cypress:serve:production"
            }
          }
        }
      }
    }
  }
}

If we look at the folder structure a complete cypress folder was added, where our tests can take place, and a cypress.json for the configuration has been added.

.
├── cypress  // <-- Complete Folder was added!
│   ├── integration
│   │   └── spec.ts
│   ├── plugins
│   │   └── index.ts
│   ├── support
│   │   ├── commands.ts
│   │   └── index.ts
│   └── tsconfig.json
├── src
│   └── ...
├── .browserslistrc
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── angular.json
├── cypress.json  // <-- Was added!
├── karma.conf.js
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json

Finishing touches for cypress

To run cypress we have to start the local Angular application on http://localhost:4200 as well and in parallel start the cypress runner to reach the site under where it is living.

We can do this by installing a small http-server https://www.npmjs.com/package/http-server and run it in parallel to either the dist build (cypress:run) or the dev build (cypress:open). To run commands in parallel we can install the package concurrently. Now we can modify the commands as below:

package.json

"cypress:open": "concurrently \"npm start\" \"cypress open\"",
"cypress:run": "npm run build && concurrently \"npm run serve:dist\" \"cypress run\"",
"serve:dist": "http-server ./dist/angular-eslint-cypress -a localhost -p 4200 -c-1"

Summary

In this blog post we saw how we can start an Angular application and add the latest tools of end to end testing and linting to it.

I hope this helped!

Fabian

Other blogposts