Essential JavaScript Tooling
Tags: javascript, typescript, tutorial, tooling
Published: 2021-09-03 21:45:30

### Quick Summary

Software development is great and fun but it can be a mess and a very bad influence if the developer experience is not given the importance and prioritized. Some standard development practices along with proper tooling can greatly improve this experience. This also aids in keeping the codebase clean and the repository in a good health. In this article, we will cover some development practices and essential tools to help you improve the development experience.

### Audience

This article is essentially for JS developers and covers tooling around the JS ecosystem, however, the same concepts can be applied in other languages with the tooling available there. We will cover the below topics in this article.

- Code Linting using [ESLint](https://eslint.org/).
- Code Formatting using [Prettier](https://prettier.io/).
- Setting up [Git Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) for linting and code-formatting.
- Conventional Commits using [Commitizen](https://github.com/commitizen/cz-cli).
- [Github Actions](https://github.com/features/actions) for CI/CD

### Code-Linting

Code linting is very essential and important especially when you are working with an interpreted language like JavaScript. Linters are essentially static code analyzers it scans through your code without running it and flags any programmatic errors, stylistic errors, and any suspicious contracts. Properly setting up a linter in your codebase can help catch errors early on and ensure that some standard practices are being followed. This makes the codebase clean and helps in code reviews.

Linters can enforce code styling and rules like no unused variables or no console.log statements or no unused imports in fact it's a very powerful tool and can do much more than that.

Setting up Linters can be tedious and tiring but it brings great results once you adopt it and start using it properly in your team. I personally believe that linters and such tools should come with the language itself and developers shouldn't have to worry about setting one up. This experience is much more improved with [deno](https://deno.land/) (A secure runtime for JavaScript and TypeScript) which ships with a built-in linter and formatter for JavaScript and TypeScript.  

Now that we have a basic understanding of linters let's see how we can set one up in a TypeScript project. We will be using ESLint which is very popular in the JS ecosystem and is fully pluggable. Every single rule in ESLint is a plugin, this means you can start off with a base configuration and then extend the rules to your needs by adding in more rules as a plugin.

Create a new folder and run `npm init -y` to initialize your project, this will create a `package.json` in the root directory with some stub fields.

Next, install the required `dev-dependencies` into the project.

```bash
yarn add -D nodemon ts-node eslint typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser
```

Let's first quickly setup TypeScript by running `tsc --init` which will initialize the `tsconfig.json` file in the project root, the only thing we are going to change is uncommenting `outdir` option and change it to `dist`

```bash
"outDir": "dist"
```

Next, add a `.eslintrc` file in the project's root directory. This is going to be the configuration file for `eslint` where we can customize our rules. To quickly get started let's add a bare minimum configuration to the file.

```json
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
  ]
}
```

Let's look at some of these options briefly, the `root` key basically tells eslint to stop looking for configuration files in the parent directories. By default, eslint looks for config files in all parent folders until the root directory which can lead to unexpected results, plus this option can be very handy when you have a monorepo in place and each project uses a different configuration.

The `extends` key lets you add the configuration you want to use for your project this can be a string that specifies a configuration or a path to a configuration file, here we are using two configurations first one is the [eslint-recommended](https://eslint.org/docs/rules/), and the next up is typescript-eslint recommended. You can also use [airbnb-typescript](https://www.npmjs.com/package/eslint-config-airbnb-typescript) config which is also very popular.

Now let's add some scripts in our `package.json` to run our project in dev mode and a script for linting.

```json
"dev": "nodemon --watch '**/*.ts' --exec 'ts-node' src/index.ts",
"lint": "eslint . --ext .ts"
```

And for the sake of testing our eslint setup let's create an `index.ts` file and very simple for loop with an intended unused variable.

```tsx
//src/index.ts

const range = 10;

for (let i = 0; i < 10; i++) {
  console.log("i : ", i);
}
```

Now let's run `yarn lint` and we will get a warning on our console.

This is great our eslint setup is working, but let's say we want to be more strict on our code base and want this to be an error and cause lint to fail, to do that head over to your `.eslintrc` file and add the below rule.

```json
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  "rules": {
    "no-unused-vars": "error"  // "error" | "warn" | "off"
  }
}
```

Now if you run `yarn lint` your console will display this as an error and lint won't succeed, also if you have the eslint extension installed on vs code it will highlight this issue.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7uhay0wlkjppm93w6eqo.png)

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/25ps9f2vnkox2sfzn2yf.png)

We can also add another script to auto-fix the lint issues, note that not all issues can be auto-fixed and you can get a list of all the rules that can be auto-fixed [here](https://eslint.org/docs/rules/).

```json
"lint-fix": "eslint . --ext .ts --fix"
```

That's pretty much for the linting setup, I hope you can now create a mental model for yourself and see how useful this tool can be if used properly.

### Code Formatting

Let's accept it, seeing different code styles in a repo is a mess and if not controlled you can see all sorts of code conventions being followed in the repo. This is where code formatters come to our rescue, but before we jump into setting one up in our project we first need to understand that code formatters and linters are essentially not the same things, although there is a very thin line between them but they both serve a different purpose. Code-formatters essentially deal with the formatting of code and apply the code styling you have specified, they don't check code for potential bugs or issues and adhere to the styling. 

Prettier is a very famous tool in the JS ecosystem and we will be setting it up in our project, with prettier we can enforce style rules like max-length, tab width, single-quotes, semicolon, etc. It's basically an opinionated code formatter that takes your code and parses it into an AST (Abstract Syntax Tree) discarding the original styling, after that it just pretty prints the AST with the code-styling you have specified.

Let get started, first we need to install the dev dependencies

```bash
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
```

Let's quickly explain the above dev-dependencies and what purpose do they serve.

- `prettier`: opinionated code-formatter.
- `eslint-config-prettier`: used to disable all eslint rules that might conflict with prettier.
- `eslint-plugin-prettier`: runs prettier as an eslint rule

Now create a `.prettierrc` file in the root of your directory and add the below config.

```json
//.prettierrc

{
  "semi": true,
  "trailingComma": "none",
  "singleQuote": false,
  "printWidth": 120,
  "tabWidth": 2,
  "arrowParens": "always",
  "bracketSpacing": true
}
```

These rules specify to put semicolons at the end, remove trailing commas, use double quotes for strings, etc, you can read more about the rules [here](https://prettier.io/docs/en/options.html) and set them up to your preference.

Now that we have the rules set up, let's add a script to format our code.

```json
"format": "prettier --config .prettierrc 'src/**/*.ts' --write"
```

That's it we have configured prettier in our project, now whenever you run this script it will format all your source code in the `src` directory according to the config you defined. Give it a test, use some single quotes or remove semicolons and then run `yarn format`. You can also install the prettier vs code extension and set it to format on save. 

Now that we have prettier setup let's configure it with eslint, update your `.eslintrc` as below and you'll have prettier working with eslint and configured as a plugin in eslint config.

```json
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "prettier"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "rules": {
    "no-unused-vars": "error",
    "prettier/prettier": "error"
  }
}
```

### Git Hooks

Cool, if you have been following till here this is where it gets the most exciting, you must be wondering it's great that we have set these tools up, but someone can still commit to the repo without running these scripts, this is where git hooks come to our rescue. Git hooks are just scripts that run automatically every time an event occurs in the repository. We will use a tool called [husky](https://github.com/typicode/husky) that makes working with git-hooks easy. 

Let's quickly install it as a dev dependency.

```bash
yarn add -D husky

// Also add a prepare script to your package.json as below.

"prepare": "husky install"

// Now run

yarn prepare
```

`prepare` is an npm lifecycle script that will run on `npm install` and this will ensure that whenever node modules are installed husky is installed too. Read more on life-cycle scrips [here](https://docs.npmjs.com/cli/v7/using-npm/scripts#life-cycle-scripts).

That's it, husky is now set up in our project, and we can configure it to run our `lint` and `format` scripts before commit, for that we can use the `pre-commit` hook that runs before a commit. Let's add that hook now, head over to the terminal, and run the below command.

```bash
npx husky add .husky/pre-commit "yarn lint && yarn format"
```

You will notice that now there's a `.husky` folder in the root of your repo and it contains a file `pre-commit` with the below contents. This hook will run every time you commit to your repo.

```bash
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint && yarn format
```

Try committing something in the repo now, and you will notice that `lint` and `format` scripts are executed first. If everything looks good, your commit will be added successfully, in case of issues it won't be committed to the repo and you'll have to fix those issues in order to make a commit. You should be able to make a mental model of this now, this ensures that no code with potential bugs or styling issues gets committed to the repo. This can help you save a lot of time in code reviews if these conventions are properly followed.

This configuration is great but if you notice there is one problem with this approach, if we have a very large codebase and we run linting and formatting on every commit in the code-base it can take up much longer and that's not the intended use case, we only want to run it on the files that have been staged for the commit because ideally only those files should be checked that are being committed to the repo, that's where another tool `lint-staged` comes to our rescue and it will ensure that our pre-commit hooks only run on staged files. This can be configured very quickly too. 

```bash
yarn add -D lint-staged

// Add below config to your package.json

"lint-staged": {
  "src/**/*.ts": [
    "yarn lint",
    "yarn format"
  ],
},
```

And update your `pre-commit` hook to run `npx lint-staged` instead of running `yarn lint && yarn format`

```bash
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged
```

That's it, now lint will only run against the staged files, you can quickly test it by creating a file in the repo with some lint-issues and don't stage it while staging some other files that are compliant with the lint rules and you should notice that git commit will run fine, but if you stage the file with the lint issues, it will block the commit and give you the lint errors on the terminal that should be fixed. If you want to lint the entire project you can still do it by running `yarn lint`. Now our tooling is set up well enough to ensure that the whole team can follow it and adhere to the same coding standards and style guides.

### Conventional Commits

Our development setup is pretty much complete but there's still one area where the team can adopt different conventions, that will be the commit messages yes that's right people can have different preferences when adding commit messages and we need to ensure that the team conforms to a standard convention. We will be adopting the [conventional commits specification](https://www.conventionalcommits.org/en/v1.0.0/) in our project and ensure it using a tool named `commitizen` 

The general syntax for this specification is as follow

```
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

// Example

feat(landing-page): add new landing page

A new landing page for the website...

Closes #<github-issue-number>
```

Alright now that we have an understanding of the specification let's configure our project to use this tool.

```
yarn add -D commitizen cz-conventional-changelog
```

And add this to your `pacakge.json` 

```json
"config": {
  "commitizen": {
    "path": "cz-conventional-changelog"
  }
}

"scripts": {
  "cz": "cz",
	... 
}
```

*Note: If you end up naming your script `commit` then it might run twice because of [this issue](https://github.com/commitizen/cz-cli#optional-install-and-run-commitizen-locally) mentioned on the repo.*

Now try committing to the repo by running `yarn cz` and you will be prompted a couple of questions on the terminal to make the proper commit. Great now our conventional-commits are set up we can also set it up as a git hook by running the below command and running commitizen with our `prepare-commit-msg` hook which is invoked by `git commit` however there's an issue with this approach which triggers the git commit twice when running `yarn cz`. The issue is opened on the repo [here](https://github.com/commitizen/cz-cli/issues/844), I would advise not to use this approach until this issue is fixed and rely on the previous script `yarn cz`

```bash
npx husky add .husky/prepare-commit-msg "exec < /dev/tty && node_modules/.bin/cz --hook || true"
```

### Github Actions

In the last part, we will focus on setting up a GitHub action to ensure that our lint and format jobs are run on every commit and how Github actions can help in our CI/CD pipeline. Setting up a good and fast CI/CD pipeline is very essential in modern software development now. As your software evolves it might become a very tedious task to compile the builds manually and if it needs to be compiled on multiple platforms you can imagine how time taking this could be.

Github actions are a handy tool to automate software workflows and provides CI/CD right out of your Github code repo. Github actions are event-driven and basically require a `.yaml` file where you can provide your configuration in steps.

Some key terminologies to keep in consideration when working with Github Actions include

- **Workflows:** automated work procedures that contain one or more jobs.
- **Events:** an activity that triggers a workflow (e.g push to a branch)
- **Jobs:** a set of steps that execute on the same runner.
- **Step:** a task that can execute commands on a job.

You can read more about these on the official [docs](https://docs.github.com/en/actions/learn-github-actions/introduction-to-github-actions).

First, initialize a git repo in your project if you haven't already by running `git init` and commit your changes to the repo. Make sure you add a `.gitignore` file and add `node_modules` and `dist` to it so they don't get committed to the repo.

Next, create a repo on your Github account and copy the remote's origin url, now head over to your local repo and run

```bash
git remote add origin <remote-repo-origin>
git push -u origin <branch-name>
```

Next head over to your Github repo and click on the Actions tab and select set up a workflow yourself

In the editor name the file `lint.yml` and clear the default action and replace it with.

```yaml
name: Lint

on: [push]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js environment
        uses: actions/setup-node@v2.4.0
        with:
          node-version: '14.16.1'
        
      - name: Install Node Modules
        run: npm install
      
      - name: Lint and format
        run: |
          npm run lint
          npm run format
```

This action file is pretty straight forward and you should be able to navigate through it very easily, we are declaring our workflow name as Lint that has a single job `lint`. A workflow can have multiple jobs and by default, jobs run in parallel but can be configured to make them run sequentially by using the `needs <job-id>` syntax in the `.yml` file

Next, we are specifying to run the job on Ubuntu's latest version and set up Node v14.16.1 using a [marketplace action](https://github.com/marketplace/actions/setup-node-js-environment). After that, we are just installing node modules and running our lint/format scripts. 

Note this is a very simple example but you can extend it to your needs, for example, let's say you have set up tests in your repo then you can define another `test` job that runs after `linting`, and if the test job succeeds, you can run a `build` job to compile a build and deploy to staging. You can basically configure it to adapt to your needs and automate your development workflow. Github actions are indeed a powerful tool and you should definitely explore them.

You can see all of this in action on my GitHub repo [here](https://github.com/asjadanis/ts-tooling)

### Conclusion

The idea with this blog was not to go into the depths of each tool but rather give you an overview of all these tools that can help you in our development process and ensure a better development experience. Setting up these tools can be a very boring and cumbersome task but these tools are your friends and once you adopt them properly in your code-base you won't regret it. Feel free to share your thoughts with me in the comment section or connect with me on [Twitter](https://twitter.com/asjadanis).