DEV Community

Cover image for Node package code starter setup
Antonio Villagra De La Cruz
Antonio Villagra De La Cruz

Posted on • Edited on • Originally published at antoniovdlc.me

2 2

Node package code starter setup

Want to write an open-source library targeting Node, but don't know where to start? Just curious about a fellow open-source library author's default setup for Node packages? Have 5 minutes of your time? Look no further, I got you covered!

In this post, I will share with you a "basic" (there is unfortunately no such thing in the JavaScript ecosystem ... yet) setup I have used to build open-source Node packages.


Always bet on ... TypeScript

This is not going to be a piece trying to convince you that you should use TypeScript, and that vanilla JavaScript is bad. For all we know, both TypeScript and JavaScript have their use cases and are both valid choices depending on the constraints of a project.

For libraries though, I would more often than not default to using TypeScript. It adds a useful layer of static analysis with its type checker, and it automatically generates type files which can be useful for consumers of your library.

ES modules where a great addition to modern JavaScript, but until fairly recently they were not natively supported in Node, meaning that most libraries defaulted to CommonJS to support both use cases, to the detriment of browsers now natively supporting ES modules. To circumvent that dichotomy, we can use a build pipeline centered around Rollup, which would generate both an ES module package and a CommonJS module. Then, we can point consumers to the right type of package via the corresponding fields in package.json.

All in all, the setup looks like the following:

package.json

{
  "name": "...",
  "version": "1.0.0",
  "description": "...",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist/index.cjs.js",
    "dist/index.esm.js",
    "dist/index.d.ts"
  ],
  "scripts": {
    ...
    "type:check": "tsc --noEmit",
    ...
    "prebuild": "rimraf dist && mkdir dist",
    "build": "npm run build:types && npm run build:lib",
    "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist",
    "build:lib": "rollup -c",
    ...
  },
  ...
  "devDependencies": {
    ...
    "@rollup/plugin-typescript": "^8.2.1",
    ...
    "rimraf": "^3.0.2",
    "rollup": "^2.52.1",
    "rollup-plugin-terser": "^7.0.2",
    "tslib": "^2.3.0",
    "typescript": "^4.3.4"
  }
}

Enter fullscreen mode Exit fullscreen mode

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
Enter fullscreen mode Exit fullscreen mode

rollup.config.js

import typescript from "@rollup/plugin-typescript";
import { terser } from "rollup-plugin-terser";

export default [
  {
    input: "src/index.ts",
    output: {
      file: "dist/index.cjs.js",
      format: "cjs",
      exports: "default", // Remove this line if using named exports
    },
    plugins: [
      typescript(),
      terser({
        format: {
          comments: false,
        },
      }),
    ],
  },
  {
    input: "src/index.ts",
    output: {
      file: "dist/index.esm.js",
      format: "es",
    },
    plugins: [
      typescript(),
      terser({
        format: {
          comments: false,
        },
      }),
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

No tests, no glory

Another primordial aspect of open-source code is testing.

In our case, we will focus on a setup centered around Jest. As we are writing our source code in TypeScript, we also need Babel to help transpile the code. One of the advantages of Jest is that it bundles a lot of tools around automated testing into one: namely, a test runner, an assertion library and code instrumentation for code coverage.

For good measure, as we are going to be writing our tests in JavaScript, let's throw in ESLint into the mix!

package.json

{
  "name": "...",
  "version": "1.0.0",
  "description": "...",
  ...
  "scripts": {
    ...
    "test": "jest",
    ...
  },
  ...
  "devDependencies": {
    "@babel/core": "^7.14.6",
    "@babel/preset-env": "^7.14.5",
    "@babel/preset-typescript": "^7.14.5",
    ...
    "@types/jest": "^26.0.23",
    "babel-jest": "^27.0.2",
    "eslint": "^7.29.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-jest": "^24.3.6",
    ...
    "jest": "^27.0.4",
    ...
  }
}

Enter fullscreen mode Exit fullscreen mode

jest.config.js

module.exports = {
  collectCoverage: true,
  coverageDirectory: "coverage",
  coverageProvider: "v8",
  coverageThreshold: {
    global: {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100,
    },
  },
  testEnvironment: "node",
};
Enter fullscreen mode Exit fullscreen mode

babel.config.js

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

Instead of using babel just to transpile the code for tests, another option is to directly use ts-jest.

.eslintrc.js

module.exports = {
  env: {
    es2021: true,
    node: true,
    "jest/globals": true,
  },
  extends: ["eslint:recommended", "prettier"],
  parserOptions: {
    ecmaVersion: 12,
    sourceType: "module",
  },
  plugins: ["jest"],
  rules: {
    "no-console": "error",
  },
};
Enter fullscreen mode Exit fullscreen mode

Automate, automate, automate

Finally, because we want to be doing the less amount of repetitive work possible, let's look at automating a few aspects of writing and maintaining open-source libraries.

First of, let's get rid of any formatting shenanigans by bringing Prettier on board. This will also help potential contributors, as their submissions will already be formatted according to the configuration of your library.

Next, we would like to ensure that code passes a certain bar of quality before being committed. To do that, we can leverage husky's pre-commit hooks, coupled with lint-staged to only affect staged changes.

package.json

{
  "name": "...",
  "version": "1.0.0",
  "description": "...",
  ...
  "scripts": {
    "prepare": "husky install",
    "type:check": "tsc --noEmit",
    "format": "prettier --write --ignore-unknown {src,test}/*",
    ...
    "pre-commit": "lint-staged",
    ...
  },
  "devDependencies": {
    ...
    "husky": "^6.0.0",
    ...
    "lint-staged": "^11.0.0",
    "prettier": "^2.3.1",
    ...
  }
}

Enter fullscreen mode Exit fullscreen mode

.husky/pre-commit

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

npm run test
npm run pre-commit
Enter fullscreen mode Exit fullscreen mode

.lintstagedrc.js

module.exports = {
  "*.ts": ["tsc --noEmit", "prettier --write"],
  "*.js": ["prettier --write", "eslint --fix"],
};
Enter fullscreen mode Exit fullscreen mode

With this setup, tests, static analysis (type checking, linting) and formatting will always be run on changes before they are committed and ready to be pushed.

Finally, we want to also automate building and publishing our package to npm (or any other relevant repositories). To achieve that, if you are hosting your code on GitHub, you can leverage GitHub Actions.

The script below runs tests, builds the code and publishes a package to npm every time a new release is created on the repository. Note that for this script to work, you will need to add a secret named NPM_TOKEN with an "Automation" token generated from your npm account.

.github/workflows/publish.yml

name: publish

on:
  release:
    types: [created]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: 14
      - run: npm ci
      - run: npm test

  publish-npm:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: 14
          registry-url: https://registry.npmjs.org/
      - run: npm ci
      - run: npm run build
      - run: npm publish --access=public
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
Enter fullscreen mode Exit fullscreen mode

There are, of course, many avenues of improvement for this setup, but I would argue it provides a good basis when writing a Node package.

What other tools would you add? How would you simplify or augment the presented setup?

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay