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:
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.