My JS Journey: Creating an npm Package

In this blog post, I will show how to create an npm package that can be used from other modules. The example is a wrapper around node fs.

Setting up

Start as you would for any ES6 project by creating the following. The steps are covered in previous blog postings: My JavaScript Journey: Basic ES6 CLI Skeleton and Mocha as a JavaScript Test Framework.

mkdir file-handler
cd file-handler
git init
npm init -y
mkdir src
cd src
create index.js
create _index.js
cd ..
mkdir test
cd test
create test.js
cd ..
npm install babel-core --save-dev
npm install babel-preset-env --save-dev
npm install babel-register --save-dev
npm install babel-cli --save-dev
npm install mocha --save-dev
create .gitignore
create .babelrc
cd src
update package.json to add scripts, etc.

Test fs.readFile

Add tests to test/test.js that call your wrapper method

import assert from 'assert';
import FileHandler from '../src/index';

describe('FileHandler', function() {
  const testFilesPath = 'test/data/';
  const fileName = 'hello.txt';
  let fileHandler = new FileHandler();
  describe('getData', function() {
    it('should return error when invalid path', function(done) {
      const path = "blah";
      const expected = "ENOENT: no such file or directory, open '" + path + "'";
      fileHandler.getData(path)
      .catch(function (e) {
        assert.equal(expected, e.message);
        done();
      })
      .catch(function(err) {
        done(err);
      });
    });

    it('should return file contents', function(done) {
      const expected = "hello";
      fileHandler.getData(`${testFilesPath}${fileName}`)
      .then(function(data) {
        assert.equal(encodeURI(data), expected);
        done();
      })
      .catch(function(err) {
        done(err);
      });
    });
  });
});

Add a wrapper method to src/index.js

Since the only thing you will be exporting is the FileHandler class, the code goes directly into /src/index.js. This is what will be imported by the module that uses it.

import fs from 'fs';

class FileHandler {
  getData(path, type = 'utf8') {
    return new Promise((resolve, reject) => {
      fs.readFile(path, type, (err, data) => {
        if (err) { reject(err); }
        resolve(data);
      })
    });
  }
}

export default FileHandler;

Run the test

Add a script to run the tests to package.json:

"test": "mocha --compilers js:babel-core/register test/test.js"

Run:
npm test

One test should pass and one should fail.

  FileHandler
    getData
      ✓ should return error when invalid path
      1) should return file contents

  1 passing (268ms)
  1 failing

  1) FileHandler getData should return file contents:
     Error: ENOENT: no such file or directory, open 'test/data/hello.txt'
      at Error (native)

To make the tests run automatically with each change, add the following scripts to package.json:

"start": "npm run dev",
"dev": "npm test -- -w",

To ensure you build after each change, add the following script to package.json ("pre" is a prefix that will run the script before either build or test):

"pretest": "npm run build",

And, to prevent the test file(s) from being included in the distribution, add the babel-cli flag --ignore to the build script in package.json:

"build": "babel ./src -d ./dist --ignore test.js",

Now, run:

npm start

You will see the scripts chained together. In the end, Mocha will be running, waiting for the next change to be saved. To quit, ctrl-c.


> file-handler@1.0.0 start /Users/<your source>/file-handler
> npm run dev

> file-handler@1.0.0 dev /Users/<your source>/file-handler
> npm test -- -w

> file-handler@1.0.0 pretest /Users/<your source>/file-handler
> npm run build

> file-handler@1.0.0 prebuild /Users/<your source>/file-handler
> npm run clean

> file-handler@1.0.0 clean /Users/<your source>/file-handler
> rm -rf dist

> file-handler@1.0.0 build /Users/<your source>/file-handler
> babel ./src -d ./dist --ignore test.js

src/_index.js -> dist/_index.js
src/index.js -> dist/index.js

> file-handler@1.0.0 test /Users/<your source>/file-handler
> mocha --compilers js:babel-core/register test/test.js "-w"

  FileHandler
    getData
      ✓ should return error when invalid path
      1) should return file contents

  1 passing (268ms)
  1 failing

  1) FileHandler getData should return file contents:
     Error: ENOENT: no such file or directory, open 'test/data/hello.txt'
      at Error (native)

