Enforcing good practices (part 5)

Enforcing good practices (part 5)

This article is the fifth and final part of the series Creating the modern developer stack template. I definitely recommend reading the first one before this to get more context.

Introduction

Hello folks,

Today, we will explore what those good practices are, why it is important to enforce them and how they were implemented as a first-class citizen of stator.

What?

  • eslint which enforces patterns based on a set of rules you defined
  • prettier which will beautifully format your code
  • Standardized commit messages
  • Automatic releases
  • File and directory naming convention
  • Branch naming convention
  • Continuous integration

Why?

Enforcing good practices should be one of your priorities early on in your project, as it will define its future. Failing to do this initially will lead to inconsistencies in coding styles, which will make your project hard to read as different sections will have different writing styles. Consistency will lead to saved time for your developers. Reviewing pull requests will be faster as we ensure the CI enforces our rules. Over is the era where you leave comments about the fact that there is an extra space or the variable name doesn't respect the standards. Maintaining existing code will also be easier as all of it will be homogeneous. By now, you should be convinced that this is useful for your project. Now, let's implement it.

Implementation in Stator

eslint

Install the required libraries:

npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/eslint-plugin eslint-config-airbnb eslint-config-prettier eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks @nrwl/eslint-plugin-nx

This will install eslint itself, pre-configured eslint configs and plugins to extend eslint base functionalities.

Create a .eslintrc file at the root of your project:

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module",
    "project": "./tsconfig.*?.json"
  },
  "ignorePatterns": ["**/*"],
  "plugins": ["@typescript-eslint", "@nrwl/nx"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier",
    "prettier/@typescript-eslint"
  ],
  "rules": {
    "@typescript-eslint/explicit-member-accessibility": "off",
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/no-parameter-properties": "off",
    "@nrwl/nx/enforce-module-boundaries": [
      "error",
      {
        "enforceBuildableLibDependency": true,
        "allow": [],
        "depConstraints": [
          { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] }
        ]
      }
    ]
  },
  "overrides": [
    {
      "files": ["*.tsx"],
      "rules": {
        "@typescript-eslint/no-unused-vars": "off"
      }
    }
  ]
}

Prettier

Install prettier and a plugin:

npm i -D prettier prettier-plugin-import-sort

prettier-plugin-import-sort will sort your imports consistently across your project. It will also separate external imports from internal ones.

In your package.json you need to add this configuration for the plugin:

"importSort": {
    ".js, .jsx, .ts, .tsx": {
      "style": "module"
    }
  }

Create a file .prettierrc:

{
  "singleQuote": false,
  "quoteProps": "as-needed",
  "arrowParens": "avoid",
  "tabWidth": 2,
  "trailingComma": "es5",
  "semi": false,
  "jsxSingleQuote": false,
  "jsxBracketSameLine": false,
  "printWidth": 120
}

Standardized commit messages

Install commitizen, commitlint and husky to easily add git hooks:

npm i -D commitizen husky @commitlint/cli @commitlint/config-conventional

Create the file commintlint.config.js which will use the config of conventional commits:

module.exports = {extends: ['@commitlint/config-conventional']}

In package.json add:

  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },

This will make sure your commit messages respect the convention.

You can also add a script in your package.json that will launch the interactive commitizen CLI:

"commit": "cz"

Automatic releases

Install the required packages of semantic-release:

npm i -D semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/github

As we previously made sure our commit messages are compliant with the format required with semantic-release, that's all you need to do. When you merge pull requests in master, sematic-release will scan for keywords that will increase your project's version. It will also create a release on your Github project with the changelog contained in it. Here is an example:

image.png

File and directory naming convention

For this one, I couldn't find any existing open-source release, so I created a straightforward script:

const fs = require("fs")
const path = require("path")

