DEV Community

Arjun Singh
Arjun Singh

Posted on

How to make an npm package with an automated workflow

Recently, I published an npm package. 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 — Developer Experience. What changes did I make?

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, 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

  • Changelogs for individual releases will also be automatically published.

Before moving on, I strongly recommend you to read up on Conventional Commits and Semantic Versioning, 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 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 – 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 – 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:

"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"
  }
Enter fullscreen mode Exit fullscreen mode

And now run npm install to install them all.

Step 2 – 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:

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

npx --no -- commitlint --edit $1
npx --no -s semantic-commit-emoji $1
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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:

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

npx --no lint-staged
Enter fullscreen mode Exit fullscreen mode

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:

"lint-staged": {
    "*.(js|ts|jsx|tsx)": "eslint"
}
Enter fullscreen mode Exit fullscreen mode

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

"eslintConfig": {
    "extends": "eslint:recommended",
    "env": {
      "browser": true,
      "node": true,
      "jest": true
    }
  }
Enter fullscreen mode Exit fullscreen mode

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:

module.exports = {
  presets: [["@babel/preset-env", { targets: { node: "current" } }]],
};
Enter fullscreen mode Exit fullscreen mode

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

"parserOptions": {
  "ecmaVersion": "latest"
}
Enter fullscreen mode Exit fullscreen mode

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:

"config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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 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 – This is supposed to contain a Github Personal Access Token. Here's 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 – This one is supposed to contain an npm automation token. Check out this guide 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:

"release": {
    "branches": [
      "main"
    ],
    "plugins": [
      "semantic-release-gitmoji",
      "@semantic-release/npm",
      "@semantic-release/github"
    ]
},
Enter fullscreen mode Exit fullscreen mode

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 – Add a nice README

Create a nice README.md using readme.so and add it to your project.


Voila! We're done. Hopefully, you found this post useful.
Do checkout the npm-pkg-gen package if you want to get going fast, and star it on Github if you like it!

Later!

Top comments (0)