Skip to content

Instantly share code, notes, and snippets.

@rcgary
Forked from hayeah/gist:6e1c49efcd4d813a55ea
Last active August 29, 2015 14:00
Show Gist options
  • Save rcgary/bf16ee65c92516a4c25f to your computer and use it in GitHub Desktop.
Save rcgary/bf16ee65c92516a4c25f to your computer and use it in GitHub Desktop.

Create An NPM Package

In this lesson we'll build a simple npm package. The package installs the greet command:

$ greet howard
hello howard

If greet had had a few more martinis then it should:

$ greet howard --drunk
hello howard, you look sexy today

By building this simple package, we'll become familiar with the core tools used in a nodejs development environment.

  • How to create an npm package
  • CommonJS module system

Create An NPM Package

Let's use the npm init command to start a new npm project in the greet directory:

$ mkdir greet && cd greet
$ npm init
name: (greet)
version: (0.0.0)
description: A simple and native greeter
entry point: (index.js)
test command: mocha
git repository:
keywords: hello-world
author: Howard Yeh
license: (ISC) MIT
Is this ok? (yes) yes

It creates the file package.json, which describes the project. Let's take a look:

$ cat package.json
{
  "name": "greet",
  "version": "0.0.0",
  "description": "A simple and naive greeter",
  "main": "index.js",
  "scripts": {
    "test": "mocha"
  },
  "keywords": [
    "hello-world"
  ],
  "author": "Howard Yeh",
  "license": "MIT"
}

Install Mocha As Package Dependency

Mocha is a test framework for NodeJS.

Let's install mocha locally (using the --save-dev option to add it to package.json as a package dependency):

> npm install mocha --save-dev
npm WARN package.json greet@0.0.0 No repository field.
npm WARN package.json greet@0.0.0 No README data
npm http GET https://registry.npmjs.org/mocha
npm http 200 https://registry.npmjs.org/mocha
npm http GET https://registry.npmjs.org/mocha/-/mocha-1.18.2.tgz
npm http 200 https://registry.npmjs.org/mocha/-/mocha-1.18.2.tgz
...
mocha@1.18.2 node_modules/mocha
├── diff@1.0.7
├── debug@0.7.4
├── growl@1.7.0
├── commander@2.0.0
├── mkdirp@0.3.5
├── jade@0.26.3 (commander@0.6.1, mkdirp@0.3.0)
└── glob@3.2.3 (inherits@2.0.1, graceful-fs@2.0.3, minimatch@0.2.14)

Now mocha is installed in the local ./node_modules/mocha directory.

And in package.json you can see the development dependencies:

"devDependencies": {
  "mocha": "^1.18.2"
}

Executables installed by node packages are available in ./node_modules/.bin/:

$ ./node_modules/.bin/mocha --version
1.18.2

This is actually a symbolic link to a file in ./node_modules/mocha:

$ ls -l ./node_modules/.bin/mocha
./node_modules/.bin/mocha -> ./node_modules/mocha/bin/mocha

If we want to run the locally installed mocha executable as mocha instead of ./node_modules/.bin/mocha, we need to add ./node_modules/.bin to the PATH environment variable:

$ export PATH=./node_modules/.bin:$PATH
$ which mocha
./node_modules/.bin/mocha
$ mocha --version
1.18.2

Add export PATH=./node_modules/.bin:$PATH to ~/.bashrc if you want to be able to run locally installed npm executables in the future.

Running Test

The scripts key is used to specify tasks that the package can run:

{
  "name": "greet",
  ...
  "scripts": {
    "test": "mocha"
  },
  ...
}

In this example, the test task runs the mocha command. We can try running the test:

$ npm test
> greet@0.0.0 test /Users/howard/workspace/node-bootcamp/greet
> mocha


