This is an exhaustive article on creating command line run scripts/executables with node and how it all works. It is broken into sections for convenience if you are looking for a specific topic. Otherwise, grab a coffee and prepare to learn more than you ever wanted to know about node cli.
For the purpose of this article the words package
and project
are interchangeable and both mean a node app which is defined by the package.json
standard.
For the purposes of this article all project JavaScript will live inside the ./src
directory. This is required for some of the techniques.
The bin
package.json declaration:
All node cli executables start with a declaration within the projects package.json
file called bin
. These may be declared in two formats.
This will use the name of the package test-app
as the command name and run the specified file ./src/index.js
as the command.
This will install two commands, test-app-index
and test-app-another
and run the specified file as each command. You may name the commands whatever you like and have as many as you would like. Because they will be potentially installed globally, they should be namespaced or at least something unique.
The files which are run may be .js
but they don’t have to be. They could be any type of executable your system knows how to handle. For instance, they could be php
or bash
if you like. You determine the type of file by adding a shebang declaration to the top of the file like so:
Be sure you have your shebang line setup in that format !/usr/bin/env
then the program that handles it. This way it will work whether you are on windows, linux, or mac.
If you are running your project’s index file you will want to use this shebang:
If you are working on the project locally and want to be able to use the bin
command(s) you now run this command to install them:
If you are using yarn
an alternative which will keep things cleaner would be:
You may now open your terminal and type in your specified command (test-app
if you are using the first above example or test-app-index
and/or test-app-another
if you are using the second above example) and your script is run.
How node handles bin
declarations:
If you are working on your project locally you may have noticed that you can run npm install
or yarn install
and you can’t run your bin
command from anywhere. Until you run npm link
your bin
declaration appears to do nothing.
Node only cares about
bin
declarations when a package is installed as a dependency or when you runnpm link
.
Packages installed as dependencies with bin
declarations
When a package is installed as dependency during npm install
, node looks for the bin
declaration and generates executables based on the shebang line in the file(s) pointed to by bin
. For instance if you use the first above example, node will generate a test-app
and a test-app.cmd
file. Both files do the same thing but one is for windows and one for mac/linux.
The content of the executable test-app
will look something like this:
The content of the executable test-app.cmd
will look something like this:
Notice how these executables call
node
before the command. This is assuming the file pointed to viabin
had the!/usr/bin/env node
shebang line. If the shebang line was pointed tobash
or something else, these executables would be callingbash
instead ofnode
.
These files will be generated within your project’s node_modules/.bin
directory.
You have probably noticed that even though these executables have been generated, you can’t call them from the command line unless you call them with a full path to the directory like:
Local projects with bin
declarations using npm link
When you run either npm link
or yarn link
on a local project, node looks for the bin
declaration and generates executables based on the shebang line in the file(s) pointed to by bin
. This works the same way as node handles dependencies with one important difference.
Node generates the executables inside it’s main
PATH
linked directory.
This directory will be located in a different location depending on your system and if you are using nvm
. The location will also be different depending on whether you use yarn link
or npm link
.
If you would like to know where this file is generated so you can inspect it, run on of the following commands:
An important takeaway here is node does not create symbolic links to the files that are declared in
bin
but rather generates executables which call the files.
Since these new executables are generated within the PATH
they are now accessible via any terminal may be run by simply typing their name such as test-app
.
Bonus: To use your local project inside another project simply run
yarn link test-app
in another project and it will use your local project instead of one installed from npm. This works via an automatically generated symbolic link.
Accessing the executables generated in the node_modules/.bin
directory.
Because you installed a package as a dependency, you probably want to be able to call the executables the package has declared. There are a couple of ways to do this.
Install a package globally.
Any package that you may install as a dependency may be installed globally. This may be done by adding a -g
flag to the npm install
command like so:
This will tell node to add the package to it’s global location and generate the
executables inside it’s main PATH
linked directory. You will now be able to call these command from any terminal simply by typing their name.
While global packages are very useful for making command line utilities available such as
grunt
ornpx
, they do saturate the global space and should be installed sparingly.When installing global packages, it’s recommended to stick with one package handler for all globals so you don’t end up with conflicts. With this in mind, I recommend using
yarn
for local installs andnpm
for globals.
Using package.json
declared scripts.
When you declare and run scripts
within your package.json
file, you have access to all the executables which exist in the .bin
directory. For example let’s say we have grunt
as a dependency. We can then declare a script called run_grunt
which calls grunt
:
Using npm’s run-script
command we can now call the grunt
executable which exists in node_modules/.bin/grunt
.
Any combination of command or arguments you can imagine may be declared within scripts
. They also have access to your full PATH
so feel free to use any global commands you have here as well.
Using the npx
package
There is a widely used package called npx
which lets you call executables found within the .bin
directory.
Npx will also auto install packages if they don’t exist before calling the executables but it’s more predictable to install packages like normal before calling
npx
.
Because npx
is intended as a command line utility, it makes sense to install it globally.
Now running any executable within .bin
is as simple as calling npx <executable>
. Using the grunt
example again:
Calling your script directly.
There are a lot of cases where you write a node script and don’t necessarily want it installed as a global executable or are not actually going to publish it. Sometimes your scripts are one-time use and you just need a quick way to write a script in a language you are used to using.
Luckily node
may be used as is for calling .js
scripts:
If you want, you can also create bash
or batch
scripts which call your .js
script and/or pass different arguments like so:
Using ES6
with node scripts.
I’m sure by now you are used to using ES6
within your JS and want to keep on using it with node scripts. The most common way to use ES6
with JS is to compile it with babel
. Here are a couple of ways to make babel
work with node scripts.
Note: Recent versions of Babel now require you to import packages separately for different features. For example if you want to use
import
you will need to addbabel-preset-es2015
as a dependency and{"presets": ["es2015"]}
within a “.babelrc” file for it to work.
Using the babel-register
package.
Babel has a package called babel-register
which places babel
in front of your JS and automatically compiles it with babel
before sending it to node
.
Install it like any other local dependency:
At the top of your ./src/index.js
file, require babel-register
. Then use a standard require
syntax to load the rest of your project through this file.
Now any of the above methods will work with your project using ES6
.
Using the babel-node
command utility
Babel has a package called babel-cli
which contains a bin
script called babel-node
. You may either install babel-cli
as a global package or a local dependency as shown in above examples.
Installed Globally
If you installed babel-cli
as a global package you can simply call:
If your script is going to be declared as a bin
you may now change the shebang line to use babel-node
and the correct executable will be pointed to when you run npm link
.
Your shebang line will look like this:
If you are going to be distributing your project as a package, be sure to add
babel-cli
as a dependency.
Installed Locally
If you installed babel-cli
as a local dependency you may use any of the methods mentioned above in “Accessing the executables generated in the node_modules/.bin
directory.” Like so:
Using package.json
declared scripts:
Using the npx
package:
Create a bash script:
Generating a stand along executable.
Sometimes you want to be able to distribute your project without forcing users to install node, install dependencies, and run a special command. Sometimes you just want to pass them a .exe
file and let them run it.
Luckily there is a package for doing just so called pkg
. Install it like normal either locally or globally.
By now I’m sure you understand the difference between local and global and how to use each so I won’t add more examples.
Generating your executables is as simple as running this command:
This will generate exectuables for windows, mac, and linux. If you want to specify a particular OS or maybe a program name, pkg
supports arguments for these types of things:
Using ES6
.
You will notice that you can’t call babel-node
with pkg
and using the babel-register
method above still gives you a bunch of errors when you try to run pkg
.
This is because pkg
is strictly node
based, it compiles your JS files directly and not like a typically rendered app. You can solve this problem by compiling your files first with babel
and then running the compiled files through pkg
.
Because you don’t want to type a bunch of commands every time you want to compile, I recommend registering a package.json
script to do this for you:
This script declares the following processes:
- Get all the files in the
./src
directory. - Compile them with
babel
. - Put all compiled files in the
./dist
. - Generate the executables based off
./dist/index.js
.
You may add additional arguments to this script if desired like so:
In Closing
That’s really I’ll I’ve got to say about that.