Strict rules for new Angular project

Tutorial how to create and configure Angular project from the ground up

Andrey Korovin
MW EXPERTS
7 min readDec 1, 2019

--

Article was updated on 29 February 2020 to follow best practices.

Intro

Very often I saw situations when project was not configured strictly enough, and it is a big mistake, because in future changing quality rules is a real pain. All team should spend 1–5 days fixing linter and typescript issues to be able pass new strict policies. So don’t make this common mistake and start you projects with maximum code quality checks.

Prepare environment

Git

First thing that should be installed on any developers environment is Git. Download and install it on your operating system.

Then we have to configure our git with our personal information. This information will be included in all your commits and anybody can identify author of each code line by this credentials.

// For configuring globally (for all your projects) use commands
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

// For configuring particular project go to project's path and use commands
$ git config user.name "John Doe"
$ git config user.email johndoe@example.com

NodeJs and NVM

Next thing that you need to start with Angular is NodeJs. The best approach to install it on any platform is to use Node Version Manager. It will give you a chance to easily switch to any version of NodeJs. And even make it automatically for each particular project.

  1. On MacOs you have to create .zshrc in home directory before installation.
$ touch ~/.zshrc 

2. Run instructions to install or update NVM.

3. Install latest LTS version of NodeJs.

$ nvm install 'lts/*'

4. Set this version as default.

$ nvm use 'lts/*'
$ nvm alias default 'lts/*'

5. For calling nvm use automatically whenever you enter a directory that contains an .nvmrc file with a string telling nvm which node to use follow this instructions.

Install CLI and create project

  1. Install the CLI using the npm package manager.
$ npm install -g @angular/cli

2. Navigate to your projects path and create bare Angular project

$ cd ~/Projects
$ ng new --create-application false --new-project-root apps project-name

We created bare project with configured directory apps for new applications. In other words we implemented foundation for monorepository.

3. Create and commit first application in your monorepository.

$ cd project-name
$ ng generate application --routing true --style scss website
$ git add .
$ git commit -m 'website application created'

If you need to add more applications just repeat this step with different app name instead of website.

Creating libs

I suggest to create libraries in libs folder. To make it we have to change angular.json file and replace line:

"newProjectRoot": "apps",

To this one:

"newProjectRoot": "libs",

And execute creation command:

$ ng generate library @group-name/lib-name

We added group-name for better grouping your libraries by domain or some scope and for modern naming conventions. If you want to publish your libs to some repository, for example npm, you have to create organisation with name of corresponding group-name and publish there. It is like a namespace for group of libs like:

"@angular/common"
"@angular/compiler"
"@angular/core"
"@angular/forms"

I will write more information on publishing your libs to npm in future articles. But for now just use this convention.

And don’t forget to revert your angular.json to "newProjectRoot": "apps"

And I advise not to use default project in angular.json, later we will setup all scripts for each project individually, so just set it as empty:

"defaultProject": ""

Additional configurations

NodeJs version

Add .nvmrc file with lts/* in it. Or you can use exact version of node, for example 12.16.1. This version would be automatically enabled if you followed my instructions or you can enable it manually using command $ nvm use.

$ touch .nvmrc
$ echo "lts/*" > .nvmrc

Strict null checks

Add following rule to tsconfig.json to add strict null checks option to your compiler. It will protect you from very common mistakes of reading property of undefined or null.

"compilerOptions": {
"strictNullChecks": true,
}

Or better, if you want more strict rules you can apply all strict options and modify to your needs.

/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */

/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */

Strict TsLint rules

Enable all tslint rules to their strictest settings, just replace in tslint.json

"extends": "tslint:recommended",

To next line

"extends": "tslint:all",

And add this lines to disable some irrelevant rules

"no-use-before-declare": false,
"file-name-casing": [
true,
"kebab-case"
],
"no-implicit-dependencies": false,
"comment-format": [
true,
"check-space"
],
"newline-per-chained-call": false,
"no-object-literal-type-assertion": [
true,
{
"allow-arguments": true
}
],
"no-floating-promises": false,
"promise-function-async": false,
"completed-docs": false,
"prefer-function-over-method": false,
"no-submodule-imports": false,
"no-unsafe-any": false,
"no-inferrable-types": false,
"typedef": [
true,
"call-signature",
"arrow-call-signature",
"parameter",
"arrow-parameter",
"property-declaration",
"variable-declaration",
"variable-declaration-ignore-function",
"member-variable-declaration",
"object-destructuring",
"array-destructuring"
],
"no-null-keyword": false,
"no-parameter-properties": false,
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-leading-underscore",
"require-const-for-all-caps"
]
},
"max-line-length": false,
"increment-decrement": [true, "allow-post"],
"object-literal-key-quotes": false,
"align": [
true,
"parameters",
"statements",
"members"
],

In process of developing disable or tune manually those rules you don’t like. And don’t forget to enable support of tslint in your IDE.

Lint SCSS with stylelint

Add stylelint for checking quality of your scss code

