Nils Sommer

Like many package managers, npm can install packages either system wide or in a local directory. Binaries of locally installed packages are not in the PATH and therefore are not known by the shell. The following post describes a simple hack that introduces a command similar to bundler's exec command.

If you install npm packages globally (npm install -g <package>), any binary part of the package is installed to a location part of the $PATH variable. Packages installed locally (npm install --save-dev <package>) are installed into ./node_modules/ and their binaries live in ./node_modules/.bin/.

Consider using the jasmine framework for the tests of a javascript project. When installing jasmine locally, you would need to call ./node_modules/.bin/jasmine init to initialize the project. Obviously this is not intuitive. Nonetheless, installing packages locally has advantages, such as being able to depend on a specific version of it.

The bundler tool which is used in the ruby world to organize packages (gems) has a special command for this scenario: bundle exec <binary>. I took this as an inspiration to write a hack that implements a similar command for npm.

Implementation

I chose to implement this by defining a bash alias for npm. Because it isn't possible to simply define an alias for npm exec including the paramter passing, I wrote a bash function which does the magic.

# Shim the npm binary to enable custom command `npm exec`.
npm_shim() {
  # Make real npm calls with full path, otherwise the alias is called recursively.
  npm_binary=$(which npm)

  if [ $# -eq 0 ]
  then
    $npm_binary
  elif [ $# -eq 1 ]
  then
    if [ $1 = 'exec' ]
    then
      echo "Binary parameter missing."
    else
      $npm_binary $1
    fi
  else
    if [ $1 = 'exec' ]
    then
      # Execute locally installed binary with all arguments.
      args=($@)
      unset args[0]
      unset args[1]
      eval "./node_modules/.bin/$2 ${args[*]}"
    else
      # Execute npm with all arguments.
      eval "$npm_binary $@"
    fi
  fi
}

alias npm='npm_shim'

It basically works like this: If the first argument to npm equals "exec", the second argument is taken as the binary name and any additional arguments are taken as arguments to the binary. If the first argument does not equal "exec", the arguments are passed to the standard npm command and executes as you would expect it. The rest is error handling and boilerplate code.

Installation

To "install" this hack, simply put the code from above into the ~/.bash_profile file. If I remember correctly, some systems (Ubuntu?) call this file ~/.bashrc. If you do this from a shell, type source ~/.bash_profile to publish the changes to the shell.

In the example from above you would be able to do the following.

npm install --save-dev jasmine
npm exec jasmine init

What's really elegant with this approach of implementation is that it's unobtrusive. It doesn't change the functionality of npm itself and it doesn't change the npm installation or any other system files. rbenv works in a similar way.

Write a comment

via