Add test/hello.txt File

cd test
mkdir data
create hello.txt

You will need to tickle src/index.js and save it to trigger the tests to run again (or ctrl-c and start again). The test will still fail since there is no content in test/data/hello.txt.

  FileHandler
    getData
      ✓ should return error when invalid path
      1) should return file contents

  1 passing (39ms)
  1 failing

  1) FileHandler getData should return file contents:

      AssertionError: '' == 'hello'
      + expected - actual

      +hello

      at test/test.js:26:16

Add ‘hello’ to test/data/hello.txt. Tickle index.js and save it to run the tests again. All happy and green now.

  FileHandler
    getData
      ✓ should return error when invalid path
      ✓ should return file contents

  2 passing (8ms)

Checkin

This is a good spot to checkin.

Currently, my package.json looks like this:

{
  "name": "file-handler",
  "version": "1.0.0",
  "description": "Library creation demonstration",
  "main": "index.js",
  "scripts": {
    "clean": "rm -rf dist",
    "prebuild": "npm run clean",
    "build": "babel ./src -d ./dist --ignore test.js",
    "start": "npm run dev",
    "dev": "npm test -- -w",
    "pretest": "npm run build",
    "test": "mocha --compilers js:babel-core/register test/test.js"
  },
  "keywords": [
    "node",
    "fs",
    "testing",
    "mocha",
    "javascript",
    "es6"
  ],
  "author": "Ramona Ridgewell",
  "license": "ISC",
  "devDependencies": {
    "babel-cli": "^6.24.1",
    "babel-core": "^6.25.0",
    "babel-preset-env": "^1.5.2",
    "babel-register": "^6.24.1",
    "mocha": "^3.4.2"
  }
}

Using the Library

We need a different app to import file-handler. How do we do that? First, to get the ES5 transpiled version of the code, change the "main" property in package.json to point to ./dist/index.js.

  "main": "./dist/index.js",

The final bit is to set which files are required by the end-user, which is the ./dist/ folder. Add this to package.json:

"files": [
  "dist"
]

Create node.js Package Module

The first step in publishing an npm module is to create the package. Navigate to the root where the package.json is. Run:

npm pack

This creates file-handler-1.0.0.tgz in the same directory. It gets the "name" field from package.json and appends the "version" from the same file, to create the file name. You can see the contents by running:

tar -tf file-handler-1.0.0.tgz

which returns:

package/package.json
package/README.md
package/dist/_index.js
package/dist/index.js

Test the Module

Create Test Module

To set up a new module, you can use this package.json or however you wish to set it up.

{
  "name": "testFileLib",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "clean": "rm -rf dist",
    "prebuild": "npm run clean",
    "build": "babel ./src -d ./dist --ignore test.js",
    "start": "npm run dev",
    "dev": "npm test -- -w",
    "pretest": "npm run build",
    "test": "mocha --compilers js:babel-core/register test/test.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-cli": "^6.24.1",
    "babel-core": "^6.25.0",
    "babel-preset-env": "^1.5.2",
    "babel-register": "^6.24.1",
    "mocha": "^3.4.2"
  }
}

Add the usual src/index.js, src/_index.js, .babelrc, .gitignore. index.js doesn’t need to do anything at this point.

const main = () => {
  console.log("hello");
};

main();

Install Your Package

Here’s the most exciting part. Navigate to the root of your test project and run:

npm install ../file-handler/file-handler-1.0.0.tgz

The path assumes your library and test project are in the same parent directory —adjust appropriately. The output looks like this:

testFileLib@1.0.0 /Users/<your source>/testFileLib
└── file-handler@1.0.0

npm WARN testFileLib@1.0.0 No repository field.

If you check ./node_modules/, you will find a file-handler directory with the built /dist/, and the package.json and README.md.

Write Some Tests

I literally copied the tests from file-handler and changed line 2 to:

import FileHandler from 'file-handler';

