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
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"
}
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.
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.
Let's package a very simple greet
function as an node module.
function greet(name) {
return "hello, " + name;
}
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.
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.
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'
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
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
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.
If you are not familiar with how require
find the right file to load, read Loading from node_modules Folders.
Create a repository called fork2-node-greet
on Github, and push your code.