Start strong with this JavaScript package template

Start strong with this JavaScript package template

Publishing a package is a rite of passage for JavaScript and TypeScript developers these days. It's incredibly easy to do, but hard to do well. It can be daunting, putting our work out there for the world to see, for people to use. But there's more to making a good contribution to the world of open source software than just publishing your package. What about concerns such as testing, typing, licensing, work flows, pull request processes, contributing guidelines, linting, styling, and documentation?

I've created a package template that I believe addresses these concerns and gets you off to a strong start. But rather than just giving you the template and leaving it at that, I'll explain the choices I've made. That way you can make your own decisions and improvements. Plus if you understand it, you can submit a pull request with improvements, so that we can all benefit.

Check out npm-package-template and give it a go. In the rest of this article, I explain exactly what, how, and why.

TypeScript Setup

Yes, this template uses TypeScript. At this point, if you're going to be providing open source packages to the javascript community, you need to provide a well typed library. It's no longer a nice to have, but a must have. Lets take a look at configuring our tsconfig

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "declaration": true,
    "declarationMap": true,
    "outDir": "./lib",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

Here we've started with "target": "es5" in this template because if you're shipping a front end library, that's what you will need to target. However for other use cases a more recent version of ECMAscript may be appropriate.

    "declaration": true,
    "declarationMap": true,

These give us our declaration (.d.ts) files and map files automatically in our output when typescript compiles. These are the type files that ship with your package and help users with type hints and doc comments.

"outDir": "./lib",

Here we're simply specifying that we want to write our output to the lib folder at compile time. This folder is where we specify users will import from when they import our package, as specified in our package.json:

  "main": "lib/index",
  "types": "lib/index",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true

The remaining checks above are all recommended typescript config options, providing strict type checking, helpers for babel via esModuleInterop, skipping of checks for declaration files, and consistent filename casing which can prevent bugs on different file systems.

Build Process

Building this package is simple and done entirely by the typescript compiler.

Building the project is done using npm run build which executes npx tsc -p .. We use npx to ensure we're using the version of typescript local to this package, rather than a global typescript installation that may be present. The -p flag with . ensures we're using this directory's tsconfig.

Testing

We've setup mocha as our test runner. The configuration for mocha lives in our package.json and is:

  "mocha": {
    "extension": [
      "ts"
    ],
    "watch-files": [
      "src/**/*.ts",
      "test/**/*.ts"
    ],
    "require": "ts-node/register"
  },

This tells mocha to test typescript files, using ts-node, and in watch mode to re-run when changes occur in either src or test directories. Starting mocha in watch mode is done by running npm run watch which calls npx mocha -w. This is the main way you will develop your package, as it allows very effective red/green/refactor cycles following TDD.

Linting & Styling

For linting we've configured ESLint to support typescript and made a few package related choices, the .eslintrc.json is:

{
  "extends": "eslint:recommended",
  "env": {
    "commonjs": true,
    "node": true
  },
  "parserOptions": {
    "ecmaVersion": 2020,
    "sourceType": "module"
  },
  "rules": {
    "no-console": "off",
    "strict": ["error", "global"],
    "curly": "warn"
  }
}

Our prettier config is stock-standard, and in package.json is just

  "prettier": {
    "singleQuote": true
  },

We've also configured lint-staged to run when we stage files, telling it to run both prettier and eslint in write and fix mode respectively:

  "lint-staged": {
    "*.ts": [
      "prettier --write",
      "eslint --cache --fix"
    ]
  },

With all of the above, writing professional, consistent code is as easy as falling off a log for you and your contributors.

Development Experience

Contributors can get started as easily as

git clone <path to our repo>
cd <our repo name>
nvm use
npm i --ci
npm run watch

nvm use ensures everyone working on this package is using the same version of node, as it picks up the node version specified in the .nvmrc file.

npm i --ci is shorthand for npm install and the --ci flag instructs npm to install dependencies precisely matching the lock file, which is considerably faster.

GitHub Setup

Following the instructions in the npm-package-template readme will result in a Github repo configured with these features:

Protected Branches

We want our main branch to only include code that passes CI and has been reviewed.

Continuous Integration

This package template also includes a basic CircleCI config and instructions on how to get up and running with CircleCI.

Documentation

Documentation can make or break an open source library. People will often read your documentation to decide if they want to use your package. In this template we use TypeDoc and TypeDoc Markdown to generate markdown documentation files from our README.md and the docstrings in our typescript source code!

That means code like this

/**
 * Use this function to create a greeting string with the given name
 * @param name The name to greet
 */
export const makeHello = (name?: string): string => `Hello ${name || 'world'}`;

results in markdown like this

## Table of contents

### Functions

- [makeHello](modules.md#makehello)

## Functions

### makeHello`Const` **makeHello**(`name?`: *string*): *string*

Use this function to create a greeting string with the given name

#### Parameters

| Name | Type | Description |
| :------ | :------ | :------ |
| `name?` | *string* | The name to greet |

**Returns:** *string*

Defined in: [index.ts:5](https://github.com/Antman261/npm-package-template/blob/f2945d1/src/index.ts#L5)

Which then gets picked up by GitHub Pages, rendered by Jekyll against a template of your choosing, and served up as html! This is how tiny-fixtures.com is generated and hosted completely free.

These docs are generated by running npm run build-docs which executes npx typedoc --out docs src/index.ts --disableOutputCheck

--out docs just specifies the output directory as docs src/index.ts specifies our starting file, everything exported here is documented. --disableOutputCheck flags that we don't mind if there are existing files in the output dir. This is because our GitHub Pages config files live in ./docs too.

GitHub Pages

Full setup for configuring GitHub Pages is outlined in the README. This gives you a free way to host your documentation!

Package.json Config

Remaining package.json config that we haven't talked about all pertains to publishing our package on npm.

  "files": [
    "lib",
    "LICENSE",
    "README.md",
    "package.json"
  ],

This simply lists the files we want to include in our package tarball sent to npm when published.

"prepublishOnly": "npm run build && npm run build-docs"

This ensures that our changes have been built before publishing! This can prevent embarrassing mishaps such as publishing a new version with no changes because we forgot to build before publishing.

Publishing

As long as we have logged in to npm, and all the template strings filled in as per the README, publishing is just npm publish. Voila!

Conclusion

I hope this guide has made publishing an npm package less daunting, and rendered your journey as an open source contributor a little easier.

Let me know how you went in the comments! And feel free to open a pull request against npm-package-template if you find a better way of doing something.