Running npm test should just work as expected.

Read File When Running Test App

To use file-handler in index.js, change it to the following:

import FileHandler from 'file-handler';

const main = () => {
  const fileHandler = new FileHandler();
  const fileName = "test/data/hello.txt"
  fileHandler.getData(fileName)
  .then((data) => {
      console.log(data);
  })
  .catch((err) => {
    console.log("error reading file", err);
  });
};

main();

If you run:

npm test

it will clean, build and run your tests. Then, run:

node src/_index

It should say hello or whatever you put in your file.

Add file-handler to npm

At this point, even though the library only supports one method, I am going to show how to make your new library available on npm.

Add User

Assuming you have never done this before, run the following:

npm adduser

It will prompt you for Username, Password, Email. Username must be unique, so you might need to try a couple of times to find an unused one. The error begins with this:

npm WARN adduser Incorrect username or password
npm WARN adduser You can reset your account by visiting:
npm WARN adduser
npm WARN adduser     https://npmjs.org/forgot
npm WARN adduser

You can verify you are not already using the email by going to the link https://npmjs.org/forgot and trying to have a reset email sent. When you give it all valid responses, it will respond with:

Logged in as on https://registry.npmjs.org/.

Confirm you were added by going to:

https://www.npmjs.com/~<your username>

If you already have an npm user, you can login using:

npm login

Publish the Module

Now, it’s time to publish. As long as you’re in the root of the project (with the package.json), you don’t need to specify the folder to point to that file. Include the tarball and, unless you’re a paying member, don’t forget to make the package public (it defaults to private).

npm publish file-handler-1.0.0.tgz --access public

If your project name is already taken on npm, which this example assumes, you will receive an obtuse error message that includes:

npm ERR! you do not have permission to publish "file-handler". Are you logged in as the correct user? : file-handler

To overcome the error without simply choosing a different package name, create and install a scoped package, which I discuss below. I think this is a better way to go since all of your packages will be bundled under your npm user name in the file hierarchy.
If you wish, you can search to try to find a unique name using:

npm search

To add scope, prefix your package.json "name" with @ followed by your npm user name. The @ is important and will be included in the import paths.

{
  "name": "@<your npm user name>/file-handler"
}

Create a new tarball. It will include the prefix.

npm pack

generates the file

<your npm user name>-file-handler-1.0.0.tgz

When you install the new tarball in your test project, you will find this folder structure in ./node_modules/:

/@<your npm user name>/file-handler/

Of course, you will then need to change your test project code paths to reflect the change. Don’t forget the @.

import FileHandler from '@<your npm user name>/file-handler';

Run your tests. They should just pass.

This time, publishing should work:

npm publish <your npm user name>-file-handler-1.0.0.tgz --access public

A successful publish will result in something like this:

+ @<your npm user name>/file-handler@1.0.0

Once you publish a package with a given name and version, it can never be used again. You will need to change the version in package.json.

Now, go back to your test project’s ./node_modules/ and delete

./@<your npm user name>/file-handler/

Then, to install your package from npm, run:

npm install @<your npm user name>/file-handler --save-dev

It should succeed. The output looks like this:

testFileLib@1.0.0 /Users/<your>/<src>/testFileLib
└── @<your npm user name>/file-handler@1.0.0

npm WARN testFileLib@1.0.0 No repository field.

If you check your package.json, you should find this line added to the devDependencies:

"@<your npm user name>/file-handler": "^1.0.0",

Run your tests. All should be well in the universe.

In Conclusion

Wow. This took a lot of research. In my next blog post, I will cover making updates to the npm package. The file-handler code is in my GitHub file-handler repository.

Copyright ©2014-17 Ramona Ridgewell. All rights reserved.

Advertisements
This entry was posted in #Education, #npm, AmWriting, Coding, GitHub, JavaScript, Programming, Science, STEM and tagged , , , , , , , , , , , , , , . Bookmark the permalink.

One Response to My JS Journey: Creating an npm Package

  1. Pingback: Mocha as a JavaScript Test Framework | RamonaRidgewell

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s