Create a gist now

Instantly share code, notes, and snippets.

Self-updating node_modules

node / coffee Makefile comforts for shared projects

This setup lets you stop thinking about manually installing or updating your node package dependencies as they change, when you or someone else changes package.json dependencies underfoot.

Just run make as usual, and have Makefile dependencies discover and install new modules and versions for you as need arises. Given that you only depend on registry-published modules (plain version strings) and git:// package urls ending in a tag name #v<package.json version num> this will just work and never hit the wire to resolve the version, which means the dependency management is lightning-fast (milliseconds), rather than many seconds each invocation, as npm usually works.

Depending on tagged versions is good hygiene, and when you encounter any project that doesn't publish any yet, ask them to run npm version x.y.z for you and git push vx.y.z for you, so you can depend on a fix state; it takes care of updating package.json and tagging things appropriately.

This slightly roundabout package-versions hack would not be necessary if npm had a flag not to hit the wire – but unfortunately it does not.

{ "name": "myproject"
, "version": "0.0.1"
, "dependencies":
{ "coffee-script-redux": "2.0.0-beta7"
}
, "devDependencies":
{ "async": "0.2.9"
}
, "engines":
{ "node": "0.8.x"
}
}
# this lets you run your project with its locally installed coffee in GNU Make,
# by running `env coffee file.coffee` without cluttery node_modules references:
export PATH := node_modules/.bin:$(PATH)
.PHONY: default setup
# for anything dependent on your npm modules, bundled coffee binary or similar,
# add a `.package-versions` dependency as here and they will get auto-installed
default: .package-versions
env coffee app.coffee
# Auto-upgrade node_modules; runs for any make target that uses node, as soon as
# package.json names a package or version not already installed. To bypass this,
# (if you are testing with some older version), "touch .package-versions" before
# you run "make dev" or so, so this automation does not kick in.
.package-versions: package.json node_modules/*/package.json bin/package-versions
@env coffee package-versions --dump > $@ || case $$? in \
1|127) rm $@ \
; echo '*** Running \x1B[32mmake setup\x1B[39m for you ***' \
; make setup ;; \
*) false ;; \
esac
# add "--registry <url>" to this install line, if your dependencies are internal
setup:
npm install
#! /usr/bin/env coffee
# Reads out all the versions of package.json dependencies, then all node_modules
# package.jsons and exit(0)s if all dependencies were met, else 1 (=>make setup)
# Pass some argument to write your current package names and versions to stdout.
fs = require 'fs'
pkg = JSON.parse fs.readFileSync 'package.json', 'utf8'
async = require 'async'
semver = require 'semver'
format = do ->
tailOp = /\ ?([\[\{,])\n ( *)(?: )/gm # trailing->leading ,-{-[s
leadOp = '\n$2$1 '
cuddle = /(^|[\[\{,] ?)\n */gm # cuddle brackets/braces/array items
(json) ->
JSON.stringify(json, null, 2).replace(tailOp, leadOp).replace(cuddle, '$1')
freshnessTests = {}
checkFreshness = (name, versionSpec) ->
if freshnessTests[name]
console.warn "*** #{name} listed in both dependencies and devDependencies!"
freshnessTests[name] = (cb) ->
unless semver.validRange versionSpec
tagOrBranchName = versionSpec.replace /^.*\#v/, '' # drop git://github/...
if semver.validRange tagOrBranchName
versionSpec = tagOrBranchName
else
help = """Found no package version for \"#{name}\" in \"#{versionSpec}\"
Please name your git tags or branches \"v<package.json version>\",
and depend on them via "git://.../package.git#v<package.json version>"
This way, starting your server and ensuring your npm modules are up to
date happens in ~10ms, with no network roundtrips -- instead of adding
multi-second delays for every git url dependency in package.json"""
error = new Error help
error.fatal = true
return cb error
fs.readFile "node_modules/#{name}/package.json", 'utf8', got = (err, raw) ->
if err?.code is 'ENOENT'
return cb new Error "#{name} is not installed"
pkg = JSON.parse raw
ver = pkg.version
if semver.satisfies ver, versionSpec
cb null, ver
else
cb new Error "#{name} version #{ver} doesn't satisfy #{versionSpec}"
checkFreshness name, version for name, version of pkg.dependencies
checkFreshness name, version for name, version of pkg.devDependencies
async.parallel freshnessTests, (error, results) ->
if error?
console.error error.message
exitCode = 1
exitCode = 2 if error.fatal
process.exit exitCode
console.log format results unless process.argv.indexOf('--dump') is -1
process.exit 0
@dbushong

Do you need the call to "env" in the Makefile?

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