How to make an npm package with an automated workflow
Tags: npm, javascript, webdev, tutorial
Published: 2022-08-03 03:09:00

Recently, I published [an npm package](https://www.npmjs.com/package/create-fse-theme). This isn't the first package I have made in my life, but this time around it felt "authentic": the other two packages I had made before didn't quite _feel professional enough_, especially from the point of view of DX &mdash; Developer Experience. What changes did I make?

- Enforced following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for my git commit messages, using [Commitizen](https://commitizen-tools.github.io/commitizen/#:~:text=Commitizen%20is%20a%20tool%20designed,and%20enforces%20writing%20descriptive%20commits.),
- Used [semantic-release](https://github.com/semantic-release/semantic-release) and [Github Actions](https://github.com/features/actions) for automated versioning and releases.
- Used [lint-staged](https://www.npmjs.com/package/lint-staged) to lint my Javascript files before every commit.
- Adding emojis to my commit messages

In this article, I am going to share with you the things I learned and help you get up and running with a similar set-up for your own npm package (or just any personal project, in general).

## The Endgoal

How exactly is this set-up going to influence your workflow? Let's take a look:

- Any time you make a git commit, you'll have a GUI-in-CLI prompt in your terminal, to help you make a [_Conventional Commit_](https://www.conventionalcommits.org/en/v1.0.0/), without having to memorize the format of such commits.

- A relevant emoji will also be added to your commit message, based on the type of the message.

- Whenever you push your code to your remote's main branch, depending on the commits since the last push, **an npm release, a Github release and a git tag will automatically be made!** All in accordance with [Semantic Versioning](https://semver.org/)

- Changelogs for individual releases will also be automatically published.

Before moving on, I strongly recommend you to read up on [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) and [Semantic Versioning](https://semver.org/), if you're unsure what they mean. It won't take long!

> ### Heads up!
>
> If you don't wanna do all the work, you can check out [this package](https://www.npmjs.com/package/npm-pkg-gen) I have made that will get you up and running in no time. Do checkout the readme for instructions on how to install and set it up, and star it if you like it!

Let's get rolling!

## Step 0 &ndash; Set up the project

Start by creating a Node.js project. `cd` into your project folder and run `npm init`. Answer the prompts that appear.

Also initialize an empty git repo and add a Github repo as your remote.

Make sure to set your package version to `0.0.0-development` as a sort of indicator for anyone looking at your code that your releases and version numbers are automatically managed for you.

## Step 1 &ndash; Install dependencies

We'll be needing quite a few dependencies for our set-up. Let's install all of them in one go. Add the following development dependencies to your `package.json`:

```JSON
"devDependencies": {
    "@commitlint/cli": "^17.0.3",
    "@commitlint/config-conventional": "^17.0.3",
    "cz-conventional-changelog": "3.3.0",
    "eslint": "^8.20.0",
    "husky": "^8.0.1",
    "lint-staged": "^13.0.3",
    "semantic-commit-emoji": "^0.6.2",
    "semantic-release": "^19.0.3",
    "semantic-release-gitmoji": "^1.4.4"
  }
```

And now run `npm install` to install them all.

## Step 2 &ndash; Customizing commits

For customizing commits, the tools we need are Husky, Commitizen, _commitlint_ and _semantic-commit-emoji_.

Basically, Git exposes some _hooks_ onto which functionality can be tied. I like to think of them as events: you can specify the code that gets executed whenever a particular hook is triggered.

Husky is a tool that makes Git hooks easier to work with. Code for Husky hooks is specified in dedicated files in the `.husky` folder at the project root. To create the `.husky` folder, run `npx husky install`.

Run `npx husky add .husky/commit-msg` in the project root to generate a hook that modifies the commit message. This will create a `commit-msg` shell file in the `.husky` folder. Put the following into that shell file:

```sh
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no -- commitlint --edit $1
npx --no -s semantic-commit-emoji $1
```

Add `commitlint.config.cjs` to your root directory and add the following contents:

```javascript
const Configuration = {
  extends: ["@commitlint/config-conventional"],
  formatter: "@commitlint/format",
  ignores: [(commit) => commit === ""],
  defaultIgnores: true,
  helpUrl:
    "https://github.com/conventional-changelog/commitlint/#what-is-commitlint",
};

module.exports = Configuration;
```

Run `npx husky add .husky/pre-commit` to generate another hook (which, as the name suggests, runs just before the pre-commit) and in the generated shell file, write:

```sh
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no lint-staged
```

This tells Husky to lint all our staged files and make any necessary changes.

Add the following to your `package.json` to specify the files to be linted:

```JSON
"lint-staged": {
    "*.(js|ts|jsx|tsx)": "eslint"
}
```

We also need to specify the configuration for _eslint_. Add the following to your `package.json`:

```JSON
"eslintConfig": {
    "extends": "eslint:recommended",
    "env": {
      "browser": true,
      "node": true,
      "jest": true
    }
  }
```

If you are using something other than Jest for testing, change accordingly, and if you're using Jest, ensure that you have its dependencies downloaded and configured properly too: install `@babel/core`, `@babel/preset-env`, `babel-jest` and `jest` as development dependencies, and create a `babel.config.cjs` at the root of your package with the following contents in it:

```javascript
module.exports = {
  presets: [["@babel/preset-env", { targets: { node: "current" } }]],
};
```

If _eslint_ gives you grief for using some latest Javascript features, you can add the following to the `eslintConfig` object above:

```JSON
"parserOptions": {
  "ecmaVersion": "latest"
}
```

If your package is of module type (i.e. you're using the fancier `import` statements instead of `require`), you also need to add `"sourceType": "module"` to this `parserOptions` object. Depending on your requirements (maybe JSX, Vue templates etc), you may want to customize this _eslint_ configuration further.

To configure Commitizen, add the following to your `package.json`:

```JSON
"config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
}
```

> ### NOTE
>
> Now that you have configured Commitizen, you must use `git cz` or `cz` instead of `git commit` to make use of it.

## Step 3 &ndash; Configure automated npm and Github releases

Create a `.github` directory in your project root. Inside, create a subdirectory `workflows` and add a `.yml` file with whatever name you want (say, `publish.yml`). Add the following contents to that file:

```YAML
name: Package release and publish
on:
  push:
    branches:
      - main
env:
  GH_TOKEN: ${{ secrets.GH_TOKEN }}
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16.x]
    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: "npm"
      - run: npm ci
      - run: npm run build --if-present
      - run: npm test -- --watch=false --browsers=ChromeHeadless
      - run: npx semantic-release
```

This tells Github to run some checks every time you push to your Github repo's main branch. It will run your tests and release a new Github and npm release. But this wouldn't work just yet!

First we'll need to make some _repo secrets_. Here's a [nice guide](https://github.com/Azure/actions-workflow-samples/blob/master/assets/create-secrets-for-GitHub-workflows.md) from Azure explaining how to do just that. It isn't difficult at all.

You'll need to make the following two secrets:

1.  `GH_TOKEN` &ndash; This is supposed to contain a Github Personal Access Token. [Here's](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) how you can make one. Make sure to make it permissive enough (might as well check all the boxes if you are unsure).
2.  `NPM_TOKEN` &ndash; This one is supposed to contain an npm automation token. Check out [this guide](https://docs.npmjs.com/creating-and-viewing-access-tokens/) to know how to make one.

We're almost there! We just need to configure _semantic-release_ to work as we intend it to. To your `package.json`, add the following:

```JSON
"release": {
    "branches": [
      "main"
    ],
    "plugins": [
      "semantic-release-gitmoji",
      "@semantic-release/npm",
      "@semantic-release/github"
    ]
},
```

This tells _semantic-release_ about the branch we want to use as our "release branch", and also extends the default functionality to support emojis using the _semantic-release-gitmoji_ plugin.

## Step 4 &ndash; Add a nice README

Create a nice `README.md` using [readme.so](https://readme.so/) and add it to your project.

---

Voila! We're done. Hopefully, you found this post useful.
Do checkout the [npm-pkg-gen](https://www.npmjs.com/package/npm-pkg-gen) package if you want to get going fast, and star it [on Github](https://github.com/xylocone/npm-pkg-gen) if you like it!

Later!