/Users/howard/workspace/node-bootcamp/greet/node_modules/mocha/bin/_mocha:432
      if (!files.length) throw new Error("cannot resolve path (or pattern) '"
                               ^
Error: cannot resolve path (or pattern) 'test'
...

This error is because we don't have the test directory. Let's create it:

$ mkdir test
$ npm test
npm test

> greet@0.0.0 test /Users/howard/workspace/node-bootcamp/greet
> mocha

  0 passing (1ms)

Cool it works now.

Create The Greet Module

Let's package a very simple greet function as an node module.

function greet(name) {
  return "hello, " + name;
}

Loading The Package Module Locally

In package.json, there's the main value:

{
  "name": "greet",
  ...
  "main": "index.js"
}

This means that index.js is the file that would be loaded when the module is required.

Create index.js. Just make it an empty file for now.

$ touch index.js

We can require this file directly (it exports an empty object by default):

$ node -e 'console.log(require("./index.js"))'
{}

Or we can require it without the .js extension:

$ node -e 'console.log(require("./index"))'
{}

Or we can require the project directory (which requires the main value of package.json:

$ node -e 'console.log(require("./"))'
{}

All of the above loads index.js as a module.

Installing The Project As Package

Instead of loading the module by path, we want to be able to load the module by name:

// require by module name
var greet = require("greet");
// is the the same as requiring
var greet = require("/path_to_greet_module/index.js");

Let's try to require the greet module:

$ node -e 'console.log(require("greet"))'

module.js:340
    throw err;
          ^
Error: Cannot find module 'greet'

We get this error because the module isn't installed. For development purposes, we can use the npm link command to install the current project directory as package:

Hint: If the global node_modules path is not writable by user, you need to run sudo npm link

$ npm link
~/.nvm/v0.10.26/lib/node_modules/greet -> ~/greet

The installed greet module is a symlink to the project directory. Let's require our module again:

$ node -e 'console.log(require("greet"))'
{}

It works.

To uninstall the linked package:

$ npm unlink -g greet
unbuild greet@0.0.0

Question: npm link installs a package by creating a symbolic link. What's a symbolic link?

  • Symbolic link is a globally-installed symbolic link to reference the local install

Package linking is a two-step process.

First, npm link in a package folder will create a globally-installed symbolic link from prefix/package-name to the current folder.

Next, in some other location, npm link package-name will create a symlink from the local node_modules folder to the global symlink.

Reference: npm-link

Question: npm link installs the current directory as a global package. What's the difference between installing a package globally and installing it locally? Read npm 1.0: Global vs Local installation

  • globally —- This drops modules in {prefix}/lib/node_modules, and puts executable files in {prefix}/bin, where {prefix} is usually something like /usr/local. It also installs man pages in {prefix}/share/man, if they’re supplied.
  • locally —- This installs your package in the current working directory. Node modules go in ./node_modules, executables go in ./node_modules/.bin/, and man pages aren’t installed at all.

The Greet CommonJS Module

Right now, our greet module is empty. It's just the empty object:

$ node
> greet = require("greet")
{}

NodeJS uses the CommonJS module spec. Basically, whatever value you assign to module.exports is the value returned by require. See documentation on module for more details.

Implement: index.js should export the greet function

Hint: In index.js, complete the following:

// file: index.js
module.exports = ...

Example:

$ node
> greet = require("greet")
[Function: greet]
> greet("howard")
'hello, howard'

Building The Greet Package

npm link is good for developing the package locally. npm pack is used to create a user installable package.

$ npm pack
greet-0.0.0.tgz

Running the packing command creates a compressed tarball (.tgz). We can look inside to see what it contains:

$ tar -ztf greet-0.0.0.tgz
package/package.json
package/index.js

You can install the tar file:

$ npm install greet-0.0.0.tgz
npm install greet-0.0.0.tgz
npm WARN package.json greet@0.0.0 No repository field.
npm WARN package.json greet@0.0.0 No README data
greet@0.0.0 node_modules/greet

Uninstall it:

$ npm uninstall greet
npm uninstall greet
unbuild greet@0.0.0

Create The greet Executable

We want to be able to use the greet module as a command. Add the bin field to package.json (detailed doc):

// in package.json
"bin" : { "greet" : "./bin/greet.js" }

This specifies that when the package is installed, the script ./bin/greet.js should be installed as the executable greet.

Now let's create the executable script at bin/greet.js:

#!/usr/bin/env node
console.log("Hello World");

The first line is a shebang directive. It's a way for the computer to know how to run the script as a program.

(Why do we use /usr/bin/env instead of the node command? Because the node executable can be at different places on different systems, and the env uses the environment's PATH to figure out which node program to run.)

Change the file permission as an executable file:

$ chmod a+x bin/greet.js

Now we can execute the script:

$ ./bin/greet.js
Hello World

Let's run npm link again:

$ npm link
~/.nvm/v0.10.26/bin/greet -> ~/greet/bin/greet.js
~/.nvm/v0.10.26/lib/node_modules/greet -> ~/greet

Notice how ~/.nvm/v0.10.26/bin/greet is a link to our script. Now we can run the greet command:

$ greet
Hello World

Implement modify bin/greet.js so greet howard uses the greet function exported by index.js

HINT: Use require with a relative path. See File Module

HINT: process.argv is an array that contains the command arguments.

Example:

$ greet howard
hello, howard

Drink A Few Nice Martinis

Let's change the greet function to accept an extra argument:

function greet(name,drunk) {
  if(drunk) {
    return "hello " + name + ", you look sexy today";
  } else {
    return "hello, " + name;
  }
}

Implement: Use the minimist package to parse command arguments, and accept the --drunk option.

Example:

$ greet howard --drunk
hello howard, you look sexy today

HINT: npm install minimist --save to add the package as dependency.

HINT: var parseArgs = require('minimist') is a function.

Module Require Path

If you are not familiar with how require find the right file to load, read Loading from node_modules Folders.

Push Repository To Github

Create a repository called fork2-node-greet on Github, and push your code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment