My first time publishing to npm was exhausting

Maybe it was because I like to use TypeScript, but working out how to get my first public package on to npm was a real challenge. Here, I would like to provide a reflection mixed with tutorial elements that can hopefully help the next hobbyist developer (and my forgetful self) get through the fog.

Tl; dr

This came about when my partner was looking for a way to practice translating big numbers – think newspaper articles on Coronavirus stimulus – as part of her English-Japanese interpreting classes at Monash University.

Resources

Here is the list of reference material that I used to get me from zero to one (when counting the number of modules I have published):

I found that each one of these resources was lacking when used on their own. Together, I managed to piece together a process that I am quite satisfied with, overall.

The Method

Step 0 – The Setup

I am assuming the following steps can be performed without the assistance of this guide:

  1. Install Node.js and npm
  2. Initialise your project including:
    • package.json
    • tsconfig.json
    • .gitignore
    • README
    • LICENSE
  3. Add your new project to GitHub
  4. Create an account on npm

NPM Defaults (optional)

There is an opportunity to store some defaults in case you plan to publish more packages into the future.

npm set init.author.name “Your Name”
npm set init.author.email “public-email@address.com”
npm set init.author.url “https://www.yourwebsite.com/"

tsconfig.json

To make sure we can talk the same language, these are the TypeScript settings I used. There is no reason to specifically adopt the settings below, rather it will provide a reference point for the instructions to follow.

{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "lib": [
      "ES2020"
    ],
    "declaration": true,
    "sourceMap": true,
    "outDir": "./lib",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}

package.json

Unlike the content above, this one is a little more prescriptive. I have only included the entries that I believe are necessary to successfully publish a package.

{
  "main": "./lib/index.min.js",
  "type": "commonjs",
  "types": "./lib/index.d.ts",
  "scripts": {
    "clean": "shx rm -rf lib/*",
    "build": "tsc && npm run minify",
    "watch": "tsc --watch",
    "test": "nyc mocha lib/*.test.js -R spec -u tdd",
    "coverage": "nyc report --reporter=text-lcov | coveralls",
    "prepack": "npm run clean && npm run build",
    "minify": "uglifyjs --compress --mangle --output lib/index.min.js -- lib/index.js"
  },
  "devDependencies": {
    "@types/mocha": "5.2.7",
    "@types/node": "12.12.2",
    "coveralls": "3.1.0",
    "mocha": "8.1.1",
    "mocha-lcov-reporter": "1.3.0",
    "nyc": "15.1.0",
    "shx": "0.3.2",
    "typescript": "3.9.7",
    "uglify-es": "3.3.9"
  },
}

Don’t forget to run npm install after setting up your package.json file.

Step 1 – Write your module

Self-explanatory to start off with. The key message here is to remember to export the relevant content that will be accessible to the users of your new module.

Now is also a good time to consider the generation of your type definition files. Make sure to appropriately comment your code to allow for automated generation of the index.d.ts file.

For more information on TypeScript modules, the documentation is quite comprehensive.

//Example module
/**
 * Adds two numbers together.
 * @param a first input number 
 * @param b second input number
 */
function add(a: number, b: number): number {
  return a + b;
}

export default function add;

Step 2 – Write some tests

You may have noticed already that once the module is compiled to ordinary JavaScript, it’s no longer going to care what data types are inputted into our add function.

console.log(add("a", "b"));
// ab

I won’t address the code solutions to that here and will instead assume that you can add a line or two to ensure the inputs are valid numbers and that any other data types will throw an error.

You may have noticed in package.json that we included Mocha for our unit tests. Given our simple example module, we won’t be taking advantage of the full gamut of features available. Make sure to check out the Mocha documentation for more nifty tricks.

For this example, I will only be using three functions within Mocha:

  • test – defines the name of the test.
  • strictEquals – validates expected values against returned values with ===.
  • throws – ensures errors are thrown when invalid arguments are supplied.
import { strict as assert } from "assert";
import add from "./";

test("add()", () => {
  assert.strictEqual(add(0, 0), 0);
  assert.strictEqual(add(1, 0), 1);
  assert.strictEqual(add(0, 1), 1);
  assert.strictEqual(add(1.9, 3.4), 5.3);
  assert.strictEqual(add(13, 82), 105);
  assert.strictEqual(add(-10, 20), 10);

  assert.throws(() => {
    add((("a" as unknown) as number), (("b" as unknown) as number));
  });
  assert.throws(() => {
    add((("%" as unknown) as number), 5);
  });
  assert.throws(() => {
    add(5, (("/" as unknown) as number));
  });
});

One of the key elements of testing is coverage. Make sure to cover as many lines as practical to ensure your code is behaving as expected.

npm run build #compiles your code to JS
npm run test #runs the test files and shows coverage

Step 3 – Write a meaningful README

One of the biggest frustrations I have when using other publicly published modules on npm is uninformative readme files. To write a decent enough readme without being onerous, make sure to include the following sections:

  • Installation
  • Usage
# add
A lightweight module to add two numbers together.

## Installation
```sh
npm i --save add
```

## Usage
### JavaScript
```javascript
const add = require('add');
const number = add(1, 2);
console.log(number); //Outputs: 3
```

### TypeScript
```typescript
import { default as add } from 'add';
const number = add(4, 5);
console.log(number); //Outputs: 9
```

## Test
```sh
npm run test
```

Now is also a great opportunity to armour up with some shields for your readme. Check out to generate yours (keep this window open for Step 5 for badges via the external apps).

Step 4 – Commit, push and publish

Now this step makes me uncomfortable. I’m not 100% sure if steps 4 and 5 have to occur in this order and it might be something I test in the future.

First, we may need to create a .npmignore file to override the .gitignore file. The format is exactly the same in both files. Once I had played with this a little bit, I only exported the following files to npm:

  • lib/index.min.js
  • lib/index.d.ts
  • LICENSE
  • README
  • package.json
#Git commit and push - make sure to include a tag
git add .
git commit -m “Initial release”
git tag v1.0.1
git push origin master --tags

#Publish
npm publish

Step 5 – External apps

Continuous Integration

Travis CI is the approach I took to continuous integration. This tool will allow you to display to your users that the module is working and passing all of the tests we wrote earlier.

Once you have set up your account and switched on your project, you will need a .travis.yml file to tell Travis how to handle your module.

language : node_js
node_js :
 - stable
install:
 - npm install
before_script:
 - npm run build
script:
 - npm run test 
after_script:
  - npm run coverage

The Travis CI documentation offers some more depth in defining your yml file.

Now that you’ve set this up, Travis CI will kick in upon your next commit to GitHub and provide you with a report on your module’s performance.

Coverage

For coverage, I went with Coveralls to publish my testing coverage. Similar to Travis CI, you have to switch on your repository and make a push to GitHub before it will engage.

Fortunately, we have already completed the background steps to make this work:

  • "coverage": "nyc report --reporter=text-lcov | coveralls" from package.json will automatically pipe the results to Coveralls.
  • npm run coverage from .travis.yml will ensure that the coverage script is run.

Step 6 – The final commit

After you are happy with your testing and coverage results, readme, and type declaration files it’s time to commit and publish one last time.

#Iterate the version number (updates package.json)
npm version patch -m "Version %s - add sweet badges"

#Get those files updated on GitHub
git push origin master --tags

#Publish the update
npm publish

Conclusion

After following the steps above, you should now be able to install your newly minted package via npm.

npm install --save add

Phew.

Leave a Reply

Your email address will not be published. Required fields are marked *