Creating a TS-written NPM package for use in Node-JS or Browser.
Tags: ts, typescript, npm, node
Published: 2019-11-27 10:48:37

---
title: Creating a TS-written NPM package for use in Node-JS or Browser.
published: true
description: In this guide, I'll explain how to create an NPM package for NodeJS or the browser using Typescript without leaving built artifacts in your repository.
tags: TS, Typescript, NPM, Node, NodeJS
---

# Creating a TS-written NPM package for use in Node-JS or Browser: The Long Guide

In this guide, I'll explain how to create an NPM package for NodeJS or the browser using Typescript without leaving built artifacts in your repository. At the end, my example library will be able to be included in any Javascript or Typescript project, including:

- Imported as a script in a `<script>` tag, using either direct download or a free CDN service.
- Installed in a client-side application using `npm` and a bundler like `webpack`.
- Installed in a server-side NodeJS application using `npm`.

Furthermore, the whole build and publish process will be automated as much as possible, while keeping the repository free from builds.

For this post, I'll be using a tiny library I wrote as an example. The library itself is meaningless and not very useful, which makes it a fine distraction-free example for this guide.

## The example library

The example library will be called `bonaroo-able`, only exporting a namespace called `Able`.

`Able` contains a small set of functions for managing a list of strings that act as abilities (permissions) for some user. This example library is written in Typescript and it has no browser- or NodeJS specific dependencies (EG it doesn't rely on the DOM or filesystem). More about this library later. For now, let's start with creating some config files.

## The NPM package

First, we need a `package.json`. The `package.json` file contains details about your Javascript package, including the name, author and dependencies. You can read about `package.json` files in the [npm docs](https://docs.npmjs.com/creating-a-package-json-file).

To create a `package.json` file, we use **npm**. In your library folder, run `npm init` and follow the instructions. For this guide, I'll be using jest to test my library. We can just use `jest` as a **test command**: We'll be installing this dependency later.

The **entry point** is the file that will be included when our package is included in another project. To allow our package to be used in non-Typescript projects, this entry point must be a regular Javascript file.

This Javascript file must include all of our library. I like to have an `index.js` file that requires all our of library. Because this is a Typescript project, we will have separate Typescript and Javascript files. We keep these in `src` (written source) and `dist` (distributed files) folders.

We'll be writing a `src/index.ts` file importing all of our library, and use the Typescript compiler to generate a Javascript variant in `dist/index.js`. This `dist/index.js` will be our package's entry point. We'll configure the Typescript compiler later.

```sh
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (bonaroo-able)
version: (1.0.0)
description: A tiny library handling abilities
entry point: (index.js) dist/index.js
test command: jest
git repository: https://github.com/tobyhinloopen/bonaroo-able
keywords: Abilities, Permissions
author: Charper Bonaroo BV
license: (ISC) UNLICENSED
About to write to /home/toby/bonaroo-able//package.json:

{
  "name": "bonaroo-able",
  "version": "1.0.0",
  "description": "A tiny library handling abilities",
  "main": "dist/index.js",
  "scripts": {
    "test": "jest"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/tobyhinloopen/bonaroo-able.git"
  },
  "keywords": [
    "Abilities",
    "Permissions"
  ],
  "author": "Charper Bonaroo BV",
  "license": "UNLICENSED",
  "bugs": {
    "url": "https://github.com/tobyhinloopen/bonaroo-able/issues"
  },
  "homepage": "https://github.com/tobyhinloopen/bonaroo-able#readme"
}


Is this OK? (yes)
$
```

Next, we'll be needing some dependencies. Obviously you'll be needing Typescript. We'll also be installing `jest`, `ts-jest` and `@types/jest`.

```sh
npm i -D typescript jest ts-jest @types/jest
```

## Configuring Typescript

Next, we need to configure Typescript. Let's create a minimal `tsconfig.json` file.

#### tsconfig.json
```json
{
  "compilerOptions": {
    "outDir": "dist",
    "lib": ["es2016"],
    "sourceMap": true
  },
  "include": [
    "src/**/*.ts"
  ]
}
```

> ### About these options
> - **compilerOptions.outDir**: Generate JS files in this directory.
> - **compilerOptions.lib**: Tell the Typescript compiler the es2016 standard functions are available, like `Array.prototype.includes`.
> - **include**: Ensure only the Typescript in `src` is compiled & ignore all other source files (like the tests)

Since you cannot invoke Node binaries directly in all environments, I like to add all my commonly used commands to npm scripts. Add `"build": "tsc"` to the `scripts` section in your package.json

#### package.json (partial)

```json
  "scripts": {
    "build": "tsc",
    "test": "jest"
  },
```

To test whether everything is setup correctly, I like to create an entry point with a dummy function.

#### src/index.ts

```Typescript
export function hello(name: string): string {
  return `Hello ${name}`;
}
```

Let's attempt to built this:

```sh
$ npm run build

> bonaroo-able@1.0.0 build /home/toby/bonaroo-able
> tsc

$
```

No errors. That's great. Also, note that Typescript has created some Javascript files for us! If you take a look at `dist/index.js`, you'll see a Javascript variant of our Typescript file. My generated file looks like this:

#### dist/index.js (generated)
```Javascript
"use strict";
exports.__esModule = true;
function hello(name) {
    return "Hello " + name;
}
exports.hello = hello;
```

Note that all type information has been stripped, and the file has been changed to be compatible with older Javascript runtimes by changing the template string to a regular string with concat operator: `"Hello " + name`.

## Writing a test

Now test our "library": Let's write a test!

I like to create tests in a `test` directory, with a filenames matching the src files. For example, to test `src/Foo.ts`, I put my tests in `test/Foo.spec.ts`.

#### test/index.spec.ts
```Typescript
import { hello } from "../src";

test("hello", () => {
  expect(hello("foo")).toEqual("Hello foo");
});
```

To be able to write our tests in Typescript, we need to configure jest first. We can [generate a config file with `ts-jest config:init`](https://kulshekhar.github.io/ts-jest/user/install).

```sh
$ node_modules/.bin/ts-jest config:init

Jest configuration written to "/home/toby/bonaroo-able/jest.config.js".
$
```

Now we're ready to confirm our test suite is working:

```sh
$ npm t

> bonaroo-able@1.0.0 test /home/toby/bonaroo-able
> jest

 PASS  test/index.spec.ts
  ✓ hello (2ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.267s, estimated 2s
Ran all test suites.
$
```

## Configuring GIT

Before we continue, let's configure source control to persist our working setup.

To keep our git repository clean, we omit `node_modules` and `dist` from the git repository.

#### .gitignore
```
dist/
node_modules/
```

Now let's create a git repository. Replace the remote with your git repo.

```sh
git init
git add --all
git commit -m "Initial commit"
git remote add origin git@github.com:tobyhinloopen/bonaroo-able.git
git push -u origin master
```

## Writing our Library

Now let's write the code for our library. Writing code is outside the scope of this guide. Here's an overview of my Able library. The filename points to the current version of the complete file on github.

#### [src/Able.ts](https://github.com/tobyhinloopen/bonaroo-able/blob/master/src/Able.ts) (overview, no function bodies)
```Typescript
export namespace Able {
  export type AbilitySet = string[];
  export interface GroupDefinition { [key: string]: AbilitySet; }
  export interface ValueMap { [key: string]: string|string[]; }

  export function flatten(definition: GroupDefinition, abilities: AbilitySet): AbilitySet;
  export function extractValues(abilities: AbilitySet): [ValueMap, AbilitySet];
  export function applyValues(abilities: AbilitySet, values: ValueMap): AbilitySet;
  export function resolve(definition: GroupDefinition, abilities: AbilitySet): AbilitySet;
  export function getMissingAbilities(abilities: AbilitySet, requiredAbilities: AbilitySet): AbilitySet;
  export function canAccess(appliedAbilities: AbilitySet, requiredAbilities: AbilitySet): boolean;
}
```

#### [src/index.ts](https://github.com/tobyhinloopen/bonaroo-able/blob/master/src/index.ts)
```Typescript
import { Able } from "./Able";
export default Able;
Object.assign(module.exports, Able);
```

#### [test/index.spec.ts](https://github.com/tobyhinloopen/bonaroo-able/blob/master/test/index.spec.ts) (snippet, remaining tests removed)
```Typescript
import { Able } from "../src/Able";

describe("Able", () => {
  it("flatten() includes own name", () => {
    expect(Able.flatten({}, ["foo"])).toContain("foo");
  });

  // ...remaining tests...
});
```

#### [test/Able.spec.ts](https://github.com/tobyhinloopen/bonaroo-able/blob/master/test/Able.spec.ts)
```Typescript
import Able from "../src";

test("Able is exported", () => {
  expect(Able).toBeInstanceOf(Object);
});
```

## Testing our build

In some cases, our tests might succeed while our build fails, or the build is
somehow invalid. To ensure the build is working, I like to add a very crude test to confirm the build is working and the exports are in-place.

This test will build the code, and run a simple JS file using the build to confirm the build is working.

In this build test, we copy one of our test suite's tests. I think it is safe to assume that if one test actually using the library succeeds, the library is built and exported correctly.

**test-build.js**
```js
const assert = require("assert");
const Able = require("./dist");

const definition = { foo: ["bar"] };
const abilities = ["foo", "bam"];
const result = Able.flatten(definition, abilities).sort();
assert.deepStrictEqual(result, ["foo", "bar", "bam"].sort());
```

Note that we're importing `./dist` here: We are explicitly importing `dist/index.js` that way. We need to build our code before we can import `dist/index.js`.

To build the code & run `test-build.js`, we'll add a script to `package.json`, called `test-build`.

**package.json** (partial)
```json
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "test-build": "npm run build && node test-build.js"
  },
```

I like to run all automated checks, currently `npm t` and `npm run test-build`, from a single script called `ci`. This script will run all automated checks and only pass when all automated checks passed.

Let's add `ci` to the scripts as well:

**package.json** (partial)
```json
  "scripts": {
    "build": "tsc",
    "ci": "npm run test-build & npm t & wait",
    "test": "jest",
    "test-build": "npm run build && node test-build.js"
  },
```

This `ci` script will be used to verify our build every release. Let's try it!

```sh
$ npm run ci

> bonaroo-able@1.0.0 ci /home/toby/bonaroo-able/
> npm run test-build & npm t & wait


> bonaroo-able@1.0.0 test-build /home/toby/bonaroo-able/
> npm run build && node test-build.js


> bonaroo-able@1.0.0 test /home/toby/bonaroo-able/
> jest


> bonaroo-able@1.0.0 build /home/toby/bonaroo-able/
> tsc

 PASS  test/Able.spec.ts
 PASS  test/index.spec.ts

Test Suites: 2 passed, 2 total
Tests:       11 passed, 11 total
Snapshots:   0 total
Time:        1.816s
Ran all test suites.
```

Later we'll make sure to only accept changes in the `master` branch that have passed this `npm run ci` call. That way, we'll make sure the `master` branch always features a valid build.

Let's commit all of our changes to git and start deploying our library.

## NPM release

The first and most useful release is the npm release. This allows our library users to `npm i` our library in most projects.

Both server-side projects and client-side projects that use a bundler like `webpack` can use an npm release without any changes.

Let's prepare our library for publication to NPM.

### Preparing our package for release

Let's first define what files we actually want to include in our package. You can peek the contents of your package-to-be using `npm publish --dry-run`:

```sh
$ npm publish --dry-run
npm notice
npm notice 📦  bonaroo-able@1.0.0
npm notice === Tarball Contents ===
npm notice 862B  package.json
npm notice 56B   .git
npm notice 69B   jest.config.js
npm notice 284B  test-build.js
npm notice 114B  tsconfig.json
npm notice 3.9kB dist/Able.d.ts
npm notice 6.1kB dist/Able.js
npm notice 3.4kB dist/Able.js.map
npm notice 52B   dist/index.d.ts
npm notice 184B  dist/index.js
npm notice 198B  dist/index.js.map
npm notice 6.0kB src/Able.ts
npm notice 24B   src/index.ts
npm notice 3.4kB test/Able.spec.ts
npm notice 108B  test/index.spec.ts
npm notice === Tarball Details ===
...
+ bonaroo-able@1.0.0
```

This built includes all kinds of things the user wouldn't care about. With `package.json`'s `files` property you can whitelist the files you want to include.

Only the built files are required to use our library: Let's add only the `dist` folder to the package:

**package.json** (partial)
```js
{
  "main": "dist/index.js",
  "files": ["dist"],
  // ...
}
```

Now let's peek at our package's contents again:

```sh
$ npm publish --dry-run
npm notice
npm notice 📦  bonaroo-able@1.0.0
npm notice === Tarball Contents ===
npm notice 1.3kB  package.json
npm notice 3.9kB  dist/Able.d.ts
npm notice 6.1kB  dist/Able.js
npm notice 3.4kB  dist/Able.js.map
npm notice 52B    dist/index.d.ts
npm notice 184B   dist/index.js
npm notice 198B   dist/index.js.map
npm notice === Tarball Details ===
npm notice name:          bonaroo-able
...
+ bonaroo-able@1.0.0
```

That seems about right to me. Let's publish it!

### Publishing to NPM

Either sign-in `npm login` or sign-up `npm adduser`. After that, we're ready to publish our package.

**npm publish**
```sh
$ npm publish
npm notice
npm notice 📦  bonaroo-able@1.0.0
npm notice === Tarball Contents ===
npm notice 883B   package.json
npm notice 3.9kB  dist/Able.d.ts
npm notice 6.1kB  dist/Able.js
npm notice 3.4kB  dist/Able.js.map
npm notice 52B    dist/index.d.ts
npm notice 184B   dist/index.js
npm notice 198B   dist/index.js.map
npm notice === Tarball Details ===
npm notice name:          bonaroo-able
npm notice version:       1.0.0
npm notice package size:  2.3 kB
npm notice unpacked size: 7.1 kB
npm notice shasum:        4b25f5d01b4ef46259d947d0c0ce1455b92b8433
npm notice integrity:     sha512-mX7RA0CS8hprb[...]lFsx3AGk5XIeA==
npm notice total files:   7
npm notice
+ bonaroo-able@1.0.0
```

Nice!

### Testing our release in Node

Now we can use our package in Node projects! Let's create a temporary Node project to test our package.

```sh
mkdir /tmp/node-test
cd $_
npm i bonaroo-able
node
```

```js
> const Able = require("bonaroo-able");
undefined
> const definition = { foo: ["bar"] };
undefined
> const abilities = ["foo", "bam"];
undefined
> result = Able.flatten(definition, abilities).sort();
[ 'bam', 'bar', 'foo' ]
```

### Testing our release in a webpack project

To use our package in the brower, the package user might be using webpack. Let's try our package in webpack!

```sh
mkdir /tmp/webpack-test
cd $_
npm init -y
npm i bonaroo-able
npm i -D webpack webpack-cli html-webpack-plugin webpack-dev-server clean-webpack-plugin
```

#### webpack.config.js
```js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  entry: {
    app: './src/index.js',
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({ title: "Titled Document" }),
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};
```

#### src/index.js
```js
const Able = require("bonaroo-able");

document.addEventListener("DOMContentLoaded", () => {
  const definition = { foo: ["bar"] };
  const abilities = ["foo", "bam"];
  const result = Able.flatten(definition, abilities);

  const code = document.createElement("code");
  code.textContent = result.join(", ");
  document.body.appendChild(code);
});
```

#### package.json (partial)

```json
  "scripts": {
    "build": "webpack",
    "start": "webpack-dev-server --open"
  },
```

Let's start the webpack dev server:

```sh
npm start
```

We are greeted with `foo, bam, bar` in our browser: Webpack build works!

## Building our library for usage in browser

One cannot use the `dist/*` files in the browser directly - we must combine these files somehow to create a single bundle for the browser.

Bundling libraries for use in the browser is a hairy subject. There are many solutions, none of them are perfect. In this guide, I'll cover only one solution: We'll be creating something called an **IIFE build** using [rollup.js](https://rollupjs.org/guide/en/).

An IIFE build looks something like this:

```js
var Able = (function() {
  var Able = {};
  var otherVars = 1;

  Able.flatten = /* ... */

  return Able;
})();
```

Because the library is defined inside a function expression that is invoked immediately using `(function() {})()`, all definitions inside the function are hidden, and only the return value is exposed to the global scoped.

Since the Function Expression is Immediately Invoked, it is called an IIFE.

Let's install **rollup**, add a build command to our package.json, and add a config file for rollup. Also, let's also add a reference to our browser bundle in the package.json's **browser** property.

```sh
npm i -D rollup rollup-plugin-commonjs rollup-plugin-node-resolve rollup-plugin-babel-minify
```

#### package.json (partial)
```js
{
  "browser": "dist/bonaroo-able.min.js",
  "scripts": {
    // ...
    "rollup": "rollup -c"
    // ...
  }
}
```

#### rollup.config.js
```js
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import minify from 'rollup-plugin-babel-minify';
import pkg from './package.json';

export default [{
  input: 'dist/index.js',
  output: {
    name: "Able",
    file: pkg.browser,
    format: 'iife',
    sourcemap: true,
  },
  plugins: [
    resolve(),
    commonjs(),
    minify({ comments: false }),
  ],
}];
```

Let's test or browser build:

#### example.html
```html
<!DOCTYPE html>
<title>bonaroo-able test</title>
<script src="./dist/bonaroo-able.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
  const definition = { foo: ["bar"] };
  const abilities = ["foo", "bam"];
  const result = Able.flatten(definition, abilities);

  const code = document.createElement("code");
  code.textContent = result.join(", ");
  document.body.appendChild(code);
});
</script>
```

You should see `foo, bam, bar` again in your browser when opening `example.html`.

---

## Build before publish

You can configure NPM to build automatically before publishing by adding a `prepublish` script to your package.json. Because `npm publish` publishes the built files, we want to make sure the files are built and tested before every publish.

We already have `npm run ci` to both build & test our build. Let's add `rollup` to `ci`, and add `npm run ci` to `prepublishOnly`:

#### package.json (partial)
```js
  "scripts": {
    // ...
    "ci": "(npm run test-build && npm run rollup) & npm t & wait",
    "prepublishOnly": "npm run ci && npm run rollup",
    // ...
  }
```

Let's publish our new build. NPM uses [semantic versioning](https://docs.npmjs.com/about-semantic-versioning). Every release, you must update your version number. Since we introduced a new feature (browser build) without breaking changes, we can release a new minor version. You can increment your build number with `npm version minor`, push our new version to git with `git push`, and finish with `npm publish` to publish our new version.

```sh
npm version minor
git push
npm publish
```

## Including our library in a browser directly from a CDN

> [**unpkg**](https://unpkg.com/) is a fast, global content delivery network for everything on npm. Use it to quickly and easily load any file from any package using a URL like:
>
> ```
> unpkg.com/:package@:version/:file
> ```

Thanks unpkg - I couldn't have explained it better myself. Let's try this!

- **package**: Our package name, `bonaroo-able`.
- **version**: We just minor-bumped our version to `1.1.0`.
- **file**: The browser file: `dist/bonaroo-able.min.js`.

That makes `https://unpkg.com/bonaroo-able@1.1.0/dist/bonaroo-able.min.js`. Let's grab our example.html again, and change the script source to this URL:

#### example.html
```html
<!DOCTYPE html>
<title>bonaroo-able test</title>
<script src="https://unpkg.com/bonaroo-able@1.1.0/dist/bonaroo-able.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
  const definition = { foo: ["bar"] };
  const abilities = ["foo", "bam"];
  const result = Able.flatten(definition, abilities);

  const code = document.createElement("code");
  code.textContent = result.join(", ");
  document.body.appendChild(code);
});
</script>
```

Great. Works for me. Now let's write a readme.

## Writing a readme

A readme is the entry point of the documentation of our library and should include a short summary of the following:

- What is our library?
- Why does it exist?
- What can it be used for?
- How to install it
- How to use it
- Requirements & dependencies

Writing a good readme is outside the scope of this guide. This guide will only cover installation instructions.

#### README.md (partial)

    ## Installation - NPM

    ```sh
    npm i bonaroo-able
    ```

    ## Installation - Browser


    ```html
    <script src="https://unpkg.com/bonaroo-able@1.1.1/dist/bonaroo-able.min.js"></script>
    ```

The script tag in the readme now includes the version number, which will not be updated automatically. Let's add a simple script that bumps the version in the readme everytime we update the NPM version.

When using `npm version`, npm will invoke [multiple hooks](https://docs.npmjs.com/misc/scripts#description) automatically, two of which are called **preversion** (Run BEFORE bumping the package version) and **version** (Run AFTER bumping the package version, but BEFORE commit).

My approach is to dump the version before bumping the version, and after bumping the version to replace all occurances of the old version in the README.md with the new version.

#### preversion.sh
```sh
#!/usr/bin/env bash
node -e 'console.log(require("./package.json").version)' > .old-version
```

#### version.sh

```sh
#!/usr/bin/env bash
sed "s/$(cat .old-version)/$(node -e 'console.log(require("./package.json").version)')/g" < README.md > ~README.md
rm README.md .old-version
mv ~README.md README.md
git add README.md
```

#### package.json (partial)
```js
  "scripts": {
    // ...
    "preversion": "./preversion.sh",
    // ...
    "version": "./version.sh",
    // ...
  },
```

#### sh
```sh
chmod +x preversion.sh version.sh
```

Now let's commit our changes & bump the library version.

#### sh
```sh
git add --all
git commit -am "Introduce README.md"
npm version patch
git push
npm publish
```

Our readme is now updated! Neat.

    ## Installation - NPM

    ```sh
    npm i bonaroo-able
    ```

    ## Installation - Browser


    ```html
    <script src="https://unpkg.com/bonaroo-able@1.1.2/dist/bonaroo-able.min.js"></script>
    ```

## Final words

Now every time you change something about your library, commit the changes, update the version, push the version change & publish your new version:

```sh
git add --all
git commit -m "Describe your changes here."
npm version minor
git push
npm publish
```

If you're still here, thanks so much for reading! And if you want to know more or have any other questions, please get in touch with us via info@bonaroo.nl