Tuesday, December 11, 2012

npm and bundler - the Good Parts

I need two things from a language to take it seriously: good testing framework and some kind of package management. I've dealt with testing in my previous blog posts, now I compare two package management tools that I like, npm and bundler.

:: NPM

Early this year I went fairly deep into node.js development. I wanted to understand how it works and where I could use it in the future. Getting node on your local development environment is easy, but getting used to coding with async callbacks takes a bit of practice to master.

All I needed on OSX to get started with node was:

$: brew install node
When I wanted to use a package - let's say mocha.js - I just used npm.

$: npm install mocha

It installed everything into the project root under the node_modules directory, locally by default like this:

$: lltree
   some_nodejs_project
   |--node_modules
   |----mocha
   |------bin
   |------images
   |- ...
Sure you can have it installed globally, but you need to use the "-g" flag to do that. I did not want to pollute my node installation, I was glad with its default behavior where it installed everything under node_modules.

You do pay a price for this behavior. When you execute "mocha" in the terminal, the executable is not found.

% mocha
zsh: command not found: mocha
You have to locate that file under the node_modules directory. It's in "node_modules/mocha/bin" dir, by the way.
To get around this I just use a Makefile with a couple tasks in it:

REPORTER = list

test: test-bdd

test-bdd:
 @./node_modules/mocha/bin/mocha \
  --reporter $(REPORTER) \
  --ui bdd \
  -- spec/*.js

test-doc:
 @./node_modules/mocha/bin/mocha \
  --reporter $(REPORTER) \
  --ui bdd \

This way I can easily run my tests by executing "make" in the terminal.

:: BUNDLER

I switched from rvm to rbenv early this year. Rbenv with bundler makes a very powerful combo but when you install a gem it's going to install it globally by default.
Not good. I want to keep my gems local to the current project and I don't want to pollute my Ruby install with different versions of gems. What if I use Rails 3.2.9 in one project but I have to use 3.1.1 in another? Sure you could use rbenv-gemsets to get around this, but I already started using node with npm and I wanted to have a similar experience.

The "--path" switch in bundler lets me specify which directory I want to install my gems into. When I start a new project I immediately create a Gemfile. It's very simple, all you need is this:

source "http://rubygems.org"
gem "light_service", "->0.0.6"
gem "rspec"

Then I install all the gems through bundler with this command:

$: bundle install --path vendor/bundle --binstubs

Bundler puts all my gems under the vendor/bundle directory and creates a bin directory with executables for the gems that produce such a file. When I run rspec for my project this is what I do:

$: bin/rspec spec

You could either use "bin/rspec" or "bundle exec rspec", either works.

As you see, nor npm, neither bundler has the best solution. But they have facets that I like in both.

  default local install easy access to executables
npm
bundler

Can we sync the good parts? Could both have local install by default with easy access to the executables?

Update

Shortly after I published this post, my good friend Joe Fiorini pinged me on twitter. Here is our conversation:

I did not know that npm creates a ".bin" directory under "node_modules" with symlinks pointing to the individual executable files. This way it's very easy to run these files:

$: node_modules/.bin/mocha -h

Thanks Joe for pointing this out!

1 comment:

Paweł Gościcki said...

You can easily configure Bundler to automatically install binstubs for every project:


vim ~/.bundle/config
---
BUNDLE_PATH: .bundle
BUNDLE_BIN: .bundle/bin


Reference:
http://hmarr.com/2012/nov/08/rubies-and-bundles/

And the second part of the puzzle is automatically adding those binstubbed paths to the $PATH. I do it like this (in zsh):


# initialy add the binstubs directory to PATH, but remove it on first `cd` to a directory without `Gemfile`
PATH=".bundle/bin:$PATH"

autoload -U add-zsh-hook
add-zsh-hook chpwd chpwd_add_binstubs_to_path

function chpwd_add_binstubs_to_path {
if [ -r $PWD/Gemfile ] && [ -d $PWD/.bundle/bin ]; then
export PATH=.bundle/bin:${PATH//\.bundle\/bin:}
else
export PATH=${PATH//\.bundle\/bin:}
fi
}

Post a Comment