TypeScript is awesome! As seen in the previous article, it can really help improving our code quality and avoiding a wide range of runtime errors.
For that reason, a lot of JavaScript libraries are actually written in TypeScript: Rx.js, Svelte, Angular and many more.

But as you may know, TypeScript needs to be compiled down to JavaScript in order to work on both browsers and servers. So what do we need to do in order to publish our library to npm?

Create your library

Let’s start with the obvious: initialize your library!
We’ll build a simple library called JScream, which will just transform any sentence into a “scream”.

const sentence = jscream("I am hungry.");
console.log(sentence); // => "I AM HUNGRY!!!"

Pretty useless I know, but we need to focus on how to publish and test it the right way!

mkdir jscream
cd jscream
npm init

Let’s initialize our JScream app using npm. Answer every prompted question and you’ll come up with a similar package.json file:

{
  "name": "jscream",
  "version": "1.0.0",
  "description": "A simple library build for a JSMonday article.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Michele Riva <ciao@micheleriva.it>",
  "license": "Unlicense"
}

Great! Now we need to create our tsconfig.json file, which contains the required instructions for the TypeScript compiler:

{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./dist",
    "module": "commonjs",
    "noImplicitAny": true,
    "lib": ["es2017", "es7", "es6", "dom"],
    "outDir": "./dist",
    "target": "es5",
    "moduleResolution": "node"
  },
  "typedocOptions": {
    "mode": "modules",
    "out": "docs"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

The compiled JavaScript output will be written inside the dist directory, which will be created from the compiler itself.
The compiler will also write a type declaration file, so if we need to import this library as a dependency, we’ll have access to its types!

Let’s build up the following folder structure:

[+] jscream
 |
 | [+] src
 |  |__index.ts
 |
 | .gitignore
 | package.json
 | tsconfig.json

As you can see, we’re omitting the dist folder.
We can now start to write our library inside our src/index.ts file:

type scream = string;

export default function scream(sentence: string): scream {
  return addExclamationPoints(sentence).toUpperCase();
}

export function addExclamationPoints(str: string): string {
  return str.replace(/\?/g, "?!").replace(/\.$/, "!!!");
}

Ok, we wrote our awesome library. Not it’s time to compile it! Let’s edit our package.json file as follows:

{
  "name": "jscream",
  "version": "1.0.0",
  "description": "A simple library build for a JSMonday article.",
  "main": "./src/index.ts",
  "repository": {
    "type": "git",
    "url": "https://github.com/jsmonday/jscream"
  },
  "scripts": {
    "build": "tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Michele Riva <ciao@micheleriva.it>",
  "license": "Unlicense"
}

As you can see, we’ve just added a build script which invokes the TypeScript compiler. It automatically looks for a tsconfig.json file and will follow its rules!
Let’s compile and inspect the output.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function scream(sentence) {
  return addExclamationPoints(sentence).toUpperCase();
}
exports.default = scream;
function addExclamationPoints(str) {
  return str.replace(/\?/g, "?!").replace(/\.$/, "!!!");
}
exports.addExclamationPoints = addExclamationPoints;

Ok, the JavaScript output looks fine! We also have an index.d.ts file which contains our library’s type definitions:

declare type scream = string;
export default function scream(sentence: string): scream;
export declare function addExclamationPoints(str: string): string;
export {};

Great! Now we need to prepare our library to be committed to git.
Let’s populate our .gitignore file:

dist
node_modules

As you can see, we’re omitting our dist folder, which will not be published on npm nor on our git repository… so how do we publish our compiled and ready-to-use JavaScript?

Preparing the bundle

{
  "name": "jscream",
  "version": "1.0.0",
  "description": "A simple library build for a JSMonday article.",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["/dist"],
  "repository": {
    "type": "git",
    "url": "https://github.com/jsmonday/jscream"
  },
  "scripts": {
    "build": "tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Michele Riva <ciao@micheleriva.it>",
  "license": "Unlicense"
}

As you can see, we’ve changed a couple of things inside our package.json file:

  1. We changed the main file to ./dist/index.js
  2. We added a types file, which also is placed inside our dist direcotry
  3. We specified which files will be published on npm. In that case, we’ll just publish every file inside the dist directory. As you can see, the file property is an array of strings, so you can add as much files/folders as you want!

We’re not ready yet to publish our library! We need to add another script command to our package.json:

{
  "name": "jscream",
  "version": "1.0.0",
  "description": "A simple library build for a JSMonday article.",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["/dist"],
  "repository": {
    "type": "git",
    "url": "https://github.com/jsmonday/jscream"
  },
  "scripts": {
    "build": "tsc",
    "prepare": "npm run build",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Michele Riva <ciao@micheleriva.it>",
  "license": "Unlicense"
}

This is probably one of the most important commands for our library: prepare.
It will be spawned right before the publication, when you type npm publish.

We can now test what npm is gonna publish by running npm pack.
It will create a .zip folder containing the complete package that will be published to npm!
This is incredibly useful to check if everything is ok right before publishing the package.

One last thing to do is to add a README.md file to our package. It will describe how the library works and will be automatically added to our npm bundle, so we don’t need to add it to the files array in our package.json file.

Adding tests

Tests are an essential part of any JavaScript application.
They ensure that every function works as expected and add an extra security layer over the introduction of regressions/bugs while working on new features.

For JScream we’ll be using Jest, which is an amazing library written by **Facebook** for testing JavaScript code.

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

First of all, let’s install both jest, ts-jest, **@types/jest and typescript **as devDependencies.
Then, we need to setup our jest.config.js file:

module.exports = {
  roots: ["<rootDir>/tests"],
  transform: {
    "^.+\\.ts$": "ts-jest"
  }
};

Great! Now let’s update our package.json file in order to run tests:

{
  "name": "jscream",
  "version": "1.0.0",
  "description": "A simple library build for a JSMonday article.",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["/dist"],
  "repository": {
    "type": "git",
    "url": "https://github.com/jsmonday/jscream"
  },
  "scripts": {
    "build": "tsc",
    "prepare": "npm run build",
    "test": "jest --coverage"
  },
  "author": "Michele Riva <ciao@micheleriva.it>",
  "license": "Unlicense",
  "devDependencies": {
    "@types/jest": "24.0.13",
    "jest": "24.8.0",
    "ts-jest": "24.0.2",
    "typescript": "3.5.1"
  }
}

As you can see, as a value for the test script property, we’re adding jest —- coverage. That means that we’re also checking how many functions/lines we’re covering with our tests.

We now need to create a tests folder and a test itself:

import scream, { addExclamationPoints } from "../src/index";

test("Testing 'scream' function", () => {
  expect(scream("Hello")).toBe("HELLO");
  expect(scream("Hello world.")).toBe("HELLO WORLD!!!");
  expect(scream("How are you?")).toBe("HOW ARE YOU?!");
});

test("Testing 'addExclamationPoints' function", () => {
  expect(addExclamationPoints("Hey.")).toBe("Hey!!!");
  expect(addExclamationPoints("How old are you?")).toBe("How old are you?!");
  expect(addExclamationPoints("Foo")).toBe("Foo");
});

Typing npm run test, we’ll run our tests and we’ll also get a coverage report which will be placed by default inside the coverage folder. Remember to add it to your .gitignore file!

The coverage report will look like this:

TypeScript

As you can see, it will report how many times a single function has been tested… and that’s awesome!

Let’s edit one more time our package.json file:

{
  "name": "jscream",
  "version": "1.0.0",
  "description": "A simple library build for a JSMonday article.",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["/dist"],
  "repository": {
    "type": "git",
    "url": "https://github.com/jsmonday/jscream"
  },
  "scripts": {
    "build": "tsc",
    "prepare": "npm run test && npm run build",
    "test": "jest --coverage"
  },
  "author": "Michele Riva <ciao@micheleriva.it>",
  "license": "Unlicense",
  "devDependencies": {
    "@types/jest": "24.0.13",
    "jest": "24.8.0",
    "ts-jest": "24.0.2",
    "typescript": "3.5.1"
  }
}

We’ve updated our prepare script: it will run tests right before building the whole library, so if our tests won’t pass, the process will interrupt and we won’t publish a broken library!

Setting up Continuous Integration

Continuous Integration is pretty important: when you adopt collaborative tools like GitHub or you work in a team, it can really help checking if merging a pull request will produce some kind of errors, or if you’re introducing a regression/bug in your codebase.

Let’s setup a Travis CI configuration file (travis.yml):

language: node_js
cache:
  directories:
    - "node_modules"
node_js:
  - "10"

Now let’s enable our repository in Travis-CI:

Travis CI

Great! Now everytime we push a commit to our master branch, it will trigger a CI build. It will run our tests, and if they fails, we’ll get a feedback from Travis!

We can also get a Coverage report history using Codecov.
Let’s install it as a devDependency:

npm i -D codecov

Now we need to edit our .travis.yml file:

language: node_js
cache:
  directories:
    - "node_modules"
node_js:
  - "10"
after_success:
  - codecov

Last but not least, we need to create a .codecov.yml file:

comment: off

coverage:
  status:
    patch:
      default:
        target: 80%
    project:
      default:
        target: auto

As you can see, we set the coverage target to 80%. That means that in order to pass, our tests must cover at least the 80% of our functions.

Conclusion

Now our publishing flow will look like this:

  1. Write new features
  2. Add tests
  3. Run tests
  4. Run CI
  5. Run Codecov
  6. Publish

You can find the source code for the JScream library here:github.com/jsmonday/jscream

Did you like this article? Consider becoming a Patron!

This article is CC0 1.0 (Public Domain) licensed.