$ npm install --save-dev stylelint stylelint-config-sass-guidelines
$ touch .stylelintrc.json

And configure rules in .stylelintrc.json

{
"extends": "stylelint-config-sass-guidelines",
"ignoreFiles": [
"dist/**/*"
],
"rules": {
"no-empty-source": true
}
}

In process of developing disable or tune manually those rules you don’t like. And don’t forget to enable support of stylelint in your IDE.

Prettier

Prettier is an opinionated code formatter. Sometimes Prettier won’t give you the exact output you desire, however, the benefits it provides outweigh the flaws.

$ npm install --save-dev --save-exact prettier
$ touch .prettierrc
$ touch .prettierignore

Add configuration to file .prettierrc

{
"printWidth": 120,
"tabWidth": 2,
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "all"
}

And configuration to file .prettierignore for ignoring directories and files

test.ts

Tests and E2E

In order to run e2e tests in headless mode we need to do the following:

1. In the Karma configuration file, apps/website/karma.conf.js, add a custom launcher called ChromeHeadlessCI below browsers:

browsers: ['Chrome'],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},

2. In the root folder of your e2e tests project, create a new file named apps/website/e2e/protractor-ci.conf.js. This new file extends the original apps/website/e2e/protractor.conf.js.

const config = require('./protractor.conf').config;

config.capabilities = {
browserName: 'chrome',
chromeOptions: {
args: ['--headless', '--no-sandbox']
}
};

exports.config = config;

HOOKS

In order to run your linters, formatters or other scripts we can setup git hooks. We will use 2 packages for this purpose:

  • husky — let us setup git hooks
  • lint-staged — let us execute commands only on staged files
$ npm i -D husky lint-staged

Configure lint-staged, just add in package.json

"lint-staged": {
"{apps,libs}/**/*.{ts,scss,html}": [
"prettier --write"
],
"{apps,libs}/**/*.scss": [
"stylelint --fix"
]
},

Next step to create our scripts for different hooks. I prefer to create folder tools for my shell scripts. We will setup 3 hooks:

  1. pre-commit — Hook that will run before commit executed. This hook will execute prettier and linters.
  2. commit-msg — Hook can change commit message, we will use it to add current branch name to commit message.
  3. pre-push — Hook executed before push, we will use it to run our tests and e2e.

Husky configuration in our package.json file:

"husky": {
"hooks": {
"pre-commit": "npm run pre-commit",
"commit-msg": "node ./tools/commit-msg.js",
"pre-push": "npm run pre-push"
}
},

Script ./tools/pre-commit.sh

#!/bin/sh

npx lint-staged || exit
npm run lint:website || exit

Script ./tools/commit-msg.js

#!/usr/bin/env nodeconst fs = require('fs');
const { execSync } = require('child_process');

const message = fs.readFileSync(process.env.HUSKY_GIT_PARAMS, 'utf8').trim();
const currentBranch = getCurrentBranch();

fs.writeFileSync(process.env.HUSKY_GIT_PARAMS, `${currentBranch}: ${message}`);
process.exit(0);

function getCurrentBranch() {
const branches = execSync('git branch', { encoding: 'utf8' });
return branches
.split('\n')
.find((b) => b.charAt(0) === '*')
.trim()
.substring(2);
}

Script ./tools/pre-push.sh

#!/bin/sh

npm run test-ci:website || exit
npm run e2e-ci:website || exit

We need to give proper file access to our tools

$ chmod 755 ./tools/pre-commit.sh
$ chmod 755 ./tools/commit-msg.js
$ chmod 755 ./tools/pre-push.sh

And finally add scripts to package.json for easy launch of commands:

"scripts": {
"------------------ website ------------------": "",
"start:website": "ng serve website --port 4200",
"build:website": "ng build website --configuration production",
"lint:website": "ng lint website --fix",
"lint-scss:website": "stylelint --formatter verbose --fix \"apps/website/**/*.scss\"",
"test:website": "ng test website --code-coverage --no-watch",
"test-watch:website": "ng test website",
"test-ci:website": "ng test website --no-watch --no-progress --browsers=ChromeHeadlessCI",
"e2e:website": "ng e2e website --port 6200",
"e2e-ci:website": "ng e2e website --port 6200 --protractor-config=apps/website/e2e/protractor-ci.conf.js",
"------------------ common ------------------": "",
"prettier": "prettier --write \"{apps,libs}/**/*.{ts,scss,html}\"",
"lint-scss": "stylelint --formatter verbose --fix \"{apps,libs}/**/*.scss\"",
"pre-commit": "./tools/pre-commit.sh",
"pre-push": "./tools/pre-push.sh"
},

Now you can run all this commands one by one and check if everything works correctly.

— — — — — — — — — — — — —

🚀 Follow us on Medium, Telegram or Twitter to read more about Angular, Software architecture, Best practices building web applications!

--

--

Andrey Korovin
MW EXPERTS

Consultant | Solution architect | Software engineer