const folderNames = []
async function* walk(dir, ignoredPaths) {
  for await (const d of await fs.promises.opendir(dir)) {
    const entry = path.join(dir, d.name)
    if (d.isDirectory() && !ignoredPaths.includes(d.name)) {
      folderNames.push(entry)
      yield* walk(entry, ignoredPaths)
    } else if (d.isFile()) {
      yield entry
    }
  }
}

async function main() {
  const ignoredFolders = ["node_modules", "dist", ".git", ".idea", ".gitkeep", ".eslintrc", ".cache", "README", "LICENSE"]
  const capitalLetterRegex = /[A-Z]/gm
  const errorPathPaths = []

  function validateEntryName(entry) {
    const entryName = path.basename(entry).replace(/\.[^\/.]+$/, "")
    if (entryName.length > 0 && !ignoredFolders.includes(entryName) && entryName.match(capitalLetterRegex)) {
      errorPathPaths.push(entry)
    }
  }

  for await (const entry of walk(path.join(__dirname, '..'), ignoredFolders)) {
    validateEntryName(entry)
  }

  for (const folderName of folderNames) {
    validateEntryName(folderName)
  }

  if (errorPathPaths.length > 0) {
    const errorMessage = `${errorPathPaths.length} files/directories do not respect the kebab-case convention enforced.`

    console.error(errorMessage)
    console.error(errorPathPaths)

    process.exit(1)
  }

  console.log("Congratulations, all your files and directories are properly named!")
}

main().then().catch(console.error)

This will navigate your project files and directories and return an error code if it has found invalid namings.

I recommend adding a script command in your package.json:

"lint:file-folder-convention": "node ./tools/enforce-file-folder-naming-convention.js",

Branch naming convention

Install branch-naming-check, which will enforce branch names based on a regex:

npm i -D @innocells/branch-naming-check

In your package.json, add a husky hook:

"pre-commit": "branch-naming-check '^\\d+(-[a-z]+)+$' && npm run lint:file-folder-convention"

Continuous integration

Create a new Github action workflow:

# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: stator CI

on:
  pull_request:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - name: Cancel previous runs
        uses: styfle/cancel-workflow-action@0.5.0
        with:
          access_token: ${{ github.token }}

      - name: Increase watcher limit
        run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Fetch latest changes
        run: git fetch --no-tags --prune --depth=5 origin master

      - name: Inject Nx Cloud token
        shell: bash
        env:
          nx_cloud_token: ${{ secrets.NX_CLOUD_TOKEN }}
        run:
          sed -i "s/nx_cloud_token/$nx_cloud_token/" $GITHUB_WORKSPACE/nx.json

      - name: Setup postgres container
        run: docker-compose -f $GITHUB_WORKSPACE/apps/database/postgres/docker-compose.yml up -d

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install npm packages
        run: npm ci

      - name: Build affected apps
        run: npm run affected:build -- --base=origin/master

      - name: Lint files
        run: npm run affected:lint -- --base=origin/master

      - name: Enforce naming conventions
        run: npm run lint:file-folder-convention

      - name: Run tests
        run: npm run affected:test -- --base=origin/master --code-coverage

      - name: Start api
        run: npm start api &

      - name: Run e2e tests
        run: npm run affected:e2e -- --base=origin/master

      - name: Coveralls
        uses: coverallsapp/github-action@master
        with:
          github-token: ${{ github.token }}
          path-to-lcov: ./coverage/apps/api/lcov.info

      - name: Archive code coverage results
        uses: actions/upload-artifact@v2
        with:
          name: code-coverage-report
          path: ./coverage

      - name: Release
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: npx semantic-release

This will ensure our application builds, the tests pass, and all the good practices we've put in place are applied and respected.

Conclusion

In this article, we've learned what good practices are, why they are important and how to implement them. Through this series, we have created a database docker file, implement the backend of a todo API and the frontend. We've upped our game by enforcing coding guidelines to ensure we have a clean and nice project. If you want to see the source code of everything we've done on this great journey, consult stator, your go-to template for the perfect stack.