From 537c16c5fc3768ce9bb4a5c310665a5d547fcf0e Mon Sep 17 00:00:00 2001 From: David Rice Date: Tue, 26 Feb 2013 03:09:59 +0000 Subject: [PATCH 001/116] Preserve ENV vars CPATH and CPPPATH (composable buildpacks) --- bin/compile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index 3e5a906c7..67c17e469 100755 --- a/bin/compile +++ b/bin/compile @@ -164,8 +164,8 @@ cp "$VENDORED_NODE/bin/node" "$BUILD_DIR/bin/node" # setting up paths for building PATH="$VENDORED_SCONS:$VENDORED_NODE/bin:$PATH" INCLUDE_PATH="$VENDORED_NODE/include" -export CPATH="$INCLUDE_PATH" -export CPPPATH="$INCLUDE_PATH" +export CPATH="$INCLUDE_PATH:$CPATH" +export CPPPATH="$INCLUDE_PATH:$CPPPATH" # install dependencies with npm echo "-----> Installing dependencies with npm" From 5271c2a5836e66ca6fcbdb04db06e7147ac4e95b Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 26 Feb 2013 22:40:14 -0800 Subject: [PATCH 002/116] default to latest versions of node and npm --- bin/compile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index 3e5a906c7..6888672e5 100755 --- a/bin/compile +++ b/bin/compile @@ -107,8 +107,8 @@ declare -A engine_versions declare -A engine_defaults declare -A engine_requests -engine_defaults["node"]="0.4.7" -engine_defaults["npm"]="1.0.106" +engine_defaults["node"]="0.8.x" +engine_defaults["npm"]="1.2.x" engine_versions["node"]=$(manifest_versions "nodejs") engine_requests["node"]=$(package_engine_version "node") From 90d7de70071b2afca1724dab4d4209231771c4f2 Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 27 Feb 2013 22:41:08 -0800 Subject: [PATCH 003/116] choose default version using semver --- bin/compile | 17 ++++++++--------- bin/test | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/bin/compile b/bin/compile index 6888672e5..baaa54243 100755 --- a/bin/compile +++ b/bin/compile @@ -60,15 +60,17 @@ function resolve_version() { requested_version="$2" default_version="$3" + args="" + for version in $available_versions; do args="${args} -v \"${version}\""; done + if [ "$2" == "" ]; then - echo $3 + args="${args} -r \"${default_version}\""; else - args="" - for version in $available_versions; do args="${args} -v \"${version}\""; done - for version in $requested_version; do args="${args} -r \"${version}\""; done - evaluated_version=$(eval $bootstrap_node/bin/node $LP_DIR/vendor/node-semver/bin/semver ${args} || echo "") - echo "$evaluated_version" | tail -n 1 + args="${args} -r \"${requested_version}\""; fi + + evaluated_versions=$(eval $bootstrap_node/bin/node $LP_DIR/vendor/node-semver/bin/semver ${args} || echo "") + echo "$evaluated_versions" | tail -n 1 } function package_engine_version() { @@ -124,9 +126,6 @@ if [ "${engine_requests["node"]}" == "" ]; then echo "WARNING: No version of Node.js specified in package.json, see:" | indent echo "https://devcenter.heroku.com/articles/nodejs-versions" | indent echo - echo "WARNING: The default version of Node.js and npm on Heroku will begin" | indent - echo "tracking the latest stable release starting September 1, 2012." | indent - echo fi NODE_VERSION=$(package_resolve_version "node") diff --git a/bin/test b/bin/test index 631c8b98f..02643c1ab 100755 --- a/bin/test +++ b/bin/test @@ -30,8 +30,8 @@ testPackageJsonWithVersion() { testPackageJsonWithoutVersion() { compile "package-json-noversion" assertCaptured "WARNING: No version of Node.js specified" - assertCaptured "Using Node.js version: 0.4.7" - assertCaptured "Using npm version: 1.0.106" + assertCaptured "Using Node.js version: 0.8" + assertCaptured "Using npm version: 1.2" assertCapturedSuccess } From 978ee28a19e05248342781c28385809c7a9c94f5 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 11 Mar 2013 11:03:59 -0700 Subject: [PATCH 004/116] update to node 0.10 --- bin/compile | 2 +- bin/test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index baaa54243..f7994ba7e 100755 --- a/bin/compile +++ b/bin/compile @@ -109,7 +109,7 @@ declare -A engine_versions declare -A engine_defaults declare -A engine_requests -engine_defaults["node"]="0.8.x" +engine_defaults["node"]="0.10.x" engine_defaults["npm"]="1.2.x" engine_versions["node"]=$(manifest_versions "nodejs") diff --git a/bin/test b/bin/test index 02643c1ab..3e5bef7cc 100755 --- a/bin/test +++ b/bin/test @@ -30,7 +30,7 @@ testPackageJsonWithVersion() { testPackageJsonWithoutVersion() { compile "package-json-noversion" assertCaptured "WARNING: No version of Node.js specified" - assertCaptured "Using Node.js version: 0.8" + assertCaptured "Using Node.js version: 0.10" assertCaptured "Using npm version: 1.2" assertCapturedSuccess } From 984654096e954b986a191e6c285537db66bb1bb1 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Thu, 21 Mar 2013 22:22:27 -0700 Subject: [PATCH 005/116] cat npm-debug.log on exit --- bin/compile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bin/compile b/bin/compile index f7994ba7e..fea53b038 100755 --- a/bin/compile +++ b/bin/compile @@ -101,6 +101,14 @@ function package_download() { curl $package -s -o - | tar xzf - -C $location } +function cat_npm_debug_log() { + if [ -f $BUILD_DIR/npm-debug.log ]; then + cat $BUILD_DIR/npm-debug.log + fi +} + +trap cat_npm_debug_log EXIT + bootstrap_node=$(mktmpdir bootstrap_node) package_download "nodejs" "0.4.7" $bootstrap_node From 68a8047d83f06f3bc34d1cd48742ed46d7cbcda8 Mon Sep 17 00:00:00 2001 From: zeke Date: Sat, 23 Mar 2013 22:07:37 -0700 Subject: [PATCH 006/116] add CONTRIBUTING.md --- CONTRIBUTING.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 10 ++++--- 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..039af388a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,75 @@ +## Testing buildpack changes using Anvil + +[Anvil](https://github.com/ddollar/anvil) is a generic build server for Heroku. + +``` +gem install anvil-cli + +``` + +The [heroku-anvil CLI plugin](https://github.com/ddollar/heroku-anvil) is a wrapper for anvil. + +``` +heroku plugins:install https://github.com/ddollar/heroku-anvil +``` + +The [ddollar/test buildpack](https://github.com/ddollar/buildpack-test) is for testing things: it runs `bin/test` on your app. + +``` +heroku build -b ddollar/test # -b can also point to a local directory +``` + +## Compiling new versions of node and npm using Vulcan + +Install [vulcan](https://github.com/heroku/vulcan) and create your own build server. Use any +app name you want and vulcan will remember it in a `~/.vulcan` config file. + +``` +gem install vulcan +vulcan create builder-bob +``` + +Store your S3 credentials in `~/.aws/` + +``` +mkdir -p ~/.aws +echo 'YOUR_AWS_KEY' > ~/.aws/key-nodejs.access +echo 'YOUR_AWS_SECRET' > ~/.aws/key-nodejs.secret +``` + +Add a credentials exporter to your `.bash_profile` or `.bashrc` + +``` +setup_nodejs_env () { + export AWS_ID=$(cat ~/.aws/key-nodejs.access) + export AWS_SECRET=$(cat ~/.aws/key-nodejs.secret) + export S3_BUCKET="heroku-buildpack-nodejs" +} +``` + +Build: + +``` +setup_nodejs_env +support/package_nodejs +support/package_npm +``` + +## Publishing buildpack updates + +``` +heroku plugins:install https://github.com/heroku/heroku-buildpacks + +cd heroku-buildpack-nodejs +git checkout master +heroku buildpacks:publish heroku/nodejs +``` + +- Email [dos@heroku.com](mailto:dos@heroku.com) if changes are significant. +- Add a [changelog item](https://devcenter.heroku.com/admin/changelog_items/new). +- Update [Node Devcenter articles](https://devcenter.heroku.com/admin/articles/owned) as necessary. + +## Keeping up with the Nodeses + +- Run `npm info npm version` to find out the latest available version of npm. +- Follow [@nodejs](https://twitter.com/nodejs) and [@npmjs](https://twitter.com/npmjs) on Twitter. \ No newline at end of file diff --git a/README.md b/README.md index 9bc6c48b2..40efd0398 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Example usage: $ ls Procfile package.json web.js - $ heroku create --stack cedar --buildpack http://github.com/heroku/heroku-buildpack-nodejs.git + $ heroku create --buildpack http://github.com/heroku/heroku-buildpack-nodejs.git $ git push heroku master ... @@ -48,10 +48,10 @@ To list the available versions of Node.js and npm, see these manifests: http://heroku-buildpack-nodejs.s3.amazonaws.com/manifest.nodejs http://heroku-buildpack-nodejs.s3.amazonaws.com/manifest.npm -Hacking -------- +Contributing +------------ -To use this buildpack, fork it on Github. Push up changes to your fork, then create a test app with `--buildpack ` and push to it. +To use this buildpack, fork it on Github. Push up changes to your fork, then create a test app with `--buildpack ` and push to it. To change the vendored binaries for Node.js, NPM, and SCons, use the helper scripts in the `support/` subdirectory. You'll need an S3-enabled AWS account and a bucket to store your binaries in. @@ -71,3 +71,5 @@ Open `bin/compile` in your editor, and change the following lines: Commit and push the changes to your buildpack to your Github fork, then push your sample app to Heroku to test. You should see: -----> Vendoring node 0.6.7 + +For more info, see [CONTRIBUTING.md](CONTRIBUTING.md) From fc8df59bddaeef4e90af1031b64bb18e5cf9dfa0 Mon Sep 17 00:00:00 2001 From: zeke Date: Sat, 23 Mar 2013 22:10:07 -0700 Subject: [PATCH 007/116] remove extra newline --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 039af388a..26def27f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,6 @@ ``` gem install anvil-cli - ``` The [heroku-anvil CLI plugin](https://github.com/ddollar/heroku-anvil) is a wrapper for anvil. From 9fe5546081c6dc2d5f148f04b9b3630e06d1ed32 Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 14 May 2013 15:00:12 -0700 Subject: [PATCH 008/116] update 'no version specified' devcenter url --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index fea53b038..20117aea6 100755 --- a/bin/compile +++ b/bin/compile @@ -132,7 +132,7 @@ echo "-----> Resolving engine versions" if [ "${engine_requests["node"]}" == "" ]; then echo echo "WARNING: No version of Node.js specified in package.json, see:" | indent - echo "https://devcenter.heroku.com/articles/nodejs-versions" | indent + echo "https://devcenter.heroku.com/articles/nodejs-support#versions" | indent echo fi From 43d01bb6b5f7f674a139d787e0a6688f7d3bf5e5 Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 10 Jul 2013 10:54:53 -0700 Subject: [PATCH 009/116] improve documentation in README --- README.md | 68 +++++++++++++++++++++++-------------------------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 40efd0398..b19a642bb 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,17 @@ -Heroku buildpack: Node.js -========================= +Heroku Buildpack for Node.js +============================ This is a [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) for Node.js apps. -It uses [NPM](http://npmjs.org/) and [SCons](http://www.scons.org/). -Usage ------ +The buildpack will detect your app as Node.js if it has a `package.json` file in the root. It will use npm to install your dependencies, and vendors a version of the Node.js runtime into your slug. -Example usage: +Example Usage +------------- $ ls Procfile package.json web.js - $ heroku create --buildpack http://github.com/heroku/heroku-buildpack-nodejs.git + $ heroku create $ git push heroku master ... @@ -27,49 +26,38 @@ Example usage: └── connect@1.6.2 Dependencies installed -The buildpack will detect your app as Node.js if it has the file `package.json` in the root. It will use NPM to install your dependencies, and vendors a version of the Node.js runtime into your slug. The `node_modules` directory will be cached between builds to allow for faster NPM install time. - Node.js and npm versions ------------------------ You can specify the versions of Node.js and npm your application requires using `package.json` - { - "name": "myapp", - "version": "0.0.1", - "engines": { - "node": ">=0.4.7 <0.7.0", - "npm": ">=1.0.0" - } - } +```json +{ + "name": "myapp", + "version": "0.0.1", + "engines": { + "node": "~0.10.13", + "npm": "~1.3.2" + } +} +``` To list the available versions of Node.js and npm, see these manifests: -http://heroku-buildpack-nodejs.s3.amazonaws.com/manifest.nodejs -http://heroku-buildpack-nodejs.s3.amazonaws.com/manifest.npm - -Contributing ------------- - -To use this buildpack, fork it on Github. Push up changes to your fork, then create a test app with `--buildpack ` and push to it. - -To change the vendored binaries for Node.js, NPM, and SCons, use the helper scripts in the `support/` subdirectory. You'll need an S3-enabled AWS account and a bucket to store your binaries in. +- [heroku-buildpack-nodejs.s3.amazonaws.com/manifest.nodejs](http://heroku-buildpack-nodejs.s3.amazonaws.com/manifest.nodejs) +- [heroku-buildpack-nodejs.s3.amazonaws.com/manifest.npm](http://heroku-buildpack-nodejs.s3.amazonaws.com/manifest.npm) -For example, you can change the default version of Node.js to v0.6.7. +Documentation +------------- -First you'll need to build a Heroku-compatible version of Node.js: +For more information about buildpacks and Node.js, see these Dev Center articles: - $ export AWS_ID=xxx AWS_SECRET=yyy S3_BUCKET=zzz - $ s3 create $S3_BUCKET - $ support/package_nodejs 0.6.7 +- [Getting Started with Node.js on Heroku](https://devcenter.heroku.com/articles/nodejs) +- [Heroku Node.js Support](https://devcenter.heroku.com/articles/nodejs-support) +- [Buildpacks](https://devcenter.heroku.com/articles/buildpacks) +- [Buildpack API](https://devcenter.heroku.com/articles/buildpack-api) -Open `bin/compile` in your editor, and change the following lines: - - DEFAULT_NODE_VERSION="0.6.7" - S3_BUCKET=zzz - -Commit and push the changes to your buildpack to your Github fork, then push your sample app to Heroku to test. You should see: - - -----> Vendoring node 0.6.7 +Contributing +------------ -For more info, see [CONTRIBUTING.md](CONTRIBUTING.md) +See [CONTRIBUTING.md](CONTRIBUTING.md) From 9d37105024daecf5695c5254282441673709486d Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 10 Jul 2013 11:05:58 -0700 Subject: [PATCH 010/116] add a readme section for hackers --- README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b19a642bb..eacf042e0 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,20 @@ For more information about buildpacks and Node.js, see these Dev Center articles - [Buildpacks](https://devcenter.heroku.com/articles/buildpacks) - [Buildpack API](https://devcenter.heroku.com/articles/buildpack-api) -Contributing ------------- +Hacking +------- -See [CONTRIBUTING.md](CONTRIBUTING.md) +To make changes to this buildpack, fork it on Github. Push up changes to your fork, then create a test app with --buildpack and push to it, or configure an existing app to use your buildpack: + +```sh +# Create a new Heroku app that uses your buildpack +heroku create --buildpack + +# Configure an existing Heroku app to use your buildpack +heroku config:set BUILDPACK_URL= + +# You can also use a git branch! +heroku config:set BUILDPACK_URL=#your-branch +``` + +For more detailed information about testing buildpacks, see [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file From 305c472dea82d4cb1dfceee10a266ad35ab6e7cf Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 10 Jul 2013 11:07:58 -0700 Subject: [PATCH 011/116] DRY up the hacking docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eacf042e0..a50977315 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ For more information about buildpacks and Node.js, see these Dev Center articles Hacking ------- -To make changes to this buildpack, fork it on Github. Push up changes to your fork, then create a test app with --buildpack and push to it, or configure an existing app to use your buildpack: +To make changes to this buildpack, fork it on Github. Push up changes to your fork, then create a new Heroku app to test it, or configure an existing app to use your buildpack: ```sh # Create a new Heroku app that uses your buildpack From 4593210ca4a6f569009081349980a2bd60abc2d8 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 15 Jul 2013 15:22:11 -0700 Subject: [PATCH 012/116] ignore .DS_Store --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7ee0c0fe6..d03264442 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .anvil +.DS_Store From c05e23bc37a50ea7764d1d3ac9abb47caaeae039 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 15 Jul 2013 15:22:50 -0700 Subject: [PATCH 013/116] use npm 1.3.x by default --- bin/compile | 2 +- bin/test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index 07b34f85d..f91d1c2d9 100755 --- a/bin/compile +++ b/bin/compile @@ -118,7 +118,7 @@ declare -A engine_defaults declare -A engine_requests engine_defaults["node"]="0.10.x" -engine_defaults["npm"]="1.2.x" +engine_defaults["npm"]="1.3.x" engine_versions["node"]=$(manifest_versions "nodejs") engine_requests["node"]=$(package_engine_version "node") diff --git a/bin/test b/bin/test index 3e5bef7cc..215e55008 100755 --- a/bin/test +++ b/bin/test @@ -31,7 +31,7 @@ testPackageJsonWithoutVersion() { compile "package-json-noversion" assertCaptured "WARNING: No version of Node.js specified" assertCaptured "Using Node.js version: 0.10" - assertCaptured "Using npm version: 1.2" + assertCaptured "Using npm version: 1.3" assertCapturedSuccess } From 4580e9b666ffeafbbd2c03113ddf47c8a267996c Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 12:54:57 -0700 Subject: [PATCH 014/116] slash and burn --- support/aws/hmac | 79 -------------- support/aws/s3 | 230 ----------------------------------------- support/manifest | 44 -------- support/package_nodejs | 48 --------- support/package_npm | 53 ---------- 5 files changed, 454 deletions(-) delete mode 100755 support/aws/hmac delete mode 100755 support/aws/s3 delete mode 100755 support/manifest delete mode 100755 support/package_nodejs delete mode 100755 support/package_npm diff --git a/support/aws/hmac b/support/aws/hmac deleted file mode 100755 index 5aa074a0d..000000000 --- a/support/aws/hmac +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -# Implement HMAC functionality on top of the OpenSSL digest functions. -# licensed under the terms of the GNU GPL v2 -# Copyright 2007 Victor Lowther - -die() { - echo $* - exit 1 -} - -check_deps() { - local res=0 - while [ $# -ne 0 ]; do - which "${1}" >& /dev/null || { res=1; echo "${1} not found."; } - shift - done - (( res == 0 )) || die "aborting." -} - -# write a byte (passed as hex) to stdout -write_byte() { - # $1 = byte to write - printf "\\x$(printf "%x" ${1})" -} - -# make an hmac pad out of a key. -# this is not the most secure way of doing it, but it is -# the most expedient. -make_hmac_pad() { - # using key in file $1 and byte in $2, create the appropriate hmac pad - # Pad keys out to $3 bytes - # if key is longer than $3, use hash $4 to hash the key first. - local x y a size remainder oifs - (( remainder = ${3} )) - # in case someone else was messing with IFS. - for x in $(echo -n "${1}" | od -v -t u1 | cut -b 9-); - do - write_byte $((${x} ^ ${2})) - (( remainder -= 1 )) - done - for ((y=0; remainder - y ;y++)); do - write_byte $((0 ^ ${2})) - done -} - -# utility functions for making hmac pads -hmac_ipad() { - make_hmac_pad "${1}" 0x36 ${2} "${3}" -} - -hmac_opad() { - make_hmac_pad "${1}" 0x5c ${2} "${3}" -} - -# hmac something -do_hmac() { - # $1 = algo to use. Must be one that openssl knows about - # $2 = keyfile to use - # $3 = file to hash. uses stdin if none is given. - # accepts input on stdin, leaves it on stdout. - # Output is binary, if you want something else pipe it accordingly. - local blocklen keysize x - case "${1}" in - sha) blocklen=64 ;; - sha1) blocklen=64 ;; - md5) blocklen=64 ;; - md4) blocklen=64 ;; - sha256) blocklen=64 ;; - sha512) blocklen=128 ;; - *) die "Unknown hash ${1} passed to hmac!" ;; - esac - cat <(hmac_ipad ${2} ${blocklen} "${1}") "${3:--}" | openssl dgst "-${1}" -binary | \ - cat <(hmac_opad ${2} ${blocklen} "${1}") - | openssl dgst "-${1}" -binary -} - -[[ ${1} ]] || die "Must pass the name of the hash function to use to ${0}". - -check_deps od openssl -do_hmac "${@}" diff --git a/support/aws/s3 b/support/aws/s3 deleted file mode 100755 index a25290bb7..000000000 --- a/support/aws/s3 +++ /dev/null @@ -1,230 +0,0 @@ -#!/bin/bash -# basic amazon s3 operations -# Licensed under the terms of the GNU GPL v2 -# Copyright 2007 Victor Lowther - -set -e - -basedir="$( cd -P "$( dirname "$0" )" && pwd )" -PATH="$basedir:$PATH" - -# print a message and bail -die() { - echo $* - exit 1 -} - -# check to see if the variable name passed exists and holds a value. -# Die if it does not. -check_or_die() { - [[ ${!1} ]] || die "Environment variable ${1} is not set." -} - -# check to see if we have all the needed S3 variables defined. -# Bail if we do not. -check_s3() { - local sak x - for x in AWS_ID AWS_SECRET; do - check_or_die ${x}; - done - sak="$(echo -n $AWS_SECRET | wc -c)" - (( ${sak%%[!0-9 ]*} == 40 )) || \ - die "S3 Secret Access Key is not exactly 40 bytes long. Please fix it." -} -# check to see if our external dependencies exist -check_dep() { - local res=0 - while [[ $# -ne 0 ]]; do - which "${1}" >& /dev/null || { res=1; echo "${1} not found."; } - shift - done - (( res == 0 )) || die "aborting." -} - -check_deps() { - check_dep openssl date hmac cat grep curl - check_s3 -} - -urlenc() { - # $1 = string to url encode - # output is on stdout - # we don't urlencode everything, just enough stuff. - echo -n "${1}" | - sed 's/%/%25/g - s/ /%20/g - s/#/%23/g - s/\$/%24/g - s/\&/%26/g - s/+/%2b/g - s/,/%2c/g - s/:/%3a/g - s/;/%3b/g - s/?/%3f/g - s/@/%40/g - s/ /%09/g' -} - -xmldec() { - # no parameters. - # accept input on stdin, put it on stdout. - # patches accepted to get more stuff - sed 's/\"/\"/g - s/\&/\&/g - s/\<//g' -} - -## basic S3 functionality. x-amz-header functionality is not implemented. -# make an S3 signature string, which will be output on stdout. -s3_signature_string() { - # $1 = HTTP verb - # $2 = date string, must be in UTC - # $3 = bucket name, if any - # $4 = resource path, if any - # $5 = content md5, if any - # $6 = content MIME type, if any - # $7 = canonicalized headers, if any - # signature string will be output on stdout - local verr="Must pass a verb to s3_signature_string!" - local verb="${1:?verr}" - local bucket="${3}" - local resource="${4}" - local derr="Must pass a date to s3_signature_string!" - local date="${2:?derr}" - local mime="${6}" - local md5="${5}" - local headers="${7}" - printf "%s\n%s\n%s\n%s\n%s\n%s%s" \ - "${verb}" "${md5}" "${mime}" "${date}" \ - "${headers}" "${bucket}" "${resource}" | \ - hmac sha1 "${AWS_SECRET}" | openssl base64 -e -a -} - -# cheesy, but it is the best way to have multiple headers. -curl_headers() { - # each arg passed will be output on its own line - local parms=$# - for ((;$#;)); do - echo "header = \"${1}\"" - shift - done -} - -s3_curl() { - # invoke curl to do all the heavy HTTP lifting - # $1 = method (one of GET, PUT, or DELETE. HEAD is not handled yet.) - # $2 = remote bucket. - # $3 = remote name - # $4 = local name. - # $5 = mime type - local bucket remote date sig md5 arg inout headers - # header handling is kinda fugly, but it works. - bucket="${2:+/${2}}/" # slashify the bucket - remote="$(urlenc "${3}")" # if you don't, strange things may happen. - stdopts="--connect-timeout 10 --fail --silent" - mime="${5}" - [[ $CURL_S3_DEBUG == true ]] && stdopts="${stdopts} --show-error --fail" - case "${1}" in - GET) arg="-o" inout="${4:--}" # stdout if no $4 - headers[${#headers[@]}]="x-amz-acl: public-read" - ;; - PUT) [[ ${2} ]] || die "PUT can has bucket?" - if [[ ! ${3} ]]; then - arg="-X PUT" - headers[${#headers[@]}]="Content-Length: 0" - elif [[ -f ${4} ]]; then - md5="$(openssl dgst -md5 -binary "${4}"|openssl base64 -e -a)" - arg="-T" inout="${4}" - headers[${#headers[@]}]="x-amz-acl: public-read" - headers[${#headers[@]}]="Expect: 100-continue" - if [ "$mime" != "" ]; then - headers[${#headers[@]}]="Content-Type: $mime" - fi - else - die "Cannot write non-existing file ${4}" - fi - ;; - DELETE) arg="-X DELETE" - ;; - HEAD) arg="-I" ;; - *) die "Unknown verb ${1}. It probably would not have worked anyways." ;; - esac - date="$(TZ=UTC date '+%a, %e %b %Y %H:%M:%S %z')" - sig=$(s3_signature_string ${1} "${date}" "${bucket}" "${remote}" "${md5}" "${mime}" "x-amz-acl:public-read") - - headers[${#headers[@]}]="Authorization: AWS ${AWS_ID}:${sig}" - headers[${#headers[@]}]="Date: ${date}" - [[ ${md5} ]] && headers[${#headers[@]}]="Content-MD5: ${md5}" - curl ${arg} "${inout}" ${stdopts} -o - -K <(curl_headers "${headers[@]}") \ - "http://s3.amazonaws.com${bucket}${remote}" - return $? -} - -s3_put() { - # $1 = remote bucket to put it into - # $2 = remote name to put - # $3 = file to put. This must be present if $2 is. - # $4 = mime type - s3_curl PUT "${1}" "${2}" "${3:-${2}}" "${4}" - return $? -} - -s3_get() { - # $1 = bucket to get file from - # $2 = remote file to get - # $3 = local file to get into. Will be overwritten if it exists. - # If this contains a path, that path must exist before calling this. - s3_curl GET "${1}" "${2}" "${3:-${2}}" - return $? -} - -s3_test() { - # same args as s3_get, but uses the HEAD verb instead of the GET verb. - s3_curl HEAD "${1}" "${2}" >/dev/null - return $? -} - -# Hideously ugly, but it works well enough. -s3_buckets() { - s3_get |grep -o '[^>]*' |sed 's/<[^>]*>//g' |xmldec - return $? -} - -# this will only return the first thousand entries, alas -# Mabye some kind soul can fix this without writing an XML parser in bash? -# Also need to add xml entity handling. -s3_list() { - # $1 = bucket to list - [ "x${1}" == "x" ] && return 1 - s3_get "${1}" |grep -o '[^>]*' |sed 's/<[^>]*>//g'| xmldec - return $? -} - -s3_delete() { - # $1 = bucket to delete from - # $2 = item to delete - s3_curl DELETE "${1}" "${2}" - return $? -} - -# because this uses s3_list, it suffers from the same flaws. -s3_rmrf() { - # $1 = bucket to delete everything from - s3_list "${1}" | while read f; do - s3_delete "${1}" "${f}"; - done -} - -check_deps -case $1 in - put) shift; s3_put "$@" ;; - get) shift; s3_get "$@" ;; - rm) shift; s3_delete "$@" ;; - ls) shift; s3_list "$@" ;; - test) shift; s3_test "$@" ;; - buckets) s3_buckets ;; - rmrf) shift; s3_rmrf "$@" ;; - *) die "Unknown command ${1}." - ;; -esac diff --git a/support/manifest b/support/manifest deleted file mode 100755 index b4376989f..000000000 --- a/support/manifest +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/sh - -set -e - -manifest_type="$1" - -if [ "$manifest_type" == "" ]; then - echo "usage: $0 " - exit 1 -fi - -if [ "$AWS_ID" == "" ]; then - echo "must set AWS_ID" - exit 1 -fi - -if [ "$AWS_SECRET" == "" ]; then - echo "must set AWS_SECRET" - exit 1 -fi - -if [ "$S3_BUCKET" == "" ]; then - echo "must set S3_BUCKET" - exit 1 -fi - -basedir="$( cd -P "$( dirname "$0" )" && pwd )" - -# make a temp directory -tempdir="$( mktemp -t node_XXXX )" -rm -rf $tempdir -mkdir -p $tempdir -pushd $tempdir - -# generate manifest -$basedir/aws/s3 ls $S3_BUCKET \ - | grep "^${manifest_type}" \ - | sed -e "s/${manifest_type}-\([0-9.]*\)\\.tgz/\\1/" \ - | awk 'BEGIN {FS="."} {printf("%03d.%03d.%03d %s\n",$1,$2,$3,$0)}' | sort -r | cut -d" " -f2 \ - > manifest.${manifest_type} - -# upload manifest to s3 -$basedir/aws/s3 put $S3_BUCKET \ - manifest.${manifest_type} "" "text/plain" diff --git a/support/package_nodejs b/support/package_nodejs deleted file mode 100755 index 22f62cbf5..000000000 --- a/support/package_nodejs +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/sh - -set -e - -node_version="$1" - -if [ "$node_version" == "" ]; then - echo "usage: $0 VERSION" - exit 1 -fi - -if [ "$AWS_ID" == "" ]; then - echo "must set AWS_ID" - exit 1 -fi - -if [ "$AWS_SECRET" == "" ]; then - echo "must set AWS_SECRET" - exit 1 -fi - -if [ "$S3_BUCKET" == "" ]; then - echo "must set S3_BUCKET" - exit 1 -fi - -basedir="$( cd -P "$( dirname "$0" )" && pwd )" - -# make a temp directory -tempdir="$( mktemp -t node_XXXX )" -rm -rf $tempdir -mkdir -p $tempdir -cd $tempdir - -# build and package nodejs for heroku -vulcan build -v \ - -n node \ - -c "cd node-v${node_version} && ./configure --prefix=/app/vendor/node && make install" \ - -p /app/vendor/node \ - -s http://nodejs.org/dist/v${node_version}/node-v${node_version}.tar.gz \ - -o $tempdir/node-${node_version}.tgz - -# upload nodejs to s3 -$basedir/aws/s3 put $S3_BUCKET \ - nodejs-${node_version}.tgz $tempdir/node-${node_version}.tgz - -# generate manifest -$basedir/manifest nodejs diff --git a/support/package_npm b/support/package_npm deleted file mode 100755 index 7de93de08..000000000 --- a/support/package_npm +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -set -e - -npm_version="$1" - -if [ "$npm_version" == "" ]; then - echo "usage: $0 VERSION" - exit 1 -fi - -if [ "$AWS_ID" == "" ]; then - echo "must set AWS_ID" - exit 1 -fi - -if [ "$AWS_SECRET" == "" ]; then - echo "must set AWS_SECRET" - exit 1 -fi - -if [ "$S3_BUCKET" == "" ]; then - echo "must set S3_BUCKET" - exit 1 -fi - -basedir="$( cd -P "$( dirname "$0" )" && pwd )" - -# make a temp directory -tempdir="$( mktemp -t node_XXXX )" -rm -rf $tempdir -mkdir -p $tempdir -cd $tempdir - -# download npm -git clone https://github.com/isaacs/npm.git - -# go into npm dir -pushd npm - -# grab the right version -git checkout v${npm_version} -git submodule update --init --recursive - -# package it up -tar czvf $tempdir/npm-${npm_version}.tgz * - -# upload npm to s3 -$basedir/aws/s3 put $S3_BUCKET \ - npm-${npm_version}.tgz $tempdir/npm-${npm_version}.tgz - -# generate manifest -$basedir/manifest npm From 32886bed560196c14dbc666940f0aac914c037f5 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 12:55:22 -0700 Subject: [PATCH 015/116] move .profile.d stuff to .profile.d --- .profile.d/nodejs.sh | 1 + 1 file changed, 1 insertion(+) create mode 100644 .profile.d/nodejs.sh diff --git a/.profile.d/nodejs.sh b/.profile.d/nodejs.sh new file mode 100644 index 000000000..71fa2aaf7 --- /dev/null +++ b/.profile.d/nodejs.sh @@ -0,0 +1 @@ +export PATH=$HOME/bin:$HOME/node_modules/.bin:$PATH \ No newline at end of file From fc04667542e07ea8d539853ed2fd26917797ab31 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 12:55:32 -0700 Subject: [PATCH 016/116] move helpers to their own file --- support/helpers.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 support/helpers.sh diff --git a/support/helpers.sh b/support/helpers.sh new file mode 100644 index 000000000..fd84993c2 --- /dev/null +++ b/support/helpers.sh @@ -0,0 +1,19 @@ +function error() { + echo " ! $*" >&2 + exit 1 +} + +function mktmpdir() { + dir=$(mktemp -t node-$1-XXXX) + rm -rf $dir + mkdir -p $dir + echo $dir +} + +function indent() { + c='s/^/ /' + case $(uname) in + Darwin) sed -l "$c";; + *) sed -u "$c";; + esac +} \ No newline at end of file From ea4662c386edfd0532d427b17c79f0b995f2864c Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 12:55:41 -0700 Subject: [PATCH 017/116] add json.sh --- vendor/JSON.sh | 191 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100755 vendor/JSON.sh diff --git a/vendor/JSON.sh b/vendor/JSON.sh new file mode 100755 index 000000000..e5d9ac5fd --- /dev/null +++ b/vendor/JSON.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash + +throw () { + echo "$*" >&2 + exit 1 +} + +BRIEF=0 +LEAFONLY=0 +PRUNE=0 + +usage() { + echo + echo "Usage: JSON.sh [-b] [-l] [-p] [-h]" + echo + echo "-p - Prune empty. Exclude fields with empty values." + echo "-l - Leaf only. Only show leaf nodes, which stops data duplication." + echo "-b - Brief. Combines 'Leaf only' and 'Prune empty' options." + echo "-h - This help text." + echo +} + +parse_options() { + set -- "$@" + local ARGN=$# + while [ $ARGN -ne 0 ] + do + case $1 in + -h) usage + exit 0 + ;; + -b) BRIEF=1 + LEAFONLY=1 + PRUNE=1 + ;; + -l) LEAFONLY=1 + ;; + -p) PRUNE=1 + ;; + ?*) echo "ERROR: Unknown option." + usage + exit 0 + ;; + esac + shift 1 + ARGN=$((ARGN-1)) + done +} + +awk_egrep () { + local pattern_string=$1 + + gawk '{ + while ($0) { + start=match($0, pattern); + token=substr($0, start, RLENGTH); + print token; + $0=substr($0, start+RLENGTH); + } + }' pattern=$pattern_string +} + +tokenize () { + local GREP + local ESCAPE + local CHAR + + if echo "test string" | egrep -ao --color=never "test" &>/dev/null + then + GREP='egrep -ao --color=never' + else + GREP='egrep -ao' + fi + + if echo "test string" | egrep -o "test" &>/dev/null + then + ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' + CHAR='[^[:cntrl:]"\\]' + else + GREP=awk_egrep + ESCAPE='(\\\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' + CHAR='[^[:cntrl:]"\\\\]' + fi + + local STRING="\"$CHAR*($ESCAPE$CHAR*)*\"" + local NUMBER='-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?' + local KEYWORD='null|false|true' + local SPACE='[[:space:]]+' + + $GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$" +} + +parse_array () { + local index=0 + local ary='' + read -r token + case "$token" in + ']') ;; + *) + while : + do + parse_value "$1" "$index" + index=$((index+1)) + ary="$ary""$value" + read -r token + case "$token" in + ']') break ;; + ',') ary="$ary," ;; + *) throw "EXPECTED , or ] GOT ${token:-EOF}" ;; + esac + read -r token + done + ;; + esac + [ "$BRIEF" -eq 0 ] && value=`printf '[%s]' "$ary"` || value= + : +} + +parse_object () { + local key + local obj='' + read -r token + case "$token" in + '}') ;; + *) + while : + do + case "$token" in + '"'*'"') key=$token ;; + *) throw "EXPECTED string GOT ${token:-EOF}" ;; + esac + read -r token + case "$token" in + ':') ;; + *) throw "EXPECTED : GOT ${token:-EOF}" ;; + esac + read -r token + parse_value "$1" "$key" + obj="$obj$key:$value" + read -r token + case "$token" in + '}') break ;; + ',') obj="$obj," ;; + *) throw "EXPECTED , or } GOT ${token:-EOF}" ;; + esac + read -r token + done + ;; + esac + [ "$BRIEF" -eq 0 ] && value=`printf '{%s}' "$obj"` || value= + : +} + +parse_value () { + local jpath="${1:+$1,}$2" isleaf=0 isempty=0 print=0 + case "$token" in + '{') parse_object "$jpath" ;; + '[') parse_array "$jpath" ;; + # At this point, the only valid single-character tokens are digits. + ''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;; + *) value=$token + isleaf=1 + [ "$value" = '""' ] && isempty=1 + ;; + esac + [ "$value" = '' ] && return + [ "$LEAFONLY" -eq 0 ] && [ "$PRUNE" -eq 0 ] && print=1 + [ "$LEAFONLY" -eq 1 ] && [ "$isleaf" -eq 1 ] && [ $PRUNE -eq 0 ] && print=1 + [ "$LEAFONLY" -eq 0 ] && [ "$PRUNE" -eq 1 ] && [ "$isempty" -eq 0 ] && print=1 + [ "$LEAFONLY" -eq 1 ] && [ "$isleaf" -eq 1 ] && \ + [ $PRUNE -eq 1 ] && [ $isempty -eq 0 ] && print=1 + [ "$print" -eq 1 ] && printf "[%s]\t%s\n" "$jpath" "$value" + : +} + +parse () { + read -r token + parse_value + read -r token + case "$token" in + '') ;; + *) throw "EXPECTED EOF GOT $token" ;; + esac +} + +parse_options "$@" + +if ([ "$0" = "$BASH_SOURCE" ] || ! [ -n "$BASH_SOURCE" ]); +then + tokenize | parse +fi From cdad423b145029fe042936add6eaaf40ac80f210 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 12:55:49 -0700 Subject: [PATCH 018/116] clean up readme --- README.md | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index a50977315..5c8033636 100644 --- a/README.md +++ b/README.md @@ -5,55 +5,30 @@ This is a [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) fo The buildpack will detect your app as Node.js if it has a `package.json` file in the root. It will use npm to install your dependencies, and vendors a version of the Node.js runtime into your slug. -Example Usage -------------- - - $ ls - Procfile package.json web.js - - $ heroku create - - $ git push heroku master - ... - -----> Heroku receiving push - -----> Fetching custom buildpack - -----> Node.js app detected - -----> Vendoring node 0.4.7 - -----> Installing dependencies with npm 1.0.8 - express@2.1.0 ./node_modules/express - ├── mime@1.2.2 - ├── qs@0.3.1 - └── connect@1.6.2 - Dependencies installed +Node.js Versions +---------------- -Node.js and npm versions ------------------------- - -You can specify the versions of Node.js and npm your application requires using `package.json` +You can specify the version of Node.js your application requires using `package.json`: ```json { "name": "myapp", "version": "0.0.1", "engines": { - "node": "~0.10.13", - "npm": "~1.3.2" + "node": "~0.10.15" } } ``` -To list the available versions of Node.js and npm, see these manifests: - -- [heroku-buildpack-nodejs.s3.amazonaws.com/manifest.nodejs](http://heroku-buildpack-nodejs.s3.amazonaws.com/manifest.nodejs) -- [heroku-buildpack-nodejs.s3.amazonaws.com/manifest.npm](http://heroku-buildpack-nodejs.s3.amazonaws.com/manifest.npm) +TODO: Link to versioning doc Documentation ------------- For more information about buildpacks and Node.js, see these Dev Center articles: -- [Getting Started with Node.js on Heroku](https://devcenter.heroku.com/articles/nodejs) - [Heroku Node.js Support](https://devcenter.heroku.com/articles/nodejs-support) +- [Getting Started with Node.js on Heroku](https://devcenter.heroku.com/articles/nodejs) - [Buildpacks](https://devcenter.heroku.com/articles/buildpacks) - [Buildpack API](https://devcenter.heroku.com/articles/buildpack-api) From aab11178b2925910fed467992772452cd8c9ab96 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 12:55:58 -0700 Subject: [PATCH 019/116] go on a diet --- bin/compile | 159 +++++++--------------------------------------------- 1 file changed, 19 insertions(+), 140 deletions(-) diff --git a/bin/compile b/bin/compile index f91d1c2d9..3b4a51676 100755 --- a/bin/compile +++ b/bin/compile @@ -1,170 +1,54 @@ #!/usr/bin/env bash -# bin/compile -# fail fast -set -e +set -e # fail fast +# set -x # debug -# debug -# set -x - -# clean up leaking environment -unset GIT_DIR - -# config -SCONS_VERSION="1.2.0" -S3_BUCKET="heroku-buildpack-nodejs" - -# parse and derive params BUILD_DIR=$1 CACHE_DIR=$2 -LP_DIR=`cd $(dirname $0); cd ..; pwd` - -function error() { - echo " ! $*" >&2 - exit 1 -} - -function mktmpdir() { - dir=$(mktemp -t node-$1-XXXX) - rm -rf $dir - mkdir -p $dir - echo $dir -} - -function indent() { - c='s/^/ /' - case $(uname) in - Darwin) sed -l "$c";; - *) sed -u "$c";; - esac -} -function run_npm() { - command="$1" +source support/helpers.sh - cd $BUILD_DIR - HOME="$BUILD_DIR" $VENDORED_NODE/bin/node $VENDORED_NPM/cli.js $command 2>&1 | indent +# download latest stable version of node +# detect desired node version in package.json using semver - if [ "${PIPESTATUS[*]}" != "0 0" ]; then - echo " ! Failed to $command dependencies with npm" - exit 1 - fi -} -function manifest_versions() { - curl "http://${S3_BUCKET}.s3.amazonaws.com/manifest.${1}" -s -o - | tr -s '\n' ' ' -} +# if !desired +# (cached) ? cached : latest-stable +# else +# (latest-stable satisfies desired) ? latest-stable : download -function resolve_version() { - available_versions="$1" - requested_version="$2" - default_version="$3" +# if desired and cached are absent, use latest +# if desired is absent and cache is present, use cached +# if desired is ">" and differs from cached and latest-stable, download +# if desired is specific and differs from cached and latest-stable, download - args="" - for version in $available_versions; do args="${args} -v \"${version}\""; done - - if [ "$2" == "" ]; then - args="${args} -r \"${default_version}\""; - else - args="${args} -r \"${requested_version}\""; - fi +semver.satisfies('0.10.15', '>=0.10.x') - evaluated_versions=$(eval $bootstrap_node/bin/node $LP_DIR/vendor/node-semver/bin/semver ${args} || echo "") - echo "$evaluated_versions" | tail -n 1 -} - -function package_engine_version() { - version=$(cat $BUILD_DIR/package.json | $bootstrap_node/bin/node $LP_DIR/vendor/json/json engines.$1 2>/dev/null) - if [ $? == 0 ]; then - echo $version | sed -e 's/\([<>=]\) /\1/g' - fi -} -function package_resolve_version() { - engine="$1" - resolved_version=$(resolve_version "${engine_versions[$engine]}" "${engine_requests[$engine]}" "${engine_defaults[$engine]}") - - if [ "${resolved_version}" == "" ]; then - error "Requested engine $engine version ${engine_requests[$engine]} does not match available versions: ${engine_versions[$engine]}" - else - echo $resolved_version - fi -} - -function package_download() { - engine="$1" - version="$2" - location="$3" - - mkdir -p $location - package="http://${S3_BUCKET}.s3.amazonaws.com/$engine-$version.tgz" - curl $package -s -o - | tar xzf - -C $location -} +package="http://s3pository.heroku.com/$engine-$version.tgz" +curl $package -s -o - | tar xzf - -C $location function cat_npm_debug_log() { if [ -f $BUILD_DIR/npm-debug.log ]; then cat $BUILD_DIR/npm-debug.log fi } - trap cat_npm_debug_log EXIT -bootstrap_node=$(mktmpdir bootstrap_node) -package_download "nodejs" "0.4.7" $bootstrap_node - -# make some associative arrays -declare -A engine_versions -declare -A engine_defaults -declare -A engine_requests - -engine_defaults["node"]="0.10.x" -engine_defaults["npm"]="1.3.x" - -engine_versions["node"]=$(manifest_versions "nodejs") -engine_requests["node"]=$(package_engine_version "node") - -engine_versions["npm"]=$(manifest_versions "npm") -engine_requests["npm"]=$(package_engine_version "npm") - echo "-----> Resolving engine versions" -# add a warning if no version of node specified -if [ "${engine_requests["node"]}" == "" ]; then - echo - echo "WARNING: No version of Node.js specified in package.json, see:" | indent - echo "https://devcenter.heroku.com/articles/nodejs-support#versions" | indent - echo -fi - NODE_VERSION=$(package_resolve_version "node") -echo "Using Node.js version: ${NODE_VERSION}" | indent - -NPM_VERSION=$(package_resolve_version "npm") -echo "Using npm version: ${NPM_VERSION}" | indent - -# cache directories -CACHE_STORE_DIR="$CACHE_DIR/node_modules/$NODE_VERSION/$NPM_VERSION" -CACHE_TARGET_DIR="$BUILD_DIR/node_modules" - -# s3 packages -NODE_PACKAGE="http://${S3_BUCKET}.s3.amazonaws.com/nodejs-${NODE_VERSION}.tgz" -NPM_PACKAGE="http://${S3_BUCKET}.s3.amazonaws.com/npm-${NPM_VERSION}.tgz" -SCONS_PACKAGE="http://${S3_BUCKET}.s3.amazonaws.com/scons-${SCONS_VERSION}.tgz" +echo "Using node version: ${NODE_VERSION}" | indent # vendor directories VENDORED_NODE="$(mktmpdir node)" VENDORED_NPM="$(mktmpdir npm)" VENDORED_SCONS="$(mktmpdir scons)" -# download and unpack packages -echo "-----> Fetching Node.js binaries" -package_download "nodejs" "${NODE_VERSION}" "${VENDORED_NODE}" -package_download "npm" "${NPM_VERSION}" "${VENDORED_NPM}" -package_download "scons" "${SCONS_VERSION}" "${VENDORED_SCONS}" +echo "-----> Fetching node binary" -# vendor node into the slug +echo "-----> Vendoring node binary into slug" PATH="$BUILD_DIR/bin:$PATH" -echo "-----> Vendoring node into slug" mkdir -p "$BUILD_DIR/bin" cp "$VENDORED_NODE/bin/node" "$BUILD_DIR/bin/node" @@ -174,12 +58,7 @@ INCLUDE_PATH="$VENDORED_NODE/include" export CPATH="$INCLUDE_PATH:$CPATH" export CPPPATH="$INCLUDE_PATH:$CPPPATH" -# install dependencies with npm echo "-----> Installing dependencies with npm" run_npm "install --production" run_npm "rebuild" echo "Dependencies installed" | indent - -echo "-----> Building runtime environment" -mkdir -p $BUILD_DIR/.profile.d -echo "export PATH=\"\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $BUILD_DIR/.profile.d/nodejs.sh From 741800c691a5ad0bc5fe054c2e8f516f108140df Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 14:52:02 -0700 Subject: [PATCH 020/116] fetch node from s3pository --- bin/compile | 72 +++++++++++----------------------------------- support/helpers.sh | 18 ++++++++++++ 2 files changed, 35 insertions(+), 55 deletions(-) diff --git a/bin/compile b/bin/compile index 3b4a51676..10129a354 100755 --- a/bin/compile +++ b/bin/compile @@ -1,64 +1,26 @@ #!/usr/bin/env bash - -set -e # fail fast -# set -x # debug +source support/helpers.sh BUILD_DIR=$1 CACHE_DIR=$2 +NODE_VERSION='0.10.15' +NODE_URL="http://s3pository.heroku.com/node/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" -source support/helpers.sh - -# download latest stable version of node -# detect desired node version in package.json using semver - - -# if !desired -# (cached) ? cached : latest-stable -# else -# (latest-stable satisfies desired) ? latest-stable : download - -# if desired and cached are absent, use latest -# if desired is absent and cache is present, use cached -# if desired is ">" and differs from cached and latest-stable, download -# if desired is specific and differs from cached and latest-stable, download - -semver.satisfies('0.10.15', '>=0.10.x') - - -package="http://s3pository.heroku.com/$engine-$version.tgz" -curl $package -s -o - | tar xzf - -C $location - -function cat_npm_debug_log() { - if [ -f $BUILD_DIR/npm-debug.log ]; then - cat $BUILD_DIR/npm-debug.log - fi -} -trap cat_npm_debug_log EXIT - -echo "-----> Resolving engine versions" - -NODE_VERSION=$(package_resolve_version "node") -echo "Using node version: ${NODE_VERSION}" | indent - -# vendor directories -VENDORED_NODE="$(mktmpdir node)" -VENDORED_NPM="$(mktmpdir npm)" -VENDORED_SCONS="$(mktmpdir scons)" +status "Downloading node v$NODE_VERSION" +echo "$NODE_URL" | indent +tar_download $NODE_URL -echo "-----> Fetching node binary" +status "Moving node and npm to build directory" +mv $BUILD_DIR/node-v$NODE_VERSION-linux-x64 $BUILD_DIR/node -echo "-----> Vendoring node binary into slug" -PATH="$BUILD_DIR/bin:$PATH" -mkdir -p "$BUILD_DIR/bin" -cp "$VENDORED_NODE/bin/node" "$BUILD_DIR/bin/node" +status "Making node and npm executable" +chmod +x $BUILD_DIR/node/bin/* -# setting up paths for building -PATH="$VENDORED_SCONS:$VENDORED_NODE/bin:$PATH" -INCLUDE_PATH="$VENDORED_NODE/include" -export CPATH="$INCLUDE_PATH:$CPATH" -export CPPPATH="$INCLUDE_PATH:$CPPPATH" +status "Adding node and npm to the \$PATH" +PATH=$PATH:$BUILD_DIR/node/bin +echo "node version $NODE_VERSION has been installed" | indent -echo "-----> Installing dependencies with npm" -run_npm "install --production" -run_npm "rebuild" -echo "Dependencies installed" | indent +status "Installing dependencies with npm" +cd $BUILD_DIR +npm install --production +echo "Dependencies installed" | indent \ No newline at end of file diff --git a/support/helpers.sh b/support/helpers.sh index fd84993c2..1b1311e6b 100644 --- a/support/helpers.sh +++ b/support/helpers.sh @@ -1,8 +1,26 @@ +# fail fast +set -e + +# debug +# set -x + +function tar_download() { + url="$1" + location="$2" + + mkdir -p $location + curl $url -s -o - | tar xzf - -C $location +} + function error() { echo " ! $*" >&2 exit 1 } +function status() { + echo "-----> $*" +} + function mktmpdir() { dir=$(mktemp -t node-$1-XXXX) rm -rf $dir From a52308572b558af68053a840af501caf07d67b93 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 14:58:25 -0700 Subject: [PATCH 021/116] use absolute path for common.sh --- support/helpers.sh => bin/common.sh | 3 ++- bin/compile | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) rename support/helpers.sh => bin/common.sh (95%) diff --git a/support/helpers.sh b/bin/common.sh similarity index 95% rename from support/helpers.sh rename to bin/common.sh index 1b1311e6b..80be8cd8b 100644 --- a/support/helpers.sh +++ b/bin/common.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + # fail fast set -e @@ -7,7 +9,6 @@ set -e function tar_download() { url="$1" location="$2" - mkdir -p $location curl $url -s -o - | tar xzf - -C $location } diff --git a/bin/compile b/bin/compile index 10129a354..dfdedcef6 100755 --- a/bin/compile +++ b/bin/compile @@ -1,11 +1,13 @@ #!/usr/bin/env bash -source support/helpers.sh BUILD_DIR=$1 CACHE_DIR=$2 -NODE_VERSION='0.10.15' +BIN_DIR=$(cd $(dirname $0); pwd) # absolute path +NODE_VERSION="0.10.15" NODE_URL="http://s3pository.heroku.com/node/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" +source $BIN_DIR/common.sh + status "Downloading node v$NODE_VERSION" echo "$NODE_URL" | indent tar_download $NODE_URL From 25df93f01d8b21596203f7d411dfd4852a99ce1b Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 15:01:08 -0700 Subject: [PATCH 022/116] use correct tar_download signature --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index dfdedcef6..43c19d197 100755 --- a/bin/compile +++ b/bin/compile @@ -10,7 +10,7 @@ source $BIN_DIR/common.sh status "Downloading node v$NODE_VERSION" echo "$NODE_URL" | indent -tar_download $NODE_URL +tar_download $NODE_URL $BUILD_DIR status "Moving node and npm to build directory" mv $BUILD_DIR/node-v$NODE_VERSION-linux-x64 $BUILD_DIR/node From 5551cd72fc2ee12618209f9c77c26e31c4b9eba1 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 15:12:39 -0700 Subject: [PATCH 023/116] use bin instead of node/bin --- bin/common.sh | 6 ++++++ bin/compile | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bin/common.sh b/bin/common.sh index 80be8cd8b..d3d0cb1aa 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -35,4 +35,10 @@ function indent() { Darwin) sed -l "$c";; *) sed -u "$c";; esac +} + +function cat_npm_debug_log() { + if [ -f $BUILD_DIR/npm-debug.log ]; then + cat $BUILD_DIR/npm-debug.log + fi } \ No newline at end of file diff --git a/bin/compile b/bin/compile index 43c19d197..3928bdc2a 100755 --- a/bin/compile +++ b/bin/compile @@ -8,19 +8,21 @@ NODE_URL="http://s3pository.heroku.com/node/v$NODE_VERSION/node-v$NODE_VERSION-l source $BIN_DIR/common.sh +# Output debug info on exit +trap cat_npm_debug_log EXIT + status "Downloading node v$NODE_VERSION" echo "$NODE_URL" | indent tar_download $NODE_URL $BUILD_DIR status "Moving node and npm to build directory" -mv $BUILD_DIR/node-v$NODE_VERSION-linux-x64 $BUILD_DIR/node +mv $BUILD_DIR/node-v$NODE_VERSION-linux-x64 $BUILD_DIR/bin status "Making node and npm executable" -chmod +x $BUILD_DIR/node/bin/* +chmod +x $BUILD_DIR/bin/* status "Adding node and npm to the \$PATH" -PATH=$PATH:$BUILD_DIR/node/bin -echo "node version $NODE_VERSION has been installed" | indent +PATH="$BUILD_DIR/bin:$PATH" status "Installing dependencies with npm" cd $BUILD_DIR From 9d7a33bbf860811d751b25a4c7289ec56e8c2d24 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 15:21:05 -0700 Subject: [PATCH 024/116] get the paths straight --- .profile.d/nodejs.sh | 2 +- bin/compile | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.profile.d/nodejs.sh b/.profile.d/nodejs.sh index 71fa2aaf7..ff650d822 100644 --- a/.profile.d/nodejs.sh +++ b/.profile.d/nodejs.sh @@ -1 +1 @@ -export PATH=$HOME/bin:$HOME/node_modules/.bin:$PATH \ No newline at end of file +export PATH=$HOME/node/bin:$HOME/node_modules/.bin:$PATH \ No newline at end of file diff --git a/bin/compile b/bin/compile index 3928bdc2a..737d19578 100755 --- a/bin/compile +++ b/bin/compile @@ -16,13 +16,13 @@ echo "$NODE_URL" | indent tar_download $NODE_URL $BUILD_DIR status "Moving node and npm to build directory" -mv $BUILD_DIR/node-v$NODE_VERSION-linux-x64 $BUILD_DIR/bin +mv $BUILD_DIR/node-v$NODE_VERSION-linux-x64 $BUILD_DIR/node status "Making node and npm executable" -chmod +x $BUILD_DIR/bin/* +chmod +x $BUILD_DIR/node/bin/* status "Adding node and npm to the \$PATH" -PATH="$BUILD_DIR/bin:$PATH" +PATH=$PATH:$BUILD_DIR/node/bin status "Installing dependencies with npm" cd $BUILD_DIR From 37320acdb073a8e9d4681ca6c4d6df11b1153388 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 15:24:25 -0700 Subject: [PATCH 025/116] build .profile.d on the fly --- .profile.d/nodejs.sh | 1 - bin/compile | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 .profile.d/nodejs.sh diff --git a/.profile.d/nodejs.sh b/.profile.d/nodejs.sh deleted file mode 100644 index ff650d822..000000000 --- a/.profile.d/nodejs.sh +++ /dev/null @@ -1 +0,0 @@ -export PATH=$HOME/node/bin:$HOME/node_modules/.bin:$PATH \ No newline at end of file diff --git a/bin/compile b/bin/compile index 737d19578..eab2f1555 100755 --- a/bin/compile +++ b/bin/compile @@ -27,4 +27,8 @@ PATH=$PATH:$BUILD_DIR/node/bin status "Installing dependencies with npm" cd $BUILD_DIR npm install --production -echo "Dependencies installed" | indent \ No newline at end of file +echo "Dependencies installed" | indent + +status "Building runtime environment" +mkdir -p $BUILD_DIR/.profile.d +echo "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $BUILD_DIR/.profile.d/nodejs.sh \ No newline at end of file From 54730a4ee615a2957fe95587b24ec1f2c40df1e2 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 15:26:16 -0700 Subject: [PATCH 026/116] try indenting npm output --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index eab2f1555..b912c015a 100755 --- a/bin/compile +++ b/bin/compile @@ -26,7 +26,7 @@ PATH=$PATH:$BUILD_DIR/node/bin status "Installing dependencies with npm" cd $BUILD_DIR -npm install --production +npm install --production | indent echo "Dependencies installed" | indent status "Building runtime environment" From f4d233646c2d3aa92c5c5bb17afbd5cb026b83c5 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 15:29:42 -0700 Subject: [PATCH 027/116] simplify deploy output --- bin/compile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bin/compile b/bin/compile index b912c015a..2401f3179 100755 --- a/bin/compile +++ b/bin/compile @@ -12,16 +12,11 @@ source $BIN_DIR/common.sh trap cat_npm_debug_log EXIT status "Downloading node v$NODE_VERSION" -echo "$NODE_URL" | indent tar_download $NODE_URL $BUILD_DIR -status "Moving node and npm to build directory" +status "Adding node and npm to \$PATH" mv $BUILD_DIR/node-v$NODE_VERSION-linux-x64 $BUILD_DIR/node - -status "Making node and npm executable" chmod +x $BUILD_DIR/node/bin/* - -status "Adding node and npm to the \$PATH" PATH=$PATH:$BUILD_DIR/node/bin status "Installing dependencies with npm" From edfa6f7cc17b504cb5811f27367e3154a4d36307 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 15:33:19 -0700 Subject: [PATCH 028/116] remove superfluous info about versions --- README.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/README.md b/README.md index 5c8033636..fcb7424f5 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,6 @@ This is a [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) fo The buildpack will detect your app as Node.js if it has a `package.json` file in the root. It will use npm to install your dependencies, and vendors a version of the Node.js runtime into your slug. -Node.js Versions ----------------- - -You can specify the version of Node.js your application requires using `package.json`: - -```json -{ - "name": "myapp", - "version": "0.0.1", - "engines": { - "node": "~0.10.15" - } -} -``` - -TODO: Link to versioning doc - Documentation ------------- From eafebb029d358fb7a81b98022d6528abd2937905 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 23:17:39 -0700 Subject: [PATCH 029/116] look for version requirement in package.json --- bin/compile | 19 ++-- vendor/JSON.sh | 191 ----------------------------------------- vendor/{json => }/json | 0 3 files changed, 14 insertions(+), 196 deletions(-) delete mode 100755 vendor/JSON.sh rename vendor/{json => }/json (100%) diff --git a/bin/compile b/bin/compile index 2401f3179..2f9969c76 100755 --- a/bin/compile +++ b/bin/compile @@ -3,24 +3,33 @@ BUILD_DIR=$1 CACHE_DIR=$2 BIN_DIR=$(cd $(dirname $0); pwd) # absolute path -NODE_VERSION="0.10.15" -NODE_URL="http://s3pository.heroku.com/node/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" +DEFAULT_VERSION="0.10.15" +NODE_URL="http://s3pository.heroku.com/node/v$DEFAULT_VERSION/node-v$DEFAULT_VERSION-linux-x64.tar.gz" source $BIN_DIR/common.sh # Output debug info on exit trap cat_npm_debug_log EXIT -status "Downloading node v$NODE_VERSION" +status "Downloading node v$DEFAULT_VERSION" tar_download $NODE_URL $BUILD_DIR status "Adding node and npm to \$PATH" -mv $BUILD_DIR/node-v$NODE_VERSION-linux-x64 $BUILD_DIR/node +mv $BUILD_DIR/node-v$DEFAULT_VERSION-linux-x64 $BUILD_DIR/node chmod +x $BUILD_DIR/node/bin/* PATH=$PATH:$BUILD_DIR/node/bin -status "Installing dependencies with npm" cd $BUILD_DIR + +# Is a node version present in package.json? +DESIRED_VERSION=$(cat $BUILD_DIR/package.json | node $HOME/vendor/json engines.node 2>/dev/null) +echo "DESIRED_VERSION: $DESIRED_VERSION" +if [ "$DESIRED_VERSION" != "" ] + SATISFIED_VERSION=$(./semver $DEFAULT_VERSION -r $DESIRED_VERSION) + echo "SATISFIED_VERSION: $SATISFIED_VERSION" +fi + +status "Installing dependencies with npm" npm install --production | indent echo "Dependencies installed" | indent diff --git a/vendor/JSON.sh b/vendor/JSON.sh deleted file mode 100755 index e5d9ac5fd..000000000 --- a/vendor/JSON.sh +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env bash - -throw () { - echo "$*" >&2 - exit 1 -} - -BRIEF=0 -LEAFONLY=0 -PRUNE=0 - -usage() { - echo - echo "Usage: JSON.sh [-b] [-l] [-p] [-h]" - echo - echo "-p - Prune empty. Exclude fields with empty values." - echo "-l - Leaf only. Only show leaf nodes, which stops data duplication." - echo "-b - Brief. Combines 'Leaf only' and 'Prune empty' options." - echo "-h - This help text." - echo -} - -parse_options() { - set -- "$@" - local ARGN=$# - while [ $ARGN -ne 0 ] - do - case $1 in - -h) usage - exit 0 - ;; - -b) BRIEF=1 - LEAFONLY=1 - PRUNE=1 - ;; - -l) LEAFONLY=1 - ;; - -p) PRUNE=1 - ;; - ?*) echo "ERROR: Unknown option." - usage - exit 0 - ;; - esac - shift 1 - ARGN=$((ARGN-1)) - done -} - -awk_egrep () { - local pattern_string=$1 - - gawk '{ - while ($0) { - start=match($0, pattern); - token=substr($0, start, RLENGTH); - print token; - $0=substr($0, start+RLENGTH); - } - }' pattern=$pattern_string -} - -tokenize () { - local GREP - local ESCAPE - local CHAR - - if echo "test string" | egrep -ao --color=never "test" &>/dev/null - then - GREP='egrep -ao --color=never' - else - GREP='egrep -ao' - fi - - if echo "test string" | egrep -o "test" &>/dev/null - then - ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' - CHAR='[^[:cntrl:]"\\]' - else - GREP=awk_egrep - ESCAPE='(\\\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' - CHAR='[^[:cntrl:]"\\\\]' - fi - - local STRING="\"$CHAR*($ESCAPE$CHAR*)*\"" - local NUMBER='-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?' - local KEYWORD='null|false|true' - local SPACE='[[:space:]]+' - - $GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$" -} - -parse_array () { - local index=0 - local ary='' - read -r token - case "$token" in - ']') ;; - *) - while : - do - parse_value "$1" "$index" - index=$((index+1)) - ary="$ary""$value" - read -r token - case "$token" in - ']') break ;; - ',') ary="$ary," ;; - *) throw "EXPECTED , or ] GOT ${token:-EOF}" ;; - esac - read -r token - done - ;; - esac - [ "$BRIEF" -eq 0 ] && value=`printf '[%s]' "$ary"` || value= - : -} - -parse_object () { - local key - local obj='' - read -r token - case "$token" in - '}') ;; - *) - while : - do - case "$token" in - '"'*'"') key=$token ;; - *) throw "EXPECTED string GOT ${token:-EOF}" ;; - esac - read -r token - case "$token" in - ':') ;; - *) throw "EXPECTED : GOT ${token:-EOF}" ;; - esac - read -r token - parse_value "$1" "$key" - obj="$obj$key:$value" - read -r token - case "$token" in - '}') break ;; - ',') obj="$obj," ;; - *) throw "EXPECTED , or } GOT ${token:-EOF}" ;; - esac - read -r token - done - ;; - esac - [ "$BRIEF" -eq 0 ] && value=`printf '{%s}' "$obj"` || value= - : -} - -parse_value () { - local jpath="${1:+$1,}$2" isleaf=0 isempty=0 print=0 - case "$token" in - '{') parse_object "$jpath" ;; - '[') parse_array "$jpath" ;; - # At this point, the only valid single-character tokens are digits. - ''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;; - *) value=$token - isleaf=1 - [ "$value" = '""' ] && isempty=1 - ;; - esac - [ "$value" = '' ] && return - [ "$LEAFONLY" -eq 0 ] && [ "$PRUNE" -eq 0 ] && print=1 - [ "$LEAFONLY" -eq 1 ] && [ "$isleaf" -eq 1 ] && [ $PRUNE -eq 0 ] && print=1 - [ "$LEAFONLY" -eq 0 ] && [ "$PRUNE" -eq 1 ] && [ "$isempty" -eq 0 ] && print=1 - [ "$LEAFONLY" -eq 1 ] && [ "$isleaf" -eq 1 ] && \ - [ $PRUNE -eq 1 ] && [ $isempty -eq 0 ] && print=1 - [ "$print" -eq 1 ] && printf "[%s]\t%s\n" "$jpath" "$value" - : -} - -parse () { - read -r token - parse_value - read -r token - case "$token" in - '') ;; - *) throw "EXPECTED EOF GOT $token" ;; - esac -} - -parse_options "$@" - -if ([ "$0" = "$BASH_SOURCE" ] || ! [ -n "$BASH_SOURCE" ]); -then - tokenize | parse -fi diff --git a/vendor/json/json b/vendor/json similarity index 100% rename from vendor/json/json rename to vendor/json From 97097991967b14c23e8b50ffd116045bdfb97b1c Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 23:19:49 -0700 Subject: [PATCH 030/116] use the right path to the semver binary --- bin/compile | 2 +- vendor/{node-semver => semver}/LICENSE | 0 vendor/{node-semver => semver}/README.md | 0 vendor/{node-semver => semver}/bin/semver | 0 vendor/{node-semver => semver}/package.json | 0 vendor/{node-semver => semver}/semver.js | 0 vendor/{node-semver => semver}/test.js | 0 7 files changed, 1 insertion(+), 1 deletion(-) rename vendor/{node-semver => semver}/LICENSE (100%) rename vendor/{node-semver => semver}/README.md (100%) rename vendor/{node-semver => semver}/bin/semver (100%) rename vendor/{node-semver => semver}/package.json (100%) rename vendor/{node-semver => semver}/semver.js (100%) rename vendor/{node-semver => semver}/test.js (100%) diff --git a/bin/compile b/bin/compile index 2f9969c76..a2fe2f292 100755 --- a/bin/compile +++ b/bin/compile @@ -25,7 +25,7 @@ cd $BUILD_DIR DESIRED_VERSION=$(cat $BUILD_DIR/package.json | node $HOME/vendor/json engines.node 2>/dev/null) echo "DESIRED_VERSION: $DESIRED_VERSION" if [ "$DESIRED_VERSION" != "" ] - SATISFIED_VERSION=$(./semver $DEFAULT_VERSION -r $DESIRED_VERSION) + SATISFIED_VERSION=$(node $HOME/vendor/semver/bin/semver $DEFAULT_VERSION -r $DESIRED_VERSION) echo "SATISFIED_VERSION: $SATISFIED_VERSION" fi diff --git a/vendor/node-semver/LICENSE b/vendor/semver/LICENSE similarity index 100% rename from vendor/node-semver/LICENSE rename to vendor/semver/LICENSE diff --git a/vendor/node-semver/README.md b/vendor/semver/README.md similarity index 100% rename from vendor/node-semver/README.md rename to vendor/semver/README.md diff --git a/vendor/node-semver/bin/semver b/vendor/semver/bin/semver similarity index 100% rename from vendor/node-semver/bin/semver rename to vendor/semver/bin/semver diff --git a/vendor/node-semver/package.json b/vendor/semver/package.json similarity index 100% rename from vendor/node-semver/package.json rename to vendor/semver/package.json diff --git a/vendor/node-semver/semver.js b/vendor/semver/semver.js similarity index 100% rename from vendor/node-semver/semver.js rename to vendor/semver/semver.js diff --git a/vendor/node-semver/test.js b/vendor/semver/test.js similarity index 100% rename from vendor/node-semver/test.js rename to vendor/semver/test.js From 3c8c87b0b8fc64f5c0d425d71a6f27ac61754c8f Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 23:25:03 -0700 Subject: [PATCH 031/116] revive LP_DIR --- bin/compile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/compile b/bin/compile index a2fe2f292..077a9d9ca 100755 --- a/bin/compile +++ b/bin/compile @@ -2,11 +2,11 @@ BUILD_DIR=$1 CACHE_DIR=$2 -BIN_DIR=$(cd $(dirname $0); pwd) # absolute path +LP_DIR=$(cd $(dirname $0); cd ..; pwd) DEFAULT_VERSION="0.10.15" NODE_URL="http://s3pository.heroku.com/node/v$DEFAULT_VERSION/node-v$DEFAULT_VERSION-linux-x64.tar.gz" -source $BIN_DIR/common.sh +source $LP_DIR/bin/common.sh # Output debug info on exit trap cat_npm_debug_log EXIT @@ -22,10 +22,10 @@ PATH=$PATH:$BUILD_DIR/node/bin cd $BUILD_DIR # Is a node version present in package.json? -DESIRED_VERSION=$(cat $BUILD_DIR/package.json | node $HOME/vendor/json engines.node 2>/dev/null) +DESIRED_VERSION=$(cat $BUILD_DIR/package.json | node $LP_DIR/vendor/json engines.node 2>/dev/null) echo "DESIRED_VERSION: $DESIRED_VERSION" if [ "$DESIRED_VERSION" != "" ] - SATISFIED_VERSION=$(node $HOME/vendor/semver/bin/semver $DEFAULT_VERSION -r $DESIRED_VERSION) + SATISFIED_VERSION=$(node $LP_DIR/vendor/semver/bin/semver $DEFAULT_VERSION -r $DESIRED_VERSION) echo "SATISFIED_VERSION: $SATISFIED_VERSION" fi From 3f792bbb81c423dc8ebd1a8c13dfa9075e2811c6 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 23:26:25 -0700 Subject: [PATCH 032/116] then... --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index 077a9d9ca..2fdaf8460 100755 --- a/bin/compile +++ b/bin/compile @@ -24,7 +24,7 @@ cd $BUILD_DIR # Is a node version present in package.json? DESIRED_VERSION=$(cat $BUILD_DIR/package.json | node $LP_DIR/vendor/json engines.node 2>/dev/null) echo "DESIRED_VERSION: $DESIRED_VERSION" -if [ "$DESIRED_VERSION" != "" ] +if [ "$DESIRED_VERSION" != "" ]; then SATISFIED_VERSION=$(node $LP_DIR/vendor/semver/bin/semver $DEFAULT_VERSION -r $DESIRED_VERSION) echo "SATISFIED_VERSION: $SATISFIED_VERSION" fi From 0c9a66162813b1557a24045ebed0fc851a4ef678 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 23:28:22 -0700 Subject: [PATCH 033/116] enable debugging --- bin/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/common.sh b/bin/common.sh index d3d0cb1aa..88cd5fd3a 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -4,7 +4,7 @@ set -e # debug -# set -x +set -x function tar_download() { url="$1" From 214970a0a9e207e52156dc69d06689517bba83fc Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 23:33:33 -0700 Subject: [PATCH 034/116] test without semver --- bin/compile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/compile b/bin/compile index 2fdaf8460..d8c64af93 100755 --- a/bin/compile +++ b/bin/compile @@ -22,15 +22,15 @@ PATH=$PATH:$BUILD_DIR/node/bin cd $BUILD_DIR # Is a node version present in package.json? -DESIRED_VERSION=$(cat $BUILD_DIR/package.json | node $LP_DIR/vendor/json engines.node 2>/dev/null) -echo "DESIRED_VERSION: $DESIRED_VERSION" -if [ "$DESIRED_VERSION" != "" ]; then - SATISFIED_VERSION=$(node $LP_DIR/vendor/semver/bin/semver $DEFAULT_VERSION -r $DESIRED_VERSION) - echo "SATISFIED_VERSION: $SATISFIED_VERSION" -fi +# DESIRED_VERSION=$(cat $BUILD_DIR/package.json | node $LP_DIR/vendor/json engines.node 2>/dev/null) +# echo "DESIRED_VERSION: $DESIRED_VERSION" +# if [ "$DESIRED_VERSION" != "" ]; then +# SATISFIED_VERSION=$(node $LP_DIR/vendor/semver/bin/semver $DEFAULT_VERSION -r "$DESIRED_VERSION") +# echo "SATISFIED_VERSION: $SATISFIED_VERSION" +# fi status "Installing dependencies with npm" -npm install --production | indent +npm install --production echo "Dependencies installed" | indent status "Building runtime environment" From dce88707ad2d917d839bded011faa6f2b1d83c24 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 23:39:48 -0700 Subject: [PATCH 035/116] use lowercase variable names --- bin/compile | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/bin/compile b/bin/compile index d8c64af93..32fe7de78 100755 --- a/bin/compile +++ b/bin/compile @@ -1,38 +1,38 @@ #!/usr/bin/env bash -BUILD_DIR=$1 -CACHE_DIR=$2 -LP_DIR=$(cd $(dirname $0); cd ..; pwd) -DEFAULT_VERSION="0.10.15" -NODE_URL="http://s3pository.heroku.com/node/v$DEFAULT_VERSION/node-v$DEFAULT_VERSION-linux-x64.tar.gz" +build_dir=$1 +cache_dir=$2 +lp_dir=$(cd $(dirname $0); cd ..; pwd) +default_version="0.10.15" +$node_url="http://s3pository.heroku.com/node/v$default_version/node-v$default_version-linux-x64.tar.gz" -source $LP_DIR/bin/common.sh +source $lp_dir/bin/common.sh # Output debug info on exit trap cat_npm_debug_log EXIT -status "Downloading node v$DEFAULT_VERSION" -tar_download $NODE_URL $BUILD_DIR +status "Downloading node v$default_version" +tar_download $$node_url $build_dir status "Adding node and npm to \$PATH" -mv $BUILD_DIR/node-v$DEFAULT_VERSION-linux-x64 $BUILD_DIR/node -chmod +x $BUILD_DIR/node/bin/* -PATH=$PATH:$BUILD_DIR/node/bin +mv $build_dir/node-v$default_version-linux-x64 $build_dir/node +chmod +x $build_dir/node/bin/* +PATH=$PATH:$build_dir/node/bin -cd $BUILD_DIR +cd $build_dir # Is a node version present in package.json? -# DESIRED_VERSION=$(cat $BUILD_DIR/package.json | node $LP_DIR/vendor/json engines.node 2>/dev/null) -# echo "DESIRED_VERSION: $DESIRED_VERSION" -# if [ "$DESIRED_VERSION" != "" ]; then -# SATISFIED_VERSION=$(node $LP_DIR/vendor/semver/bin/semver $DEFAULT_VERSION -r "$DESIRED_VERSION") -# echo "SATISFIED_VERSION: $SATISFIED_VERSION" -# fi +desired_version=$(cat $build_dir/package.json | node $lp_dir/vendor/json engines.node 2>/dev/null) +echo "desired_version: $desired_version" +if [ "$desired_version" != "" ]; then + default_satisfies=$(node $lp_dir/vendor/semver/bin/semver $default_version -r "$desired_version") + echo "default_satisfies: $default_satisfies" +fi status "Installing dependencies with npm" npm install --production echo "Dependencies installed" | indent status "Building runtime environment" -mkdir -p $BUILD_DIR/.profile.d -echo "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $BUILD_DIR/.profile.d/nodejs.sh \ No newline at end of file +mkdir -p $build_dir/.profile.d +echo "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh \ No newline at end of file From 998af9551bda1d40c39f98b9aa02577cbadd73c8 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 9 Aug 2013 23:41:18 -0700 Subject: [PATCH 036/116] fix the node_url variable --- bin/compile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index 32fe7de78..aa8b0ad77 100755 --- a/bin/compile +++ b/bin/compile @@ -4,7 +4,7 @@ build_dir=$1 cache_dir=$2 lp_dir=$(cd $(dirname $0); cd ..; pwd) default_version="0.10.15" -$node_url="http://s3pository.heroku.com/node/v$default_version/node-v$default_version-linux-x64.tar.gz" +node_url="http://s3pository.heroku.com/node/v$default_version/node-v$default_version-linux-x64.tar.gz" source $lp_dir/bin/common.sh @@ -12,7 +12,7 @@ source $lp_dir/bin/common.sh trap cat_npm_debug_log EXIT status "Downloading node v$default_version" -tar_download $$node_url $build_dir +tar_download $node_url $build_dir status "Adding node and npm to \$PATH" mv $build_dir/node-v$default_version-linux-x64 $build_dir/node From 89484b77d4fa3ddd6ba406446621cf6f94a70a3f Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 12 Aug 2013 22:41:46 -0700 Subject: [PATCH 037/116] fetch all available versions from nodejs.org --- bin/common.sh | 36 +++++++++++++++++++++++++++++++----- bin/compile | 29 +++++++++++++---------------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/bin/common.sh b/bin/common.sh index 88cd5fd3a..037039e74 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -6,11 +6,37 @@ set -e # debug set -x -function tar_download() { - url="$1" - location="$2" - mkdir -p $location - curl $url -s -o - | tar xzf - -C $location +download_node() { + version="$1" + + status "Downloading node $version" + node_url="http://s3pository.heroku.com/node/v$version/node-v$version-linux-x64.tar.gz" + curl $node_url -s -o - | tar xzf - -C $build_dir + mv $build_dir/node-v$version-linux-x64 $build_dir/node + + status "Adding node and npm to \$PATH" + chmod +x $build_dir/node/bin/* + PATH=$PATH:$build_dir/node/bin +} + +query_stable_version() { + curl -s http://nodejs.org/dist/ \ + | egrep -o '[0-9]+\.[0-9]*[02468]\.[0-9]+' \ + | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \ + | tail -n1 +} + +query_latest_version() { + curl -s http://nodejs.org/dist/ \ + | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \ + | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \ + | tail -n1 +} + +all_versions() { + curl -s http://nodejs.org/dist/ \ + | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \ + | sort -u -k 1,1n -k 2,2n -k 3,3n -t . } function error() { diff --git a/bin/compile b/bin/compile index aa8b0ad77..6e4b7af94 100755 --- a/bin/compile +++ b/bin/compile @@ -3,34 +3,31 @@ build_dir=$1 cache_dir=$2 lp_dir=$(cd $(dirname $0); cd ..; pwd) -default_version="0.10.15" -node_url="http://s3pository.heroku.com/node/v$default_version/node-v$default_version-linux-x64.tar.gz" +stable_version="0.10.15" source $lp_dir/bin/common.sh # Output debug info on exit trap cat_npm_debug_log EXIT -status "Downloading node v$default_version" -tar_download $node_url $build_dir +download_node $stable_version -status "Adding node and npm to \$PATH" -mv $build_dir/node-v$default_version-linux-x64 $build_dir/node -chmod +x $build_dir/node/bin/* -PATH=$PATH:$build_dir/node/bin +# cd $build_dir -cd $build_dir - -# Is a node version present in package.json? -desired_version=$(cat $build_dir/package.json | node $lp_dir/vendor/json engines.node 2>/dev/null) -echo "desired_version: $desired_version" -if [ "$desired_version" != "" ]; then - default_satisfies=$(node $lp_dir/vendor/semver/bin/semver $default_version -r "$desired_version") - echo "default_satisfies: $default_satisfies" +# Is a node version specified in package.json? +requested_version=$(cat $build_dir/package.json | node $lp_dir/vendor/json engines.node 2>/dev/null) +# echo "requested_version: $requested_version" +if test $requested_version; then + default_satisfies=$(node $lp_dir/vendor/semver/bin/semver $stable_version -r "$requested_version") + if ! test $default_satisfies; then + download_node $requested_version + fi fi status "Installing dependencies with npm" +npm prune npm install --production +npm rebuild echo "Dependencies installed" | indent status "Building runtime environment" From ad1e2ba56445d5729dff06f329a4723917278528 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 13:45:08 -0700 Subject: [PATCH 038/116] when the default stable version doesn't satisfy, get all available versions from nodejs.org/dist --- bin/common.sh | 10 ++- bin/compile | 78 ++++++++++++++++--- bin/test | 25 +++--- test/node-modules-caching/package.json | 11 +++ test/package-json-invalidversion/package.json | 3 +- 5 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 test/node-modules-caching/package.json diff --git a/bin/common.sh b/bin/common.sh index 037039e74..7e2162f9e 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -4,9 +4,9 @@ set -e # debug -set -x +# set -x -download_node() { +download_and_install_node() { version="$1" status "Downloading node $version" @@ -33,13 +33,15 @@ query_latest_version() { | tail -n1 } -all_versions() { +query_all_versions() { curl -s http://nodejs.org/dist/ \ | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \ | sort -u -k 1,1n -k 2,2n -k 3,3n -t . } -function error() { + + +error() { echo " ! $*" >&2 exit 1 } diff --git a/bin/compile b/bin/compile index 6e4b7af94..33dcc9f99 100755 --- a/bin/compile +++ b/bin/compile @@ -2,34 +2,88 @@ build_dir=$1 cache_dir=$2 -lp_dir=$(cd $(dirname $0); cd ..; pwd) -stable_version="0.10.15" +bp_dir=$(cd $(dirname $0); cd ..; pwd) +stable_version="0.10.17" -source $lp_dir/bin/common.sh +source $bp_dir/bin/common.sh # Output debug info on exit trap cat_npm_debug_log EXIT -download_node $stable_version - -# cd $build_dir +status "Downloading node $stable_version" +download_and_install_node $stable_version # Is a node version specified in package.json? -requested_version=$(cat $build_dir/package.json | node $lp_dir/vendor/json engines.node 2>/dev/null) -# echo "requested_version: $requested_version" +requested_version=$(cat $build_dir/package.json | node $bp_dir/vendor/json engines.node 2>/dev/null) + +# Give a warning if no node engine is specified +if ! test $requested_version; then + echo + echo "WARNING: No version of Node.js specified in package.json, see:" | indent + echo "https://devcenter.heroku.com/articles/nodejs-support#versions" | indent + echo +fi + if test $requested_version; then - default_satisfies=$(node $lp_dir/vendor/semver/bin/semver $stable_version -r "$requested_version") + + # Does the already-downloaded stable version of node satisfy the requested version? + default_satisfies=$(node $bp_dir/vendor/semver/bin/semver -v $stable_version -r "$requested_version" || echo "") + if ! test $default_satisfies; then - download_node $requested_version + status "Using node $stable_version" + else + # Find all available versions from nodejs.org/dist + available_versions="" + for version in query_all_versions; do + available_versions="${available_versions} -v \"${version}\"" + done + + # Determine which available versions satisfy the requested version + # https://github.com/isaacs/node-semver/blob/master/bin/semver + evaluated_versions=$(node $bp_dir/vendor/semver/bin/semver $available_versions -r "$requested_version" || echo "") + newest_matching_version=$("$evaluated_versions" | tail -n 1) + + if test $newest_matching_version; then + status "Using node $newest_matching_version" + download_and_install_node $newest_matching_version + else + error "Requested node version ${requested_version} not found among available versions: ${available_versions}" + fi + fi fi -status "Installing dependencies with npm" -npm prune +# Configure cache directories +cache_store_dir="$cache_dir/node_modules/$NODE_VERSION/$NPM_VERSION" +cache_target_dir="$build_dir/node_modules" + +# Restore node_modules cache +if [ -d $cache_store_dir ]; then + status "Restoring node_modules cache" + if [ -d $cache_target_dir ]; then + cp -r $cache_store_dir/* $cache_target_dir/ + else + cp -r $cache_store_dir $cache_target_dir + fi + status "Pruning unused dependencies" + npm prune +fi + +# Install dependencies +status "Installing dependencies" npm install --production npm rebuild echo "Dependencies installed" | indent +# Cache node_modules for future builds +if [ -d $cache_target_dir ]; then + status "Caching node_modules for future builds" + rm -rf $cache_store_dir + mkdir -p $(dirname $cache_store_dir) + cp -r $cache_target_dir $cache_store_dir +fi + +# Update the PATH status "Building runtime environment" mkdir -p $build_dir/.profile.d echo "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh \ No newline at end of file diff --git a/bin/test b/bin/test index 215e55008..03eef403a 100755 --- a/bin/test +++ b/bin/test @@ -22,8 +22,7 @@ testDetectWithoutPackageJson() { testPackageJsonWithVersion() { compile "package-json-version" assertNotCaptured "WARNING: No version of Node.js specified" - assertCaptured "Using Node.js version: 0.6.11" - assertCaptured "Using npm version: 1.1.9" + assertCaptured "Using node 0.10.17" assertCapturedSuccess } @@ -31,29 +30,27 @@ testPackageJsonWithoutVersion() { compile "package-json-noversion" assertCaptured "WARNING: No version of Node.js specified" assertCaptured "Using Node.js version: 0.10" - assertCaptured "Using npm version: 1.3" assertCapturedSuccess } testPackageJsonWithInvalidVersion() { compile "package-json-invalidversion" - assertCapturedError 1 "Requested engine npm version 1.1.5 does not" -} - -testNothingCached() { - cache=$(mktmpdir) - compile "package-json-version" $cache - assertCapturedSuccess - assertEquals "0" "$(ls -1 $cache | wc -l)" + assertCapturedError 1 "Requested node version 5.0 not found" } testProfileCreated() { compile "package-json-version" assertCaptured "Building runtime environment" - assertFile "export PATH=\"\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" + assertFile "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" assertCapturedSuccess } +testNodeModulesCached() { + cache=$(mktmpdir) + compile "node-modules-caching" $cache + assertEquals "1" "$(ls -1 $cache/node_modules/0.10.15/1.3.5 | wc -l)" +} + ## utils ######################################## pushd $(dirname 0) >/dev/null @@ -78,11 +75,11 @@ COMPILE_DIR="" compile() { COMPILE_DIR=$(mktmpdir) cp -r ${BASE}/test/$1/* ${COMPILE_DIR}/ - capture ${BASE}/bin/compile ${COMPILE_DIR} $2 + capture ${BASE}/bin/compile ${COMPILE_DIR} ${2:-$(mktmpdir)} } assertFile() { assertEquals "$1" "$(cat ${COMPILE_DIR}/$2)" } -source ${BASE}/vendor/shunit2/shunit2 +source ${BASE}/vendor/shunit2/shunit2 \ No newline at end of file diff --git a/test/node-modules-caching/package.json b/test/node-modules-caching/package.json new file mode 100644 index 000000000..38dbab9df --- /dev/null +++ b/test/node-modules-caching/package.json @@ -0,0 +1,11 @@ +{ + "name": "myapp", + "version": "0.0.1", + "engines": { + "node": "0.10.15", + "npm": "1.3.5" + }, + "dependencies": { + "express": "latest" + } +} \ No newline at end of file diff --git a/test/package-json-invalidversion/package.json b/test/package-json-invalidversion/package.json index 78c12adfb..70c79437b 100644 --- a/test/package-json-invalidversion/package.json +++ b/test/package-json-invalidversion/package.json @@ -3,7 +3,6 @@ "version": "0.0.1", "engines": { - "node": "0.6.11", - "npm": "1.1.5" + "node": "5.0" } } From a34979647c838e9e9727149d09345445e42eca03 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 14:07:58 -0700 Subject: [PATCH 039/116] changelog's been dead for 18 months --- CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 5629b6ecc..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -## v7 (2012-03-27) - -* remove build asset caching -* prevent installation of devDependencies -* add warning if no nodejs version specified From f6e81b161d53762978bfd7a5c5a108da4cd403d2 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 14:09:16 -0700 Subject: [PATCH 040/116] update year in license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index ad04dc171..4972b4505 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License: -Copyright (C) 2012 Heroku, Inc. +Copyright (C) 2012-2013 Heroku, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: From c747df7709db1c5208992c9c0510a2100465ca23 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 14:27:30 -0700 Subject: [PATCH 041/116] improve comments and status messages --- bin/compile | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/bin/compile b/bin/compile index 33dcc9f99..39900321f 100755 --- a/bin/compile +++ b/bin/compile @@ -3,14 +3,14 @@ build_dir=$1 cache_dir=$2 bp_dir=$(cd $(dirname $0); cd ..; pwd) -stable_version="0.10.17" +stable_version="0.10.18" source $bp_dir/bin/common.sh # Output debug info on exit trap cat_npm_debug_log EXIT -status "Downloading node $stable_version" +status "Downloading node version $stable_version" download_and_install_node $stable_version # Is a node version specified in package.json? @@ -18,19 +18,18 @@ requested_version=$(cat $build_dir/package.json | node $bp_dir/vendor/json engin # Give a warning if no node engine is specified if ! test $requested_version; then + node_version=$stable_version echo - echo "WARNING: No version of Node.js specified in package.json, see:" | indent + echo "WARNING: No node version specified in package.json, see:" | indent echo "https://devcenter.heroku.com/articles/nodejs-support#versions" | indent echo -fi - -if test $requested_version; then +else # Does the already-downloaded stable version of node satisfy the requested version? default_satisfies=$(node $bp_dir/vendor/semver/bin/semver -v $stable_version -r "$requested_version" || echo "") if ! test $default_satisfies; then - status "Using node $stable_version" + status "Using node version $stable_version" else # Find all available versions from nodejs.org/dist available_versions="" @@ -41,23 +40,24 @@ if test $requested_version; then # Determine which available versions satisfy the requested version # https://github.com/isaacs/node-semver/blob/master/bin/semver evaluated_versions=$(node $bp_dir/vendor/semver/bin/semver $available_versions -r "$requested_version" || echo "") - newest_matching_version=$("$evaluated_versions" | tail -n 1) - if test $newest_matching_version; then - status "Using node $newest_matching_version" - download_and_install_node $newest_matching_version + # Use the latest of the evaluated versions + node_version=$("$evaluated_versions" | tail -n 1) + + if test $node_version; then + status "Using node version $node_version" + download_and_install_node $node_version else error "Requested node version ${requested_version} not found among available versions: ${available_versions}" fi - fi fi # Configure cache directories -cache_store_dir="$cache_dir/node_modules/$NODE_VERSION/$NPM_VERSION" +cache_store_dir="$cache_dir/node_modules/$node_version" cache_target_dir="$build_dir/node_modules" -# Restore node_modules cache +# Restore node_modules from cache, if present if [ -d $cache_store_dir ]; then status "Restoring node_modules cache" if [ -d $cache_target_dir ]; then @@ -65,7 +65,7 @@ if [ -d $cache_store_dir ]; then else cp -r $cache_store_dir $cache_target_dir fi - status "Pruning unused dependencies" + status "Pruning any unused dependencies" npm prune fi @@ -73,7 +73,6 @@ fi status "Installing dependencies" npm install --production npm rebuild -echo "Dependencies installed" | indent # Cache node_modules for future builds if [ -d $cache_target_dir ]; then From b55ebc231a160b7815588af3884d809fc4aa547b Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 14:27:38 -0700 Subject: [PATCH 042/116] bump node test version --- test/package-json-version/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/package-json-version/package.json b/test/package-json-version/package.json index ce13aa44d..d174b940d 100644 --- a/test/package-json-version/package.json +++ b/test/package-json-version/package.json @@ -3,7 +3,6 @@ "version": "0.0.1", "engines": { - "node": "0.6.11", - "npm": "1.1.9" + "node": "0.10.18" } } From 2abc83a5fe2a0a23674b26c105db42a77828f25d Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 14:27:52 -0700 Subject: [PATCH 043/116] update expected test output --- bin/test | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/test b/bin/test index 03eef403a..1f85d9c4f 100755 --- a/bin/test +++ b/bin/test @@ -19,17 +19,17 @@ testDetectWithoutPackageJson() { assertCapturedError 1 "" } -testPackageJsonWithVersion() { +testPackageJsonWithVersion() {s compile "package-json-version" - assertNotCaptured "WARNING: No version of Node.js specified" - assertCaptured "Using node 0.10.17" + assertNotCaptured "WARNING: No node version specified" + assertCaptured "Using node 0.10" assertCapturedSuccess } testPackageJsonWithoutVersion() { compile "package-json-noversion" - assertCaptured "WARNING: No version of Node.js specified" - assertCaptured "Using Node.js version: 0.10" + assertCaptured "WARNING: No node version specified" + assertCaptured "Using node 0.10" assertCapturedSuccess } @@ -48,7 +48,7 @@ testProfileCreated() { testNodeModulesCached() { cache=$(mktmpdir) compile "node-modules-caching" $cache - assertEquals "1" "$(ls -1 $cache/node_modules/0.10.15/1.3.5 | wc -l)" + assertEquals "1" "$(ls -1 $cache/node_modules/0.10.17 | wc -l)" } ## utils ######################################## From c5d7a0c4bd473b85c15bb0d057c32f9cb05ba902 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 17:20:38 -0700 Subject: [PATCH 044/116] add readies and update package.json files in tests to keep newer versions of npm from polluting stderr --- test/node-modules-caching/README.md | 1 + test/node-modules-caching/package.json | 5 +++++ test/package-json-invalidversion/README.md | 1 + test/package-json-invalidversion/package.json | 6 +++++- test/package-json-noversion/README.md | 1 + test/package-json-noversion/package.json | 7 ++++++- test/package-json-stable-version/README.md | 1 + test/package-json-stable-version/package.json | 12 ++++++++++++ test/package-json-unstable-version/README.md | 1 + test/package-json-unstable-version/package.json | 12 ++++++++++++ test/package-json-version/package.json | 8 -------- 11 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 test/node-modules-caching/README.md create mode 100644 test/package-json-invalidversion/README.md create mode 100644 test/package-json-noversion/README.md create mode 100644 test/package-json-stable-version/README.md create mode 100644 test/package-json-stable-version/package.json create mode 100644 test/package-json-unstable-version/README.md create mode 100644 test/package-json-unstable-version/package.json delete mode 100644 test/package-json-version/package.json diff --git a/test/node-modules-caching/README.md b/test/node-modules-caching/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/node-modules-caching/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/node-modules-caching/package.json b/test/node-modules-caching/package.json index 38dbab9df..464d272ae 100644 --- a/test/node-modules-caching/package.json +++ b/test/node-modules-caching/package.json @@ -1,6 +1,11 @@ { "name": "myapp", "version": "0.0.1", + "description": "It's a fake app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, "engines": { "node": "0.10.15", "npm": "1.3.5" diff --git a/test/package-json-invalidversion/README.md b/test/package-json-invalidversion/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/package-json-invalidversion/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/package-json-invalidversion/package.json b/test/package-json-invalidversion/package.json index 70c79437b..25d264c96 100644 --- a/test/package-json-invalidversion/package.json +++ b/test/package-json-invalidversion/package.json @@ -1,7 +1,11 @@ { "name": "myapp", "version": "0.0.1", - + "description": "It's a fake app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, "engines": { "node": "5.0" } diff --git a/test/package-json-noversion/README.md b/test/package-json-noversion/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/package-json-noversion/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/package-json-noversion/package.json b/test/package-json-noversion/package.json index e9c82de3b..b2570b9b9 100644 --- a/test/package-json-noversion/package.json +++ b/test/package-json-noversion/package.json @@ -1,4 +1,9 @@ { "name": "myapp", - "version": "0.0.1" + "version": "0.0.1", + "description": "It's a fake app with no engine specified", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + } } diff --git a/test/package-json-stable-version/README.md b/test/package-json-stable-version/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/package-json-stable-version/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/package-json-stable-version/package.json b/test/package-json-stable-version/package.json new file mode 100644 index 000000000..3476e709d --- /dev/null +++ b/test/package-json-stable-version/package.json @@ -0,0 +1,12 @@ +{ + "name": "myapp", + "version": "0.0.1", + "description": "It's a fake app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "engines": { + "node": "0.10.x" + } +} diff --git a/test/package-json-unstable-version/README.md b/test/package-json-unstable-version/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/package-json-unstable-version/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/package-json-unstable-version/package.json b/test/package-json-unstable-version/package.json new file mode 100644 index 000000000..f01cae8d9 --- /dev/null +++ b/test/package-json-unstable-version/package.json @@ -0,0 +1,12 @@ +{ + "name": "myapp-using-unstable-node", + "version": "0.0.1", + "description": "It's a fake app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "engines": { + "node": "0.11.5" + } +} diff --git a/test/package-json-version/package.json b/test/package-json-version/package.json deleted file mode 100644 index d174b940d..000000000 --- a/test/package-json-version/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "myapp", - "version": "0.0.1", - - "engines": { - "node": "0.10.18" - } -} From 2faeed570d8c4d793488f573113401891cb86a16 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 17:21:01 -0700 Subject: [PATCH 045/116] passing tests: testPackageJsonWithoutVersion, testPackageJsonWithStableVersion, testPackageJsonWithUnstableVersion --- bin/common.sh | 2 -- bin/compile | 62 +++++++++++++++++++++++++++---------------------- bin/test | 64 ++++++++++++++++++++++++++++----------------------- 3 files changed, 69 insertions(+), 59 deletions(-) diff --git a/bin/common.sh b/bin/common.sh index 7e2162f9e..dd4f84e3b 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -39,8 +39,6 @@ query_all_versions() { | sort -u -k 1,1n -k 2,2n -k 3,3n -t . } - - error() { echo " ! $*" >&2 exit 1 diff --git a/bin/compile b/bin/compile index 39900321f..4999d1493 100755 --- a/bin/compile +++ b/bin/compile @@ -8,13 +8,17 @@ stable_version="0.10.18" source $bp_dir/bin/common.sh # Output debug info on exit -trap cat_npm_debug_log EXIT +# trap cat_npm_debug_log EXIT status "Downloading node version $stable_version" download_and_install_node $stable_version # Is a node version specified in package.json? -requested_version=$(cat $build_dir/package.json | node $bp_dir/vendor/json engines.node 2>/dev/null) +determine_requested_version() { + v=$(cat $build_dir/package.json | node $bp_dir/vendor/json engines.node 2>/dev/null) + if [ $? == 0 ]; then echo $v; fi +} +requested_version=$(determine_requested_version) # Give a warning if no node engine is specified if ! test $requested_version; then @@ -23,32 +27,32 @@ if ! test $requested_version; then echo "WARNING: No node version specified in package.json, see:" | indent echo "https://devcenter.heroku.com/articles/nodejs-support#versions" | indent echo + status "Using latest stable node version $stable_version" else # Does the already-downloaded stable version of node satisfy the requested version? - default_satisfies=$(node $bp_dir/vendor/semver/bin/semver -v $stable_version -r "$requested_version" || echo "") + default_satisfies=$(node $bp_dir/vendor/semver/bin/semver -v "${stable_version}" -r "${requested_version}" || echo "") - if ! test $default_satisfies; then - status "Using node version $stable_version" + if test $default_satisfies; then + status "Using latest stable node version $stable_version" else # Find all available versions from nodejs.org/dist - available_versions="" - for version in query_all_versions; do - available_versions="${available_versions} -v \"${version}\"" - done + args="" + for version in $(query_all_versions); do args="${args} -v \"${version}\""; done + args="${args} -r \"${requested_version}\"" # Determine which available versions satisfy the requested version # https://github.com/isaacs/node-semver/blob/master/bin/semver - evaluated_versions=$(node $bp_dir/vendor/semver/bin/semver $available_versions -r "$requested_version" || echo "") + evaluated_versions=$(eval node $bp_dir/vendor/semver/bin/semver ${args} || echo "") # Use the latest of the evaluated versions - node_version=$("$evaluated_versions" | tail -n 1) + node_version=$(echo $evaluated_versions | tail -n 1) if test $node_version; then status "Using node version $node_version" download_and_install_node $node_version else - error "Requested node version ${requested_version} not found among available versions: ${available_versions}" + error "Requested node version ${requested_version} not found among available versions on nodejs.org/dist" fi fi fi @@ -58,16 +62,18 @@ cache_store_dir="$cache_dir/node_modules/$node_version" cache_target_dir="$build_dir/node_modules" # Restore node_modules from cache, if present -if [ -d $cache_store_dir ]; then - status "Restoring node_modules cache" - if [ -d $cache_target_dir ]; then - cp -r $cache_store_dir/* $cache_target_dir/ - else - cp -r $cache_store_dir $cache_target_dir - fi - status "Pruning any unused dependencies" - npm prune -fi +# if [ -d $cache_store_dir ]; then +# status "Restoring node_modules cache" +# if [ -d $cache_target_dir ]; then +# cp -r $cache_store_dir/* $cache_target_dir/ +# else +# cp -r $cache_store_dir $cache_target_dir +# fi +# status "Pruning any unused dependencies" +# npm prune +# fi + +cd $build_dir # Install dependencies status "Installing dependencies" @@ -75,12 +81,12 @@ npm install --production npm rebuild # Cache node_modules for future builds -if [ -d $cache_target_dir ]; then - status "Caching node_modules for future builds" - rm -rf $cache_store_dir - mkdir -p $(dirname $cache_store_dir) - cp -r $cache_target_dir $cache_store_dir -fi +# if [ -d $cache_target_dir ]; then +# status "Caching node_modules for future builds" +# rm -rf $cache_store_dir +# mkdir -p $(dirname $cache_store_dir) +# cp -r $cache_target_dir $cache_store_dir +# fi # Update the PATH status "Building runtime environment" diff --git a/bin/test b/bin/test index 1f85d9c4f..c3ae92f24 100755 --- a/bin/test +++ b/bin/test @@ -8,48 +8,54 @@ # run the tests # -testDetectWithPackageJson() { - detect "package-json-version" - assertCaptured "Node.js" - assertCapturedSuccess -} +# testDetectWithPackageJson() { +# detect "package-json-stable-version" +# assertCaptured "Node.js" +# assertCapturedSuccess +# } -testDetectWithoutPackageJson() { - detect "no-package-json" - assertCapturedError 1 "" -} - -testPackageJsonWithVersion() {s - compile "package-json-version" - assertNotCaptured "WARNING: No node version specified" - assertCaptured "Using node 0.10" - assertCapturedSuccess -} +# testDetectWithoutPackageJson() { +# detect "no-package-json" +# assertCapturedError 1 "" +# } testPackageJsonWithoutVersion() { compile "package-json-noversion" assertCaptured "WARNING: No node version specified" - assertCaptured "Using node 0.10" + assertCaptured "Using latest stable node" assertCapturedSuccess } -testPackageJsonWithInvalidVersion() { - compile "package-json-invalidversion" - assertCapturedError 1 "Requested node version 5.0 not found" +testPackageJsonWithStableVersion() { + compile "package-json-stable-version" + assertNotCaptured "WARNING: No node version specified" + assertCaptured "Using latest stable node" + assertCapturedSuccess } -testProfileCreated() { - compile "package-json-version" - assertCaptured "Building runtime environment" - assertFile "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" +testPackageJsonWithUnstableVersion() { + compile "package-json-unstable-version" + assertCaptured "Using node version 0.11" assertCapturedSuccess } -testNodeModulesCached() { - cache=$(mktmpdir) - compile "node-modules-caching" $cache - assertEquals "1" "$(ls -1 $cache/node_modules/0.10.17 | wc -l)" -} +# testPackageJsonWithInvalidVersion() { +# compile "package-json-invalidversion" +# assertCapturedError 1 "Requested node version 5.0 not found" +# } + +# testProfileCreated() { +# compile "package-json-version" +# assertCaptured "Building runtime environment" +# assertFile "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" +# assertCapturedSuccess +# } + +# testNodeModulesCached() { +# cache=$(mktmpdir) +# compile "node-modules-caching" $cache +# assertEquals "1" "$(ls -1 $cache/node_modules/0.10.17 | wc -l)" +# } ## utils ######################################## From b8a6635fed18c478982ba5dc0703ed0c61ae2384 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 17:25:13 -0700 Subject: [PATCH 046/116] testProfileCreated --- bin/test | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/test b/bin/test index c3ae92f24..b9ae42f7e 100755 --- a/bin/test +++ b/bin/test @@ -44,12 +44,12 @@ testPackageJsonWithUnstableVersion() { # assertCapturedError 1 "Requested node version 5.0 not found" # } -# testProfileCreated() { -# compile "package-json-version" -# assertCaptured "Building runtime environment" -# assertFile "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" -# assertCapturedSuccess -# } +testProfileCreated() { + compile "package-json-stable-version" + assertCaptured "Building runtime environment" + assertFile "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" + assertCapturedSuccess +} # testNodeModulesCached() { # cache=$(mktmpdir) From ef57f219fa1b601710035ce6856b409f86ec2fbe Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Sep 2013 17:33:37 -0700 Subject: [PATCH 047/116] bring caching back --- bin/compile | 35 +++++++++++++------------- bin/test | 10 ++++---- test/node-modules-caching/package.json | 3 +-- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/bin/compile b/bin/compile index 4999d1493..f526cacbe 100755 --- a/bin/compile +++ b/bin/compile @@ -62,31 +62,30 @@ cache_store_dir="$cache_dir/node_modules/$node_version" cache_target_dir="$build_dir/node_modules" # Restore node_modules from cache, if present -# if [ -d $cache_store_dir ]; then -# status "Restoring node_modules cache" -# if [ -d $cache_target_dir ]; then -# cp -r $cache_store_dir/* $cache_target_dir/ -# else -# cp -r $cache_store_dir $cache_target_dir -# fi -# status "Pruning any unused dependencies" -# npm prune -# fi - -cd $build_dir +if [ -d $cache_store_dir ]; then + status "Restoring node_modules cache" + if [ -d $cache_target_dir ]; then + cp -r $cache_store_dir/* $cache_target_dir/ + else + cp -r $cache_store_dir $cache_target_dir + fi + status "Pruning any unused dependencies" + npm prune +fi # Install dependencies status "Installing dependencies" +cd $build_dir npm install --production npm rebuild # Cache node_modules for future builds -# if [ -d $cache_target_dir ]; then -# status "Caching node_modules for future builds" -# rm -rf $cache_store_dir -# mkdir -p $(dirname $cache_store_dir) -# cp -r $cache_target_dir $cache_store_dir -# fi +if [ -d $cache_target_dir ]; then + status "Caching node_modules for future builds" + rm -rf $cache_store_dir + mkdir -p $(dirname $cache_store_dir) + cp -r $cache_target_dir $cache_store_dir +fi # Update the PATH status "Building runtime environment" diff --git a/bin/test b/bin/test index b9ae42f7e..acd14e018 100755 --- a/bin/test +++ b/bin/test @@ -51,11 +51,11 @@ testProfileCreated() { assertCapturedSuccess } -# testNodeModulesCached() { -# cache=$(mktmpdir) -# compile "node-modules-caching" $cache -# assertEquals "1" "$(ls -1 $cache/node_modules/0.10.17 | wc -l)" -# } +testNodeModulesCached() { + cache=$(mktmpdir) + compile "node-modules-caching" $cache + assertEquals "1" "$(ls -1 $cache/node_modules/0.10.18 | wc -l)" +} ## utils ######################################## diff --git a/test/node-modules-caching/package.json b/test/node-modules-caching/package.json index 464d272ae..55c1d53d1 100644 --- a/test/node-modules-caching/package.json +++ b/test/node-modules-caching/package.json @@ -7,8 +7,7 @@ "url" : "http://github.com/example/example.git" }, "engines": { - "node": "0.10.15", - "npm": "1.3.5" + "node": "0.10.18" }, "dependencies": { "express": "latest" From c462dcd5333cd6535a2f92a6a252f4495c371d1d Mon Sep 17 00:00:00 2001 From: David Dollar Date: Tue, 10 Sep 2013 16:15:45 -0400 Subject: [PATCH 048/116] newline at end of files --- bin/compile | 2 +- bin/test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index f526cacbe..0915b5d14 100755 --- a/bin/compile +++ b/bin/compile @@ -90,4 +90,4 @@ fi # Update the PATH status "Building runtime environment" mkdir -p $build_dir/.profile.d -echo "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh \ No newline at end of file +echo "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh diff --git a/bin/test b/bin/test index acd14e018..7f569cde1 100755 --- a/bin/test +++ b/bin/test @@ -88,4 +88,4 @@ assertFile() { assertEquals "$1" "$(cat ${COMPILE_DIR}/$2)" } -source ${BASE}/vendor/shunit2/shunit2 \ No newline at end of file +source ${BASE}/vendor/shunit2/shunit2 From d4e8ad929fbc118412f31bbe820bc6321e8cc1ed Mon Sep 17 00:00:00 2001 From: David Dollar Date: Tue, 10 Sep 2013 16:16:03 -0400 Subject: [PATCH 049/116] make sure node_version is set, test for caching output --- bin/compile | 1 + bin/test | 1 + 2 files changed, 2 insertions(+) diff --git a/bin/compile b/bin/compile index 0915b5d14..0aebe74be 100755 --- a/bin/compile +++ b/bin/compile @@ -35,6 +35,7 @@ else if test $default_satisfies; then status "Using latest stable node version $stable_version" + node_version=$stable_version else # Find all available versions from nodejs.org/dist args="" diff --git a/bin/test b/bin/test index 7f569cde1..6b847789d 100755 --- a/bin/test +++ b/bin/test @@ -54,6 +54,7 @@ testProfileCreated() { testNodeModulesCached() { cache=$(mktmpdir) compile "node-modules-caching" $cache + assertCaptured "Caching node_modules for future builds" assertEquals "1" "$(ls -1 $cache/node_modules/0.10.18 | wc -l)" } From 3ed926d597d9d09faeb00146fc7b95b2aa83e61a Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 10 Sep 2013 14:42:15 -0700 Subject: [PATCH 050/116] add info to readme about buildpack changes --- README.md | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fcb7424f5..4b8ca3665 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,38 @@ Heroku Buildpack for Node.js ============================ -This is a [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) for Node.js apps. +This is a [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) for Node.js apps. It will detect your app as Node.js if it has a `package.json` file in the root. It uses npm to install your dependencies, and vendors a version of the Node.js runtime into your slug. -The buildpack will detect your app as Node.js if it has a `package.json` file in the root. It will use npm to install your dependencies, and vendors a version of the Node.js runtime into your slug. +If you specify a version of node in the [`engines` field of your package.json](https://npmjs.org/doc/json.html#engines), the buildpack will attempt to find the specified version on [nodejs.org/dist](http://nodejs.org/dist/) and download it from our S3 caching proxy. + +If you don't specify a version of node, the latest stable version will be used. + +About this Refactor +------------------- + +This branch of the buildpack is intended to replace the official buildpack once it has been vetted by some users. Here's a summary of the differences between the current official buildpack and this _diet_ version: + +The old buildpack: + +- Contains a lot of code for compiling node and npm binaries and moving them to S3. This code is orthogonal to the core function of the buildpack, and is only used internally by Node maintainers at Heroku. +- Downloads and compiles node and npm separately. +- Requires manual intervention each time a new version of node or npm is released. +- Does not support pre-release versions of node. +- Uses SCONS to support really old versions of node and npm. +- Maintains S3 manifests of our hand-compiled versions of node and npm. +- Does not cache anything. + +The new buildpack: + +- Uses the latest stable version of node and npm by default. +- Allows any recent version of node to be used, including pre-release versions, as soon as they become available on [nodejs.org/dist](http://nodejs.org/dist/). +- Uses the version of npm that comes bundled with node instead of downloading and compiling them separately. npm has been bundled with node since [v0.6.3 (Nov 2011)](http://blog.nodejs.org/2011/11/25/node-v0-6-3/). This effectively means that node versions `<0.6.3` are no longer supported, and that the `engines.npm` field in package.json is now ignored. +- Makes use of an s3 caching proxy of nodejs.org for faster downloads of the node binaries. +- Makes fewer HTTP requests when resolving node versions. +- Uses an updated version of [node-semver](https://github.com/isaacs/node-semver) for dependency resolution. +- No longer depends on SCONS. +- Caches the `node_modules` directory across builds. +- Runs `npm prune` after restoring cached modules, to ensure that any modules formerly used by your app aren't needlessly installed and/or compiled. Documentation ------------- @@ -31,4 +60,4 @@ heroku config:set BUILDPACK_URL= heroku config:set BUILDPACK_URL=#your-branch ``` -For more detailed information about testing buildpacks, see [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file +For more detailed information about testing buildpacks, see [CONTRIBUTING.md](CONTRIBUTING.md) From 51a28bdb20f0590dd4977a499ba0b8e1f2c69d3f Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 10 Sep 2013 14:51:59 -0700 Subject: [PATCH 051/116] add diet usage instructions --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b8ca3665..a6ba1cae6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,15 @@ If you don't specify a version of node, the latest stable version will be used. About this Refactor ------------------- -This branch of the buildpack is intended to replace the official buildpack once it has been vetted by some users. Here's a summary of the differences between the current official buildpack and this _diet_ version: +This branch of the buildpack is intended to replace the [official Node.js buildpack](https://github.com/heroku/heroku-buildpack-nodejs#readme) once it has been tested by some users. To use this buildpack for your node app, simply change your BUILDPACK_URL [config var](https://devcenter.heroku.com/articles/config-vars) and push your app to heroku. + +``` +heroku config:set BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-nodejs#diet -a my-node-app +git commit -am "fakeout" --allow-empty +git push heroku +``` + +Here's a summary of the differences between the current official buildpack and this _diet_ version: The old buildpack: From bb5f09e840a562125d9ac1644948d889a8fd6c15 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 12:19:50 -0700 Subject: [PATCH 052/116] remove some comments, add some comments --- bin/common.sh | 24 ++++++++---------------- bin/compile | 12 ++++++------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/bin/common.sh b/bin/common.sh index dd4f84e3b..7a1289b01 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -8,13 +8,10 @@ set -e download_and_install_node() { version="$1" - - status "Downloading node $version" + status "Downloading and installing node v$version" node_url="http://s3pository.heroku.com/node/v$version/node-v$version-linux-x64.tar.gz" curl $node_url -s -o - | tar xzf - -C $build_dir mv $build_dir/node-v$version-linux-x64 $build_dir/node - - status "Adding node and npm to \$PATH" chmod +x $build_dir/node/bin/* PATH=$PATH:$build_dir/node/bin } @@ -44,18 +41,15 @@ error() { exit 1 } -function status() { +status() { echo "-----> $*" } -function mktmpdir() { - dir=$(mktemp -t node-$1-XXXX) - rm -rf $dir - mkdir -p $dir - echo $dir -} -function indent() { +# sed -l basically makes sed replace and buffer through stdin to stdout +# so you get updates while the command runs and dont wait for the end +# e.g. npm install | indent +indent() { c='s/^/ /' case $(uname) in Darwin) sed -l "$c";; @@ -64,7 +58,5 @@ function indent() { } function cat_npm_debug_log() { - if [ -f $BUILD_DIR/npm-debug.log ]; then - cat $BUILD_DIR/npm-debug.log - fi -} \ No newline at end of file + [ -f $build_dir/npm-debug.log ] && cat $build_dir/npm-debug.log +} diff --git a/bin/compile b/bin/compile index 0aebe74be..549a7b64c 100755 --- a/bin/compile +++ b/bin/compile @@ -8,9 +8,10 @@ stable_version="0.10.18" source $bp_dir/bin/common.sh # Output debug info on exit -# trap cat_npm_debug_log EXIT +trap cat_npm_debug_log EXIT -status "Downloading node version $stable_version" +# Bootstrap the build process with latest stable version of node +# We'll use it to parse package.json and do semver detection download_and_install_node $stable_version # Is a node version specified in package.json? @@ -20,21 +21,21 @@ determine_requested_version() { } requested_version=$(determine_requested_version) -# Give a warning if no node engine is specified +# Give a warning if engines.node is unspecified if ! test $requested_version; then node_version=$stable_version echo echo "WARNING: No node version specified in package.json, see:" | indent echo "https://devcenter.heroku.com/articles/nodejs-support#versions" | indent echo - status "Using latest stable node version $stable_version" + status "Defaulting to node v$stable_version (latest stable)" else # Does the already-downloaded stable version of node satisfy the requested version? default_satisfies=$(node $bp_dir/vendor/semver/bin/semver -v "${stable_version}" -r "${requested_version}" || echo "") if test $default_satisfies; then - status "Using latest stable node version $stable_version" + status "Using node v$stable_version" node_version=$stable_version else # Find all available versions from nodejs.org/dist @@ -50,7 +51,6 @@ else node_version=$(echo $evaluated_versions | tail -n 1) if test $node_version; then - status "Using node version $node_version" download_and_install_node $node_version else error "Requested node version ${requested_version} not found among available versions on nodejs.org/dist" From 66fa291ec3ef86cb77bda9b4c48a2904377b45ea Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 12:21:46 -0700 Subject: [PATCH 053/116] Simplify the anvil docs --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26def27f1..ea8874041 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ The [heroku-anvil CLI plugin](https://github.com/ddollar/heroku-anvil) is a wrap heroku plugins:install https://github.com/ddollar/heroku-anvil ``` -The [ddollar/test buildpack](https://github.com/ddollar/buildpack-test) is for testing things: it runs `bin/test` on your app. +The [ddollar/test](https://github.com/ddollar/buildpack-test) buildpack runs `bin/test` on your app/buildpack. ``` heroku build -b ddollar/test # -b can also point to a local directory @@ -71,4 +71,4 @@ heroku buildpacks:publish heroku/nodejs ## Keeping up with the Nodeses - Run `npm info npm version` to find out the latest available version of npm. -- Follow [@nodejs](https://twitter.com/nodejs) and [@npmjs](https://twitter.com/npmjs) on Twitter. \ No newline at end of file +- Follow [@nodejs](https://twitter.com/nodejs) and [@npmjs](https://twitter.com/npmjs) on Twitter. From 45f6360d542c99f64dcab22073734f5e1816111f Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 12:23:05 -0700 Subject: [PATCH 054/116] don't need these vulcan build instructions any more! --- CONTRIBUTING.md | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea8874041..46245b42e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,42 +18,6 @@ The [ddollar/test](https://github.com/ddollar/buildpack-test) buildpack runs `bi heroku build -b ddollar/test # -b can also point to a local directory ``` -## Compiling new versions of node and npm using Vulcan - -Install [vulcan](https://github.com/heroku/vulcan) and create your own build server. Use any -app name you want and vulcan will remember it in a `~/.vulcan` config file. - -``` -gem install vulcan -vulcan create builder-bob -``` - -Store your S3 credentials in `~/.aws/` - -``` -mkdir -p ~/.aws -echo 'YOUR_AWS_KEY' > ~/.aws/key-nodejs.access -echo 'YOUR_AWS_SECRET' > ~/.aws/key-nodejs.secret -``` - -Add a credentials exporter to your `.bash_profile` or `.bashrc` - -``` -setup_nodejs_env () { - export AWS_ID=$(cat ~/.aws/key-nodejs.access) - export AWS_SECRET=$(cat ~/.aws/key-nodejs.secret) - export S3_BUCKET="heroku-buildpack-nodejs" -} -``` - -Build: - -``` -setup_nodejs_env -support/package_nodejs -support/package_npm -``` - ## Publishing buildpack updates ``` @@ -72,3 +36,4 @@ heroku buildpacks:publish heroku/nodejs - Run `npm info npm version` to find out the latest available version of npm. - Follow [@nodejs](https://twitter.com/nodejs) and [@npmjs](https://twitter.com/npmjs) on Twitter. +- Find node-npm version pairings at [nodejs.org/dist/npm-versions.txt](http://nodejs.org/dist/npm-versions.txt) From 58a473e0acc0ffe04df664f2554b318be4961bb5 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 13:41:52 -0700 Subject: [PATCH 055/116] line up the strings and get the tests passing again --- bin/common.sh | 4 ++-- bin/compile | 9 +++++---- bin/test | 32 ++++++++++++++++---------------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/bin/common.sh b/bin/common.sh index 7a1289b01..ef026342e 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -52,8 +52,8 @@ status() { indent() { c='s/^/ /' case $(uname) in - Darwin) sed -l "$c";; - *) sed -u "$c";; + Darwin) sed -l "$c";; # mac/bsd sed: -l buffers on line boundaries + *) sed -u "$c";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data esac } diff --git a/bin/compile b/bin/compile index 549a7b64c..f4dbba711 100755 --- a/bin/compile +++ b/bin/compile @@ -8,7 +8,7 @@ stable_version="0.10.18" source $bp_dir/bin/common.sh # Output debug info on exit -trap cat_npm_debug_log EXIT +# trap cat_npm_debug_log EXIT # Bootstrap the build process with latest stable version of node # We'll use it to parse package.json and do semver detection @@ -16,6 +16,7 @@ download_and_install_node $stable_version # Is a node version specified in package.json? determine_requested_version() { + # https://github.com/trentm/json v=$(cat $build_dir/package.json | node $bp_dir/vendor/json engines.node 2>/dev/null) if [ $? == 0 ]; then echo $v; fi } @@ -28,14 +29,14 @@ if ! test $requested_version; then echo "WARNING: No node version specified in package.json, see:" | indent echo "https://devcenter.heroku.com/articles/nodejs-support#versions" | indent echo - status "Defaulting to node v$stable_version (latest stable)" + status "Defaulting to latest stable node, v$stable_version" else # Does the already-downloaded stable version of node satisfy the requested version? default_satisfies=$(node $bp_dir/vendor/semver/bin/semver -v "${stable_version}" -r "${requested_version}" || echo "") if test $default_satisfies; then - status "Using node v$stable_version" + status "Latest stable node v$stable_version satisfies engines.node: $requested" node_version=$stable_version else # Find all available versions from nodejs.org/dist @@ -53,7 +54,7 @@ else if test $node_version; then download_and_install_node $node_version else - error "Requested node version ${requested_version} not found among available versions on nodejs.org/dist" + error "node ${requested_version} not found among available versions on nodejs.org/dist" fi fi fi diff --git a/bin/test b/bin/test index 6b847789d..e061f8e29 100755 --- a/bin/test +++ b/bin/test @@ -8,41 +8,41 @@ # run the tests # -# testDetectWithPackageJson() { -# detect "package-json-stable-version" -# assertCaptured "Node.js" -# assertCapturedSuccess -# } +testDetectWithPackageJson() { + detect "package-json-stable-version" + assertCaptured "Node.js" + assertCapturedSuccess +} -# testDetectWithoutPackageJson() { -# detect "no-package-json" -# assertCapturedError 1 "" -# } +testDetectWithoutPackageJson() { + detect "no-package-json" + assertCapturedError 1 "" +} testPackageJsonWithoutVersion() { compile "package-json-noversion" assertCaptured "WARNING: No node version specified" - assertCaptured "Using latest stable node" + assertCaptured "Defaulting to latest stable node" assertCapturedSuccess } testPackageJsonWithStableVersion() { compile "package-json-stable-version" assertNotCaptured "WARNING: No node version specified" - assertCaptured "Using latest stable node" + assertCaptured "satisfies engines.node" assertCapturedSuccess } testPackageJsonWithUnstableVersion() { compile "package-json-unstable-version" - assertCaptured "Using node version 0.11" + assertCaptured "Downloading and installing node v0.11" assertCapturedSuccess } -# testPackageJsonWithInvalidVersion() { -# compile "package-json-invalidversion" -# assertCapturedError 1 "Requested node version 5.0 not found" -# } +testPackageJsonWithInvalidVersion() { + compile "package-json-invalidversion" + assertCapturedError 1 "not found among available versions" +} testProfileCreated() { compile "package-json-stable-version" From 286aa21c3b0f190426a8c11aa60e79c8edf0ede4 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 13:42:03 -0700 Subject: [PATCH 056/116] update the json parser --- vendor/json | 1160 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 998 insertions(+), 162 deletions(-) diff --git a/vendor/json b/vendor/json index 0eb8ed47f..9ca8f080d 100755 --- a/vendor/json +++ b/vendor/json @@ -1,16 +1,20 @@ #!/usr/bin/env node // -// json -- pipe in your JSON for nicer output and for extracting data bits +// json -- a 'json' command for massaging JSON on the command line // // See . // -var VERSION = "2.0.4"; +var VERSION = "6.0.1"; +var p = console.warn; var util = require('util'); -var pathlib = require('path'); -var runInNewContext = require('vm').runInNewContext; +var assert = require('assert'); +var path = require('path'); +var vm = require('vm'); +var fs = require('fs'); var warn = console.warn; +var EventEmitter = require('events').EventEmitter; @@ -22,7 +26,7 @@ exports.parseLookup = parseLookup; // As an exported API, these are still experimental: exports.lookupDatum = lookupDatum; -exports.printDatum = printDatum; +exports.printDatum = printDatum; // DEPRECATED @@ -48,59 +52,182 @@ function getVersion() { return VERSION; } -function isArray(ar) { - return ar instanceof Array || - Array.isArray(ar) || - (ar && ar !== Object.prototype && isArray(ar.__proto__)); +/** + * Return a *shallow* copy of the given object. + * + * Only support objects that you get out of JSON, i.e. no functions. + */ +function objCopy(obj) { + var copy; + if (Array.isArray(obj)) { + copy = obj.slice(); + } else if (typeof(obj) === 'object') { + copy = {}; + Object.keys(obj).forEach(function (k) { + copy[k] = obj[k]; + }); + } else { + copy = obj; // immutable type + } + return copy; +} + +if (util.format) { + format = util.format; +} else { + // From : + var formatRegExp = /%[sdj%]/g; + function format(f) { + if (typeof f !== 'string') { + var objects = []; + for (var i = 0; i < arguments.length; i++) { + objects.push(util.inspect(arguments[i])); + } + return objects.join(' '); + } + var i = 1; + var args = arguments; + var len = args.length; + var str = String(f).replace(formatRegExp, function(x) { + if (i >= len) return x; + switch (x) { + case '%s': return String(args[i++]); + case '%d': return Number(args[i++]); + case '%j': return JSON.stringify(args[i++]); + case '%%': return '%'; + default: + return x; + } + }); + for (var x = args[i]; i < len; x = args[++i]) { + if (x === null || typeof x !== 'object') { + str += ' ' + x; + } else { + str += ' ' + util.inspect(x); + } + } + return str; + }; +} + +/** + * Parse the given string into a JS string. Basically: handle escapes. + */ +function _parseString(s) { + var quoted = '"' + s.replace(/\\"/, '"').replace('"', '\\"') + '"'; + return eval(quoted); } +// json_parse.js () +// START json_parse +var json_parse=function(){"use strict";var a,b,c={'"':'"',"\\":"\\","/":"/",b:"\b",f:"\f",n:"\n",r:"\r",t:"\t"},d,e=function(b){throw{name:"SyntaxError",message:b,at:a,text:d}},f=function(c){return c&&c!==b&&e("Expected '"+c+"' instead of '"+b+"'"),b=d.charAt(a),a+=1,b},g=function(){var a,c="";b==="-"&&(c="-",f("-"));while(b>="0"&&b<="9")c+=b,f();if(b==="."){c+=".";while(f()&&b>="0"&&b<="9")c+=b}if(b==="e"||b==="E"){c+=b,f();if(b==="-"||b==="+")c+=b,f();while(b>="0"&&b<="9")c+=b,f()}a=+c;if(!isFinite(a))e("Bad number");else return a},h=function(){var a,d,g="",h;if(b==='"')while(f()){if(b==='"')return f(),g;if(b==="\\"){f();if(b==="u"){h=0;for(d=0;d<4;d+=1){a=parseInt(f(),16);if(!isFinite(a))break;h=h*16+a}g+=String.fromCharCode(h)}else if(typeof c[b]=="string")g+=c[b];else break}else g+=b}e("Bad string")},i=function(){while(b&&b<=" ")f()},j=function(){switch(b){case"t":return f("t"),f("r"),f("u"),f("e"),!0;case"f":return f("f"),f("a"),f("l"),f("s"),f("e"),!1;case"n":return f("n"),f("u"),f("l"),f("l"),null}e("Unexpected '"+b+"'")},k,l=function(){var a=[];if(b==="["){f("["),i();if(b==="]")return f("]"),a;while(b){a.push(k()),i();if(b==="]")return f("]"),a;f(","),i()}}e("Bad array")},m=function(){var a,c={};if(b==="{"){f("{"),i();if(b==="}")return f("}"),c;while(b){a=h(),i(),f(":"),Object.hasOwnProperty.call(c,a)&&e('Duplicate key "'+a+'"'),c[a]=k(),i();if(b==="}")return f("}"),c;f(","),i()}}e("Bad object")};return k=function(){i();switch(b){case"{":return m();case"[":return l();case'"':return h();case"-":return g();default:return b>="0"&&b<="9"?g():j()}},function(c,f){var g;return d=c,a=0,b=" ",g=k(),i(),b&&e("Syntax error"),typeof f=="function"?function h(a,b){var c,d,e=a[b];if(e&&typeof e=="object")for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=h(e,c),d!==undefined?e[c]=d:delete e[c]);return f.call(a,b,e)}({"":g},""):g}}(); +// END json_parse + function printHelp() { util.puts("Usage:"); util.puts(" | json [OPTIONS] [LOOKUPS...]"); + util.puts(" json -f FILE [OPTIONS] [LOOKUPS...]"); + util.puts(""); + util.puts("Pipe in your JSON for pretty-printing, JSON validation, filtering, "); + util.puts("and modification. Supply one or more `LOOKUPS` to extract a "); + util.puts("subset of the JSON. HTTP header blocks are skipped by default."); + util.puts("Roughly in order of processing, features are:"); + util.puts(""); + util.puts("Grouping:"); + util.puts(" Use '-g' or '--group' to group adjacent objects, separated by"); + util.puts(" by no space or a by a newline, or adjacent arrays, separate by"); + util.puts(" by a newline. This can be helpful for, e.g.: "); + util.puts(" $ cat *.json | json -g ... "); + util.puts(" and similar."); util.puts(""); - util.puts("Pipe in your JSON for nicer output or supply one or more `LOOKUPS`"); - util.puts("to extract a subset of the JSON. HTTP header blocks (as from `curl -i`)"); - util.puts("are skipped by default."); + util.puts("Execution:"); + util.puts(" Use the '-e CODE' option to execute JavaScript code on the input JSON."); + util.puts(" $ echo '{\"name\":\"trent\",\"age\":38}' | json -e 'age++'"); + util.puts(" {"); + util.puts(" \"name\": \"trent\","); + util.puts(" \"age\": 39"); + util.puts(" }"); + util.puts(" If input is an array, this will automatically process each"); + util.puts(" item separately."); util.puts(""); - util.puts("Auto-arrayification:"); - util.puts(" Adjacent objects or arrays are 'arrayified'. To attempt to avoid"); - util.puts(" false positives inside JSON strings, *adjacent* elements must"); - util.puts(" have either no whitespace separation or at least a newline"); - util.puts(" separation."); + util.puts("Conditional filtering:"); + util.puts(" Use the '-c CODE' option to filter the input JSON."); + util.puts(" $ echo '[{\"age\":38},{\"age\":4}]' | json -c 'age>21'"); + util.puts(" [{\"age\":38}]"); + util.puts(" If input is an array, this will automatically process each"); + util.puts(" item separately. Note: 'CODE' is JavaScript code."); util.puts(""); - util.puts("Examples:"); - util.puts(" # pretty printing"); - util.puts(" $ curl -s http://search.twitter.com/search.json?q=node.js | json"); + util.puts("Lookups:"); + util.puts(" Use lookup arguments to extract particular values:"); + util.puts(" $ echo '{\"name\":\"trent\",\"age\":38}' | json name"); + util.puts(" trent"); util.puts(""); - util.puts(" # lookup fields"); - util.puts(" $ curl -s http://search.twitter.com/search.json?q=node.js | json results[0]"); - util.puts(" {"); - util.puts(" \"created_at\": \"Tue, 08 Nov 2011 19:07:25 +0000\","); - util.puts(" \"from_user\": \"im2b\","); - util.puts(" ..."); + util.puts(" Use '-a' for *array processing* of lookups and *tabular output*:"); + util.puts(" $ echo '{\"name\":\"trent\",\"age\":38}' | json name age"); + util.puts(" trent"); + util.puts(" 38"); + util.puts(" $ echo '[{\"name\":\"trent\",\"age\":38},"); + util.puts(" {\"name\":\"ewan\",\"age\":4}]' | json -a name age"); + util.puts(" trent 38"); + util.puts(" ewan 4"); util.puts(""); - util.puts(" # array processing"); - util.puts(" $ curl -s http://search.twitter.com/search.json?q=node.js | json results \\"); - util.puts(" json -a from_user"); - util.puts(" im2b"); - util.puts(" myalltop_paul"); - util.puts(" ..."); + util.puts("In-place editing:"); + util.puts(" Use '-I, --in-place' to edit a file in place:"); + util.puts(" $ json -I -f config.json # reformat"); + util.puts(" $ json -I -f config.json -c 'this.logLevel=\"debug\"' # add field"); util.puts(""); - util.puts(" # auto-arrayification") - util.puts(" $ echo '{\"a\":1}{\"b\":2}' | json -o json-0"); - util.puts(" [{\"a\":1},{\"b\":2}]"); - util.puts(" $ echo '[1,2][3,4]' | json -o json-0"); - util.puts(" [{\"a\":1},{\"b\":2}]"); + util.puts("Pretty-printing:"); + util.puts(" Output is 'jsony' by default: 2-space indented JSON, except a"); + util.puts(" single string value is printed without quotes."); + util.puts(" $ echo '{\"name\": \"trent\", \"age\": 38}' | json"); + util.puts(" {"); + util.puts(" \"name\": \"trent\","); + util.puts(" \"age\": 38"); + util.puts(" }"); + util.puts(" $ echo '{\"name\": \"trent\", \"age\": 38}' | json name"); + util.puts(" trent"); + util.puts(""); + util.puts(" Use '-j' or '-o json' for explicit JSON, '-o json-N' for N-space indent:"); + util.puts(" $ echo '{\"name\": \"trent\", \"age\": 38}' | json -o json-0"); + util.puts(" {\"name\":\"trent\",\"age\":38}"); util.puts(""); util.puts("Options:"); - util.puts(" -h, --help print this help info and exit"); - util.puts(" --version print version of this command and exit"); - util.puts(" -q, --quiet don't warn if input isn't valid JSON"); + util.puts(" -h, --help Print this help info and exit."); + util.puts(" --version Print version of this command and exit."); + util.puts(" -q, --quiet Don't warn if input isn't valid JSON."); + util.puts(""); + util.puts(" -f FILE Path to a file to process. If not given, then"); + util.puts(" stdin is used."); + util.puts(" -I, --in-place In-place edit of the file given with '-f'."); + util.puts(" Lookups are not allow with in-place editing"); + util.puts(" because it makes it too easy to lose content."); + util.puts(""); + util.puts(" -H Drop any HTTP header block (as from `curl -i ...`)."); + util.puts(" -g, --group Group adjacent objects or arrays into an array."); + util.puts(" --merge Merge adjacent objects into one. Keys in last "); + util.puts(" object win."); + util.puts(" --deep-merge Same as '--merge', but will recurse into objects "); + util.puts(" under the same key in both.") + util.puts(" -a, --array Process input as an array of separate inputs"); + util.puts(" and output in tabular form."); + util.puts(" -A Process input as a single object, i.e. stop"); + util.puts(" '-e' and '-c' automatically processing each"); + util.puts(" item of an input array."); + util.puts(" -d DELIM Delimiter char for tabular output (default is ' ')."); + util.puts(" -D DELIM Delimiter char between lookups (default is '.'). E.g.:"); + util.puts(" $ echo '{\"a.b\": {\"b\": 1}}' | json -D / a.b/b"); + util.puts(""); + util.puts(" -e CODE Execute the given JavaScript code on the input. If input"); + util.puts(" is an array, then each item of the array is processed"); + util.puts(" separately (use '-A' to override)."); + util.puts(" -c CODE Filter the input with JavaScript `CODE`. If `CODE`"); + util.puts(" returns false-y, then the item is filtered out. If"); + util.puts(" input is an array, then each item of the array is "); + util.puts(" processed separately (use '-A' to override)."); util.puts(""); - util.puts(" -H drop any HTTP header block (as from `curl -i ...`)"); - util.puts(" -a, --array process input as an array of separate inputs"); - util.puts(" and output in tabular form"); - util.puts(" -d DELIM delimiter string for tabular output (default is ' ')"); + util.puts(" -k, --keys Output the input object's keys."); + util.puts(" -n, --validate Just validate the input (no processing or output)."); + util.puts(" Use with '-q' for silent validation (exit status)."); util.puts(""); util.puts(" -o, --output MODE Specify an output mode. One of"); util.puts(" jsony (default): JSON with string quotes elided"); @@ -110,7 +237,8 @@ function printHelp() { util.puts(" -i shortcut for `-o inspect`"); util.puts(" -j shortcut for `-o json`"); util.puts(""); - util.puts("See for more complete docs."); + util.puts("See for more docs and "); + util.puts(" for project details."); } @@ -132,16 +260,30 @@ function parseArgv(argv) { help: false, quiet: false, dropHeaders: false, + exeSnippets: [], + condSnippets: [], outputMode: OM_JSONY, jsonIndent: 2, - delim: ' ' + array: null, + delim: ' ', + lookupDelim: '.', + outputKeys: false, + group: false, + merge: null, // --merge -> "shallow", --deep-merge -> "deep" + inputFiles: [], + validate: false, + inPlace: false }; // Turn '-iH' into '-i -H', except for argument-accepting options. var args = argv.slice(2); // drop ['node', 'scriptname'] var newArgs = []; - var optTakesArg = {'d': true, 'o': true}; + var optTakesArg = {'d': true, 'o': true, 'D': true}; for (var i = 0; i < args.length; i++) { + if (args[i] === '--') { + newArgs = newArgs.concat(args.slice(i)); + break; + } if (args[i].charAt(0) === "-" && args[i].charAt(1) !== '-' && args[i].length > 2) { var splitOpts = args[i].slice(1).split(""); for (var j = 0; j < splitOpts.length; j++) { @@ -184,6 +326,9 @@ function parseArgv(argv) { case "-o": case "--output": var name = args.shift(); + if (!name) { + throw new Error("no argument given for '-o|--output' option"); + } var idx = name.lastIndexOf('-'); if (idx !== -1) { var indent = Number(name.slice(idx+1)); @@ -197,6 +342,10 @@ function parseArgv(argv) { throw new Error("unknown output mode: '"+name+"'"); } break; + case "-I": + case "--in-place": + parsed.inPlace = true; + break; case "-i": // output with util.inspect parsed.outputMode = OM_INSPECT; break; @@ -207,8 +356,46 @@ function parseArgv(argv) { case "--array": parsed.array = true; break; + case "-A": + parsed.array = false; + break; case "-d": - parsed.delim = args.shift(); + parsed.delim = _parseString(args.shift()); + break; + case "-D": + parsed.lookupDelim = args.shift(); + if (parsed.lookupDelim.length !== 1) { + throw new Error(format( + "invalid lookup delim '%s' (must be a single char)", + parsed.lookupDelim)); + } + break; + case "-e": + parsed.exeSnippets.push(args.shift()); + break; + case "-c": + parsed.condSnippets.push(args.shift()); + break; + case "-k": + case "--keys": + parsed.outputKeys = true; + break; + case "-g": + case "--group": + parsed.group = true; + break; + case "--merge": + parsed.merge = "shallow"; + break; + case "--deep-merge": + parsed.merge = "deep"; + break; + case "-f": + parsed.inputFiles.push(args.shift()); + break; + case "-n": + case "--validate": + parsed.validate = true; break; default: // arguments if (!endOfOptions && arg.length > 0 && arg[0] === '-') { @@ -218,13 +405,281 @@ function parseArgv(argv) { break; } } - //TODO: '--' handling and error on a first arg that looks like an option. + + if (parsed.group && parsed.merge) { + throw new Error("cannot use -g|--group and --merge options together"); + } + if (parsed.outputKeys && parsed.args.length > 0) { + throw new Error("cannot use -k|--keys option and lookup arguments together"); + } + if (parsed.inPlace && parsed.inputFiles.length !== 1) { + throw new Error("must specify exactly one file with '-f FILE' to " + + "use -I/--in-place"); + } + if (parsed.inPlace && parsed.args.length > 0) { + throw new Error("lookups cannot be specified with in-place editing " + + "(-I/--in-place), too easy to lose content"); + } return parsed; } +/** + * Streams chunks from given file paths or stdin. + * + * @param opts {Object} Parsed options. + * @returns {Object} An emitter that emits 'chunk', 'error', and 'end'. + * - `emit('chunk', chunk, [obj])` where chunk is a complete block of JSON + * ready to parse. If `obj` is provided, it is the already parsed + * JSON. + * - `emit('error', error)` when an underlying stream emits an error + * - `emit('end')` when all streams are done + */ +function chunkEmitter(opts) { + var emitter = new EventEmitter(); + var streaming = true; + var chunks = []; + var leftover = ''; + var finishedHeaders = false; + + function stripHeaders(s) { + // Take off a leading HTTP header if any and pass it through. + while (true) { + if (s.slice(0,5) === "HTTP/") { + var index = s.indexOf('\r\n\r\n'); + var sepLen = 4; + if (index == -1) { + index = s.indexOf('\n\n'); + sepLen = 2; + } + if (index != -1) { + if (! opts.dropHeaders) { + emit(s.slice(0, index+sepLen)); + } + var is100Continue = (s.slice(0, 21) === "HTTP/1.1 100 Continue"); + s = s.slice(index+sepLen); + if (is100Continue) { + continue; + } + finishedHeaders = true; + } + } else { + finishedHeaders = true; + } + break; + } + //console.warn("stripHeaders done, finishedHeaders=%s", finishedHeaders) + return s; + } + + function emitChunks(block, emitter) { + //console.warn("emitChunks start: block='%s'", block) + var splitter = /(})(\s*\n\s*)?({\s*")/; + var leftTrimmedBlock = block.trimLeft(); + if (leftTrimmedBlock && leftTrimmedBlock[0] !== '{') { + // Currently (at least), only support streaming consecutive *objects*. + streaming = false; + chunks.push(block); + return ''; + } + /* Example: + * > '{"a":"b"}\n{"a":"b"}\n{"a":"b"}'.split(/(})(\s*\n\s*)?({\s*")/) + * [ '{"a":"b"', + * '}', + * '\n', + * '{"', + * 'a":"b"', + * '}', + * '\n', + * '{"', + * 'a":"b"}' ] + */ + var bits = block.split(splitter); + //console.warn("emitChunks: bits (length %d): %j", bits.length, bits); + if (bits.length === 1) { + /* + * An unwanted side-effect of using a regex to find newline-separated + * objects *with a regex*, is that we are looking for the end of one + * object leading into the start of a another. That means that we + * can end up buffering a complete object until a subsequent one + * comes in. If the input stream has large delays between objects, then + * this is unwanted buffering. + * + * One solution would be full stream parsing of objects a la + * . This would nicely also + * remove the artibrary requirement that the input stream be newline + * separated. jsonparse apparently has some issues tho, so I don't + * want to use it right now. It also isn't *small* so not sure I + * want to inline it (`json` doesn't have external deps). + * + * An alternative: The block we have so far one of: + * 1. some JSON that we don't support grouping (e.g. a stream of + * non-objects), + * 2. a JSON object fragment, or + * 3. a complete JSON object (with a possible trailing '{') + * + * If #3, then we can just emit this as a chunk right now. + * + * TODO(PERF): Try out avoiding the first more complete regex split + * for a presumed common case of single-line newline-separated JSON + * objects (e.g. a bunyan log). + */ + // An object must end with '}'. This is an early out to avoid + // `JSON.parse` which I *presuming* is slower. + var trimmed = block.split(/\s*\r?\n/)[0]; + //console.warn("XXX trimmed: '%s'", trimmed); + if (trimmed[trimmed.length - 1] === '}') { + var obj; + try { + obj = JSON.parse(block); + } catch (e) { + /* pass through */ + } + if (obj !== undefined) { + // Emit the parsed `obj` to avoid re-parsing it later. + emitter.emit('chunk', block, obj); + block = ''; + } + } + return block; + } else { + var n = bits.length - 2; + emitter.emit('chunk', bits[0] + bits[1]); + for (var i = 3; i < n; i += 4) { + emitter.emit('chunk', bits[i] + bits[i+1] + bits[i+2]); + } + return bits[n] + bits[n+1]; + } + } + + function addDataListener(stream) { + stream.on('data', function (chunk) { + var s = leftover + chunk; + if (!finishedHeaders) { + s = stripHeaders(s); + } + if (!finishedHeaders) { + leftover = s; + } else { + if (!streaming) { + chunks.push(chunk); + return; + } + leftover = emitChunks(s, emitter); + //console.warn("XXX leftover: '%s'", leftover) + } + }); + } + + if (opts.inputFiles.length > 0) { + // Stream each file in order. + var i = 0; + function addErrorListener(file) { + file.on('error', function (err) { + emitter.emit( + 'error', + format('could not read "%s": %s', opts.inputFiles[i], e) + ); + }); + } + function addEndListener(file) { + file.on('end', function () { + if (i < opts.inputFiles.length) { + var next = opts.inputFiles[i++]; + var nextFile = fs.createReadStream(next, {encoding: 'utf8'}); + addErrorListener(nextFile); + addEndListener(nextFile); + addDataListener(nextFile); + } else { + if (!streaming) { + emitter.emit('chunk', chunks.join('')); + } else if (leftover) { + leftover = emitChunks(leftover, emitter); + emitter.emit('chunk', leftover); + } + emitter.emit('end'); + } + }); + } + var first = fs.createReadStream(opts.inputFiles[i++], {encoding: 'utf8'}); + addErrorListener(first); + addEndListener(first); + addDataListener(first); + } else { + // Streaming from stdin. + var stdin = process.openStdin(); + stdin.setEncoding('utf8'); + addDataListener(stdin); + stdin.on('end', function () { + if (!streaming) { + emitter.emit('chunk', chunks.join('')); + } else if (leftover) { + leftover = emitChunks(leftover, emitter); + emitter.emit('chunk', leftover); + } + emitter.emit('end'); + }); + } + return emitter; +} + +/** + * Get input from either given file paths or stdin. If `opts.inPlace` then + * this calls the callback once for each `opts.inputFiles`. + * + * @param opts {Object} Parsed options. + * @param callback {Function} `function (err, content, filename)` where err + * is an error string if there was a problem, `content` is the read + * content and `filename` is the associated file name from which content + * was loaded if applicable. `filename` is only included when + * `opts.inPlace`. + */ +function getInput(opts, callback) { + if (opts.inputFiles.length === 0) { + // Read from stdin. + var chunks = []; + + var stdin = process.openStdin(); + stdin.setEncoding('utf8'); + stdin.on('data', function (chunk) { + chunks.push(chunk); + }); + + stdin.on('end', function () { + callback(null, chunks.join('')); + }); + } else if (opts.inPlace) { + for (var i = 0; i < opts.inputFiles.length; i++) { + var file = opts.inputFiles[i]; + var content; + try { + content = fs.readFileSync(file, 'utf8'); + } catch (e) { + callback(e, null, file); + } + if (content) { + callback(null, content, file); + } + } + } else { + // Read input files. + var i = 0; + var chunks = []; + try { + for (; i < opts.inputFiles.length; i++) { + chunks.push(fs.readFileSync(opts.inputFiles[i], 'utf8')); + } + } catch (e) { + return callback( + format('could not read "%s": %s', opts.inputFiles[i], e)); + } + callback(null, chunks.join('')); + } +} + + function isInteger(s) { return (s.search(/^-?[0-9]+$/) == 0); } @@ -233,7 +688,8 @@ function isInteger(s) { // Parse a lookup string into a list of lookup bits. E.g.: // "a.b.c" -> ["a","b","c"] // "b['a']" -> ["b","['a']"] -function parseLookup(lookup) { +// Optionally receives an alternative lookup delimiter (other than '.') +function parseLookup(lookup, lookupDelim) { //var debug = console.warn; var debug = function () {}; @@ -241,6 +697,7 @@ function parseLookup(lookup) { debug("\n*** "+lookup+" ***") bits = []; + lookupDelim = lookupDelim || "."; var bit = ""; var states = [null]; var escaped = false; @@ -272,7 +729,7 @@ function parseLookup(lookup) { } bit += ch; break; - case '.': + case lookupDelim: if (bit !== "") { bits.push(bit); bit = "" @@ -348,53 +805,114 @@ function parseLookup(lookup) { * "error": ... error object if there was an error ..., * "datum": ... parsed object if content was JSON ... * } + * + * @param buffer {String} The text to parse as JSON. + * @param obj {Object} Optional. Some streaming code paths will provide + * this, an already parsed JSON object. Use this to avoid reparsing. + * @param group {Boolean} Default false. If true, then non-JSON input + * will be attempted to be "arrayified" (see inline comment). + * @param merge {Boolean} Default null. Can be "shallow" or "deep". An + * attempt will be made to interpret the input as adjacent objects to + * be merged, last key wins. See inline comment for limitations. */ -function parseInput(buffer) { - try { - return {datum: JSON.parse(buffer)}; - } catch(e) { - // Special case: Auto-arrayification of unjoined list of objects: - // {"one": 1}{"two": 2} - // and auto-concatenation of unjoined list of arrays: - // ["a", "b"]["c", "d"] - // - // This can be nice to process a stream of JSON objects generated from - // multiple calls to another tool or `cat *.json | json`. - // - // Rules: - // - Only JS objects and arrays. Don't see strong need for basic - // JS types right now and this limitation simplifies. - // - The break between JS objects has to include a newline: - // {"one": 1} - // {"two": 2} - // or no spaces at all: - // {"one": 1}{"two": 2} - // I.e., not this: - // {"one": 1} {"two": 2} - // This condition should be fine for typical use cases and ensures - // no false matches inside JS strings. +function parseInput(buffer, obj, group, merge) { + if (obj) { + return {datum: obj}; + } else if (group) { + /** + * Special case: Grouping (previously called auto-arrayification) + * of unjoined list of objects: + * {"one": 1}{"two": 2} + * and auto-concatenation of unjoined list of arrays: + * ["a", "b"]["c", "d"] + * + * This can be nice to process a stream of JSON objects generated from + * multiple calls to another tool or `cat *.json | json`. + * + * Rules: + * - Only JS objects and arrays. Don't see strong need for basic + * JS types right now and this limitation simplifies. + * - The break between JS objects has to include a newline: + * {"one": 1} + * {"two": 2} + * or no spaces at all: + * {"one": 1}{"two": 2} + * I.e., not this: + * {"one": 1} {"two": 2} + * This condition should be fine for typical use cases and ensures + * no false matches inside JS strings. + * - The break between JS *arrays* has to include a newline: + * ["one", "two"] + * ["three"] + * The "no spaces" case is NOT supported for JS arrays as of v6.0.0 + * because shows that that + * is not safe. + */ var newBuffer = buffer; [/(})\s*\n\s*({)/g, /(})({")/g].forEach(function (pat) { newBuffer = newBuffer.replace(pat, "$1,\n$2"); }); - [/(\])\s*\n\s*(\[)/g, /(\])(\[)/g].forEach(function (pat) { + [/(\])\s*\n\s*(\[)/g].forEach(function (pat) { newBuffer = newBuffer.replace(pat, ",\n"); }); - if (buffer !== newBuffer) { - newBuffer = newBuffer.trim(); - if (newBuffer[0] !== '[') { - newBuffer = '[\n' + newBuffer; + newBuffer = newBuffer.trim(); + if (newBuffer[0] !== '[') { + newBuffer = '[\n' + newBuffer; + } + if (newBuffer.slice(-1) !== ']') { + newBuffer = newBuffer + '\n]\n'; + } + try { + return {datum: JSON.parse(newBuffer)}; + } catch (e2) { + return {error: e2}; + } + } else if (merge) { + // See the "Rules" above for limitations on boundaries for "adjacent" + // objects: KISS. + var newBuffer = buffer; + [/(})\s*\n\s*({)/g, /(})({")/g].forEach(function (pat) { + newBuffer = newBuffer.replace(pat, "$1,\n$2"); + }); + newBuffer = '[\n' + newBuffer + '\n]\n'; + var objs; + try { + objs = JSON.parse(newBuffer); + } catch(e) { + return {error: e}; + } + var merged = objs[0]; + if (merge === "shallow") { + for (var i = 1; i < objs.length; i++) { + var obj = objs[i]; + Object.keys(obj).forEach(function (k) { + merged[k] = obj[k]; + }); } - if (newBuffer.slice(-1) !== ']') { - newBuffer = newBuffer + '\n]\n'; + } else if (merge === "deep") { + function deepExtend(a, b) { + Object.keys(b).forEach(function (k) { + if (a[k] && b[k] && toString.call(a[k]) === '[object Object]' + && toString.call(b[k]) === '[object Object]') { + deepExtend(a[k], b[k]) + } else { + a[k] = b[k]; + } + }); } - try { - return {datum: JSON.parse(newBuffer)}; - } catch (e2) { + for (var i = 1; i < objs.length; i++) { + deepExtend(merged, objs[i]); } + } else { + throw new Error(format('unknown value for "merge": "%s"', merge)); + } + return {datum: merged}; + } else { + try { + return {datum: JSON.parse(buffer)}; + } catch(e) { + return {error: e}; } - - return {error: e} } } @@ -404,37 +922,100 @@ function parseInput(buffer) { * * @argument datum {Object} * @argument lookup {Array} The parsed lookup (from - * `parseLookup()`). Might be empty. + * `parseLookup(, )`). Might be empty. * @returns {Object} The result of the lookup. */ function lookupDatum(datum, lookup) { // Put it back together with some convenience transformations. var lookupCode = ""; var isJSIdentifier = /^[$A-Za-z_][0-9A-Za-z_]*$/; + var isNegArrayIndex = /^-\d+$/; for (var i=0; i < lookup.length; i++) { var bit = lookup[i]; if (bit[0] === '[') { lookupCode += bit; - } else if (! isJSIdentifier.exec(lookup[i])) { - // Allow a non-JS-indentifier token, e.g. `json foo-bar`. - lookupCode += '["' + lookup[i].replace('"', '\\"') + '"]'; + // Support Python-style negative array indexing. + } else if (bit === '-1') { + lookupCode += '.slice(-1)[0]'; + } else if (isNegArrayIndex.test(bit)) { + lookupCode += format('.slice(%s, %d)[0]', bit, Number(bit) + 1); + } else if (! isJSIdentifier.test(bit)) { + // Allow a non-JS-indentifier token, e.g. `json foo-bar`. This also + // works for array index lookups: `json 0` becomes a `["0"]` lookup. + lookupCode += '["' + bit.replace(/"/g, '\\"') + '"]'; } else { - lookupCode += '.' + lookup[i]; + lookupCode += '.' + bit; + } + } + try { + return vm.runInNewContext("(" + JSON.stringify(datum) + ")" + lookupCode); + } catch (e) { + if (e.name === 'TypeError') { + // Skip the following for a lookup 'foo.bar' where 'foo' is undefined: + // TypeError: Cannot read property 'bar' of undefined + // TODO: Are there other potential TypeError's in here to avoid? + return undefined; } + throw e; } - return runInNewContext("(" + JSON.stringify(datum) + ")" + lookupCode); } +/** + * Output the given datasets. + * + * @param datasets {Array} Array of data sets to print, in the form: + * `[ [, , ], ... ]` + * @param filename {String} The filename to which to write the output. If + * not set, then emit to stdout. + * @param headers {String} The HTTP header block string, if any, to emit + * first. + * @param opts {Object} Parsed tool options. + */ +function printDatasets(datasets, filename, headers, opts) { + var isTTY = (filename ? false : process.stdout.isTTY) + var write = emit; + if (filename) { + var tmpPath = path.resolve(path.dirname(filename), + format('.%s-json-%s-%s.tmp', path.basename(filename), process.pid, + Date.now())); + var stats = fs.statSync(filename); + var f = fs.createWriteStream(tmpPath, + {encoding: 'utf8', mode: stats.mode}); + write = f.write.bind(f); + } + if (headers && headers.length > 0) { + write(headers) + } + for (var i = 0; i < datasets.length; i++) { + var dataset = datasets[i]; + var output = stringifyDatum(dataset[0], opts, isTTY); + var sep = dataset[1]; + if (output && output.length) { + write(output); + write(sep); + } else if (dataset[2]) { + write(sep); + } + } + if (filename) { + f.end(); + fs.renameSync(tmpPath, filename); + if (! opts.quiet) { + warn('json: updated "%s" in-place', filename); + } + } +} + /** - * Print out a single result, considering input options. + * Stringify the given datum according to the given output options. */ -function printDatum(datum, opts, sep, alwaysPrintSep) { +function stringifyDatum(datum, opts, isTTY) { var output = null; switch (opts.outputMode) { case OM_INSPECT: - output = util.inspect(datum, false, Infinity, true); + output = util.inspect(datum, false, Infinity, isTTY); break; case OM_JSON: if (typeof datum !== 'undefined') { @@ -447,7 +1028,7 @@ function printDatum(datum, opts, sep, alwaysPrintSep) { // fit elements on one line as much as reasonable. if (datum === undefined) { // pass - } else if (isArray(datum)) { + } else if (Array.isArray(datum)) { var bits = ['[\n']; datum.forEach(function (d) { bits.push(' ') @@ -471,6 +1052,17 @@ function printDatum(datum, opts, sep, alwaysPrintSep) { default: throw new Error("unknown output mode: "+opts.outputMode); } + return output; +} + + +/** + * Print out a single result, considering input options. + * + * @deprecated + */ +function printDatum(datum, opts, sep, alwaysPrintSep) { + var output = stringifyDatum(datum, opts); if (output && output.length) { emit(output); emit(sep); @@ -493,7 +1085,8 @@ function emit(s) { process.stdout.on("error", function (err) { if (err.code === "EPIPE") { - // Pass. See . + // See . + drainStdoutAndExit(0); } else { warn(err) drainStdoutAndExit(1); @@ -522,6 +1115,7 @@ function drainStdoutAndExit(code) { + //---- mainline function main(argv) { @@ -542,99 +1136,341 @@ function main(argv) { return; } var lookupStrs = opts.args; - //XXX ditch this hack - if (lookupStrs.length == 0) { - lookupStrs.push(""); - } - - var buffer = ""; - var stdin = process.openStdin(); - stdin.setEncoding('utf8'); - stdin.on('data', function (chunk) { - buffer += chunk; - }); + if (opts.group && opts.array && opts.outputMode !== OM_JSON) { + // streaming + var chunker = chunkEmitter(opts); + chunker.on('error', function(error) { + warn("json: error: %s", err); + return drainStdoutAndExit(1); + }); + chunker.on('chunk', parseChunk); + } else if (opts.inPlace) { + assert.equal(opts.inputFiles.length, 1, + 'cannot handle more than one file with -I'); + getInput(opts, function (err, content, filename) { + if (err) { + warn("json: error: %s", err) + return drainStdoutAndExit(1); + } - stdin.on('end', function () { - // Take off a leading HTTP header if any and pass it through. - while (true) { - if (buffer.slice(0,5) === "HTTP/") { - var index = buffer.indexOf('\r\n\r\n'); - var sepLen = 4; - if (index == -1) { - index = buffer.indexOf('\n\n'); - sepLen = 2; + // Take off a leading HTTP header if any and pass it through. + var headers = []; + while (true) { + if (content.slice(0,5) === "HTTP/") { + var index = content.indexOf('\r\n\r\n'); + var sepLen = 4; + if (index == -1) { + index = content.indexOf('\n\n'); + sepLen = 2; + } + if (index != -1) { + if (! opts.dropHeaders) { + headers.push(content.slice(0, index+sepLen)); + } + var is100Continue = (content.slice(0, 21) === "HTTP/1.1 100 Continue"); + content = content.slice(index+sepLen); + if (is100Continue) { + continue; + } + } } - if (index != -1) { - if (! opts.dropHeaders) { - emit(buffer.slice(0, index+sepLen)); + break; + } + parseChunk(content, undefined, filename, headers.join('')); + }); + } else { + // not streaming + getInput(opts, function (err, buffer) { + if (err) { + warn("json: error: %s", err) + return drainStdoutAndExit(1); + } + // Take off a leading HTTP header if any and pass it through. + while (true) { + if (buffer.slice(0,5) === "HTTP/") { + var index = buffer.indexOf('\r\n\r\n'); + var sepLen = 4; + if (index == -1) { + index = buffer.indexOf('\n\n'); + sepLen = 2; } - var is100Continue = (buffer.slice(0, 21) === "HTTP/1.1 100 Continue"); - buffer = buffer.slice(index+sepLen); - if (is100Continue) { - continue; + if (index != -1) { + if (! opts.dropHeaders) { + emit(buffer.slice(0, index+sepLen)); + } + var is100Continue = (buffer.slice(0, 21) === "HTTP/1.1 100 Continue"); + buffer = buffer.slice(index+sepLen); + if (is100Continue) { + continue; + } } } + break; } - break; - } + parseChunk(buffer); + }); + } - // Expect the remainder to be JSON. - if (! buffer.length) { + /** + * Parse a single chunk of JSON. This may be called more than once + * (when streaming or when operating on multiple files). + * + * @param chunk {String} The JSON-encoded string. + * @param obj {Object} Optional. For some code paths while streaming `obj` + * will be provided. This is an already parsed JSON object. + * @param filename {String} Optional. The filename from which this content + * came, if relevant. This is only set if `opts.inPlace`. + * @param headers {String} Optional. Leading HTTP headers, if any to emit. + */ + function parseChunk(chunk, obj, filename, headers) { + // Expect the chunk to be JSON. + if (! chunk.length) { return; } - var input = parseInput(buffer); // -> {datum: , error: } + // parseInput() -> {datum: , error: } + var input = parseInput(chunk, obj, opts.group, opts.merge); if (input.error) { // Doesn't look like JSON. Just print it out and move on. if (! opts.quiet) { - warn("json: error: doesn't look like JSON: %s (input='%s')", - input.error, JSON.stringify(buffer)); + // Use JSON-js' "json_parse" parser to get more detail on the + // syntax error. + var details = ""; + var normBuffer = chunk.replace(/\r\n|\n|\r/, '\n'); + try { + json_parse(normBuffer); + details = input.error; + } catch(err) { + // err.at has the position. Get line/column from that. + var at = err.at - 1; // `err.at` looks to be 1-based. + var lines = chunk.split('\n'); + var line, col, pos = 0; + for (line = 0; line < lines.length; line++) { + pos += lines[line].length + 1; + if (pos > at) { + col = at - (pos - lines[line].length - 1); + break; + } + } + var spaces = ''; + for (var i=0; i for why. + data = data[lookups[0].join('.')]; + } else if (lookupsAreIndeces) { + // Special case: Lookups that are all indeces into an input array + // are more likely to be wanted as an array of selected items rather + // than a "JSON table" thing that we use otherwise. + var flattened = []; + for (i = 0; i < lookups.length; i++) { + var lookupStr = lookups[i].join('.'); + if (data.hasOwnProperty(lookupStr)) { + flattened.push(data[lookupStr]) + } + } + data = flattened; + } + // If JSON output mode, then always just output full set of data to + // ensure valid JSON output. + datasets.push([data, '\n', false]); + } else if (lookups.length) { + if (opts.array) { + // Output `data` as a "table" of lookup results. + for (j = 0; j < data.length; j++) { + var row = data[j]; + for (i = 0; i < lookups.length-1; i++) { + datasets.push([row[lookups[i].join('.')], opts.delim, true]); + } + datasets.push([row[lookups[i].join('.')], '\n', true]); } - printDatum(row[c], opts, '\n', true); - }); - } else { - if (lookups.length === 0) { - results = input.datum; } else { - for (var i=0; i < lookups.length; i++) { - results.push(lookupDatum(input.datum, lookups[i])); + for (i = 0; i < lookups.length; i++) { + datasets.push([data[lookups[i].join('.')], '\n', false]); } } - results.forEach(function (r) { - printDatum(r, opts, '\n', false); - }); + } else if (opts.array) { + if (!Array.isArray(data)) data = [data]; + for (j = 0; j < data.length; j++) { + datasets.push([data[j], '\n', false]); + } + } else { + // Output `data` as is. + datasets.push([data, '\n', false]); } - }); + printDatasets(datasets, filename, headers, opts); + } } if (require.main === module) { + // HACK guard for . + // We override the `process.stdout.end` guard that core node.js puts in + // place. The real fix is that `.end()` shouldn't be called on stdout + // in node core. Hopefully node v0.6.9 will fix that. Only guard + // for v0.6.0..v0.6.8. + var nodeVer = process.versions.node.split('.').map(Number); + if ([0,6,0] <= nodeVer && nodeVer <= [0,6,8]) { + var stdout = process.stdout; + stdout.end = stdout.destroy = stdout.destroySoon = function() { + /* pass */ + }; + } + main(process.argv); } From e1cfe21e7dad2edea53cf7ef4fbc41efe6622a2e Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 13:56:54 -0700 Subject: [PATCH 057/116] indent stdout from npm commands --- bin/compile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/compile b/bin/compile index f4dbba711..8800cc095 100755 --- a/bin/compile +++ b/bin/compile @@ -72,14 +72,14 @@ if [ -d $cache_store_dir ]; then cp -r $cache_store_dir $cache_target_dir fi status "Pruning any unused dependencies" - npm prune + npm prune | indent fi # Install dependencies status "Installing dependencies" cd $build_dir -npm install --production -npm rebuild +npm install --production | indent +npm rebuild | indent # Cache node_modules for future builds if [ -d $cache_target_dir ]; then From 5f0305ef14a22a56072d24f7e22c74f57d072022 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 14:20:53 -0700 Subject: [PATCH 058/116] simplify requested version detection, move node and npm binaries to /app/vendor --- bin/common.sh | 8 ++++---- bin/compile | 12 +++++------- bin/test | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/bin/common.sh b/bin/common.sh index ef026342e..9ecb2d3d4 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -8,12 +8,12 @@ set -e download_and_install_node() { version="$1" - status "Downloading and installing node v$version" node_url="http://s3pository.heroku.com/node/v$version/node-v$version-linux-x64.tar.gz" curl $node_url -s -o - | tar xzf - -C $build_dir - mv $build_dir/node-v$version-linux-x64 $build_dir/node - chmod +x $build_dir/node/bin/* - PATH=$PATH:$build_dir/node/bin + mkdir -p $build_dir/vendor + mv $build_dir/node-v$version-linux-x64 $build_dir/vendor/node + chmod +x $build_dir/vendor/node/bin/* + PATH=$PATH:$build_dir/vendor/node/bin } query_stable_version() { diff --git a/bin/compile b/bin/compile index 8800cc095..24910c7cc 100755 --- a/bin/compile +++ b/bin/compile @@ -12,15 +12,12 @@ source $bp_dir/bin/common.sh # Bootstrap the build process with latest stable version of node # We'll use it to parse package.json and do semver detection +status "Bootstrapping node v$version" download_and_install_node $stable_version # Is a node version specified in package.json? -determine_requested_version() { - # https://github.com/trentm/json - v=$(cat $build_dir/package.json | node $bp_dir/vendor/json engines.node 2>/dev/null) - if [ $? == 0 ]; then echo $v; fi -} -requested_version=$(determine_requested_version) +# https://github.com/trentm/json +requested_version=$(cat $build_dir/package.json | $bp_dir/vendor/json engines.node) # Give a warning if engines.node is unspecified if ! test $requested_version; then @@ -52,6 +49,7 @@ else node_version=$(echo $evaluated_versions | tail -n 1) if test $node_version; then + status "Downloading and installing node v$version" download_and_install_node $node_version else error "node ${requested_version} not found among available versions on nodejs.org/dist" @@ -92,4 +90,4 @@ fi # Update the PATH status "Building runtime environment" mkdir -p $build_dir/.profile.d -echo "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh +echo "export PATH=\"\$HOME/vendor/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh diff --git a/bin/test b/bin/test index e061f8e29..c64f262cb 100755 --- a/bin/test +++ b/bin/test @@ -47,7 +47,7 @@ testPackageJsonWithInvalidVersion() { testProfileCreated() { compile "package-json-stable-version" assertCaptured "Building runtime environment" - assertFile "export PATH=\"\$HOME/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" + assertFile "export PATH=\"\$HOME/vendor/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" assertCapturedSuccess } From ab1c5a082fe391b9d8f43d879be187fdfb839e56 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 14:22:21 -0700 Subject: [PATCH 059/116] fix that stable typo --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index 24910c7cc..27d9f22e8 100755 --- a/bin/compile +++ b/bin/compile @@ -12,7 +12,7 @@ source $bp_dir/bin/common.sh # Bootstrap the build process with latest stable version of node # We'll use it to parse package.json and do semver detection -status "Bootstrapping node v$version" +status "Bootstrapping node v$stable_version" download_and_install_node $stable_version # Is a node version specified in package.json? From b8953749fa4edf647a5b647045e6c3abb6905dec Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 14:26:20 -0700 Subject: [PATCH 060/116] remove node binaries in case we're overwriting them --- bin/common.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/common.sh b/bin/common.sh index 9ecb2d3d4..dac4967c0 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -11,6 +11,7 @@ download_and_install_node() { node_url="http://s3pository.heroku.com/node/v$version/node-v$version-linux-x64.tar.gz" curl $node_url -s -o - | tar xzf - -C $build_dir mkdir -p $build_dir/vendor + rm -rf $build_dir/vendor/node mv $build_dir/node-v$version-linux-x64 $build_dir/vendor/node chmod +x $build_dir/vendor/node/bin/* PATH=$PATH:$build_dir/vendor/node/bin From c1f13bf036eac7e9c82ac7430d0a08c0173315c6 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 16:22:30 -0700 Subject: [PATCH 061/116] stringify semver pattern for requested version --- bin/common.sh | 9 +++++--- bin/compile | 22 ++++++++++--------- bin/test | 9 +------- test/package-json-invalidversion/package.json | 2 +- test/package-json-stable-version/package.json | 2 +- .../package.json | 2 +- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/bin/common.sh b/bin/common.sh index dac4967c0..e8a54797d 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -3,7 +3,8 @@ # fail fast set -e -# debug +# Uncomment the line below to enable debugging when +# working on the buildpack # set -x download_and_install_node() { @@ -11,7 +12,10 @@ download_and_install_node() { node_url="http://s3pository.heroku.com/node/v$version/node-v$version-linux-x64.tar.gz" curl $node_url -s -o - | tar xzf - -C $build_dir mkdir -p $build_dir/vendor + + # Remove node in case we're overwriting a previously-downloaded version rm -rf $build_dir/vendor/node + mv $build_dir/node-v$version-linux-x64 $build_dir/vendor/node chmod +x $build_dir/vendor/node/bin/* PATH=$PATH:$build_dir/vendor/node/bin @@ -46,7 +50,6 @@ status() { echo "-----> $*" } - # sed -l basically makes sed replace and buffer through stdin to stdout # so you get updates while the command runs and dont wait for the end # e.g. npm install | indent @@ -59,5 +62,5 @@ indent() { } function cat_npm_debug_log() { - [ -f $build_dir/npm-debug.log ] && cat $build_dir/npm-debug.log + test -f $build_dir/npm-debug.log && cat $build_dir/npm-debug.log } diff --git a/bin/compile b/bin/compile index 27d9f22e8..48e7333fd 100755 --- a/bin/compile +++ b/bin/compile @@ -30,23 +30,25 @@ if ! test $requested_version; then else # Does the already-downloaded stable version of node satisfy the requested version? - default_satisfies=$(node $bp_dir/vendor/semver/bin/semver -v "${stable_version}" -r "${requested_version}" || echo "") + default_satisfies=$(node $bp_dir/vendor/semver/bin/semver -v "$stable_version" -r "$requested_version" || echo "") if test $default_satisfies; then status "Latest stable node v$stable_version satisfies engines.node: $requested" node_version=$stable_version else - # Find all available versions from nodejs.org/dist + # Fetch all versions of node from nodejs.org/dist and format them into + # a string that the semver binary will appreciate. + # e.g. semver -v "0.10.0" -v "0.10.1" -v "0.10.2" -r ">0.8" + # See https://github.com/isaacs/node-semver/blob/master/bin/semver args="" for version in $(query_all_versions); do args="${args} -v \"${version}\""; done args="${args} -r \"${requested_version}\"" - # Determine which available versions satisfy the requested version - # https://github.com/isaacs/node-semver/blob/master/bin/semver - evaluated_versions=$(eval node $bp_dir/vendor/semver/bin/semver ${args} || echo "") + # Find all versions that satisfy. + satisfying_versions=$(eval $bp_dir/vendor/semver/bin/semver ${args}) - # Use the latest of the evaluated versions - node_version=$(echo $evaluated_versions | tail -n 1) + # Use the latest one. + node_version=$(echo "$satisfying_versions" | tail -n 1) if test $node_version; then status "Downloading and installing node v$version" @@ -62,9 +64,9 @@ cache_store_dir="$cache_dir/node_modules/$node_version" cache_target_dir="$build_dir/node_modules" # Restore node_modules from cache, if present -if [ -d $cache_store_dir ]; then +if test -d $cache_store_dir; then status "Restoring node_modules cache" - if [ -d $cache_target_dir ]; then + if test -d $cache_target_dir; then cp -r $cache_store_dir/* $cache_target_dir/ else cp -r $cache_store_dir $cache_target_dir @@ -80,7 +82,7 @@ npm install --production | indent npm rebuild | indent # Cache node_modules for future builds -if [ -d $cache_target_dir ]; then +if test -d $cache_target_dir; then status "Caching node_modules for future builds" rm -rf $cache_store_dir mkdir -p $(dirname $cache_store_dir) diff --git a/bin/test b/bin/test index c64f262cb..7f7d1f843 100755 --- a/bin/test +++ b/bin/test @@ -1,12 +1,5 @@ #!/usr/bin/env bash - -# -# Create a Heroku app with the following buildpack: -# https://github.com/ddollar/buildpack-test -# -# Push this Node.js buildpack to that Heroku app to -# run the tests -# +# See CONTRIBUTING.md for info on running these tests. testDetectWithPackageJson() { detect "package-json-stable-version" diff --git a/test/package-json-invalidversion/package.json b/test/package-json-invalidversion/package.json index 25d264c96..1249e8956 100644 --- a/test/package-json-invalidversion/package.json +++ b/test/package-json-invalidversion/package.json @@ -7,6 +7,6 @@ "url" : "http://github.com/example/example.git" }, "engines": { - "node": "5.0" + "node": ">2.0" } } diff --git a/test/package-json-stable-version/package.json b/test/package-json-stable-version/package.json index 3476e709d..b7b213242 100644 --- a/test/package-json-stable-version/package.json +++ b/test/package-json-stable-version/package.json @@ -7,6 +7,6 @@ "url" : "http://github.com/example/example.git" }, "engines": { - "node": "0.10.x" + "node": ">0.10.0" } } diff --git a/test/package-json-unstable-version/package.json b/test/package-json-unstable-version/package.json index f01cae8d9..11fb1310c 100644 --- a/test/package-json-unstable-version/package.json +++ b/test/package-json-unstable-version/package.json @@ -7,6 +7,6 @@ "url" : "http://github.com/example/example.git" }, "engines": { - "node": "0.11.5" + "node": ">0.11.0" } } From e5163904fc47dc8ff78a36653dc8226da3948db3 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 12 Sep 2013 16:30:52 -0700 Subject: [PATCH 062/116] handle semver match failures --- bin/common.sh | 3 +-- bin/compile | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/common.sh b/bin/common.sh index e8a54797d..1ddde42cb 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -3,8 +3,7 @@ # fail fast set -e -# Uncomment the line below to enable debugging when -# working on the buildpack +# Uncomment the line below to enable debugging # set -x download_and_install_node() { diff --git a/bin/compile b/bin/compile index 48e7333fd..ee0127067 100755 --- a/bin/compile +++ b/bin/compile @@ -30,12 +30,13 @@ if ! test $requested_version; then else # Does the already-downloaded stable version of node satisfy the requested version? - default_satisfies=$(node $bp_dir/vendor/semver/bin/semver -v "$stable_version" -r "$requested_version" || echo "") + default_satisfies=$($bp_dir/vendor/semver/bin/semver -v "$stable_version" -r "$requested_version" || echo "") if test $default_satisfies; then - status "Latest stable node v$stable_version satisfies engines.node: $requested" + status "Latest stable node v$stable_version satisfies engines.node: $requested_version" node_version=$stable_version else + # Fetch all versions of node from nodejs.org/dist and format them into # a string that the semver binary will appreciate. # e.g. semver -v "0.10.0" -v "0.10.1" -v "0.10.2" -r ">0.8" @@ -45,7 +46,7 @@ else args="${args} -r \"${requested_version}\"" # Find all versions that satisfy. - satisfying_versions=$(eval $bp_dir/vendor/semver/bin/semver ${args}) + satisfying_versions=$(eval $bp_dir/vendor/semver/bin/semver ${args} || echo "") # Use the latest one. node_version=$(echo "$satisfying_versions" | tail -n 1) From 437e2b1ee7f219f05c69fb62326260635c9c53e1 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 13 Sep 2013 11:04:00 -0700 Subject: [PATCH 063/116] restore node_modules from cache if package.json unchanged --- bin/compile | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bin/compile b/bin/compile index ee0127067..383caabeb 100755 --- a/bin/compile +++ b/bin/compile @@ -64,24 +64,24 @@ fi cache_store_dir="$cache_dir/node_modules/$node_version" cache_target_dir="$build_dir/node_modules" -# Restore node_modules from cache, if present -if test -d $cache_store_dir; then - status "Restoring node_modules cache" +cd $build_dir + +# Restore node_modules from cache if package.json unchanged. +if test -d $cache_store_dir && diff $cache_store_dir/package.json $build_dir/package.json >/dev/null; then + status "package.json unchanged; restoring node_modules from cache" if test -d $cache_target_dir; then cp -r $cache_store_dir/* $cache_target_dir/ else cp -r $cache_store_dir $cache_target_dir fi - status "Pruning any unused dependencies" - npm prune | indent +else + # Install dependencies anew + status "Installing dependencies" + npm install --production | indent + status "Rebuilding dependencies" + npm rebuild | indent fi -# Install dependencies -status "Installing dependencies" -cd $build_dir -npm install --production | indent -npm rebuild | indent - # Cache node_modules for future builds if test -d $cache_target_dir; then status "Caching node_modules for future builds" From 01580e32044f357693c7fc319d34d08342e3cd84 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 13 Sep 2013 15:51:35 -0700 Subject: [PATCH 064/116] add some pending tests --- bin/test | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bin/test b/bin/test index 7f7d1f843..20f6bd1c6 100755 --- a/bin/test +++ b/bin/test @@ -51,6 +51,14 @@ testNodeModulesCached() { assertEquals "1" "$(ls -1 $cache/node_modules/0.10.18 | wc -l)" } +# Pending + +# testNodeBinariesAddedToPath() { +# } + +# testNodeModulesRestoredFromCache() { +# } + ## utils ######################################## pushd $(dirname 0) >/dev/null From 29427c56b2043ed26854a6f6f75f5c1e11a70bc2 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 13 Sep 2013 15:54:54 -0700 Subject: [PATCH 065/116] restore cache only if package.json and node version are unchanged --- bin/compile | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bin/compile b/bin/compile index 383caabeb..3d9353fae 100755 --- a/bin/compile +++ b/bin/compile @@ -60,15 +60,19 @@ else fi fi +# Run subsequent node/npm commands from the build path +cd $build_dir + # Configure cache directories -cache_store_dir="$cache_dir/node_modules/$node_version" +package_checksum=$(cat $build_dir/package.json | md5sum | awk '{print $1}') +cache_store_dir="$cache_dir/node_modules/$node_version/$package_checksum" cache_target_dir="$build_dir/node_modules" -cd $build_dir - -# Restore node_modules from cache if package.json unchanged. -if test -d $cache_store_dir && diff $cache_store_dir/package.json $build_dir/package.json >/dev/null; then - status "package.json unchanged; restoring node_modules from cache" +# Restore node_modules from cache if resolved node version and +# package.json match the previous build +if test -d $cache_store_dir; then + status "package.json and node version unchanged since last build" + status "Restoring node_modules from cache" if test -d $cache_target_dir; then cp -r $cache_store_dir/* $cache_target_dir/ else From 6cb3786a3ad5902d7d15e10a9f3ca0b3203da14c Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 13 Sep 2013 22:53:29 -0700 Subject: [PATCH 066/116] cache node too --- bin/compile | 48 +++++++++++++++++++++++------------------------- bin/test | 4 ++-- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/bin/compile b/bin/compile index 3d9353fae..128828b48 100755 --- a/bin/compile +++ b/bin/compile @@ -1,7 +1,7 @@ #!/usr/bin/env bash build_dir=$1 -cache_dir=$2 +cache_basedir=$2 bp_dir=$(cd $(dirname $0); cd ..; pwd) stable_version="0.10.18" @@ -51,10 +51,8 @@ else # Use the latest one. node_version=$(echo "$satisfying_versions" | tail -n 1) - if test $node_version; then - status "Downloading and installing node v$version" - download_and_install_node $node_version - else + # Bail if no matching version was found + if ! test $node_version; then error "node ${requested_version} not found among available versions on nodejs.org/dist" fi fi @@ -63,35 +61,35 @@ fi # Run subsequent node/npm commands from the build path cd $build_dir -# Configure cache directories +# Configure cache directory package_checksum=$(cat $build_dir/package.json | md5sum | awk '{print $1}') -cache_store_dir="$cache_dir/node_modules/$node_version/$package_checksum" -cache_target_dir="$build_dir/node_modules" +cache_dir="$cache_basedir/$node_version/$package_checksum" -# Restore node_modules from cache if resolved node version and -# package.json match the previous build -if test -d $cache_store_dir; then +# Restore from cache if node and package.json haven't changed +if test -d $cache_dir; then status "package.json and node version unchanged since last build" - status "Restoring node_modules from cache" - if test -d $cache_target_dir; then - cp -r $cache_store_dir/* $cache_target_dir/ - else - cp -r $cache_store_dir $cache_target_dir - fi + status "Restoring node v$node_version and node_modules from cache" + test -d $cache_dir/node_modules && cp -r $cache_dir/node_modules $build_dir/ + cp -r $cache_dir/vendor/node $build_dir/vendor/ else - # Install dependencies anew + + if [ $stable_version != $node_version ]; then + status "Downloading and installing node v$version" + download_and_install_node $node_version + fi + status "Installing dependencies" npm install --production | indent + status "Rebuilding dependencies" npm rebuild | indent -fi -# Cache node_modules for future builds -if test -d $cache_target_dir; then - status "Caching node_modules for future builds" - rm -rf $cache_store_dir - mkdir -p $(dirname $cache_store_dir) - cp -r $cache_target_dir $cache_store_dir + status "Caching node and node_modules for future builds" + rm -rf $cache_dir + mkdir -p $cache_dir + mkdir -p $cache_dir/vendor + test -d $build_dir/node_modules && cp -r $build_dir/node_modules $cache_dir/ + cp -r $build_dir/vendor/node $cache_dir/vendor/ fi # Update the PATH diff --git a/bin/test b/bin/test index 20f6bd1c6..2ed0d9743 100755 --- a/bin/test +++ b/bin/test @@ -47,8 +47,8 @@ testProfileCreated() { testNodeModulesCached() { cache=$(mktmpdir) compile "node-modules-caching" $cache - assertCaptured "Caching node_modules for future builds" - assertEquals "1" "$(ls -1 $cache/node_modules/0.10.18 | wc -l)" + assertCaptured "Caching node" + assertEquals "1" "$(ls -1 $cache/0.10.18 | wc -l)" } # Pending From 5700178250afabcf65f83ed26792d99b20eba2a8 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 16 Sep 2013 12:09:29 -0700 Subject: [PATCH 067/116] post to nomnom --- bin/compile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bin/compile b/bin/compile index 128828b48..a0d45901e 100755 --- a/bin/compile +++ b/bin/compile @@ -96,3 +96,12 @@ fi status "Building runtime environment" mkdir -p $build_dir/.profile.d echo "export PATH=\"\$HOME/vendor/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh + +# Package analytics +curl \ + -f \ + -s \ + -X POST \ + -H "content-type: application/json" \ + -d @$build_dir/package.json \ + http://nomnom.heroku.com/?build_id=$REQUEST_ID From aaccb3b763a70d4cbfb2be605a03f5c6984beb4a Mon Sep 17 00:00:00 2001 From: zeke Date: Sat, 21 Sep 2013 14:10:25 -0700 Subject: [PATCH 068/116] bin/release is dead --- bin/release | 8 -------- 1 file changed, 8 deletions(-) delete mode 100755 bin/release diff --git a/bin/release b/bin/release deleted file mode 100755 index 133982fd7..000000000 --- a/bin/release +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -# bin/release - -cat < Date: Sat, 21 Sep 2013 21:39:51 -0700 Subject: [PATCH 069/116] output npm debug info on error --- bin/compile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index a0d45901e..cb49359da 100755 --- a/bin/compile +++ b/bin/compile @@ -7,8 +7,8 @@ stable_version="0.10.18" source $bp_dir/bin/common.sh -# Output debug info on exit -# trap cat_npm_debug_log EXIT +# Output npm debug info on error +trap cat_npm_debug_log ERR # Bootstrap the build process with latest stable version of node # We'll use it to parse package.json and do semver detection From 7a56e6b6d369f2077694d3e0c65f0e127ceb6bf5 Mon Sep 17 00:00:00 2001 From: zeke Date: Sat, 21 Sep 2013 21:44:56 -0700 Subject: [PATCH 070/116] suppress nomnom output --- bin/compile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index cb49359da..f8f438fa3 100755 --- a/bin/compile +++ b/bin/compile @@ -104,4 +104,5 @@ curl \ -X POST \ -H "content-type: application/json" \ -d @$build_dir/package.json \ - http://nomnom.heroku.com/?build_id=$REQUEST_ID + http://nomnom.heroku.com/?build_id=$REQUEST_ID \ + > /dev/null 2>&1 From 4dcacde8a7dca34ff251a6c77be226a1803c2424 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 23 Sep 2013 11:49:00 -0700 Subject: [PATCH 071/116] use $node_version, not $version --- bin/compile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index f8f438fa3..8455883d1 100755 --- a/bin/compile +++ b/bin/compile @@ -12,7 +12,7 @@ trap cat_npm_debug_log ERR # Bootstrap the build process with latest stable version of node # We'll use it to parse package.json and do semver detection -status "Bootstrapping node v$stable_version" +status "Bootstrapping node" download_and_install_node $stable_version # Is a node version specified in package.json? @@ -74,7 +74,7 @@ if test -d $cache_dir; then else if [ $stable_version != $node_version ]; then - status "Downloading and installing node v$version" + status "Downloading and installing node v$node_version" download_and_install_node $node_version fi From ccc11bf5593ee91e761c6eccfaa87411db754e91 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 23 Sep 2013 14:40:17 -0700 Subject: [PATCH 072/116] update build_id to request_id and use human flag names --- bin/compile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/compile b/bin/compile index 8455883d1..ed225df57 100755 --- a/bin/compile +++ b/bin/compile @@ -97,12 +97,12 @@ status "Building runtime environment" mkdir -p $build_dir/.profile.d echo "export PATH=\"\$HOME/vendor/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh -# Package analytics +# Post to nomnom curl \ - -f \ - -s \ - -X POST \ - -H "content-type: application/json" \ - -d @$build_dir/package.json \ - http://nomnom.heroku.com/?build_id=$REQUEST_ID \ - > /dev/null 2>&1 + --data @$build_dir/package.json \ + --fail \ + --silent \ + --request POST \ + --header "content-type: application/json" \ + https://nomnom.heroku.com/?request_id=$REQUEST_ID \ + > /dev/null From d72d36f9f24c38ada781d48da4884b21b6d34358 Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 24 Sep 2013 00:14:25 -0700 Subject: [PATCH 073/116] give the test apps consistent package.json names so nomnom can ignore them --- test/node-modules-caching/package.json | 6 +++--- test/package-json-invalidversion/package.json | 4 ++-- test/package-json-noversion/package.json | 4 ++-- test/package-json-stable-version/package.json | 4 ++-- test/package-json-unstable-version/package.json | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/node-modules-caching/package.json b/test/node-modules-caching/package.json index 55c1d53d1..c4e987036 100644 --- a/test/node-modules-caching/package.json +++ b/test/node-modules-caching/package.json @@ -1,7 +1,7 @@ { - "name": "myapp", + "name": "node-buildpack-test-app", "version": "0.0.1", - "description": "It's a fake app", + "description": "node buildpack integration test app", "repository" : { "type" : "git", "url" : "http://github.com/example/example.git" @@ -12,4 +12,4 @@ "dependencies": { "express": "latest" } -} \ No newline at end of file +} diff --git a/test/package-json-invalidversion/package.json b/test/package-json-invalidversion/package.json index 1249e8956..8895f66c5 100644 --- a/test/package-json-invalidversion/package.json +++ b/test/package-json-invalidversion/package.json @@ -1,7 +1,7 @@ { - "name": "myapp", + "name": "node-buildpack-test-app", "version": "0.0.1", - "description": "It's a fake app", + "description": "node buildpack integration test app", "repository" : { "type" : "git", "url" : "http://github.com/example/example.git" diff --git a/test/package-json-noversion/package.json b/test/package-json-noversion/package.json index b2570b9b9..e5379cb5b 100644 --- a/test/package-json-noversion/package.json +++ b/test/package-json-noversion/package.json @@ -1,7 +1,7 @@ { - "name": "myapp", + "name": "node-buildpack-test-app", "version": "0.0.1", - "description": "It's a fake app with no engine specified", + "description": "node buildpack integration test app", "repository" : { "type" : "git", "url" : "http://github.com/example/example.git" diff --git a/test/package-json-stable-version/package.json b/test/package-json-stable-version/package.json index b7b213242..fada51f08 100644 --- a/test/package-json-stable-version/package.json +++ b/test/package-json-stable-version/package.json @@ -1,7 +1,7 @@ { - "name": "myapp", + "name": "node-buildpack-test-app", "version": "0.0.1", - "description": "It's a fake app", + "description": "node buildpack integration test app", "repository" : { "type" : "git", "url" : "http://github.com/example/example.git" diff --git a/test/package-json-unstable-version/package.json b/test/package-json-unstable-version/package.json index 11fb1310c..916c34a4a 100644 --- a/test/package-json-unstable-version/package.json +++ b/test/package-json-unstable-version/package.json @@ -1,7 +1,7 @@ { - "name": "myapp-using-unstable-node", + "name": "node-buildpack-test-app", "version": "0.0.1", - "description": "It's a fake app", + "description": "node buildpack integration test app", "repository" : { "type" : "git", "url" : "http://github.com/example/example.git" From cdedbf033afca2238cdb17ad96afa1754a9cf3bd Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 26 Sep 2013 10:00:37 -0700 Subject: [PATCH 074/116] pass engines.node to node-semver-service.heroku.com --- bin/compile | 75 +++++++++++++++-------------------------------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/bin/compile b/bin/compile index ed225df57..a094ead63 100755 --- a/bin/compile +++ b/bin/compile @@ -3,99 +3,66 @@ build_dir=$1 cache_basedir=$2 bp_dir=$(cd $(dirname $0); cd ..; pwd) -stable_version="0.10.18" source $bp_dir/bin/common.sh # Output npm debug info on error trap cat_npm_debug_log ERR -# Bootstrap the build process with latest stable version of node -# We'll use it to parse package.json and do semver detection -status "Bootstrapping node" -download_and_install_node $stable_version - # Is a node version specified in package.json? -# https://github.com/trentm/json -requested_version=$(cat $build_dir/package.json | $bp_dir/vendor/json engines.node) +semver_range=$(cat $build_dir/package.json | $bp_dir/vendor/json engines.node) + +# Resolve node version using a service +node_version=$(curl --silent https://node-semver-service.heroku.com/${semver_range}) -# Give a warning if engines.node is unspecified -if ! test $requested_version; then - node_version=$stable_version +# Warn if engines.node is unspecified +if ! test $semver_range; then echo echo "WARNING: No node version specified in package.json, see:" | indent echo "https://devcenter.heroku.com/articles/nodejs-support#versions" | indent echo - status "Defaulting to latest stable node, v$stable_version" - -else - # Does the already-downloaded stable version of node satisfy the requested version? - default_satisfies=$($bp_dir/vendor/semver/bin/semver -v "$stable_version" -r "$requested_version" || echo "") - - if test $default_satisfies; then - status "Latest stable node v$stable_version satisfies engines.node: $requested_version" - node_version=$stable_version - else - - # Fetch all versions of node from nodejs.org/dist and format them into - # a string that the semver binary will appreciate. - # e.g. semver -v "0.10.0" -v "0.10.1" -v "0.10.2" -r ">0.8" - # See https://github.com/isaacs/node-semver/blob/master/bin/semver - args="" - for version in $(query_all_versions); do args="${args} -v \"${version}\""; done - args="${args} -r \"${requested_version}\"" - - # Find all versions that satisfy. - satisfying_versions=$(eval $bp_dir/vendor/semver/bin/semver ${args} || echo "") - - # Use the latest one. - node_version=$(echo "$satisfying_versions" | tail -n 1) - - # Bail if no matching version was found - if ! test $node_version; then - error "node ${requested_version} not found among available versions on nodejs.org/dist" - fi - fi + status "Defaulting to latest stable node, v$node_version" fi +# TODO: Warn if engines.node is "*" or ">foo" + +# Download and install node +status "Downloading and installing node v$node_version" +download_and_install_node $node_version + # Run subsequent node/npm commands from the build path cd $build_dir # Configure cache directory package_checksum=$(cat $build_dir/package.json | md5sum | awk '{print $1}') -cache_dir="$cache_basedir/$node_version/$package_checksum" +cache_dir="$cache_basedir/$package_checksum" -# Restore from cache if node and package.json haven't changed +# Restore from cache package.json hasn't changed if test -d $cache_dir; then - status "package.json and node version unchanged since last build" - status "Restoring node v$node_version and node_modules from cache" + status "package.json unchanged since last build" + status "Restoring node_modules from cache" test -d $cache_dir/node_modules && cp -r $cache_dir/node_modules $build_dir/ - cp -r $cache_dir/vendor/node $build_dir/vendor/ + # cp -r $cache_dir/vendor/node $build_dir/vendor/ else - if [ $stable_version != $node_version ]; then - status "Downloading and installing node v$node_version" - download_and_install_node $node_version - fi - status "Installing dependencies" npm install --production | indent status "Rebuilding dependencies" npm rebuild | indent - status "Caching node and node_modules for future builds" + status "Caching node_modules for future builds" rm -rf $cache_dir mkdir -p $cache_dir mkdir -p $cache_dir/vendor test -d $build_dir/node_modules && cp -r $build_dir/node_modules $cache_dir/ - cp -r $build_dir/vendor/node $cache_dir/vendor/ + # cp -r $build_dir/vendor/node $cache_dir/vendor/ fi # Update the PATH status "Building runtime environment" mkdir -p $build_dir/.profile.d -echo "export PATH=\"\$HOME/vendor/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh +echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh # Post to nomnom curl \ From d839024986109aed632dc2238293bc143cc09d5d Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 26 Sep 2013 15:34:54 -0700 Subject: [PATCH 075/116] rebuild, then install. fixes #47 --- bin/compile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/compile b/bin/compile index ed225df57..8c86e00a6 100755 --- a/bin/compile +++ b/bin/compile @@ -78,12 +78,12 @@ else download_and_install_node $node_version fi - status "Installing dependencies" - npm install --production | indent - status "Rebuilding dependencies" npm rebuild | indent + status "Installing dependencies" + npm install --production | indent + status "Caching node and node_modules for future builds" rm -rf $cache_dir mkdir -p $cache_dir From 2c88fc0bfadde492fc6d73ba30909ec88d4c614e Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 1 Oct 2013 14:50:21 -0700 Subject: [PATCH 076/116] bump stable to 0.10.20 --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index 8c86e00a6..26e6fd883 100755 --- a/bin/compile +++ b/bin/compile @@ -3,7 +3,7 @@ build_dir=$1 cache_basedir=$2 bp_dir=$(cd $(dirname $0); cd ..; pwd) -stable_version="0.10.18" +stable_version="0.10.20" source $bp_dir/bin/common.sh From 12020b35691fd91ecae09cb20b3a7d11b2654119 Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 9 Oct 2013 23:26:24 -0700 Subject: [PATCH 077/116] use semver.io for great success --- bin/common.sh | 51 +- bin/compile | 55 +- bin/test | 67 +- .../README.md | 0 .../package.json | 0 .../README.md | 0 .../package.json | 2 +- .../README.md | 0 test/dangerous-range-star/package.json | 12 + .../README.md | 0 .../package.json | 0 .../README.md | 0 .../package.json | 0 test/stable-node/README.md | 1 + test/stable-node/package.json | 12 + test/unstable-version/README.md | 1 + .../package.json | 0 vendor/jq | Bin 0 -> 616928 bytes vendor/json | 1476 ----------------- vendor/semver/LICENSE | 23 - vendor/semver/README.md | 119 -- vendor/semver/bin/semver | 71 - vendor/semver/package.json | 11 - vendor/semver/semver.js | 304 ---- vendor/semver/test.js | 397 ----- 25 files changed, 114 insertions(+), 2488 deletions(-) rename test/{node-modules-caching => caching}/README.md (100%) rename test/{node-modules-caching => caching}/package.json (100%) rename test/{package-json-invalidversion => dangerous-range-greater-than}/README.md (100%) rename test/{package-json-stable-version => dangerous-range-greater-than}/package.json (91%) rename test/{package-json-noversion => dangerous-range-star}/README.md (100%) create mode 100644 test/dangerous-range-star/package.json rename test/{package-json-stable-version => invalid-node-version}/README.md (100%) rename test/{package-json-invalidversion => invalid-node-version}/package.json (100%) rename test/{package-json-unstable-version => no-version}/README.md (100%) rename test/{package-json-noversion => no-version}/package.json (100%) create mode 100644 test/stable-node/README.md create mode 100644 test/stable-node/package.json create mode 100644 test/unstable-version/README.md rename test/{package-json-unstable-version => unstable-version}/package.json (100%) create mode 100755 vendor/jq delete mode 100755 vendor/json delete mode 100644 vendor/semver/LICENSE delete mode 100644 vendor/semver/README.md delete mode 100755 vendor/semver/bin/semver delete mode 100644 vendor/semver/package.json delete mode 100644 vendor/semver/semver.js delete mode 100644 vendor/semver/test.js diff --git a/bin/common.sh b/bin/common.sh index 1ddde42cb..b8474a8d2 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -1,45 +1,3 @@ -#!/usr/bin/env bash - -# fail fast -set -e - -# Uncomment the line below to enable debugging -# set -x - -download_and_install_node() { - version="$1" - node_url="http://s3pository.heroku.com/node/v$version/node-v$version-linux-x64.tar.gz" - curl $node_url -s -o - | tar xzf - -C $build_dir - mkdir -p $build_dir/vendor - - # Remove node in case we're overwriting a previously-downloaded version - rm -rf $build_dir/vendor/node - - mv $build_dir/node-v$version-linux-x64 $build_dir/vendor/node - chmod +x $build_dir/vendor/node/bin/* - PATH=$PATH:$build_dir/vendor/node/bin -} - -query_stable_version() { - curl -s http://nodejs.org/dist/ \ - | egrep -o '[0-9]+\.[0-9]*[02468]\.[0-9]+' \ - | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \ - | tail -n1 -} - -query_latest_version() { - curl -s http://nodejs.org/dist/ \ - | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \ - | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \ - | tail -n1 -} - -query_all_versions() { - curl -s http://nodejs.org/dist/ \ - | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \ - | sort -u -k 1,1n -k 2,2n -k 3,3n -t . -} - error() { echo " ! $*" >&2 exit 1 @@ -49,6 +7,13 @@ status() { echo "-----> $*" } +protip() { + echo + echo "PRO TIP: $*" | indent + echo "See https://devcenter.heroku.com/articles/nodejs-support" + echo +} + # sed -l basically makes sed replace and buffer through stdin to stdout # so you get updates while the command runs and dont wait for the end # e.g. npm install | indent @@ -60,6 +25,6 @@ indent() { esac } -function cat_npm_debug_log() { +cat_npm_debug_log() { test -f $build_dir/npm-debug.log && cat $build_dir/npm-debug.log } diff --git a/bin/compile b/bin/compile index a094ead63..1a02793fe 100755 --- a/bin/compile +++ b/bin/compile @@ -1,34 +1,55 @@ #!/usr/bin/env bash +# fail fast +set -e + +# enable debugging +# set -x + +# Configure directories build_dir=$1 cache_basedir=$2 bp_dir=$(cd $(dirname $0); cd ..; pwd) +# Load some convenience functions like status() echo(), indent source $bp_dir/bin/common.sh # Output npm debug info on error trap cat_npm_debug_log ERR -# Is a node version specified in package.json? -semver_range=$(cat $build_dir/package.json | $bp_dir/vendor/json engines.node) +# Look in package.json's engines.node field for a semver range +semver_range=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .engines.node) + +# Recommend using semver ranges in a safe manner +if [ "$semver_range" == "null" ]; then + protip "Specify a node version in package.json" + semver_range="" +elif [ "$semver_range" == "*" ]; then + protip "Don't use semver ranges like *" +elif [ ${semver_range:0:1} == ">" ]; then + protip "Don't use semver ranges starting with >" +fi -# Resolve node version using a service -node_version=$(curl --silent https://node-semver-service.heroku.com/${semver_range}) +# Resolve node version using semver.io +semver_url=http://semver.io/node/$semver_range +node_version=$(curl --silent $semver_url) -# Warn if engines.node is unspecified -if ! test $semver_range; then - echo - echo "WARNING: No node version specified in package.json, see:" | indent - echo "https://devcenter.heroku.com/articles/nodejs-support#versions" | indent - echo - status "Defaulting to latest stable node, v$node_version" +if [ "$semver_range" == "" ]; then + status "Defaulting to latest stable node: $node_version" +else + status "Resolved node version for $semver_range is $node_version" fi -# TODO: Warn if engines.node is "*" or ">foo" - -# Download and install node +# Download node from Heroku's S3 mirror of nodejs.org/dist status "Downloading and installing node v$node_version" -download_and_install_node $node_version +node_url="http://s3pository.heroku.com/node/v$node_version/node-v$node_version-linux-x64.tar.gz" +curl $node_url -s -o - | tar xzf - -C $build_dir + +# Move node into ./vendor and make it executable +mkdir -p $build_dir/vendor +mv $build_dir/node-v$node_version-linux-x64 $build_dir/vendor/node +chmod +x $build_dir/vendor/node/bin/* +PATH=$PATH:$build_dir/vendor/node/bin # Run subsequent node/npm commands from the build path cd $build_dir @@ -37,7 +58,7 @@ cd $build_dir package_checksum=$(cat $build_dir/package.json | md5sum | awk '{print $1}') cache_dir="$cache_basedir/$package_checksum" -# Restore from cache package.json hasn't changed +# Restore from cache if package.json hasn't changed if test -d $cache_dir; then status "package.json unchanged since last build" status "Restoring node_modules from cache" @@ -54,7 +75,7 @@ else status "Caching node_modules for future builds" rm -rf $cache_dir mkdir -p $cache_dir - mkdir -p $cache_dir/vendor + # mkdir -p $cache_dir/vendor test -d $build_dir/node_modules && cp -r $build_dir/node_modules $cache_dir/ # cp -r $build_dir/vendor/node $cache_dir/vendor/ fi diff --git a/bin/test b/bin/test index 2ed0d9743..e7f4b4558 100755 --- a/bin/test +++ b/bin/test @@ -2,7 +2,7 @@ # See CONTRIBUTING.md for info on running these tests. testDetectWithPackageJson() { - detect "package-json-stable-version" + detect "stable-node" assertCaptured "Node.js" assertCapturedSuccess } @@ -12,44 +12,59 @@ testDetectWithoutPackageJson() { assertCapturedError 1 "" } -testPackageJsonWithoutVersion() { - compile "package-json-noversion" - assertCaptured "WARNING: No node version specified" +testNoVersion() { + compile "no-version" + assertCaptured "PRO TIP: Specify a node version in package.json" assertCaptured "Defaulting to latest stable node" assertCapturedSuccess } -testPackageJsonWithStableVersion() { - compile "package-json-stable-version" - assertNotCaptured "WARNING: No node version specified" - assertCaptured "satisfies engines.node" - assertCapturedSuccess -} +testDangerousRangeStar() { + compile "dangerous-range-star" + assertCaptured "PRO TIP: Don't use semver ranges like *" -testPackageJsonWithUnstableVersion() { - compile "package-json-unstable-version" - assertCaptured "Downloading and installing node v0.11" assertCapturedSuccess } -testPackageJsonWithInvalidVersion() { - compile "package-json-invalidversion" - assertCapturedError 1 "not found among available versions" +testDangerousRangeGreaterThan() { + compile "dangerous-range-greater-than" + assertCaptured "PRO TIP: Don't use semver ranges starting with >" + assertCaptured "Resolved node version for >" + assertCapturedSuccess } -testProfileCreated() { - compile "package-json-stable-version" - assertCaptured "Building runtime environment" - assertFile "export PATH=\"\$HOME/vendor/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" +testStableVersion() { + compile "stable-node" + assertNotCaptured "PRO TIP" + assertCaptured "Resolved node version" assertCapturedSuccess } -testNodeModulesCached() { - cache=$(mktmpdir) - compile "node-modules-caching" $cache - assertCaptured "Caching node" - assertEquals "1" "$(ls -1 $cache/0.10.18 | wc -l)" -} +# testUnstableVersion() { +# compile "unstable-version" +# assertCaptured "Resolved node version for >0.11.0 is 0.11" + +# assertCapturedSuccess +# } + +# testInvalidVersion() { +# compile "invalid-node-version" +# assertCapturedError 1 "not found among available versions" +# } + +# testProfileCreated() { +# compile "stable-node" +# assertCaptured "Building runtime environment" +# assertFile "export PATH=\"\$HOME/vendor/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" +# assertCapturedSuccess +# } + +# testNodeModulesCached() { +# cache=$(mktmpdir) +# compile "caching" $cache +# assertCaptured "Caching node" +# assertEquals "1" "$(ls -1 $cache/0.10.18 | wc -l)" +# } # Pending diff --git a/test/node-modules-caching/README.md b/test/caching/README.md similarity index 100% rename from test/node-modules-caching/README.md rename to test/caching/README.md diff --git a/test/node-modules-caching/package.json b/test/caching/package.json similarity index 100% rename from test/node-modules-caching/package.json rename to test/caching/package.json diff --git a/test/package-json-invalidversion/README.md b/test/dangerous-range-greater-than/README.md similarity index 100% rename from test/package-json-invalidversion/README.md rename to test/dangerous-range-greater-than/README.md diff --git a/test/package-json-stable-version/package.json b/test/dangerous-range-greater-than/package.json similarity index 91% rename from test/package-json-stable-version/package.json rename to test/dangerous-range-greater-than/package.json index fada51f08..2c7751642 100644 --- a/test/package-json-stable-version/package.json +++ b/test/dangerous-range-greater-than/package.json @@ -7,6 +7,6 @@ "url" : "http://github.com/example/example.git" }, "engines": { - "node": ">0.10.0" + "node": ">0.4" } } diff --git a/test/package-json-noversion/README.md b/test/dangerous-range-star/README.md similarity index 100% rename from test/package-json-noversion/README.md rename to test/dangerous-range-star/README.md diff --git a/test/dangerous-range-star/package.json b/test/dangerous-range-star/package.json new file mode 100644 index 000000000..32841fe12 --- /dev/null +++ b/test/dangerous-range-star/package.json @@ -0,0 +1,12 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "engines": { + "node": "*" + } +} diff --git a/test/package-json-stable-version/README.md b/test/invalid-node-version/README.md similarity index 100% rename from test/package-json-stable-version/README.md rename to test/invalid-node-version/README.md diff --git a/test/package-json-invalidversion/package.json b/test/invalid-node-version/package.json similarity index 100% rename from test/package-json-invalidversion/package.json rename to test/invalid-node-version/package.json diff --git a/test/package-json-unstable-version/README.md b/test/no-version/README.md similarity index 100% rename from test/package-json-unstable-version/README.md rename to test/no-version/README.md diff --git a/test/package-json-noversion/package.json b/test/no-version/package.json similarity index 100% rename from test/package-json-noversion/package.json rename to test/no-version/package.json diff --git a/test/stable-node/README.md b/test/stable-node/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/stable-node/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/stable-node/package.json b/test/stable-node/package.json new file mode 100644 index 000000000..fe43c0312 --- /dev/null +++ b/test/stable-node/package.json @@ -0,0 +1,12 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "engines": { + "node": "~0.10.0" + } +} diff --git a/test/unstable-version/README.md b/test/unstable-version/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/unstable-version/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/package-json-unstable-version/package.json b/test/unstable-version/package.json similarity index 100% rename from test/package-json-unstable-version/package.json rename to test/unstable-version/package.json diff --git a/vendor/jq b/vendor/jq new file mode 100755 index 0000000000000000000000000000000000000000..96241f766694c180c240cb887952f1e04f288330 GIT binary patch literal 616928 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

3Uh;BEzZeO7GSipp_z!}}wKcT$lazd^Y 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∾3Q*o=_-1Q447D@OiuVax{gdi-W;GVI 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 C0UkC0 literal 0 HcmV?d00001 diff --git a/vendor/json b/vendor/json deleted file mode 100755 index 9ca8f080d..000000000 --- a/vendor/json +++ /dev/null @@ -1,1476 +0,0 @@ -#!/usr/bin/env node -// -// json -- a 'json' command for massaging JSON on the command line -// -// See . -// - -var VERSION = "6.0.1"; - -var p = console.warn; -var util = require('util'); -var assert = require('assert'); -var path = require('path'); -var vm = require('vm'); -var fs = require('fs'); -var warn = console.warn; -var EventEmitter = require('events').EventEmitter; - - - -//--- exports for module usage - -exports.main = main; -exports.getVersion = getVersion; -exports.parseLookup = parseLookup; - -// As an exported API, these are still experimental: -exports.lookupDatum = lookupDatum; -exports.printDatum = printDatum; // DEPRECATED - - - -//---- globals and constants - -// Output modes. -var OM_JSONY = 1; -var OM_JSON = 2; -var OM_INSPECT = 3; -var OM_COMPACT = 4; -var OM_FROM_NAME = { - "jsony": OM_JSONY, - "json": OM_JSON, - "inspect": OM_INSPECT, - "compact": OM_COMPACT -} - - - -//---- support functions - -function getVersion() { - return VERSION; -} - -/** - * Return a *shallow* copy of the given object. - * - * Only support objects that you get out of JSON, i.e. no functions. - */ -function objCopy(obj) { - var copy; - if (Array.isArray(obj)) { - copy = obj.slice(); - } else if (typeof(obj) === 'object') { - copy = {}; - Object.keys(obj).forEach(function (k) { - copy[k] = obj[k]; - }); - } else { - copy = obj; // immutable type - } - return copy; -} - -if (util.format) { - format = util.format; -} else { - // From : - var formatRegExp = /%[sdj%]/g; - function format(f) { - if (typeof f !== 'string') { - var objects = []; - for (var i = 0; i < arguments.length; i++) { - objects.push(util.inspect(arguments[i])); - } - return objects.join(' '); - } - var i = 1; - var args = arguments; - var len = args.length; - var str = String(f).replace(formatRegExp, function(x) { - if (i >= len) return x; - switch (x) { - case '%s': return String(args[i++]); - case '%d': return Number(args[i++]); - case '%j': return JSON.stringify(args[i++]); - case '%%': return '%'; - default: - return x; - } - }); - for (var x = args[i]; i < len; x = args[++i]) { - if (x === null || typeof x !== 'object') { - str += ' ' + x; - } else { - str += ' ' + util.inspect(x); - } - } - return str; - }; -} - -/** - * Parse the given string into a JS string. Basically: handle escapes. - */ -function _parseString(s) { - var quoted = '"' + s.replace(/\\"/, '"').replace('"', '\\"') + '"'; - return eval(quoted); -} - -// json_parse.js () -// START json_parse -var json_parse=function(){"use strict";var a,b,c={'"':'"',"\\":"\\","/":"/",b:"\b",f:"\f",n:"\n",r:"\r",t:"\t"},d,e=function(b){throw{name:"SyntaxError",message:b,at:a,text:d}},f=function(c){return c&&c!==b&&e("Expected '"+c+"' instead of '"+b+"'"),b=d.charAt(a),a+=1,b},g=function(){var a,c="";b==="-"&&(c="-",f("-"));while(b>="0"&&b<="9")c+=b,f();if(b==="."){c+=".";while(f()&&b>="0"&&b<="9")c+=b}if(b==="e"||b==="E"){c+=b,f();if(b==="-"||b==="+")c+=b,f();while(b>="0"&&b<="9")c+=b,f()}a=+c;if(!isFinite(a))e("Bad number");else return a},h=function(){var a,d,g="",h;if(b==='"')while(f()){if(b==='"')return f(),g;if(b==="\\"){f();if(b==="u"){h=0;for(d=0;d<4;d+=1){a=parseInt(f(),16);if(!isFinite(a))break;h=h*16+a}g+=String.fromCharCode(h)}else if(typeof c[b]=="string")g+=c[b];else break}else g+=b}e("Bad string")},i=function(){while(b&&b<=" ")f()},j=function(){switch(b){case"t":return f("t"),f("r"),f("u"),f("e"),!0;case"f":return f("f"),f("a"),f("l"),f("s"),f("e"),!1;case"n":return f("n"),f("u"),f("l"),f("l"),null}e("Unexpected '"+b+"'")},k,l=function(){var a=[];if(b==="["){f("["),i();if(b==="]")return f("]"),a;while(b){a.push(k()),i();if(b==="]")return f("]"),a;f(","),i()}}e("Bad array")},m=function(){var a,c={};if(b==="{"){f("{"),i();if(b==="}")return f("}"),c;while(b){a=h(),i(),f(":"),Object.hasOwnProperty.call(c,a)&&e('Duplicate key "'+a+'"'),c[a]=k(),i();if(b==="}")return f("}"),c;f(","),i()}}e("Bad object")};return k=function(){i();switch(b){case"{":return m();case"[":return l();case'"':return h();case"-":return g();default:return b>="0"&&b<="9"?g():j()}},function(c,f){var g;return d=c,a=0,b=" ",g=k(),i(),b&&e("Syntax error"),typeof f=="function"?function h(a,b){var c,d,e=a[b];if(e&&typeof e=="object")for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=h(e,c),d!==undefined?e[c]=d:delete e[c]);return f.call(a,b,e)}({"":g},""):g}}(); -// END json_parse - -function printHelp() { - util.puts("Usage:"); - util.puts(" | json [OPTIONS] [LOOKUPS...]"); - util.puts(" json -f FILE [OPTIONS] [LOOKUPS...]"); - util.puts(""); - util.puts("Pipe in your JSON for pretty-printing, JSON validation, filtering, "); - util.puts("and modification. Supply one or more `LOOKUPS` to extract a "); - util.puts("subset of the JSON. HTTP header blocks are skipped by default."); - util.puts("Roughly in order of processing, features are:"); - util.puts(""); - util.puts("Grouping:"); - util.puts(" Use '-g' or '--group' to group adjacent objects, separated by"); - util.puts(" by no space or a by a newline, or adjacent arrays, separate by"); - util.puts(" by a newline. This can be helpful for, e.g.: "); - util.puts(" $ cat *.json | json -g ... "); - util.puts(" and similar."); - util.puts(""); - util.puts("Execution:"); - util.puts(" Use the '-e CODE' option to execute JavaScript code on the input JSON."); - util.puts(" $ echo '{\"name\":\"trent\",\"age\":38}' | json -e 'age++'"); - util.puts(" {"); - util.puts(" \"name\": \"trent\","); - util.puts(" \"age\": 39"); - util.puts(" }"); - util.puts(" If input is an array, this will automatically process each"); - util.puts(" item separately."); - util.puts(""); - util.puts("Conditional filtering:"); - util.puts(" Use the '-c CODE' option to filter the input JSON."); - util.puts(" $ echo '[{\"age\":38},{\"age\":4}]' | json -c 'age>21'"); - util.puts(" [{\"age\":38}]"); - util.puts(" If input is an array, this will automatically process each"); - util.puts(" item separately. Note: 'CODE' is JavaScript code."); - util.puts(""); - util.puts("Lookups:"); - util.puts(" Use lookup arguments to extract particular values:"); - util.puts(" $ echo '{\"name\":\"trent\",\"age\":38}' | json name"); - util.puts(" trent"); - util.puts(""); - util.puts(" Use '-a' for *array processing* of lookups and *tabular output*:"); - util.puts(" $ echo '{\"name\":\"trent\",\"age\":38}' | json name age"); - util.puts(" trent"); - util.puts(" 38"); - util.puts(" $ echo '[{\"name\":\"trent\",\"age\":38},"); - util.puts(" {\"name\":\"ewan\",\"age\":4}]' | json -a name age"); - util.puts(" trent 38"); - util.puts(" ewan 4"); - util.puts(""); - util.puts("In-place editing:"); - util.puts(" Use '-I, --in-place' to edit a file in place:"); - util.puts(" $ json -I -f config.json # reformat"); - util.puts(" $ json -I -f config.json -c 'this.logLevel=\"debug\"' # add field"); - util.puts(""); - util.puts("Pretty-printing:"); - util.puts(" Output is 'jsony' by default: 2-space indented JSON, except a"); - util.puts(" single string value is printed without quotes."); - util.puts(" $ echo '{\"name\": \"trent\", \"age\": 38}' | json"); - util.puts(" {"); - util.puts(" \"name\": \"trent\","); - util.puts(" \"age\": 38"); - util.puts(" }"); - util.puts(" $ echo '{\"name\": \"trent\", \"age\": 38}' | json name"); - util.puts(" trent"); - util.puts(""); - util.puts(" Use '-j' or '-o json' for explicit JSON, '-o json-N' for N-space indent:"); - util.puts(" $ echo '{\"name\": \"trent\", \"age\": 38}' | json -o json-0"); - util.puts(" {\"name\":\"trent\",\"age\":38}"); - util.puts(""); - util.puts("Options:"); - util.puts(" -h, --help Print this help info and exit."); - util.puts(" --version Print version of this command and exit."); - util.puts(" -q, --quiet Don't warn if input isn't valid JSON."); - util.puts(""); - util.puts(" -f FILE Path to a file to process. If not given, then"); - util.puts(" stdin is used."); - util.puts(" -I, --in-place In-place edit of the file given with '-f'."); - util.puts(" Lookups are not allow with in-place editing"); - util.puts(" because it makes it too easy to lose content."); - util.puts(""); - util.puts(" -H Drop any HTTP header block (as from `curl -i ...`)."); - util.puts(" -g, --group Group adjacent objects or arrays into an array."); - util.puts(" --merge Merge adjacent objects into one. Keys in last "); - util.puts(" object win."); - util.puts(" --deep-merge Same as '--merge', but will recurse into objects "); - util.puts(" under the same key in both.") - util.puts(" -a, --array Process input as an array of separate inputs"); - util.puts(" and output in tabular form."); - util.puts(" -A Process input as a single object, i.e. stop"); - util.puts(" '-e' and '-c' automatically processing each"); - util.puts(" item of an input array."); - util.puts(" -d DELIM Delimiter char for tabular output (default is ' ')."); - util.puts(" -D DELIM Delimiter char between lookups (default is '.'). E.g.:"); - util.puts(" $ echo '{\"a.b\": {\"b\": 1}}' | json -D / a.b/b"); - util.puts(""); - util.puts(" -e CODE Execute the given JavaScript code on the input. If input"); - util.puts(" is an array, then each item of the array is processed"); - util.puts(" separately (use '-A' to override)."); - util.puts(" -c CODE Filter the input with JavaScript `CODE`. If `CODE`"); - util.puts(" returns false-y, then the item is filtered out. If"); - util.puts(" input is an array, then each item of the array is "); - util.puts(" processed separately (use '-A' to override)."); - util.puts(""); - util.puts(" -k, --keys Output the input object's keys."); - util.puts(" -n, --validate Just validate the input (no processing or output)."); - util.puts(" Use with '-q' for silent validation (exit status)."); - util.puts(""); - util.puts(" -o, --output MODE Specify an output mode. One of"); - util.puts(" jsony (default): JSON with string quotes elided"); - util.puts(" json: JSON output, 2-space indent"); - util.puts(" json-N: JSON output, N-space indent, e.g. 'json-4'"); - util.puts(" inspect: node.js `util.inspect` output"); - util.puts(" -i shortcut for `-o inspect`"); - util.puts(" -j shortcut for `-o json`"); - util.puts(""); - util.puts("See for more docs and "); - util.puts(" for project details."); -} - - -/** - * Parse the command-line options and arguments into an object. - * - * { - * 'args': [...] // arguments - * 'help': true, // true if '-h' option given - * // etc. - * } - * - * @return {Object} The parsed options. `.args` is the argument list. - * @throws {Error} If there is an error parsing argv. - */ -function parseArgv(argv) { - var parsed = { - args: [], - help: false, - quiet: false, - dropHeaders: false, - exeSnippets: [], - condSnippets: [], - outputMode: OM_JSONY, - jsonIndent: 2, - array: null, - delim: ' ', - lookupDelim: '.', - outputKeys: false, - group: false, - merge: null, // --merge -> "shallow", --deep-merge -> "deep" - inputFiles: [], - validate: false, - inPlace: false - }; - - // Turn '-iH' into '-i -H', except for argument-accepting options. - var args = argv.slice(2); // drop ['node', 'scriptname'] - var newArgs = []; - var optTakesArg = {'d': true, 'o': true, 'D': true}; - for (var i = 0; i < args.length; i++) { - if (args[i] === '--') { - newArgs = newArgs.concat(args.slice(i)); - break; - } - if (args[i].charAt(0) === "-" && args[i].charAt(1) !== '-' && args[i].length > 2) { - var splitOpts = args[i].slice(1).split(""); - for (var j = 0; j < splitOpts.length; j++) { - newArgs.push('-' + splitOpts[j]) - if (optTakesArg[splitOpts[j]]) { - var optArg = splitOpts.slice(j+1).join(""); - if (optArg.length) { - newArgs.push(optArg); - } - break; - } - } - } else { - newArgs.push(args[i]); - } - } - args = newArgs; - - endOfOptions = false; - while (args.length > 0) { - var arg = args.shift(); - switch(arg) { - case "--": - endOfOptions = true; - break; - case "-h": // display help and exit - case "--help": - parsed.help = true; - break; - case "--version": - parsed.version = true; - break; - case "-q": - case "--quiet": - parsed.quiet = true; - break; - case "-H": // drop any headers - parsed.dropHeaders = true; - break; - case "-o": - case "--output": - var name = args.shift(); - if (!name) { - throw new Error("no argument given for '-o|--output' option"); - } - var idx = name.lastIndexOf('-'); - if (idx !== -1) { - var indent = Number(name.slice(idx+1)); - if (! isNaN(indent)) { - parsed.jsonIndent = indent; - name = name.slice(0, idx); - } - } - parsed.outputMode = OM_FROM_NAME[name]; - if (parsed.outputMode === undefined) { - throw new Error("unknown output mode: '"+name+"'"); - } - break; - case "-I": - case "--in-place": - parsed.inPlace = true; - break; - case "-i": // output with util.inspect - parsed.outputMode = OM_INSPECT; - break; - case "-j": // output with JSON.stringify - parsed.outputMode = OM_JSON; - break; - case "-a": - case "--array": - parsed.array = true; - break; - case "-A": - parsed.array = false; - break; - case "-d": - parsed.delim = _parseString(args.shift()); - break; - case "-D": - parsed.lookupDelim = args.shift(); - if (parsed.lookupDelim.length !== 1) { - throw new Error(format( - "invalid lookup delim '%s' (must be a single char)", - parsed.lookupDelim)); - } - break; - case "-e": - parsed.exeSnippets.push(args.shift()); - break; - case "-c": - parsed.condSnippets.push(args.shift()); - break; - case "-k": - case "--keys": - parsed.outputKeys = true; - break; - case "-g": - case "--group": - parsed.group = true; - break; - case "--merge": - parsed.merge = "shallow"; - break; - case "--deep-merge": - parsed.merge = "deep"; - break; - case "-f": - parsed.inputFiles.push(args.shift()); - break; - case "-n": - case "--validate": - parsed.validate = true; - break; - default: // arguments - if (!endOfOptions && arg.length > 0 && arg[0] === '-') { - throw new Error("unknown option '"+arg+"'"); - } - parsed.args.push(arg); - break; - } - } - - if (parsed.group && parsed.merge) { - throw new Error("cannot use -g|--group and --merge options together"); - } - if (parsed.outputKeys && parsed.args.length > 0) { - throw new Error("cannot use -k|--keys option and lookup arguments together"); - } - if (parsed.inPlace && parsed.inputFiles.length !== 1) { - throw new Error("must specify exactly one file with '-f FILE' to " - + "use -I/--in-place"); - } - if (parsed.inPlace && parsed.args.length > 0) { - throw new Error("lookups cannot be specified with in-place editing " - + "(-I/--in-place), too easy to lose content"); - } - - return parsed; -} - - - -/** - * Streams chunks from given file paths or stdin. - * - * @param opts {Object} Parsed options. - * @returns {Object} An emitter that emits 'chunk', 'error', and 'end'. - * - `emit('chunk', chunk, [obj])` where chunk is a complete block of JSON - * ready to parse. If `obj` is provided, it is the already parsed - * JSON. - * - `emit('error', error)` when an underlying stream emits an error - * - `emit('end')` when all streams are done - */ -function chunkEmitter(opts) { - var emitter = new EventEmitter(); - var streaming = true; - var chunks = []; - var leftover = ''; - var finishedHeaders = false; - - function stripHeaders(s) { - // Take off a leading HTTP header if any and pass it through. - while (true) { - if (s.slice(0,5) === "HTTP/") { - var index = s.indexOf('\r\n\r\n'); - var sepLen = 4; - if (index == -1) { - index = s.indexOf('\n\n'); - sepLen = 2; - } - if (index != -1) { - if (! opts.dropHeaders) { - emit(s.slice(0, index+sepLen)); - } - var is100Continue = (s.slice(0, 21) === "HTTP/1.1 100 Continue"); - s = s.slice(index+sepLen); - if (is100Continue) { - continue; - } - finishedHeaders = true; - } - } else { - finishedHeaders = true; - } - break; - } - //console.warn("stripHeaders done, finishedHeaders=%s", finishedHeaders) - return s; - } - - function emitChunks(block, emitter) { - //console.warn("emitChunks start: block='%s'", block) - var splitter = /(})(\s*\n\s*)?({\s*")/; - var leftTrimmedBlock = block.trimLeft(); - if (leftTrimmedBlock && leftTrimmedBlock[0] !== '{') { - // Currently (at least), only support streaming consecutive *objects*. - streaming = false; - chunks.push(block); - return ''; - } - /* Example: - * > '{"a":"b"}\n{"a":"b"}\n{"a":"b"}'.split(/(})(\s*\n\s*)?({\s*")/) - * [ '{"a":"b"', - * '}', - * '\n', - * '{"', - * 'a":"b"', - * '}', - * '\n', - * '{"', - * 'a":"b"}' ] - */ - var bits = block.split(splitter); - //console.warn("emitChunks: bits (length %d): %j", bits.length, bits); - if (bits.length === 1) { - /* - * An unwanted side-effect of using a regex to find newline-separated - * objects *with a regex*, is that we are looking for the end of one - * object leading into the start of a another. That means that we - * can end up buffering a complete object until a subsequent one - * comes in. If the input stream has large delays between objects, then - * this is unwanted buffering. - * - * One solution would be full stream parsing of objects a la - * . This would nicely also - * remove the artibrary requirement that the input stream be newline - * separated. jsonparse apparently has some issues tho, so I don't - * want to use it right now. It also isn't *small* so not sure I - * want to inline it (`json` doesn't have external deps). - * - * An alternative: The block we have so far one of: - * 1. some JSON that we don't support grouping (e.g. a stream of - * non-objects), - * 2. a JSON object fragment, or - * 3. a complete JSON object (with a possible trailing '{') - * - * If #3, then we can just emit this as a chunk right now. - * - * TODO(PERF): Try out avoiding the first more complete regex split - * for a presumed common case of single-line newline-separated JSON - * objects (e.g. a bunyan log). - */ - // An object must end with '}'. This is an early out to avoid - // `JSON.parse` which I *presuming* is slower. - var trimmed = block.split(/\s*\r?\n/)[0]; - //console.warn("XXX trimmed: '%s'", trimmed); - if (trimmed[trimmed.length - 1] === '}') { - var obj; - try { - obj = JSON.parse(block); - } catch (e) { - /* pass through */ - } - if (obj !== undefined) { - // Emit the parsed `obj` to avoid re-parsing it later. - emitter.emit('chunk', block, obj); - block = ''; - } - } - return block; - } else { - var n = bits.length - 2; - emitter.emit('chunk', bits[0] + bits[1]); - for (var i = 3; i < n; i += 4) { - emitter.emit('chunk', bits[i] + bits[i+1] + bits[i+2]); - } - return bits[n] + bits[n+1]; - } - } - - function addDataListener(stream) { - stream.on('data', function (chunk) { - var s = leftover + chunk; - if (!finishedHeaders) { - s = stripHeaders(s); - } - if (!finishedHeaders) { - leftover = s; - } else { - if (!streaming) { - chunks.push(chunk); - return; - } - leftover = emitChunks(s, emitter); - //console.warn("XXX leftover: '%s'", leftover) - } - }); - } - - if (opts.inputFiles.length > 0) { - // Stream each file in order. - var i = 0; - function addErrorListener(file) { - file.on('error', function (err) { - emitter.emit( - 'error', - format('could not read "%s": %s', opts.inputFiles[i], e) - ); - }); - } - function addEndListener(file) { - file.on('end', function () { - if (i < opts.inputFiles.length) { - var next = opts.inputFiles[i++]; - var nextFile = fs.createReadStream(next, {encoding: 'utf8'}); - addErrorListener(nextFile); - addEndListener(nextFile); - addDataListener(nextFile); - } else { - if (!streaming) { - emitter.emit('chunk', chunks.join('')); - } else if (leftover) { - leftover = emitChunks(leftover, emitter); - emitter.emit('chunk', leftover); - } - emitter.emit('end'); - } - }); - } - var first = fs.createReadStream(opts.inputFiles[i++], {encoding: 'utf8'}); - addErrorListener(first); - addEndListener(first); - addDataListener(first); - } else { - // Streaming from stdin. - var stdin = process.openStdin(); - stdin.setEncoding('utf8'); - addDataListener(stdin); - stdin.on('end', function () { - if (!streaming) { - emitter.emit('chunk', chunks.join('')); - } else if (leftover) { - leftover = emitChunks(leftover, emitter); - emitter.emit('chunk', leftover); - } - emitter.emit('end'); - }); - } - return emitter; -} - -/** - * Get input from either given file paths or stdin. If `opts.inPlace` then - * this calls the callback once for each `opts.inputFiles`. - * - * @param opts {Object} Parsed options. - * @param callback {Function} `function (err, content, filename)` where err - * is an error string if there was a problem, `content` is the read - * content and `filename` is the associated file name from which content - * was loaded if applicable. `filename` is only included when - * `opts.inPlace`. - */ -function getInput(opts, callback) { - if (opts.inputFiles.length === 0) { - // Read from stdin. - var chunks = []; - - var stdin = process.openStdin(); - stdin.setEncoding('utf8'); - stdin.on('data', function (chunk) { - chunks.push(chunk); - }); - - stdin.on('end', function () { - callback(null, chunks.join('')); - }); - } else if (opts.inPlace) { - for (var i = 0; i < opts.inputFiles.length; i++) { - var file = opts.inputFiles[i]; - var content; - try { - content = fs.readFileSync(file, 'utf8'); - } catch (e) { - callback(e, null, file); - } - if (content) { - callback(null, content, file); - } - } - } else { - // Read input files. - var i = 0; - var chunks = []; - try { - for (; i < opts.inputFiles.length; i++) { - chunks.push(fs.readFileSync(opts.inputFiles[i], 'utf8')); - } - } catch (e) { - return callback( - format('could not read "%s": %s', opts.inputFiles[i], e)); - } - callback(null, chunks.join('')); - } -} - - -function isInteger(s) { - return (s.search(/^-?[0-9]+$/) == 0); -} - - -// Parse a lookup string into a list of lookup bits. E.g.: -// "a.b.c" -> ["a","b","c"] -// "b['a']" -> ["b","['a']"] -// Optionally receives an alternative lookup delimiter (other than '.') -function parseLookup(lookup, lookupDelim) { - //var debug = console.warn; - var debug = function () {}; - - var bits = []; - debug("\n*** "+lookup+" ***") - - bits = []; - lookupDelim = lookupDelim || "."; - var bit = ""; - var states = [null]; - var escaped = false; - var ch = null; - for (var i=0; i < lookup.length; ++i) { - var escaped = (!escaped && ch === '\\'); - var ch = lookup[i]; - debug("-- i="+i+", ch="+JSON.stringify(ch)+" escaped="+JSON.stringify(escaped)) - debug("states: "+JSON.stringify(states)) - - if (escaped) { - bit += ch; - continue; - } - - switch (states[states.length-1]) { - case null: - switch (ch) { - case '"': - case "'": - states.push(ch); - bit += ch; - break; - case '[': - states.push(ch); - if (bit !== "") { - bits.push(bit); - bit = "" - } - bit += ch; - break; - case lookupDelim: - if (bit !== "") { - bits.push(bit); - bit = "" - } - break; - default: - bit += ch; - break; - } - break; - - case '[': - bit += ch; - switch (ch) { - case '"': - case "'": - case '[': - states.push(ch); - break; - case ']': - states.pop(); - if (states[states.length-1] === null) { - bits.push(bit); - bit = "" - } - break; - } - break; - - case '"': - bit += ch; - switch (ch) { - case '"': - states.pop(); - if (states[states.length-1] === null) { - bits.push(bit); - bit = "" - } - break; - } - break; - - case "'": - bit += ch; - switch (ch) { - case "'": - states.pop(); - if (states[states.length-1] === null) { - bits.push(bit); - bit = "" - } - break; - } - break; - } - debug("bit: "+JSON.stringify(bit)) - debug("bits: "+JSON.stringify(bits)) - } - - if (bit !== "") { - bits.push(bit); - bit = "" - } - - debug(JSON.stringify(lookup)+" -> "+JSON.stringify(bits)) - return bits -} - - -/** - * Parse the given stdin input into: - * { - * "error": ... error object if there was an error ..., - * "datum": ... parsed object if content was JSON ... - * } - * - * @param buffer {String} The text to parse as JSON. - * @param obj {Object} Optional. Some streaming code paths will provide - * this, an already parsed JSON object. Use this to avoid reparsing. - * @param group {Boolean} Default false. If true, then non-JSON input - * will be attempted to be "arrayified" (see inline comment). - * @param merge {Boolean} Default null. Can be "shallow" or "deep". An - * attempt will be made to interpret the input as adjacent objects to - * be merged, last key wins. See inline comment for limitations. - */ -function parseInput(buffer, obj, group, merge) { - if (obj) { - return {datum: obj}; - } else if (group) { - /** - * Special case: Grouping (previously called auto-arrayification) - * of unjoined list of objects: - * {"one": 1}{"two": 2} - * and auto-concatenation of unjoined list of arrays: - * ["a", "b"]["c", "d"] - * - * This can be nice to process a stream of JSON objects generated from - * multiple calls to another tool or `cat *.json | json`. - * - * Rules: - * - Only JS objects and arrays. Don't see strong need for basic - * JS types right now and this limitation simplifies. - * - The break between JS objects has to include a newline: - * {"one": 1} - * {"two": 2} - * or no spaces at all: - * {"one": 1}{"two": 2} - * I.e., not this: - * {"one": 1} {"two": 2} - * This condition should be fine for typical use cases and ensures - * no false matches inside JS strings. - * - The break between JS *arrays* has to include a newline: - * ["one", "two"] - * ["three"] - * The "no spaces" case is NOT supported for JS arrays as of v6.0.0 - * because shows that that - * is not safe. - */ - var newBuffer = buffer; - [/(})\s*\n\s*({)/g, /(})({")/g].forEach(function (pat) { - newBuffer = newBuffer.replace(pat, "$1,\n$2"); - }); - [/(\])\s*\n\s*(\[)/g].forEach(function (pat) { - newBuffer = newBuffer.replace(pat, ",\n"); - }); - newBuffer = newBuffer.trim(); - if (newBuffer[0] !== '[') { - newBuffer = '[\n' + newBuffer; - } - if (newBuffer.slice(-1) !== ']') { - newBuffer = newBuffer + '\n]\n'; - } - try { - return {datum: JSON.parse(newBuffer)}; - } catch (e2) { - return {error: e2}; - } - } else if (merge) { - // See the "Rules" above for limitations on boundaries for "adjacent" - // objects: KISS. - var newBuffer = buffer; - [/(})\s*\n\s*({)/g, /(})({")/g].forEach(function (pat) { - newBuffer = newBuffer.replace(pat, "$1,\n$2"); - }); - newBuffer = '[\n' + newBuffer + '\n]\n'; - var objs; - try { - objs = JSON.parse(newBuffer); - } catch(e) { - return {error: e}; - } - var merged = objs[0]; - if (merge === "shallow") { - for (var i = 1; i < objs.length; i++) { - var obj = objs[i]; - Object.keys(obj).forEach(function (k) { - merged[k] = obj[k]; - }); - } - } else if (merge === "deep") { - function deepExtend(a, b) { - Object.keys(b).forEach(function (k) { - if (a[k] && b[k] && toString.call(a[k]) === '[object Object]' - && toString.call(b[k]) === '[object Object]') { - deepExtend(a[k], b[k]) - } else { - a[k] = b[k]; - } - }); - } - for (var i = 1; i < objs.length; i++) { - deepExtend(merged, objs[i]); - } - } else { - throw new Error(format('unknown value for "merge": "%s"', merge)); - } - return {datum: merged}; - } else { - try { - return {datum: JSON.parse(buffer)}; - } catch(e) { - return {error: e}; - } - } -} - - -/** - * Apply a lookup to the given datum. - * - * @argument datum {Object} - * @argument lookup {Array} The parsed lookup (from - * `parseLookup(, )`). Might be empty. - * @returns {Object} The result of the lookup. - */ -function lookupDatum(datum, lookup) { - // Put it back together with some convenience transformations. - var lookupCode = ""; - var isJSIdentifier = /^[$A-Za-z_][0-9A-Za-z_]*$/; - var isNegArrayIndex = /^-\d+$/; - for (var i=0; i < lookup.length; i++) { - var bit = lookup[i]; - if (bit[0] === '[') { - lookupCode += bit; - // Support Python-style negative array indexing. - } else if (bit === '-1') { - lookupCode += '.slice(-1)[0]'; - } else if (isNegArrayIndex.test(bit)) { - lookupCode += format('.slice(%s, %d)[0]', bit, Number(bit) + 1); - } else if (! isJSIdentifier.test(bit)) { - // Allow a non-JS-indentifier token, e.g. `json foo-bar`. This also - // works for array index lookups: `json 0` becomes a `["0"]` lookup. - lookupCode += '["' + bit.replace(/"/g, '\\"') + '"]'; - } else { - lookupCode += '.' + bit; - } - } - try { - return vm.runInNewContext("(" + JSON.stringify(datum) + ")" + lookupCode); - } catch (e) { - if (e.name === 'TypeError') { - // Skip the following for a lookup 'foo.bar' where 'foo' is undefined: - // TypeError: Cannot read property 'bar' of undefined - // TODO: Are there other potential TypeError's in here to avoid? - return undefined; - } - throw e; - } -} - - -/** - * Output the given datasets. - * - * @param datasets {Array} Array of data sets to print, in the form: - * `[ [, , ], ... ]` - * @param filename {String} The filename to which to write the output. If - * not set, then emit to stdout. - * @param headers {String} The HTTP header block string, if any, to emit - * first. - * @param opts {Object} Parsed tool options. - */ -function printDatasets(datasets, filename, headers, opts) { - var isTTY = (filename ? false : process.stdout.isTTY) - var write = emit; - if (filename) { - var tmpPath = path.resolve(path.dirname(filename), - format('.%s-json-%s-%s.tmp', path.basename(filename), process.pid, - Date.now())); - var stats = fs.statSync(filename); - var f = fs.createWriteStream(tmpPath, - {encoding: 'utf8', mode: stats.mode}); - write = f.write.bind(f); - } - if (headers && headers.length > 0) { - write(headers) - } - for (var i = 0; i < datasets.length; i++) { - var dataset = datasets[i]; - var output = stringifyDatum(dataset[0], opts, isTTY); - var sep = dataset[1]; - if (output && output.length) { - write(output); - write(sep); - } else if (dataset[2]) { - write(sep); - } - } - if (filename) { - f.end(); - fs.renameSync(tmpPath, filename); - if (! opts.quiet) { - warn('json: updated "%s" in-place', filename); - } - } -} - - -/** - * Stringify the given datum according to the given output options. - */ -function stringifyDatum(datum, opts, isTTY) { - var output = null; - switch (opts.outputMode) { - case OM_INSPECT: - output = util.inspect(datum, false, Infinity, isTTY); - break; - case OM_JSON: - if (typeof datum !== 'undefined') { - output = JSON.stringify(datum, null, opts.jsonIndent); - } - break; - case OM_COMPACT: - // Dev Note: A still relatively experimental attempt at a more - // compact ouput somewhat a la Python's repr of a dict. I.e. try to - // fit elements on one line as much as reasonable. - if (datum === undefined) { - // pass - } else if (Array.isArray(datum)) { - var bits = ['[\n']; - datum.forEach(function (d) { - bits.push(' ') - bits.push(JSON.stringify(d, null, 0).replace(/,"(?![,:])/g, ', "')); - bits.push(',\n'); - }); - bits.push(bits.pop().slice(0, -2) + '\n') // drop last comma - bits.push(']'); - output = bits.join(''); - } else { - output = JSON.stringify(datum, null, 0); - } - break; - case OM_JSONY: - if (typeof datum === 'string') { - output = datum; - } else if (typeof datum !== 'undefined') { - output = JSON.stringify(datum, null, opts.jsonIndent); - } - break; - default: - throw new Error("unknown output mode: "+opts.outputMode); - } - return output; -} - - -/** - * Print out a single result, considering input options. - * - * @deprecated - */ -function printDatum(datum, opts, sep, alwaysPrintSep) { - var output = stringifyDatum(datum, opts); - if (output && output.length) { - emit(output); - emit(sep); - } else if (alwaysPrintSep) { - emit(sep); - } -} - - -var stdoutFlushed = true; -function emit(s) { - // TODO:PERF If this is try/catch is too slow (too granular): move up to - // mainline and be sure to only catch this particular error. - try { - stdoutFlushed = process.stdout.write(s); - } catch (e) { - // Handle any exceptions in stdout writing in the "error" event above. - } -} - -process.stdout.on("error", function (err) { - if (err.code === "EPIPE") { - // See . - drainStdoutAndExit(0); - } else { - warn(err) - drainStdoutAndExit(1); - } -}); - - -/** - * A hacked up version of "process.exit" that will first drain stdout - * before exiting. *WARNING: This doesn't stop event processing.* IOW, - * callers have to be careful that code following this call isn't - * accidentally executed. - * - * In node v0.6 "process.stdout and process.stderr are blocking when they - * refer to regular files or TTY file descriptors." However, this hack might - * still be necessary in a shell pipeline. - */ -function drainStdoutAndExit(code) { - process.stdout.on('drain', function () { - process.exit(code); - }); - if (stdoutFlushed) { - process.exit(code); - } -} - - - - -//---- mainline - -function main(argv) { - var opts; - try { - opts = parseArgv(argv); - } catch (e) { - warn("json: error: %s", e.message) - return drainStdoutAndExit(1); - } - //warn(opts); - if (opts.help) { - printHelp(); - return; - } - if (opts.version) { - util.puts("json " + getVersion()); - return; - } - var lookupStrs = opts.args; - - if (opts.group && opts.array && opts.outputMode !== OM_JSON) { - // streaming - var chunker = chunkEmitter(opts); - chunker.on('error', function(error) { - warn("json: error: %s", err); - return drainStdoutAndExit(1); - }); - chunker.on('chunk', parseChunk); - } else if (opts.inPlace) { - assert.equal(opts.inputFiles.length, 1, - 'cannot handle more than one file with -I'); - getInput(opts, function (err, content, filename) { - if (err) { - warn("json: error: %s", err) - return drainStdoutAndExit(1); - } - - // Take off a leading HTTP header if any and pass it through. - var headers = []; - while (true) { - if (content.slice(0,5) === "HTTP/") { - var index = content.indexOf('\r\n\r\n'); - var sepLen = 4; - if (index == -1) { - index = content.indexOf('\n\n'); - sepLen = 2; - } - if (index != -1) { - if (! opts.dropHeaders) { - headers.push(content.slice(0, index+sepLen)); - } - var is100Continue = (content.slice(0, 21) === "HTTP/1.1 100 Continue"); - content = content.slice(index+sepLen); - if (is100Continue) { - continue; - } - } - } - break; - } - parseChunk(content, undefined, filename, headers.join('')); - }); - } else { - // not streaming - getInput(opts, function (err, buffer) { - if (err) { - warn("json: error: %s", err) - return drainStdoutAndExit(1); - } - // Take off a leading HTTP header if any and pass it through. - while (true) { - if (buffer.slice(0,5) === "HTTP/") { - var index = buffer.indexOf('\r\n\r\n'); - var sepLen = 4; - if (index == -1) { - index = buffer.indexOf('\n\n'); - sepLen = 2; - } - if (index != -1) { - if (! opts.dropHeaders) { - emit(buffer.slice(0, index+sepLen)); - } - var is100Continue = (buffer.slice(0, 21) === "HTTP/1.1 100 Continue"); - buffer = buffer.slice(index+sepLen); - if (is100Continue) { - continue; - } - } - } - break; - } - parseChunk(buffer); - }); - } - - /** - * Parse a single chunk of JSON. This may be called more than once - * (when streaming or when operating on multiple files). - * - * @param chunk {String} The JSON-encoded string. - * @param obj {Object} Optional. For some code paths while streaming `obj` - * will be provided. This is an already parsed JSON object. - * @param filename {String} Optional. The filename from which this content - * came, if relevant. This is only set if `opts.inPlace`. - * @param headers {String} Optional. Leading HTTP headers, if any to emit. - */ - function parseChunk(chunk, obj, filename, headers) { - // Expect the chunk to be JSON. - if (! chunk.length) { - return; - } - // parseInput() -> {datum: , error: } - var input = parseInput(chunk, obj, opts.group, opts.merge); - if (input.error) { - // Doesn't look like JSON. Just print it out and move on. - if (! opts.quiet) { - // Use JSON-js' "json_parse" parser to get more detail on the - // syntax error. - var details = ""; - var normBuffer = chunk.replace(/\r\n|\n|\r/, '\n'); - try { - json_parse(normBuffer); - details = input.error; - } catch(err) { - // err.at has the position. Get line/column from that. - var at = err.at - 1; // `err.at` looks to be 1-based. - var lines = chunk.split('\n'); - var line, col, pos = 0; - for (line = 0; line < lines.length; line++) { - pos += lines[line].length + 1; - if (pos > at) { - col = at - (pos - lines[line].length - 1); - break; - } - } - var spaces = ''; - for (var i=0; i for why. - data = data[lookups[0].join('.')]; - } else if (lookupsAreIndeces) { - // Special case: Lookups that are all indeces into an input array - // are more likely to be wanted as an array of selected items rather - // than a "JSON table" thing that we use otherwise. - var flattened = []; - for (i = 0; i < lookups.length; i++) { - var lookupStr = lookups[i].join('.'); - if (data.hasOwnProperty(lookupStr)) { - flattened.push(data[lookupStr]) - } - } - data = flattened; - } - // If JSON output mode, then always just output full set of data to - // ensure valid JSON output. - datasets.push([data, '\n', false]); - } else if (lookups.length) { - if (opts.array) { - // Output `data` as a "table" of lookup results. - for (j = 0; j < data.length; j++) { - var row = data[j]; - for (i = 0; i < lookups.length-1; i++) { - datasets.push([row[lookups[i].join('.')], opts.delim, true]); - } - datasets.push([row[lookups[i].join('.')], '\n', true]); - } - } else { - for (i = 0; i < lookups.length; i++) { - datasets.push([data[lookups[i].join('.')], '\n', false]); - } - } - } else if (opts.array) { - if (!Array.isArray(data)) data = [data]; - for (j = 0; j < data.length; j++) { - datasets.push([data[j], '\n', false]); - } - } else { - // Output `data` as is. - datasets.push([data, '\n', false]); - } - printDatasets(datasets, filename, headers, opts); - } -} - -if (require.main === module) { - // HACK guard for . - // We override the `process.stdout.end` guard that core node.js puts in - // place. The real fix is that `.end()` shouldn't be called on stdout - // in node core. Hopefully node v0.6.9 will fix that. Only guard - // for v0.6.0..v0.6.8. - var nodeVer = process.versions.node.split('.').map(Number); - if ([0,6,0] <= nodeVer && nodeVer <= [0,6,8]) { - var stdout = process.stdout; - stdout.end = stdout.destroy = stdout.destroySoon = function() { - /* pass */ - }; - } - - main(process.argv); -} diff --git a/vendor/semver/LICENSE b/vendor/semver/LICENSE deleted file mode 100644 index 05a401094..000000000 --- a/vendor/semver/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright 2009, 2010, 2011 Isaac Z. Schlueter. -All rights reserved. - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/semver/README.md b/vendor/semver/README.md deleted file mode 100644 index 6fa37a3d8..000000000 --- a/vendor/semver/README.md +++ /dev/null @@ -1,119 +0,0 @@ -semver(1) -- The semantic versioner for npm -=========================================== - -## Usage - - $ npm install semver - - semver.valid('1.2.3') // true - semver.valid('a.b.c') // false - semver.clean(' =v1.2.3 ') // '1.2.3' - semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3') // true - semver.gt('1.2.3', '9.8.7') // false - semver.lt('1.2.3', '9.8.7') // true - -As a command-line utility: - - $ semver -h - - Usage: semver -v [-r ] - Test if version(s) satisfy the supplied range(s), - and sort them. - - Multiple versions or ranges may be supplied. - - Program exits successfully if any valid version satisfies - all supplied ranges, and prints all satisfying versions. - - If no versions are valid, or ranges are not satisfied, - then exits failure. - - Versions are printed in ascending order, so supplying - multiple versions to the utility will just sort them. - -## Versions - -A version is the following things, in this order: - -* a number (Major) -* a period -* a number (minor) -* a period -* a number (patch) -* OPTIONAL: a hyphen, followed by a number (build) -* OPTIONAL: a collection of pretty much any non-whitespace characters - (tag) - -A leading `"="` or `"v"` character is stripped off and ignored. - -## Comparisons - -The ordering of versions is done using the following algorithm, given -two versions and asked to find the greater of the two: - -* If the majors are numerically different, then take the one - with a bigger major number. `2.3.4 > 1.3.4` -* If the minors are numerically different, then take the one - with the bigger minor number. `2.3.4 > 2.2.4` -* If the patches are numerically different, then take the one with the - bigger patch number. `2.3.4 > 2.3.3` -* If only one of them has a build number, then take the one with the - build number. `2.3.4-0 > 2.3.4` -* If they both have build numbers, and the build numbers are numerically - different, then take the one with the bigger build number. - `2.3.4-10 > 2.3.4-9` -* If only one of them has a tag, then take the one without the tag. - `2.3.4 > 2.3.4-beta` -* If they both have tags, then take the one with the lexicographically - larger tag. `2.3.4-beta > 2.3.4-alpha` -* At this point, they're equal. - -## Ranges - -The following range styles are supported: - -* `>1.2.3` Greater than a specific version. -* `<1.2.3` Less than -* `1.2.3 - 2.3.4` := `>=1.2.3 <=2.3.4` -* `~1.2.3` := `>=1.2.3 <1.3.0` -* `~1.2` := `>=1.2.0 <2.0.0` -* `~1` := `>=1.0.0 <2.0.0` -* `1.2.x` := `>=1.2.0 <1.3.0` -* `1.x` := `>=1.0.0 <2.0.0` - -Ranges can be joined with either a space (which implies "and") or a -`||` (which implies "or"). - -## Functions - -* valid(v): Return the parsed version, or null if it's not valid. -* inc(v, release): Return the version incremented by the release type - (major, minor, patch, or build), or null if it's not valid. - -### Comparison - -* gt(v1, v2): `v1 > v2` -* gte(v1, v2): `v1 >= v2` -* lt(v1, v2): `v1 < v2` -* lte(v1, v2): `v1 <= v2` -* eq(v1, v2): `v1 == v2` This is true if they're logically equivalent, - even if they're not the exact same string. You already know how to - compare strings. -* neq(v1, v2): `v1 != v2` The opposite of eq. -* cmp(v1, comparator, v2): Pass in a comparison string, and it'll call - the corresponding function above. `"==="` and `"!=="` do simple - string comparison, but are included for completeness. Throws if an - invalid comparison string is provided. -* compare(v1, v2): Return 0 if v1 == v2, or 1 if v1 is greater, or -1 if - v2 is greater. Sorts in ascending order if passed to Array.sort(). -* rcompare(v1, v2): The reverse of compare. Sorts an array of versions - in descending order when passed to Array.sort(). - - -### Ranges - -* validRange(range): Return the valid range or null if it's not valid -* satisfies(version, range): Return true if the version satisfies the - range. -* maxSatisfying(versions, range): Return the highest version in the list - that satisfies the range, or null if none of them do. diff --git a/vendor/semver/bin/semver b/vendor/semver/bin/semver deleted file mode 100755 index 3e6afb40d..000000000 --- a/vendor/semver/bin/semver +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node -// Standalone semver comparison program. -// Exits successfully and prints matching version(s) if -// any supplied version is valid and passes all tests. - -var argv = process.argv.slice(2) - , versions = [] - , range = [] - , gt = [] - , lt = [] - , eq = [] - , semver = require("../semver") - -main() - -function main () { - if (!argv.length) return help() - while (argv.length) { - var a - switch (a = argv.shift()) { - case "-v": case "--version": - versions.push(argv.shift()) - break - case "-r": case "--range": - range.push(argv.shift()) - break - case "-h": case "--help": case "-?": - return help() - default: - versions.push(a) - break - } - } - - versions = versions.filter(semver.valid) - for (var i = 0, l = range.length; i < l ; i ++) { - versions = versions.filter(function (v) { - return semver.satisfies(v, range[i]) - }) - if (!versions.length) return fail() - } - return success(versions) -} - -function fail () { process.exit(1) } - -function success () { - versions.sort(semver.compare) - .map(semver.clean) - .forEach(function (v,i,_) { console.log(v) }) -} - -function help () { - console.log(["Usage: semver -v [-r ]" - ,"Test if version(s) satisfy the supplied range(s)," - ,"and sort them." - ,"" - ,"Multiple versions or ranges may be supplied." - ,"" - ,"Program exits successfully if any valid version satisfies" - ,"all supplied ranges, and prints all satisfying versions." - ,"" - ,"If no versions are valid, or ranges are not satisfied," - ,"then exits failure." - ,"" - ,"Versions are printed in ascending order, so supplying" - ,"multiple versions to the utility will just sort them." - ].join("\n")) -} - - diff --git a/vendor/semver/package.json b/vendor/semver/package.json deleted file mode 100644 index cd10e341c..000000000 --- a/vendor/semver/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ "name" : "semver" -, "version" : "1.0.12" -, "description" : "The semantic version parser used by npm." -, "main" : "semver.js" -, "scripts" : { "test" : "tap test.js" } -, "devDependencies": { "tap" : "0.x >=0.0.4" } -, "license" : - { "type" : "MIT" - , "url" : "https://github.com/isaacs/semver/raw/master/LICENSE" } -, "repository" : "git://github.com/isaacs/node-semver.git" -, "bin" : { "semver" : "./bin/semver" } } diff --git a/vendor/semver/semver.js b/vendor/semver/semver.js deleted file mode 100644 index ab9c65246..000000000 --- a/vendor/semver/semver.js +++ /dev/null @@ -1,304 +0,0 @@ -;(function (exports) { // nothing in here is node-specific. - -// See http://semver.org/ -// This implementation is a *hair* less strict in that it allows -// v1.2.3 things, and also tags that don't begin with a char. - -var semver = "\\s*[v=]*\\s*([0-9]+)" // major - + "\\.([0-9]+)" // minor - + "\\.([0-9]+)" // patch - + "(-[0-9]+-?)?" // build - + "([a-zA-Z-][a-zA-Z0-9-\.:]*)?" // tag - , exprComparator = "^((<|>)?=?)\s*("+semver+")$|^$" - , xRangePlain = "[v=]*([0-9]+|x|X|\\*)" - + "(?:\\.([0-9]+|x|X|\\*)" - + "(?:\\.([0-9]+|x|X|\\*)" - + "([a-zA-Z-][a-zA-Z0-9-\.:]*)?)?)?" - , xRange = "((?:<|>)?=?)?\\s*" + xRangePlain - , exprSpermy = "(?:~>?)"+xRange - , expressions = exports.expressions = - { parse : new RegExp("^\\s*"+semver+"\\s*$") - , parsePackage : new RegExp("^\\s*([^\/]+)[-@](" +semver+")\\s*$") - , parseRange : new RegExp( - "^\\s*(" + semver + ")\\s+-\\s+(" + semver + ")\\s*$") - , validComparator : new RegExp("^"+exprComparator+"$") - , parseXRange : new RegExp("^"+xRange+"$") - , parseSpermy : new RegExp("^"+exprSpermy+"$") - } - - -Object.getOwnPropertyNames(expressions).forEach(function (i) { - exports[i] = function (str) { - return ("" + (str || "")).match(expressions[i]) - } -}) - -exports.rangeReplace = ">=$1 <=$7" -exports.clean = clean -exports.compare = compare -exports.satisfies = satisfies -exports.gt = gt -exports.gte = gte -exports.lt = lt -exports.lte = lte -exports.eq = eq -exports.neq = neq -exports.cmp = cmp -exports.inc = inc - -exports.valid = valid -exports.validPackage = validPackage -exports.validRange = validRange -exports.maxSatisfying = maxSatisfying - -exports.replaceStars = replaceStars -exports.toComparators = toComparators - -function stringify (version) { - var v = version - return [v[1]||'', v[2]||'', v[3]||''].join(".") + (v[4]||'') + (v[5]||'') -} - -function clean (version) { - version = exports.parse(version) - if (!version) return version - return stringify(version) -} - -function valid (version) { - if (typeof version !== "string") return null - return exports.parse(version) && version.trim().replace(/^[v=]+/, '') -} - -function validPackage (version) { - if (typeof version !== "string") return null - return version.match(expressions.parsePackage) && version.trim() -} - -// range can be one of: -// "1.0.3 - 2.0.0" range, inclusive, like ">=1.0.3 <=2.0.0" -// ">1.0.2" like 1.0.3 - 9999.9999.9999 -// ">=1.0.2" like 1.0.2 - 9999.9999.9999 -// "<2.0.0" like 0.0.0 - 1.9999.9999 -// ">1.0.2 <2.0.0" like 1.0.3 - 1.9999.9999 -var starExpression = /(<|>)?=?\s*\*/g - , starReplace = "" - , compTrimExpression = new RegExp("((<|>)?=?)\\s*(" - +semver+"|"+xRangePlain+")", "g") - , compTrimReplace = "$1$3" - -function toComparators (range) { - var ret = (range || "").trim() - .replace(expressions.parseRange, exports.rangeReplace) - .replace(compTrimExpression, compTrimReplace) - .split(/\s+/) - .join(" ") - .split("||") - .map(function (orchunk) { - return orchunk - .split(" ") - .map(replaceXRanges) - .map(replaceSpermies) - .map(replaceStars) - .join(" ").trim() - }) - .map(function (orchunk) { - return orchunk - .trim() - .split(/\s+/) - .filter(function (c) { return c.match(expressions.validComparator) }) - }) - .filter(function (c) { return c.length }) - return ret -} - -function replaceStars (stars) { - return stars.trim().replace(starExpression, starReplace) -} - -// "2.x","2.x.x" --> ">=2.0.0- <2.1.0-" -// "2.3.x" --> ">=2.3.0- <2.4.0-" -function replaceXRanges (ranges) { - return ranges.split(/\s+/) - .map(replaceXRange) - .join(" ") -} - -function replaceXRange (version) { - return version.trim().replace(expressions.parseXRange, - function (v, gtlt, M, m, p, t) { - var anyX = !M || M.toLowerCase() === "x" || M === "*" - || !m || m.toLowerCase() === "x" || m === "*" - || !p || p.toLowerCase() === "x" || p === "*" - , ret = v - - if (gtlt && anyX) { - // just replace x'es with zeroes - ;(!M || M === "*" || M.toLowerCase() === "x") && (M = 0) - ;(!m || m === "*" || m.toLowerCase() === "x") && (m = 0) - ;(!p || p === "*" || p.toLowerCase() === "x") && (p = 0) - ret = gtlt + M+"."+m+"."+p+"-" - } else if (!M || M === "*" || M.toLowerCase() === "x") { - ret = "*" // allow any - } else if (!m || m === "*" || m.toLowerCase() === "x") { - // append "-" onto the version, otherwise - // "1.x.x" matches "2.0.0beta", since the tag - // *lowers* the version value - ret = ">="+M+".0.0- <"+(+M+1)+".0.0-" - } else if (!p || p === "*" || p.toLowerCase() === "x") { - ret = ">="+M+"."+m+".0- <"+M+"."+(+m+1)+".0-" - } - //console.error("parseXRange", [].slice.call(arguments), ret) - return ret - }) -} - -// ~, ~> --> * (any, kinda silly) -// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 -// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 -// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 -// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 -// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 -function replaceSpermies (version) { - return version.trim().replace(expressions.parseSpermy, - function (v, gtlt, M, m, p, t) { - if (gtlt) throw new Error( - "Using '"+gtlt+"' with ~ makes no sense. Don't do it.") - - if (!M || M.toLowerCase() === "x") { - return "" - } - // ~1 == >=1.0.0- <2.0.0- - if (!m || m.toLowerCase() === "x") { - return ">="+M+".0.0- <"+(+M+1)+".0.0-" - } - // ~1.2 == >=1.2.0- <1.3.0- - if (!p || p.toLowerCase() === "x") { - return ">="+M+"."+m+".0- <"+M+"."+(+m+1)+".0-" - } - // ~1.2.3 == >=1.2.3- <1.3.0- - t = t || "-" - return ">="+M+"."+m+"."+p+t+" <"+M+"."+(+m+1)+".0-" - }) -} - -function validRange (range) { - range = replaceStars(range) - var c = toComparators(range) - return (c.length === 0) - ? null - : c.map(function (c) { return c.join(" ") }).join("||") -} - -// returns the highest satisfying version in the list, or undefined -function maxSatisfying (versions, range) { - return versions - .filter(function (v) { return satisfies(v, range) }) - .sort(compare) - .pop() -} -function satisfies (version, range) { - version = valid(version) - if (!version) return false - range = toComparators(range) - for (var i = 0, l = range.length ; i < l ; i ++) { - var ok = false - for (var j = 0, ll = range[i].length ; j < ll ; j ++) { - var r = range[i][j] - , gtlt = r.charAt(0) === ">" ? gt - : r.charAt(0) === "<" ? lt - : false - , eq = r.charAt(!!gtlt) === "=" - , sub = (!!eq) + (!!gtlt) - if (!gtlt) eq = true - r = r.substr(sub) - r = (r === "") ? r : valid(r) - ok = (r === "") || (eq && r === version) || (gtlt && gtlt(version, r)) - if (!ok) break - } - if (ok) return true - } - return false -} - -// return v1 > v2 ? 1 : -1 -function compare (v1, v2) { - var g = gt(v1, v2) - return g === null ? 0 : g ? 1 : -1 -} - -function rcompare (v1, v2) { - return compare(v2, v1) -} - -function lt (v1, v2) { return gt(v2, v1) } -function gte (v1, v2) { return !lt(v1, v2) } -function lte (v1, v2) { return !gt(v1, v2) } -function eq (v1, v2) { return gt(v1, v2) === null } -function neq (v1, v2) { return gt(v1, v2) !== null } -function cmp (v1, c, v2) { - switch (c) { - case ">": return gt(v1, v2) - case "<": return lt(v1, v2) - case ">=": return gte(v1, v2) - case "<=": return lte(v1, v2) - case "==": return eq(v1, v2) - case "!=": return neq(v1, v2) - case "===": return v1 === v2 - case "!==": return v1 !== v2 - default: throw new Error("Y U NO USE VALID COMPARATOR!? "+c) - } -} - -// return v1 > v2 -function num (v) { - return v === undefined ? -1 : parseInt((v||"0").replace(/[^0-9]+/g, ''), 10) -} -function gt (v1, v2) { - v1 = exports.parse(v1) - v2 = exports.parse(v2) - if (!v1 || !v2) return false - - for (var i = 1; i < 5; i ++) { - v1[i] = num(v1[i]) - v2[i] = num(v2[i]) - if (v1[i] > v2[i]) return true - else if (v1[i] !== v2[i]) return false - } - // no tag is > than any tag, or use lexicographical order. - var tag1 = v1[5] || "" - , tag2 = v2[5] || "" - - // kludge: null means they were equal. falsey, and detectable. - // embarrassingly overclever, though, I know. - return tag1 === tag2 ? null - : !tag1 ? true - : !tag2 ? false - : tag1 > tag2 -} - -function inc (version, release) { - version = exports.parse(version) - if (!version) return null - - var parsedIndexLookup = - { 'major': 1 - , 'minor': 2 - , 'patch': 3 - , 'build': 4 } - var incIndex = parsedIndexLookup[release] - if (incIndex === undefined) return null - - var current = num(version[incIndex]) - version[incIndex] = current === -1 ? 1 : current + 1 - - for (var i = incIndex + 1; i < 5; i ++) { - if (num(version[i]) !== -1) version[i] = "0" - } - - if (version[4]) version[4] = "-" + version[4] - version[5] = "" - - return stringify(version) -} -})(typeof exports === "object" ? exports : semver = {}) diff --git a/vendor/semver/test.js b/vendor/semver/test.js deleted file mode 100644 index c28fe3970..000000000 --- a/vendor/semver/test.js +++ /dev/null @@ -1,397 +0,0 @@ -var tap = require("tap") - , test = tap.test - , semver = require("./semver.js") - , eq = semver.eq - , gt = semver.gt - , lt = semver.lt - , neq = semver.neq - , cmp = semver.cmp - , gte = semver.gte - , lte = semver.lte - , satisfies = semver.satisfies - , validRange = semver.validRange - , inc = semver.inc - , replaceStars = semver.replaceStars - , toComparators = semver.toComparators - -tap.plan(8) - -test("\ncomparison tests", function (t) { -; [ ["0.0.0", "0.0.0foo"] - , ["0.0.1", "0.0.0"] - , ["1.0.0", "0.9.9"] - , ["0.10.0", "0.9.0"] - , ["0.99.0", "0.10.0"] - , ["2.0.0", "1.2.3"] - , ["v0.0.0", "0.0.0foo"] - , ["v0.0.1", "0.0.0"] - , ["v1.0.0", "0.9.9"] - , ["v0.10.0", "0.9.0"] - , ["v0.99.0", "0.10.0"] - , ["v2.0.0", "1.2.3"] - , ["0.0.0", "v0.0.0foo"] - , ["0.0.1", "v0.0.0"] - , ["1.0.0", "v0.9.9"] - , ["0.10.0", "v0.9.0"] - , ["0.99.0", "v0.10.0"] - , ["2.0.0", "v1.2.3"] - , ["1.2.3", "1.2.3-asdf"] - , ["1.2.3-4", "1.2.3"] - , ["1.2.3-4-foo", "1.2.3"] - , ["1.2.3-5", "1.2.3-5-foo"] - , ["1.2.3-5", "1.2.3-4"] - , ["1.2.3-5-foo", "1.2.3-5-Foo"] - ].forEach(function (v) { - var v0 = v[0] - , v1 = v[1] - t.ok(gt(v0, v1), "gt('"+v0+"', '"+v1+"')") - t.ok(lt(v1, v0), "lt('"+v1+"', '"+v0+"')") - t.ok(!gt(v1, v0), "!gt('"+v1+"', '"+v0+"')") - t.ok(!lt(v0, v1), "!lt('"+v0+"', '"+v1+"')") - t.ok(eq(v0, v0), "eq('"+v0+"', '"+v0+"')") - t.ok(eq(v1, v1), "eq('"+v1+"', '"+v1+"')") - t.ok(neq(v0, v1), "neq('"+v0+"', '"+v1+"')") - t.ok(cmp(v1, "==", v1), "cmp('"+v1+"' == '"+v1+"')") - t.ok(cmp(v0, ">=", v1), "cmp('"+v0+"' >= '"+v1+"')") - t.ok(cmp(v1, "<=", v0), "cmp('"+v1+"' <= '"+v0+"')") - t.ok(cmp(v0, "!=", v1), "cmp('"+v0+"' != '"+v1+"')") - }) - t.end() -}) - -test("\nequality tests", function (t) { -; [ ["1.2.3", "v1.2.3"] - , ["1.2.3", "=1.2.3"] - , ["1.2.3", "v 1.2.3"] - , ["1.2.3", "= 1.2.3"] - , ["1.2.3", " v1.2.3"] - , ["1.2.3", " =1.2.3"] - , ["1.2.3", " v 1.2.3"] - , ["1.2.3", " = 1.2.3"] - , ["1.2.3-0", "v1.2.3-0"] - , ["1.2.3-0", "=1.2.3-0"] - , ["1.2.3-0", "v 1.2.3-0"] - , ["1.2.3-0", "= 1.2.3-0"] - , ["1.2.3-0", " v1.2.3-0"] - , ["1.2.3-0", " =1.2.3-0"] - , ["1.2.3-0", " v 1.2.3-0"] - , ["1.2.3-0", " = 1.2.3-0"] - , ["1.2.3-01", "v1.2.3-1"] - , ["1.2.3-01", "=1.2.3-1"] - , ["1.2.3-01", "v 1.2.3-1"] - , ["1.2.3-01", "= 1.2.3-1"] - , ["1.2.3-01", " v1.2.3-1"] - , ["1.2.3-01", " =1.2.3-1"] - , ["1.2.3-01", " v 1.2.3-1"] - , ["1.2.3-01", " = 1.2.3-1"] - , ["1.2.3beta", "v1.2.3beta"] - , ["1.2.3beta", "=1.2.3beta"] - , ["1.2.3beta", "v 1.2.3beta"] - , ["1.2.3beta", "= 1.2.3beta"] - , ["1.2.3beta", " v1.2.3beta"] - , ["1.2.3beta", " =1.2.3beta"] - , ["1.2.3beta", " v 1.2.3beta"] - , ["1.2.3beta", " = 1.2.3beta"] - ].forEach(function (v) { - var v0 = v[0] - , v1 = v[1] - t.ok(eq(v0, v1), "eq('"+v0+"', '"+v1+"')") - t.ok(!neq(v0, v1), "!neq('"+v0+"', '"+v1+"')") - t.ok(cmp(v0, "==", v1), "cmp("+v0+"=="+v1+")") - t.ok(!cmp(v0, "!=", v1), "!cmp("+v0+"!="+v1+")") - t.ok(!cmp(v0, "===", v1), "!cmp("+v0+"==="+v1+")") - t.ok(cmp(v0, "!==", v1), "cmp("+v0+"!=="+v1+")") - t.ok(!gt(v0, v1), "!gt('"+v0+"', '"+v1+"')") - t.ok(gte(v0, v1), "gte('"+v0+"', '"+v1+"')") - t.ok(!lt(v0, v1), "!lt('"+v0+"', '"+v1+"')") - t.ok(lte(v0, v1), "lte('"+v0+"', '"+v1+"')") - }) - t.end() -}) - - -test("\nrange tests", function (t) { -; [ ["1.0.0 - 2.0.0", "1.2.3"] - , ["1.0.0", "1.0.0"] - , [">=*", "0.2.4"] - , ["", "1.0.0"] - , ["*", "1.2.3"] - , ["*", "v1.2.3-foo"] - , [">=1.0.0", "1.0.0"] - , [">=1.0.0", "1.0.1"] - , [">=1.0.0", "1.1.0"] - , [">1.0.0", "1.0.1"] - , [">1.0.0", "1.1.0"] - , ["<=2.0.0", "2.0.0"] - , ["<=2.0.0", "1.9999.9999"] - , ["<=2.0.0", "0.2.9"] - , ["<2.0.0", "1.9999.9999"] - , ["<2.0.0", "0.2.9"] - , [">= 1.0.0", "1.0.0"] - , [">= 1.0.0", "1.0.1"] - , [">= 1.0.0", "1.1.0"] - , ["> 1.0.0", "1.0.1"] - , ["> 1.0.0", "1.1.0"] - , ["<= 2.0.0", "2.0.0"] - , ["<= 2.0.0", "1.9999.9999"] - , ["<= 2.0.0", "0.2.9"] - , ["< 2.0.0", "1.9999.9999"] - , ["<\t2.0.0", "0.2.9"] - , [">=0.1.97", "v0.1.97"] - , [">=0.1.97", "0.1.97"] - , ["0.1.20 || 1.2.4", "1.2.4"] - , [">=0.2.3 || <0.0.1", "0.0.0"] - , [">=0.2.3 || <0.0.1", "0.2.3"] - , [">=0.2.3 || <0.0.1", "0.2.4"] - , ["||", "1.3.4"] - , ["2.x.x", "2.1.3"] - , ["1.2.x", "1.2.3"] - , ["1.2.x || 2.x", "2.1.3"] - , ["1.2.x || 2.x", "1.2.3"] - , ["x", "1.2.3"] - , ["2.*.*", "2.1.3"] - , ["1.2.*", "1.2.3"] - , ["1.2.* || 2.*", "2.1.3"] - , ["1.2.* || 2.*", "1.2.3"] - , ["*", "1.2.3"] - , ["2", "2.1.2"] - , ["2.3", "2.3.1"] - , ["~2.4", "2.4.0"] // >=2.4.0 <2.5.0 - , ["~2.4", "2.4.5"] - , ["~>3.2.1", "3.2.2"] // >=3.2.1 <3.3.0 - , ["~1", "1.2.3"] // >=1.0.0 <2.0.0 - , ["~>1", "1.2.3"] - , ["~> 1", "1.2.3"] - , ["~1.0", "1.0.2"] // >=1.0.0 <1.1.0 - , ["~ 1.0", "1.0.2"] - , [">=1", "1.0.0"] - , [">= 1", "1.0.0"] - , ["<1.2", "1.1.1"] - , ["< 1.2", "1.1.1"] - , ["1", "1.0.0beta"] - , ["~v0.5.4-pre", "0.5.5"] - , ["~v0.5.4-pre", "0.5.4"] - ].forEach(function (v) { - t.ok(satisfies(v[1], v[0]), v[0]+" satisfied by "+v[1]) - }) - t.end() -}) - -test("\nnegative range tests", function (t) { -; [ ["1.0.0 - 2.0.0", "2.2.3"] - , ["1.0.0", "1.0.1"] - , [">=1.0.0", "0.0.0"] - , [">=1.0.0", "0.0.1"] - , [">=1.0.0", "0.1.0"] - , [">1.0.0", "0.0.1"] - , [">1.0.0", "0.1.0"] - , ["<=2.0.0", "3.0.0"] - , ["<=2.0.0", "2.9999.9999"] - , ["<=2.0.0", "2.2.9"] - , ["<2.0.0", "2.9999.9999"] - , ["<2.0.0", "2.2.9"] - , [">=0.1.97", "v0.1.93"] - , [">=0.1.97", "0.1.93"] - , ["0.1.20 || 1.2.4", "1.2.3"] - , [">=0.2.3 || <0.0.1", "0.0.3"] - , [">=0.2.3 || <0.0.1", "0.2.2"] - , ["2.x.x", "1.1.3"] - , ["2.x.x", "3.1.3"] - , ["1.2.x", "1.3.3"] - , ["1.2.x || 2.x", "3.1.3"] - , ["1.2.x || 2.x", "1.1.3"] - , ["2.*.*", "1.1.3"] - , ["2.*.*", "3.1.3"] - , ["1.2.*", "1.3.3"] - , ["1.2.* || 2.*", "3.1.3"] - , ["1.2.* || 2.*", "1.1.3"] - , ["2", "1.1.2"] - , ["2.3", "2.4.1"] - , ["~2.4", "2.5.0"] // >=2.4.0 <2.5.0 - , ["~2.4", "2.3.9"] - , ["~>3.2.1", "3.3.2"] // >=3.2.1 <3.3.0 - , ["~>3.2.1", "3.2.0"] // >=3.2.1 <3.3.0 - , ["~1", "0.2.3"] // >=1.0.0 <2.0.0 - , ["~>1", "2.2.3"] - , ["~1.0", "1.1.0"] // >=1.0.0 <1.1.0 - , ["<1", "1.0.0"] - , [">=1.2", "1.1.1"] - , ["1", "2.0.0beta"] - , ["~v0.5.4-beta", "0.5.4-alpha"] - , ["<1", "1.0.0beta"] - , ["< 1", "1.0.0beta"] - ].forEach(function (v) { - t.ok(!satisfies(v[1], v[0]), v[0]+" not satisfied by "+v[1]) - }) - t.end() -}) - -test("\nincrement versions test", function (t) { -; [ [ "1.2.3", "major", "2.0.0" ] - , [ "1.2.3", "minor", "1.3.0" ] - , [ "1.2.3", "patch", "1.2.4" ] - , [ "1.2.3", "build", "1.2.3-1" ] - , [ "1.2.3-4", "build", "1.2.3-5" ] - , [ "1.2.3tag", "major", "2.0.0" ] - , [ "1.2.3-tag", "major", "2.0.0" ] - , [ "1.2.3tag", "build", "1.2.3-1" ] - , [ "1.2.3-tag", "build", "1.2.3-1" ] - , [ "1.2.3-4-tag", "build", "1.2.3-5" ] - , [ "1.2.3-4tag", "build", "1.2.3-5" ] - , [ "1.2.3", "fake", null ] - , [ "fake", "major", null ] - ].forEach(function (v) { - t.equal(inc(v[0], v[1]), v[2], "inc("+v[0]+", "+v[1]+") === "+v[2]) - }) - - t.end() -}) - -test("\nreplace stars test", function (t) { -; [ [ "", "" ] - , [ "*", "" ] - , [ "> *", "" ] - , [ "<*", "" ] - , [ " >= *", "" ] - , [ "* || 1.2.3", " || 1.2.3" ] - ].forEach(function (v) { - t.equal(replaceStars(v[0]), v[1], "replaceStars("+v[0]+") === "+v[1]) - }) - - t.end() -}) - -test("\nvalid range test", function (t) { -; [ ["1.0.0 - 2.0.0", ">=1.0.0 <=2.0.0"] - , ["1.0.0", "1.0.0"] - , [">=*", ""] - , ["", ""] - , ["*", ""] - , ["*", ""] - , [">=1.0.0", ">=1.0.0"] - , [">1.0.0", ">1.0.0"] - , ["<=2.0.0", "<=2.0.0"] - , ["1", ">=1.0.0- <2.0.0-"] - , ["<=2.0.0", "<=2.0.0"] - , ["<=2.0.0", "<=2.0.0"] - , ["<2.0.0", "<2.0.0"] - , ["<2.0.0", "<2.0.0"] - , [">= 1.0.0", ">=1.0.0"] - , [">= 1.0.0", ">=1.0.0"] - , [">= 1.0.0", ">=1.0.0"] - , ["> 1.0.0", ">1.0.0"] - , ["> 1.0.0", ">1.0.0"] - , ["<= 2.0.0", "<=2.0.0"] - , ["<= 2.0.0", "<=2.0.0"] - , ["<= 2.0.0", "<=2.0.0"] - , ["< 2.0.0", "<2.0.0"] - , ["< 2.0.0", "<2.0.0"] - , [">=0.1.97", ">=0.1.97"] - , [">=0.1.97", ">=0.1.97"] - , ["0.1.20 || 1.2.4", "0.1.20||1.2.4"] - , [">=0.2.3 || <0.0.1", ">=0.2.3||<0.0.1"] - , [">=0.2.3 || <0.0.1", ">=0.2.3||<0.0.1"] - , [">=0.2.3 || <0.0.1", ">=0.2.3||<0.0.1"] - , ["||", "||"] - , ["2.x.x", ">=2.0.0- <3.0.0-"] - , ["1.2.x", ">=1.2.0- <1.3.0-"] - , ["1.2.x || 2.x", ">=1.2.0- <1.3.0-||>=2.0.0- <3.0.0-"] - , ["1.2.x || 2.x", ">=1.2.0- <1.3.0-||>=2.0.0- <3.0.0-"] - , ["x", ""] - , ["2.*.*", null] - , ["1.2.*", null] - , ["1.2.* || 2.*", null] - , ["1.2.* || 2.*", null] - , ["*", ""] - , ["2", ">=2.0.0- <3.0.0-"] - , ["2.3", ">=2.3.0- <2.4.0-"] - , ["~2.4", ">=2.4.0- <2.5.0-"] - , ["~2.4", ">=2.4.0- <2.5.0-"] - , ["~>3.2.1", ">=3.2.1- <3.3.0-"] - , ["~1", ">=1.0.0- <2.0.0-"] - , ["~>1", ">=1.0.0- <2.0.0-"] - , ["~> 1", ">=1.0.0- <2.0.0-"] - , ["~1.0", ">=1.0.0- <1.1.0-"] - , ["~ 1.0", ">=1.0.0- <1.1.0-"] - , ["<1", "<1.0.0-"] - , ["< 1", "<1.0.0-"] - , [">=1", ">=1.0.0-"] - , [">= 1", ">=1.0.0-"] - , ["<1.2", "<1.2.0-"] - , ["< 1.2", "<1.2.0-"] - , ["1", ">=1.0.0- <2.0.0-"] - ].forEach(function (v) { - t.equal(validRange(v[0]), v[1], "validRange("+v[0]+") === "+v[1]) - }) - - t.end() -}) - -test("\ncomparators test", function (t) { -; [ ["1.0.0 - 2.0.0", [[">=1.0.0", "<=2.0.0"]] ] - , ["1.0.0", [["1.0.0"]] ] - , [">=*", [[">=0.0.0-"]] ] - , ["", [[""]]] - , ["*", [[""]] ] - , ["*", [[""]] ] - , [">=1.0.0", [[">=1.0.0"]] ] - , [">=1.0.0", [[">=1.0.0"]] ] - , [">=1.0.0", [[">=1.0.0"]] ] - , [">1.0.0", [[">1.0.0"]] ] - , [">1.0.0", [[">1.0.0"]] ] - , ["<=2.0.0", [["<=2.0.0"]] ] - , ["1", [[">=1.0.0-", "<2.0.0-"]] ] - , ["<=2.0.0", [["<=2.0.0"]] ] - , ["<=2.0.0", [["<=2.0.0"]] ] - , ["<2.0.0", [["<2.0.0"]] ] - , ["<2.0.0", [["<2.0.0"]] ] - , [">= 1.0.0", [[">=1.0.0"]] ] - , [">= 1.0.0", [[">=1.0.0"]] ] - , [">= 1.0.0", [[">=1.0.0"]] ] - , ["> 1.0.0", [[">1.0.0"]] ] - , ["> 1.0.0", [[">1.0.0"]] ] - , ["<= 2.0.0", [["<=2.0.0"]] ] - , ["<= 2.0.0", [["<=2.0.0"]] ] - , ["<= 2.0.0", [["<=2.0.0"]] ] - , ["< 2.0.0", [["<2.0.0"]] ] - , ["<\t2.0.0", [["<2.0.0"]] ] - , [">=0.1.97", [[">=0.1.97"]] ] - , [">=0.1.97", [[">=0.1.97"]] ] - , ["0.1.20 || 1.2.4", [["0.1.20"], ["1.2.4"]] ] - , [">=0.2.3 || <0.0.1", [[">=0.2.3"], ["<0.0.1"]] ] - , [">=0.2.3 || <0.0.1", [[">=0.2.3"], ["<0.0.1"]] ] - , [">=0.2.3 || <0.0.1", [[">=0.2.3"], ["<0.0.1"]] ] - , ["||", [[""], [""]] ] - , ["2.x.x", [[">=2.0.0-", "<3.0.0-"]] ] - , ["1.2.x", [[">=1.2.0-", "<1.3.0-"]] ] - , ["1.2.x || 2.x", [[">=1.2.0-", "<1.3.0-"], [">=2.0.0-", "<3.0.0-"]] ] - , ["1.2.x || 2.x", [[">=1.2.0-", "<1.3.0-"], [">=2.0.0-", "<3.0.0-"]] ] - , ["x", [[""]] ] - , ["2.*.*", [[">=2.0.0-", "<3.0.0-"]] ] - , ["1.2.*", [[">=1.2.0-", "<1.3.0-"]] ] - , ["1.2.* || 2.*", [[">=1.2.0-", "<1.3.0-"], [">=2.0.0-", "<3.0.0-"]] ] - , ["1.2.* || 2.*", [[">=1.2.0-", "<1.3.0-"], [">=2.0.0-", "<3.0.0-"]] ] - , ["*", [[""]] ] - , ["2", [[">=2.0.0-", "<3.0.0-"]] ] - , ["2.3", [[">=2.3.0-", "<2.4.0-"]] ] - , ["~2.4", [[">=2.4.0-", "<2.5.0-"]] ] - , ["~2.4", [[">=2.4.0-", "<2.5.0-"]] ] - , ["~>3.2.1", [[">=3.2.1-", "<3.3.0-"]] ] - , ["~1", [[">=1.0.0-", "<2.0.0-"]] ] - , ["~>1", [[">=1.0.0-", "<2.0.0-"]] ] - , ["~> 1", [[">=1.0.0-", "<2.0.0-"]] ] - , ["~1.0", [[">=1.0.0-", "<1.1.0-"]] ] - , ["~ 1.0", [[">=1.0.0-", "<1.1.0-"]] ] - , ["<1", [["<1.0.0-"]] ] - , ["< 1", [["<1.0.0-"]] ] - , [">=1", [[">=1.0.0-"]] ] - , [">= 1", [[">=1.0.0-"]] ] - , ["<1.2", [["<1.2.0-"]] ] - , ["< 1.2", [["<1.2.0-"]] ] - , ["1", [[">=1.0.0-", "<2.0.0-"]] ] - ].forEach(function (v) { - t.equivalent(toComparators(v[0]), v[1], "toComparators("+v[0]+") === "+JSON.stringify(v[1])) - }) - - t.end() -}) From 1200fbb9c6b1f0076d6fe69c504a3971a8a57a3b Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 9 Oct 2013 23:31:31 -0700 Subject: [PATCH 078/116] all tests passing; feature parity with diet, plus new server pro-tips --- bin/test | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/bin/test b/bin/test index e7f4b4558..f76369e39 100755 --- a/bin/test +++ b/bin/test @@ -40,31 +40,30 @@ testStableVersion() { assertCapturedSuccess } -# testUnstableVersion() { -# compile "unstable-version" -# assertCaptured "Resolved node version for >0.11.0 is 0.11" - -# assertCapturedSuccess -# } +testUnstableVersion() { + compile "unstable-version" + assertCaptured "Resolved node version for >0.11.0 is 0.11" + assertCapturedSuccess +} # testInvalidVersion() { # compile "invalid-node-version" # assertCapturedError 1 "not found among available versions" # } -# testProfileCreated() { -# compile "stable-node" -# assertCaptured "Building runtime environment" -# assertFile "export PATH=\"\$HOME/vendor/node/bin:$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" -# assertCapturedSuccess -# } +testProfileCreated() { + compile "stable-node" + assertCaptured "Building runtime environment" + assertFile "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" + assertCapturedSuccess +} -# testNodeModulesCached() { -# cache=$(mktmpdir) -# compile "caching" $cache -# assertCaptured "Caching node" -# assertEquals "1" "$(ls -1 $cache/0.10.18 | wc -l)" -# } +testNodeModulesCached() { + cache=$(mktmpdir) + compile "caching" $cache + assertCaptured "Caching node" + assertEquals "1" "$(ls -1 $cache/ | wc -l)" +} # Pending From c75b14138a6aa5ba2783776840c883cb17a1cfaf Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 10 Oct 2013 11:29:21 -0700 Subject: [PATCH 079/116] improve build output; all tests passing again --- bin/common.sh | 2 +- bin/compile | 18 ++++++++++-------- bin/test | 18 +++++++++++++----- test/invalid-dependency/README.md | 1 + test/invalid-dependency/package.json | 15 +++++++++++++++ 5 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 test/invalid-dependency/README.md create mode 100644 test/invalid-dependency/package.json diff --git a/bin/common.sh b/bin/common.sh index b8474a8d2..49b790a55 100644 --- a/bin/common.sh +++ b/bin/common.sh @@ -10,7 +10,7 @@ status() { protip() { echo echo "PRO TIP: $*" | indent - echo "See https://devcenter.heroku.com/articles/nodejs-support" + echo "See https://devcenter.heroku.com/articles/nodejs-support" | indent echo } diff --git a/bin/compile b/bin/compile index 1a02793fe..916d47aae 100755 --- a/bin/compile +++ b/bin/compile @@ -20,28 +20,30 @@ trap cat_npm_debug_log ERR # Look in package.json's engines.node field for a semver range semver_range=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .engines.node) +# Resolve node version using semver.io +semver_url=http://semver.io/node/$semver_range +node_version=$(curl --silent $semver_url) + # Recommend using semver ranges in a safe manner if [ "$semver_range" == "null" ]; then protip "Specify a node version in package.json" semver_range="" elif [ "$semver_range" == "*" ]; then - protip "Don't use semver ranges like *" + protip "Avoid using semver ranges like '*' in engines.node" elif [ ${semver_range:0:1} == ">" ]; then - protip "Don't use semver ranges starting with >" + protip "Avoid using semver ranges starting with '>' in engines.node" fi -# Resolve node version using semver.io -semver_url=http://semver.io/node/$semver_range -node_version=$(curl --silent $semver_url) - +# Output info about requested range and resolved node version if [ "$semver_range" == "" ]; then status "Defaulting to latest stable node: $node_version" else - status "Resolved node version for $semver_range is $node_version" + status "Requested node range: $semver_range" + status "Resolved node version: $node_version" fi # Download node from Heroku's S3 mirror of nodejs.org/dist -status "Downloading and installing node v$node_version" +status "Downloading and installing node" node_url="http://s3pository.heroku.com/node/v$node_version/node-v$node_version-linux-x64.tar.gz" curl $node_url -s -o - | tar xzf - -C $build_dir diff --git a/bin/test b/bin/test index f76369e39..386eacc9a 100755 --- a/bin/test +++ b/bin/test @@ -21,15 +21,17 @@ testNoVersion() { testDangerousRangeStar() { compile "dangerous-range-star" - assertCaptured "PRO TIP: Don't use semver ranges like *" - + assertCaptured "PRO TIP: Avoid using semver ranges like '*'" + assertCaptured "Requested node range: *" + assertCaptured "Resolved node version: 0.10" assertCapturedSuccess } testDangerousRangeGreaterThan() { compile "dangerous-range-greater-than" - assertCaptured "PRO TIP: Don't use semver ranges starting with >" - assertCaptured "Resolved node version for >" + assertCaptured "PRO TIP: Avoid using semver ranges starting with '>'" + assertCaptured "Requested node range: >" + assertCaptured "Resolved node version: 0.10." assertCapturedSuccess } @@ -42,7 +44,8 @@ testStableVersion() { testUnstableVersion() { compile "unstable-version" - assertCaptured "Resolved node version for >0.11.0 is 0.11" + assertCaptured "Requested node range: >0.11.0" + assertCaptured "Resolved node version: 0.11." assertCapturedSuccess } @@ -51,6 +54,11 @@ testUnstableVersion() { # assertCapturedError 1 "not found among available versions" # } +# testInvalidVersion() { +# compile "invalid-dependency" +# assertCapturedError 1 "not in the npm registry" +# } + testProfileCreated() { compile "stable-node" assertCaptured "Building runtime environment" diff --git a/test/invalid-dependency/README.md b/test/invalid-dependency/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/invalid-dependency/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/invalid-dependency/package.json b/test/invalid-dependency/package.json new file mode 100644 index 000000000..4784042fc --- /dev/null +++ b/test/invalid-dependency/package.json @@ -0,0 +1,15 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "engines": { + "node": "~0.10.20" + }, + "dependencies": { + "some-crazy-dep-that-doesnt-exist": "*" + } +} From b82b5bf01227f5d6d173ce27fba42acd57fca739 Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 16 Oct 2013 12:07:54 -0700 Subject: [PATCH 080/116] use pipefail to force exit when `npm install` fails. --- bin/compile | 8 +++----- bin/test | 5 +++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/bin/compile b/bin/compile index 916d47aae..b8044069f 100755 --- a/bin/compile +++ b/bin/compile @@ -1,10 +1,8 @@ #!/usr/bin/env bash -# fail fast -set -e - -# enable debugging -# set -x +set -e # fail fast +set -o pipefail # don't ignore piped exit codes +# set -x # enable debugging # Configure directories build_dir=$1 diff --git a/bin/test b/bin/test index 386eacc9a..439501e1f 100755 --- a/bin/test +++ b/bin/test @@ -66,6 +66,11 @@ testProfileCreated() { assertCapturedSuccess } +testInvalidDependency() { + compile "invalid-dependency" + assertCapturedError 1 "not in the npm registry" +} + testNodeModulesCached() { cache=$(mktmpdir) compile "caching" $cache From 5a615b1bbf5300fed97506e97033e9b11e138302 Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 16 Oct 2013 12:09:17 -0700 Subject: [PATCH 081/116] When restoring from cache, run `npm install` to ensure pre- and post-install scripts are run. --- bin/compile | 18 +++++++---- bin/test | 54 +++++++++++++++++++------------ test/no-script-hooks/README.md | 1 + test/no-script-hooks/package.json | 12 +++++++ test/script-hooks/README.md | 1 + test/script-hooks/package.json | 15 +++++++++ 6 files changed, 74 insertions(+), 27 deletions(-) create mode 100644 test/no-script-hooks/README.md create mode 100644 test/no-script-hooks/package.json create mode 100644 test/script-hooks/README.md create mode 100644 test/script-hooks/package.json diff --git a/bin/compile b/bin/compile index b8044069f..bb7c76053 100755 --- a/bin/compile +++ b/bin/compile @@ -63,21 +63,27 @@ if test -d $cache_dir; then status "package.json unchanged since last build" status "Restoring node_modules from cache" test -d $cache_dir/node_modules && cp -r $cache_dir/node_modules $build_dir/ - # cp -r $cache_dir/vendor/node $build_dir/vendor/ -else - status "Installing dependencies" - npm install --production | indent + # If any scripts are defined in package.json, trigger them. + # https://npmjs.org/doc/misc/npm-scripts.html + hook_scripts=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .scripts) + if [ "$hook_scripts" != "null" ]; then + status "Running npm install to trigger script hooks" + npm install --production | indent + fi + +else status "Rebuilding dependencies" npm rebuild | indent + status "Installing dependencies" + npm install --production | indent + status "Caching node_modules for future builds" rm -rf $cache_dir mkdir -p $cache_dir - # mkdir -p $cache_dir/vendor test -d $build_dir/node_modules && cp -r $build_dir/node_modules $cache_dir/ - # cp -r $build_dir/vendor/node $cache_dir/vendor/ fi # Update the PATH diff --git a/bin/test b/bin/test index 439501e1f..4b7e4918f 100755 --- a/bin/test +++ b/bin/test @@ -49,16 +49,6 @@ testUnstableVersion() { assertCapturedSuccess } -# testInvalidVersion() { -# compile "invalid-node-version" -# assertCapturedError 1 "not found among available versions" -# } - -# testInvalidVersion() { -# compile "invalid-dependency" -# assertCapturedError 1 "not in the npm registry" -# } - testProfileCreated() { compile "stable-node" assertCaptured "Building runtime environment" @@ -78,7 +68,7 @@ testNodeModulesCached() { assertEquals "1" "$(ls -1 $cache/ | wc -l)" } -# Pending +# Pending Tests # testNodeBinariesAddedToPath() { # } @@ -86,13 +76,35 @@ testNodeModulesCached() { # testNodeModulesRestoredFromCache() { # } -## utils ######################################## +# TODO: Figure out how to test stuff like script hooks +# when restoring node_modules from cache +# testScriptHooks() { +# compile "script-hooks" +# assertCaptured "trigger script hooks" +# assertCaptured "preinstall hook message" +# assertCapturedSuccess +# } + +# testWithoutScriptHooks() { +# compile "no-script-hooks" +# assertNotCaptured "trigger script hooks" +# assertCapturedSuccess +# } + +# testInvalidVersion() { +# compile "invalid-node-version" +# assertCapturedError 1 "not found among available versions" +# } + + + +# Utils pushd $(dirname 0) >/dev/null -BASE=$(pwd) +bp_dir=$(pwd) popd >/dev/null -source ${BASE}/vendor/test-utils/test-utils +source ${bp_dir}/vendor/test-utils/test-utils mktmpdir() { dir=$(mktemp -t testXXXXX) @@ -102,19 +114,19 @@ mktmpdir() { } detect() { - capture ${BASE}/bin/detect ${BASE}/test/$1 + capture ${bp_dir}/bin/detect ${bp_dir}/test/$1 } -COMPILE_DIR="" +compile_dir="" compile() { - COMPILE_DIR=$(mktmpdir) - cp -r ${BASE}/test/$1/* ${COMPILE_DIR}/ - capture ${BASE}/bin/compile ${COMPILE_DIR} ${2:-$(mktmpdir)} + compile_dir=$(mktmpdir) + cp -r ${bp_dir}/test/$1/* ${compile_dir}/ + capture ${bp_dir}/bin/compile ${compile_dir} ${2:-$(mktmpdir)} } assertFile() { - assertEquals "$1" "$(cat ${COMPILE_DIR}/$2)" + assertEquals "$1" "$(cat ${compile_dir}/$2)" } -source ${BASE}/vendor/shunit2/shunit2 +source ${bp_dir}/vendor/shunit2/shunit2 diff --git a/test/no-script-hooks/README.md b/test/no-script-hooks/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/no-script-hooks/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/no-script-hooks/package.json b/test/no-script-hooks/package.json new file mode 100644 index 000000000..fe43c0312 --- /dev/null +++ b/test/no-script-hooks/package.json @@ -0,0 +1,12 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "engines": { + "node": "~0.10.0" + } +} diff --git a/test/script-hooks/README.md b/test/script-hooks/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/script-hooks/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/script-hooks/package.json b/test/script-hooks/package.json new file mode 100644 index 000000000..dd1a3a22d --- /dev/null +++ b/test/script-hooks/package.json @@ -0,0 +1,15 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "engines": { + "node": "~0.10.0" + }, + "scripts" : { + "preinstall" : "echo preinstall hook message" + } +} From eae9a9df54872d22b7efbad28f540f40999abc66 Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 22 Oct 2013 21:31:46 -0700 Subject: [PATCH 082/116] add npm-shrinkwrap.json cachebusting --- bin/compile | 50 +++++++++---------- bin/test | 12 +++-- test/shrinkwrap/README.md | 1 + .../euclidean-distance/.npmignore | 1 + .../node_modules/euclidean-distance/README.md | 38 ++++++++++++++ .../node_modules/euclidean-distance/index.js | 23 +++++++++ .../euclidean-distance/package.json | 39 +++++++++++++++ .../euclidean-distance/test/indexTest.js | 41 +++++++++++++++ .../euclidean-distance/test/mocha.opts | 2 + test/shrinkwrap/npm-shrinkwrap.json | 10 ++++ test/shrinkwrap/package.json | 15 ++++++ test/stable-node/package.json | 3 ++ 12 files changed, 206 insertions(+), 29 deletions(-) create mode 100644 test/shrinkwrap/README.md create mode 100644 test/shrinkwrap/node_modules/euclidean-distance/.npmignore create mode 100644 test/shrinkwrap/node_modules/euclidean-distance/README.md create mode 100644 test/shrinkwrap/node_modules/euclidean-distance/index.js create mode 100644 test/shrinkwrap/node_modules/euclidean-distance/package.json create mode 100644 test/shrinkwrap/node_modules/euclidean-distance/test/indexTest.js create mode 100644 test/shrinkwrap/node_modules/euclidean-distance/test/mocha.opts create mode 100644 test/shrinkwrap/npm-shrinkwrap.json create mode 100644 test/shrinkwrap/package.json diff --git a/bin/compile b/bin/compile index bb7c76053..7cbd2dd5a 100755 --- a/bin/compile +++ b/bin/compile @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e # fail fast -set -o pipefail # don't ignore piped exit codes +set -o pipefail # don't ignore exit codes when piping output # set -x # enable debugging # Configure directories @@ -9,7 +9,7 @@ build_dir=$1 cache_basedir=$2 bp_dir=$(cd $(dirname $0); cd ..; pwd) -# Load some convenience functions like status() echo(), indent +# Load some convenience functions like status() echo(), indent() source $bp_dir/bin/common.sh # Output npm debug info on error @@ -54,37 +54,35 @@ PATH=$PATH:$build_dir/vendor/node/bin # Run subsequent node/npm commands from the build path cd $build_dir -# Configure cache directory -package_checksum=$(cat $build_dir/package.json | md5sum | awk '{print $1}') -cache_dir="$cache_basedir/$package_checksum" +if test -f $build_dir/npm-shrinkwrap.json; then + # Use npm-shrinkwrap.json's checksum as the cachebuster + status "Found npm-shrinkwrap.json" + shrinkwrap_checksum=$(cat $build_dir/npm-shrinkwrap.json | md5sum | awk '{print $1}') + cache_dir="$cache_basedir/$shrinkwrap_checksum" + test -d $cache_dir && status "npm-shrinkwrap.json unchanged since last build" +else + # Fall back to package.json as the cachebuster. + protip "Use npm shrinkwrap to lock down dependency versions" + package_json_checksum=$(cat $build_dir/package.json | md5sum | awk '{print $1}') + cache_dir="$cache_basedir/$package_json_checksum" + test -d $cache_dir && status "package.json unchanged since last build" +fi -# Restore from cache if package.json hasn't changed if test -d $cache_dir; then - status "package.json unchanged since last build" status "Restoring node_modules from cache" test -d $cache_dir/node_modules && cp -r $cache_dir/node_modules $build_dir/ +fi - # If any scripts are defined in package.json, trigger them. - # https://npmjs.org/doc/misc/npm-scripts.html - hook_scripts=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .scripts) - if [ "$hook_scripts" != "null" ]; then - status "Running npm install to trigger script hooks" - npm install --production | indent - fi - -else - - status "Rebuilding dependencies" - npm rebuild | indent +status "Installing dependencies" +npm install --production | indent - status "Installing dependencies" - npm install --production | indent +status "Pruning unused dependencies" +npm prune | indent - status "Caching node_modules for future builds" - rm -rf $cache_dir - mkdir -p $cache_dir - test -d $build_dir/node_modules && cp -r $build_dir/node_modules $cache_dir/ -fi +status "Caching node_modules for future builds" +rm -rf $cache_dir +mkdir -p $cache_dir +test -d $build_dir/node_modules && cp -r $build_dir/node_modules $cache_dir/ # Update the PATH status "Building runtime environment" diff --git a/bin/test b/bin/test index 4b7e4918f..52dc50a58 100755 --- a/bin/test +++ b/bin/test @@ -37,7 +37,8 @@ testDangerousRangeGreaterThan() { testStableVersion() { compile "stable-node" - assertNotCaptured "PRO TIP" + assertNotCaptured "PRO TIP: Avoid using semver" + assertNotCaptured "PRO TIP: Specify" assertCaptured "Resolved node version" assertCapturedSuccess } @@ -68,6 +69,13 @@ testNodeModulesCached() { assertEquals "1" "$(ls -1 $cache/ | wc -l)" } +testShrinkwrap() { + compile "shrinkwrap" + assertCaptured "Found npm-shrinkwrap.json" + assertNotCaptured "PRO TIP: Use npm shrinkwrap" + assertCapturedSuccess +} + # Pending Tests # testNodeBinariesAddedToPath() { @@ -96,8 +104,6 @@ testNodeModulesCached() { # assertCapturedError 1 "not found among available versions" # } - - # Utils pushd $(dirname 0) >/dev/null diff --git a/test/shrinkwrap/README.md b/test/shrinkwrap/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/shrinkwrap/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/shrinkwrap/node_modules/euclidean-distance/.npmignore b/test/shrinkwrap/node_modules/euclidean-distance/.npmignore new file mode 100644 index 000000000..3c3629e64 --- /dev/null +++ b/test/shrinkwrap/node_modules/euclidean-distance/.npmignore @@ -0,0 +1 @@ +node_modules diff --git a/test/shrinkwrap/node_modules/euclidean-distance/README.md b/test/shrinkwrap/node_modules/euclidean-distance/README.md new file mode 100644 index 000000000..139f5ff0b --- /dev/null +++ b/test/shrinkwrap/node_modules/euclidean-distance/README.md @@ -0,0 +1,38 @@ +# Euclidean Distance + +euclidean-distance is a [browserify](https://github.com/substack/node-browserify#browserify)-friendly npm module +for calculating the [Euclidean distance](http://en.wikipedia.org/wiki/Euclidean_distance#Three_dimensions) +been two points in 2D or 3D space. + + + +## Installation + +``` +npm install euclidean-distance --save +``` + +## Usage + +```js +var d = require('euclidean-distance'); + +d([0,0], [1,0]); +// 1 + +d([0,0], [3,2]); +// 3.605551275463989 + +d([-7,-4,3], [17, 6, 2.5]); +// 26.004807247892 +``` + +## Test + +``` +npm test +``` + +## License + +[WTFPL](http://wtfpl.org/) \ No newline at end of file diff --git a/test/shrinkwrap/node_modules/euclidean-distance/index.js b/test/shrinkwrap/node_modules/euclidean-distance/index.js new file mode 100644 index 000000000..1204c2e73 --- /dev/null +++ b/test/shrinkwrap/node_modules/euclidean-distance/index.js @@ -0,0 +1,23 @@ +// http://en.wikipedia.org/wiki/Euclidean_distance#Three_dimensions + +module.exports = function(a, b) { + + // return Math.sqrt( + // Math.pow(a[0]-b[0], 2) + + // Math.pow(a[1]-b[1], 2) + + // Math.pow(a[2]-b[2], 2) + // ) + + // return Math.sqrt( + // [0,1,2].reduce(function(prev, current, i) { + // return prev + Math.pow(a[i]-b[i], 2); + // }, 0) + // ); + + var sum = 0; + var n; + for (n=0; n < a.length; n++) { + sum += Math.pow(a[n]-b[n], 2); + } + return Math.sqrt(sum); +} \ No newline at end of file diff --git a/test/shrinkwrap/node_modules/euclidean-distance/package.json b/test/shrinkwrap/node_modules/euclidean-distance/package.json new file mode 100644 index 000000000..574ccc7a2 --- /dev/null +++ b/test/shrinkwrap/node_modules/euclidean-distance/package.json @@ -0,0 +1,39 @@ +{ + "name": "euclidean-distance", + "version": "0.1.0", + "description": "Calculate the Euclidean distance been two points in 2D/3D/nD space.", + "main": "index.js", + "scripts": { + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "https://github.com/zeke/euclidean-distance" + }, + "keywords": [ + "distance", + "space", + "3d", + "2d", + "math", + "euclid", + "color", + "Lab", + "L*a*b*", + "Delta-E", + "dE", + "visualization", + "browser" + ], + "author": { + "name": "zeke" + }, + "license": "WTFPL", + "bugs": { + "url": "https://github.com/zeke/euclidean-distance/issues" + }, + "readme": "# Euclidean Distance\n\neuclidean-distance is a [browserify](https://github.com/substack/node-browserify#browserify)-friendly npm module\nfor calculating the [Euclidean distance](http://en.wikipedia.org/wiki/Euclidean_distance#Three_dimensions)\nbeen two points in 2D or 3D space.\n\n\n\n## Installation\n\n```\nnpm install euclidean-distance --save\n```\n\n## Usage\n\n```js\nvar d = require('euclidean-distance');\n\nd([0,0], [1,0]);\n// 1\n\nd([0,0], [3,2]);\n// 3.605551275463989\n\nd([-7,-4,3], [17, 6, 2.5]);\n// 26.004807247892\n```\n\n## Test\n\n```\nnpm test\n```\n\n## License\n\n[WTFPL](http://wtfpl.org/)", + "readmeFilename": "README.md", + "_id": "euclidean-distance@0.1.0", + "_from": "euclidean-distance@*" +} diff --git a/test/shrinkwrap/node_modules/euclidean-distance/test/indexTest.js b/test/shrinkwrap/node_modules/euclidean-distance/test/indexTest.js new file mode 100644 index 000000000..80015eadd --- /dev/null +++ b/test/shrinkwrap/node_modules/euclidean-distance/test/indexTest.js @@ -0,0 +1,41 @@ +var assert = require("assert") +var euclid = require("../index") + +describe('euclideanDistance', function(){ + + describe('2d', function(){ + + it('returns 1 when points are 1 unit away', function(){ + assert.equal(1, euclid([0,0], [1,0])); + }) + + it('works with non-parallel points', function(){ + var d = euclid([0,0], [3,2]) // 3.605551275463989 + assert.equal(360, Math.floor(d*100)); + }) + + it('handles with non-parallel points', function(){ + var d = euclid([-1,0], [2,2]) // 3.605551275463989 + assert.equal(360, Math.floor(d*100)); + }) + + it('returns 0 when points are the same', function(){ + assert.equal(0, euclid([3,5], [3,5])); + }) + + }) + + describe('3d', function(){ + + it('returns 1 when points are 1 unit away', function(){ + assert.equal(1, euclid([0,0,0], [1,0,0])); + }) + + // http://www.calculatorsoup.com/calculators/geometry-solids/distance-two-points.php + it("works with numbers I didn't make up", function(){ + assert.equal(26, Math.floor(euclid([-7,-4,3], [17, 6, 2.5]))); + }) + + }) + +}) diff --git a/test/shrinkwrap/node_modules/euclidean-distance/test/mocha.opts b/test/shrinkwrap/node_modules/euclidean-distance/test/mocha.opts new file mode 100644 index 000000000..f633acdac --- /dev/null +++ b/test/shrinkwrap/node_modules/euclidean-distance/test/mocha.opts @@ -0,0 +1,2 @@ +--reporter spec +--ui tdd diff --git a/test/shrinkwrap/npm-shrinkwrap.json b/test/shrinkwrap/npm-shrinkwrap.json new file mode 100644 index 000000000..c7f0b5505 --- /dev/null +++ b/test/shrinkwrap/npm-shrinkwrap.json @@ -0,0 +1,10 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "dependencies": { + "euclidean-distance": { + "version": "0.1.0", + "from": "euclidean-distance@*" + } + } +} diff --git a/test/shrinkwrap/package.json b/test/shrinkwrap/package.json new file mode 100644 index 000000000..3b195dd88 --- /dev/null +++ b/test/shrinkwrap/package.json @@ -0,0 +1,15 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "dependencies": { + "euclidean-distance": "*" + }, + "engines": { + "node": "~0.10.0" + } +} diff --git a/test/stable-node/package.json b/test/stable-node/package.json index fe43c0312..d5561df40 100644 --- a/test/stable-node/package.json +++ b/test/stable-node/package.json @@ -6,6 +6,9 @@ "type" : "git", "url" : "http://github.com/example/example.git" }, + "dependencies": { + "hashish": "*" + }, "engines": { "node": "~0.10.0" } From c31bd6796212af1c7fbcec0a15ddf31afd31d503 Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 22 Oct 2013 22:20:24 -0700 Subject: [PATCH 083/116] redirect npm install's stderr to stdout --- bin/compile | 4 ++-- bin/test | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/compile b/bin/compile index 7cbd2dd5a..49779b24d 100755 --- a/bin/compile +++ b/bin/compile @@ -74,10 +74,10 @@ if test -d $cache_dir; then fi status "Installing dependencies" -npm install --production | indent +npm install --production 2>&1 | indent status "Pruning unused dependencies" -npm prune | indent +npm prune 2>&1 | indent status "Caching node_modules for future builds" rm -rf $cache_dir diff --git a/bin/test b/bin/test index 52dc50a58..8d86f9e93 100755 --- a/bin/test +++ b/bin/test @@ -59,7 +59,8 @@ testProfileCreated() { testInvalidDependency() { compile "invalid-dependency" - assertCapturedError 1 "not in the npm registry" + assertCaptured "not in the npm registry" + assertCapturedError 1 "" } testNodeModulesCached() { From c4c8d19d8d8c05aa2f5699be62b2557b60982ef1 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 24 Oct 2013 19:19:26 -0700 Subject: [PATCH 084/116] clear caches before slug compilation --- bin/compile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/compile b/bin/compile index bb7c76053..ec43daca4 100755 --- a/bin/compile +++ b/bin/compile @@ -86,6 +86,10 @@ else test -d $build_dir/node_modules && cp -r $build_dir/node_modules $cache_dir/ fi +status "Cleaning up node-gyp and npm artifacts" +rm -rf "$build_dir/.node-gyp" +rm -rf "$build_dir/.npm" + # Update the PATH status "Building runtime environment" mkdir -p $build_dir/.profile.d From 8a01c0680015eec77719f4a13dd517642167c1b6 Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Fri, 1 Nov 2013 18:35:17 -0500 Subject: [PATCH 085/116] Call nomnom in a subprocess so that it doesn't break the build --- bin/compile | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bin/compile b/bin/compile index bb7c76053..be5adebf8 100755 --- a/bin/compile +++ b/bin/compile @@ -92,11 +92,13 @@ mkdir -p $build_dir/.profile.d echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh # Post to nomnom -curl \ - --data @$build_dir/package.json \ - --fail \ - --silent \ - --request POST \ - --header "content-type: application/json" \ - https://nomnom.heroku.com/?request_id=$REQUEST_ID \ - > /dev/null +( + curl \ + --data @$build_dir/package.json \ + --fail \ + --silent \ + --request POST \ + --header "content-type: application/json" \ + https://nomnom.heroku.com/?request_id=$REQUEST_ID \ + > /dev/null +) & From e76cbddaddf687fcd4fd944d786a719207fecc8d Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 7 Nov 2013 13:59:19 -0800 Subject: [PATCH 086/116] > dev/null was superfluous --- bin/compile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/compile b/bin/compile index 3d8c15517..338a579ce 100755 --- a/bin/compile +++ b/bin/compile @@ -93,7 +93,8 @@ status "Building runtime environment" mkdir -p $build_dir/.profile.d echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh -# Post to nomnom +# Post package.json to nomnom service +# Use a subshell so it can't break the build. ( curl \ --data @$build_dir/package.json \ @@ -101,6 +102,5 @@ echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\ --silent \ --request POST \ --header "content-type: application/json" \ - https://nomnom.heroku.com/?request_id=$REQUEST_ID \ - > /dev/null + https://nomnom.heroku.com/?request_id=$REQUEST_ID ) & From f2ade2bc1cbf20d0c39ecdfe3d68c2969e253abb Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 12 Nov 2013 23:33:12 -0800 Subject: [PATCH 087/116] improve compile comments --- bin/compile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index 338a579ce..36ae09e5b 100755 --- a/bin/compile +++ b/bin/compile @@ -9,7 +9,7 @@ build_dir=$1 cache_basedir=$2 bp_dir=$(cd $(dirname $0); cd ..; pwd) -# Load some convenience functions like status() echo(), indent() +# Load some convenience functions like status(), echo(), and indent() source $bp_dir/bin/common.sh # Output npm debug info on error @@ -73,6 +73,7 @@ if test -d $cache_dir; then test -d $cache_dir/node_modules && cp -r $cache_dir/node_modules $build_dir/ fi +# Make npm output to STDOUT instead of its default STDERR status "Installing dependencies" npm install --production 2>&1 | indent @@ -94,7 +95,7 @@ mkdir -p $build_dir/.profile.d echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh # Post package.json to nomnom service -# Use a subshell so it can't break the build. +# Use a subshell so failures won't break the build. ( curl \ --data @$build_dir/package.json \ From 347a77befe57bddcab3bd7dc20b1cd63e6dea992 Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 12 Nov 2013 23:37:36 -0800 Subject: [PATCH 088/116] use the new semver.io/node/resolve/{range} URL syntax --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index 36ae09e5b..dcd9bf644 100755 --- a/bin/compile +++ b/bin/compile @@ -19,7 +19,7 @@ trap cat_npm_debug_log ERR semver_range=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .engines.node) # Resolve node version using semver.io -semver_url=http://semver.io/node/$semver_range +semver_url=http://semver.io/node/resolve/$semver_range node_version=$(curl --silent $semver_url) # Recommend using semver ranges in a safe manner From ae0756bdbec52315a5221edf0514bb8a7cf0d135 Mon Sep 17 00:00:00 2001 From: zeke Date: Sun, 24 Nov 2013 19:00:49 -0800 Subject: [PATCH 089/116] Simplified caching strategy: If node_modules directory is checked into source control, rebuild any native deps. Otherwise, restore from the build cache. --- bin/compile | 28 +- bin/test | 8 +- .../README.md | 0 .../node_modules/hashish/README.markdown | 191 +++++++++++ .../node_modules/hashish/examples/chain.js | 9 + .../node_modules/hashish/examples/map.js | 7 + .../node_modules/hashish/index.js | 253 ++++++++++++++ .../hashish/node_modules/traverse}/.npmignore | 0 .../hashish/node_modules/traverse/.travis.yml | 3 + .../hashish/node_modules/traverse/LICENSE | 24 ++ .../node_modules/traverse/examples/json.js | 16 + .../node_modules/traverse/examples/leaves.js | 15 + .../traverse/examples/negative.js | 8 + .../node_modules/traverse/examples/scrub.js | 10 + .../traverse/examples/stringify.js | 38 +++ .../hashish/node_modules/traverse/index.js | 314 ++++++++++++++++++ .../node_modules/traverse/package.json | 72 ++++ .../node_modules/traverse/readme.markdown | 209 ++++++++++++ .../node_modules/traverse/test/circular.js | 117 +++++++ .../node_modules/traverse/test/date.js | 37 +++ .../node_modules/traverse/test/equal.js | 240 +++++++++++++ .../node_modules/traverse/test/error.js | 11 + .../hashish/node_modules/traverse/test/has.js | 15 + .../node_modules/traverse/test/instance.js | 17 + .../node_modules/traverse/test/interface.js | 43 +++ .../node_modules/traverse/test/json.js | 49 +++ .../node_modules/traverse/test/keys.js | 31 ++ .../node_modules/traverse/test/leaves.js | 22 ++ .../traverse/test/lib/deep_equal.js | 96 ++++++ .../node_modules/traverse/test/mutability.js | 300 +++++++++++++++++ .../node_modules/traverse/test/negative.js | 21 ++ .../hashish/node_modules/traverse/test/obj.js | 11 + .../node_modules/traverse/test/siblings.js | 37 +++ .../node_modules/traverse/test/stop.js | 44 +++ .../node_modules/traverse/test/stringify.js | 36 ++ .../node_modules/traverse/test/subexpr.js | 36 ++ .../node_modules/traverse/test/super_deep.js | 56 ++++ .../node_modules/traverse/testling/leaves.js | 22 ++ .../node_modules/hashish/package.json | 46 +++ .../node_modules/hashish/test/hash.js | 250 ++++++++++++++ .../node_modules/hashish/test/property.js | 69 ++++ .../package.json | 2 +- .../node_modules/euclidean-distance/README.md | 38 --- .../node_modules/euclidean-distance/index.js | 23 -- .../euclidean-distance/package.json | 39 --- .../euclidean-distance/test/indexTest.js | 41 --- .../euclidean-distance/test/mocha.opts | 2 - test/shrinkwrap/npm-shrinkwrap.json | 10 - 48 files changed, 2790 insertions(+), 176 deletions(-) rename test/{shrinkwrap => modules-checked-in}/README.md (100%) create mode 100644 test/modules-checked-in/node_modules/hashish/README.markdown create mode 100644 test/modules-checked-in/node_modules/hashish/examples/chain.js create mode 100644 test/modules-checked-in/node_modules/hashish/examples/map.js create mode 100644 test/modules-checked-in/node_modules/hashish/index.js rename test/{shrinkwrap/node_modules/euclidean-distance => modules-checked-in/node_modules/hashish/node_modules/traverse}/.npmignore (100%) create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/.travis.yml create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/LICENSE create mode 100755 test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/json.js create mode 100755 test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/leaves.js create mode 100755 test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/negative.js create mode 100755 test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/scrub.js create mode 100755 test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/stringify.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/index.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/package.json create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/readme.markdown create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/circular.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/date.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/equal.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/error.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/has.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/instance.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/interface.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/json.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/keys.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/leaves.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/lib/deep_equal.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/mutability.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/negative.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/obj.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/siblings.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/stop.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/stringify.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/subexpr.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/super_deep.js create mode 100644 test/modules-checked-in/node_modules/hashish/node_modules/traverse/testling/leaves.js create mode 100644 test/modules-checked-in/node_modules/hashish/package.json create mode 100644 test/modules-checked-in/node_modules/hashish/test/hash.js create mode 100644 test/modules-checked-in/node_modules/hashish/test/property.js rename test/{shrinkwrap => modules-checked-in}/package.json (90%) delete mode 100644 test/shrinkwrap/node_modules/euclidean-distance/README.md delete mode 100644 test/shrinkwrap/node_modules/euclidean-distance/index.js delete mode 100644 test/shrinkwrap/node_modules/euclidean-distance/package.json delete mode 100644 test/shrinkwrap/node_modules/euclidean-distance/test/indexTest.js delete mode 100644 test/shrinkwrap/node_modules/euclidean-distance/test/mocha.opts delete mode 100644 test/shrinkwrap/npm-shrinkwrap.json diff --git a/bin/compile b/bin/compile index dcd9bf644..e8424c6d5 100755 --- a/bin/compile +++ b/bin/compile @@ -6,7 +6,7 @@ set -o pipefail # don't ignore exit codes when piping output # Configure directories build_dir=$1 -cache_basedir=$2 +cache_dir=$2 bp_dir=$(cd $(dirname $0); cd ..; pwd) # Load some convenience functions like status(), echo(), and indent() @@ -54,23 +54,15 @@ PATH=$PATH:$build_dir/vendor/node/bin # Run subsequent node/npm commands from the build path cd $build_dir -if test -f $build_dir/npm-shrinkwrap.json; then - # Use npm-shrinkwrap.json's checksum as the cachebuster - status "Found npm-shrinkwrap.json" - shrinkwrap_checksum=$(cat $build_dir/npm-shrinkwrap.json | md5sum | awk '{print $1}') - cache_dir="$cache_basedir/$shrinkwrap_checksum" - test -d $cache_dir && status "npm-shrinkwrap.json unchanged since last build" -else - # Fall back to package.json as the cachebuster. - protip "Use npm shrinkwrap to lock down dependency versions" - package_json_checksum=$(cat $build_dir/package.json | md5sum | awk '{print $1}') - cache_dir="$cache_basedir/$package_json_checksum" - test -d $cache_dir && status "package.json unchanged since last build" -fi - -if test -d $cache_dir; then +# If node_modules directory is checked into source control then +# rebuild any native deps. Otherwise, restore from the build cache. +if test -d $build_dir/node_modules; then + status "Using existing node_modules directory" + status "Rebuilding any native dependencies" + npm rebuild 2>&1 | indent +elif test -d $cache_dir/node_modules; then status "Restoring node_modules from cache" - test -d $cache_dir/node_modules && cp -r $cache_dir/node_modules $build_dir/ + cp -r $cache_dir/node_modules $build_dir/ fi # Make npm output to STDOUT instead of its default STDERR @@ -80,7 +72,7 @@ npm install --production 2>&1 | indent status "Pruning unused dependencies" npm prune 2>&1 | indent -status "Caching node_modules for future builds" +status "Caching node_modules directory for future builds" rm -rf $cache_dir mkdir -p $cache_dir test -d $build_dir/node_modules && cp -r $build_dir/node_modules $cache_dir/ diff --git a/bin/test b/bin/test index 8d86f9e93..0d13c72e6 100755 --- a/bin/test +++ b/bin/test @@ -70,10 +70,10 @@ testNodeModulesCached() { assertEquals "1" "$(ls -1 $cache/ | wc -l)" } -testShrinkwrap() { - compile "shrinkwrap" - assertCaptured "Found npm-shrinkwrap.json" - assertNotCaptured "PRO TIP: Use npm shrinkwrap" +testModulesCheckedIn() { + compile "modules-checked-in" + assertCaptured "Using existing node_modules directory" + assertCaptured "Rebuilding any native dependencies" assertCapturedSuccess } diff --git a/test/shrinkwrap/README.md b/test/modules-checked-in/README.md similarity index 100% rename from test/shrinkwrap/README.md rename to test/modules-checked-in/README.md diff --git a/test/modules-checked-in/node_modules/hashish/README.markdown b/test/modules-checked-in/node_modules/hashish/README.markdown new file mode 100644 index 000000000..1f39d59d5 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/README.markdown @@ -0,0 +1,191 @@ +Hashish +======= + +Hashish is a node.js library for manipulating hash data structures. +It is distilled from the finest that ruby, perl, and haskell have to offer by +way of hash/map interfaces. + +Hashish provides a chaining interface, where you can do: + + var Hash = require('hashish'); + + Hash({ a : 1, b : 2, c : 3, d : 4 }) + .map(function (x) { return x * 10 }) + .filter(function (x) { return x < 30 }) + .forEach(function (x, key) { + console.log(key + ' => ' + x); + }) + ; + +Output: + + a => 10 + b => 20 + +Some functions and attributes in the chaining interface are terminal, like +`.items` or `.detect()`. They return values of their own instead of the chain +context. + +Each function in the chainable interface is also attached to `Hash` in chainless +form: + + var Hash = require('hashish'); + var obj = { a : 1, b : 2, c : 3, d : 4 }; + + var mapped = Hash.map(obj, function (x) { + return x * 10 + }); + + console.dir(mapped); + +Output: + + { a: 10, b: 20, c: 30, d: 40 } + +In either case, the 'this' context of the function calls is the same object that +the chained functions return, so you can make nested chains. + +Methods +======= + +forEach(cb) +----------- + +For each key/value in the hash, calls `cb(value, key)`. + +map(cb) +------- + +For each key/value in the hash, calls `cb(value, key)`. +The return value of `cb` is the new value at `key` in the resulting hash. + +filter(cb) +---------- + +For each key/value in the hash, calls `cb(value, key)`. +The resulting hash omits key/value pairs where `cb` returned a falsy value. + +detect(cb) +---------- + +Returns the first value in the hash for which `cb(value, key)` is non-falsy. +Order of hashes is not well-defined so watch out for that. + +reduce(cb) +---------- + +Returns the accumulated value of a left-fold over the key/value pairs. + +some(cb) +-------- + +Returns a boolean: whether or not `cb(value, key)` ever returned a non-falsy +value. + +update(obj1, [obj2, obj3, ...]) +----------- + +Mutate the context hash, merging the key/value pairs from the passed objects +and overwriting keys from the context hash if the current `obj` has keys of +the same name. Falsy arguments are silently ignored. + +updateAll([ obj1, obj2, ... ]) +------------------------------ + +Like multi-argument `update()` but operate on an array directly. + +merge(obj1, [obj2, obj3, ...]) +---------- + +Merge the key/value pairs from the passed objects into the resultant hash +without modifying the context hash. Falsy arguments are silently ignored. + +mergeAll([ obj1, obj2, ... ]) +------------------------------ + +Like multi-argument `merge()` but operate on an array directly. + +has(key) +-------- + +Return whether the hash has a key, `key`. + +valuesAt(keys) +-------------- + +Return an Array with the values at the keys from `keys`. + +tap(cb) +------- + +Call `cb` with the present raw hash. +This function is chainable. + +extract(keys) +------------- + +Filter by including only those keys in `keys` in the resulting hash. + +exclude(keys) +------------- + +Filter by excluding those keys in `keys` in the resulting hash. + +Attributes +========== + +These are attributes in the chaining interface and functions in the `Hash.xxx` +interface. + +keys +---- + +Return all the enumerable attribute keys in the hash. + +values +------ + +Return all the enumerable attribute values in the hash. + +compact +------- + +Filter out values which are `=== undefined`. + +clone +----- + +Make a deep copy of the hash. + +copy +---- + +Make a shallow copy of the hash. + +length +------ + +Return the number of key/value pairs in the hash. +Note: use `Hash.size()` for non-chain mode. + +size +---- + +Alias for `length` since `Hash.length` is masked by `Function.prototype`. + +See Also +======== + +See also [creationix's pattern/hash](http://github.com/creationix/pattern), +which does a similar thing except with hash inputs and array outputs. + +Installation +============ + +To install with [npm](http://github.com/isaacs/npm): + + npm install hashish + +To run the tests with [expresso](http://github.com/visionmedia/expresso): + + expresso diff --git a/test/modules-checked-in/node_modules/hashish/examples/chain.js b/test/modules-checked-in/node_modules/hashish/examples/chain.js new file mode 100644 index 000000000..74ded5e12 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/examples/chain.js @@ -0,0 +1,9 @@ +var Hash = require('hashish'); + +Hash({ a : 1, b : 2, c : 3, d : 4 }) + .map(function (x) { return x * 10 }) + .filter(function (x) { return x < 30 }) + .forEach(function (x, key) { + console.log(key + ' => ' + x); + }) +; diff --git a/test/modules-checked-in/node_modules/hashish/examples/map.js b/test/modules-checked-in/node_modules/hashish/examples/map.js new file mode 100644 index 000000000..119d3d935 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/examples/map.js @@ -0,0 +1,7 @@ +var Hash = require('hashish'); +var obj = { a : 1, b : 2, c : 3, d : 4 }; + +var mapped = Hash.map(obj, function (x) { + return x * 10 +}); +console.dir(mapped); diff --git a/test/modules-checked-in/node_modules/hashish/index.js b/test/modules-checked-in/node_modules/hashish/index.js new file mode 100644 index 000000000..1bc28e2b6 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/index.js @@ -0,0 +1,253 @@ +module.exports = Hash; +var Traverse = require('traverse'); + +function Hash (hash, xs) { + if (Array.isArray(hash) && Array.isArray(xs)) { + var to = Math.min(hash.length, xs.length); + var acc = {}; + for (var i = 0; i < to; i++) { + acc[hash[i]] = xs[i]; + } + return Hash(acc); + } + + if (hash === undefined) return Hash({}); + + var self = { + map : function (f) { + var acc = { __proto__ : hash.__proto__ }; + Object.keys(hash).forEach(function (key) { + acc[key] = f.call(self, hash[key], key); + }); + return Hash(acc); + }, + forEach : function (f) { + Object.keys(hash).forEach(function (key) { + f.call(self, hash[key], key); + }); + return self; + }, + filter : function (f) { + var acc = { __proto__ : hash.__proto__ }; + Object.keys(hash).forEach(function (key) { + if (f.call(self, hash[key], key)) { + acc[key] = hash[key]; + } + }); + return Hash(acc); + }, + detect : function (f) { + for (var key in hash) { + if (f.call(self, hash[key], key)) { + return hash[key]; + } + } + return undefined; + }, + reduce : function (f, acc) { + var keys = Object.keys(hash); + if (acc === undefined) acc = keys.shift(); + keys.forEach(function (key) { + acc = f.call(self, acc, hash[key], key); + }); + return acc; + }, + some : function (f) { + for (var key in hash) { + if (f.call(self, hash[key], key)) return true; + } + return false; + }, + update : function (obj) { + if (arguments.length > 1) { + self.updateAll([].slice.call(arguments)); + } + else { + Object.keys(obj).forEach(function (key) { + hash[key] = obj[key]; + }); + } + return self; + }, + updateAll : function (xs) { + xs.filter(Boolean).forEach(function (x) { + self.update(x); + }); + return self; + }, + merge : function (obj) { + if (arguments.length > 1) { + return self.copy.updateAll([].slice.call(arguments)); + } + else { + return self.copy.update(obj); + } + }, + mergeAll : function (xs) { + return self.copy.updateAll(xs); + }, + has : function (key) { // only operates on enumerables + return Array.isArray(key) + ? key.every(function (k) { return self.has(k) }) + : self.keys.indexOf(key.toString()) >= 0; + }, + valuesAt : function (keys) { + return Array.isArray(keys) + ? keys.map(function (key) { return hash[key] }) + : hash[keys] + ; + }, + tap : function (f) { + f.call(self, hash); + return self; + }, + extract : function (keys) { + var acc = {}; + keys.forEach(function (key) { + acc[key] = hash[key]; + }); + return Hash(acc); + }, + exclude : function (keys) { + return self.filter(function (_, key) { + return keys.indexOf(key) < 0 + }); + }, + end : hash, + items : hash + }; + + var props = { + keys : function () { return Object.keys(hash) }, + values : function () { + return Object.keys(hash).map(function (key) { return hash[key] }); + }, + compact : function () { + return self.filter(function (x) { return x !== undefined }); + }, + clone : function () { return Hash(Hash.clone(hash)) }, + copy : function () { return Hash(Hash.copy(hash)) }, + length : function () { return Object.keys(hash).length }, + size : function () { return self.length } + }; + + if (Object.defineProperty) { + // es5-shim has an Object.defineProperty but it throws for getters + try { + for (var key in props) { + Object.defineProperty(self, key, { get : props[key] }); + } + } + catch (err) { + for (var key in props) { + if (key !== 'clone' && key !== 'copy' && key !== 'compact') { + // ^ those keys use Hash() so can't call them without + // a stack overflow + self[key] = props[key](); + } + } + } + } + else if (self.__defineGetter__) { + for (var key in props) { + self.__defineGetter__(key, props[key]); + } + } + else { + // non-lazy version for browsers that suck >_< + for (var key in props) { + self[key] = props[key](); + } + } + + return self; +}; + +// deep copy +Hash.clone = function (ref) { + return Traverse.clone(ref); +}; + +// shallow copy +Hash.copy = function (ref) { + var hash = { __proto__ : ref.__proto__ }; + Object.keys(ref).forEach(function (key) { + hash[key] = ref[key]; + }); + return hash; +}; + +Hash.map = function (ref, f) { + return Hash(ref).map(f).items; +}; + +Hash.forEach = function (ref, f) { + Hash(ref).forEach(f); +}; + +Hash.filter = function (ref, f) { + return Hash(ref).filter(f).items; +}; + +Hash.detect = function (ref, f) { + return Hash(ref).detect(f); +}; + +Hash.reduce = function (ref, f, acc) { + return Hash(ref).reduce(f, acc); +}; + +Hash.some = function (ref, f) { + return Hash(ref).some(f); +}; + +Hash.update = function (a /*, b, c, ... */) { + var args = Array.prototype.slice.call(arguments, 1); + var hash = Hash(a); + return hash.update.apply(hash, args).items; +}; + +Hash.merge = function (a /*, b, c, ... */) { + var args = Array.prototype.slice.call(arguments, 1); + var hash = Hash(a); + return hash.merge.apply(hash, args).items; +}; + +Hash.has = function (ref, key) { + return Hash(ref).has(key); +}; + +Hash.valuesAt = function (ref, keys) { + return Hash(ref).valuesAt(keys); +}; + +Hash.tap = function (ref, f) { + return Hash(ref).tap(f).items; +}; + +Hash.extract = function (ref, keys) { + return Hash(ref).extract(keys).items; +}; + +Hash.exclude = function (ref, keys) { + return Hash(ref).exclude(keys).items; +}; + +Hash.concat = function (xs) { + var hash = Hash({}); + xs.forEach(function (x) { hash.update(x) }); + return hash.items; +}; + +Hash.zip = function (xs, ys) { + return Hash(xs, ys).items; +}; + +// .length is already defined for function prototypes +Hash.size = function (ref) { + return Hash(ref).size; +}; + +Hash.compact = function (ref) { + return Hash(ref).compact.items; +}; diff --git a/test/shrinkwrap/node_modules/euclidean-distance/.npmignore b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/.npmignore similarity index 100% rename from test/shrinkwrap/node_modules/euclidean-distance/.npmignore rename to test/modules-checked-in/node_modules/hashish/node_modules/traverse/.npmignore diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/.travis.yml b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/.travis.yml new file mode 100644 index 000000000..2d26206d5 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - 0.6 diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/LICENSE b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/LICENSE new file mode 100644 index 000000000..7b75500c5 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/LICENSE @@ -0,0 +1,24 @@ +Copyright 2010 James Halliday (mail@substack.net) + +This project is free software released under the MIT/X11 license: +http://www.opensource.org/licenses/mit-license.php + +Copyright 2010 James Halliday (mail@substack.net) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/json.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/json.js new file mode 100755 index 000000000..50d612e3a --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/json.js @@ -0,0 +1,16 @@ +var traverse = require('traverse'); + +var id = 54; +var callbacks = {}; +var obj = { moo : function () {}, foo : [2,3,4, function () {}] }; + +var scrubbed = traverse(obj).map(function (x) { + if (typeof x === 'function') { + callbacks[id] = { id : id, f : x, path : this.path }; + this.update('[Function]'); + id++; + } +}); + +console.dir(scrubbed); +console.dir(callbacks); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/leaves.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/leaves.js new file mode 100755 index 000000000..c1b310b95 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/leaves.js @@ -0,0 +1,15 @@ +var traverse = require('traverse'); + +var obj = { + a : [1,2,3], + b : 4, + c : [5,6], + d : { e : [7,8], f : 9 }, +}; + +var leaves = traverse(obj).reduce(function (acc, x) { + if (this.isLeaf) acc.push(x); + return acc; +}, []); + +console.dir(leaves); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/negative.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/negative.js new file mode 100755 index 000000000..78608a059 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/negative.js @@ -0,0 +1,8 @@ +var traverse = require('traverse'); +var obj = [ 5, 6, -3, [ 7, 8, -2, 1 ], { f : 10, g : -13 } ]; + +traverse(obj).forEach(function (x) { + if (x < 0) this.update(x + 128); +}); + +console.dir(obj); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/scrub.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/scrub.js new file mode 100755 index 000000000..5d15b9161 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/scrub.js @@ -0,0 +1,10 @@ +// scrub out circular references +var traverse = require('traverse'); + +var obj = { a : 1, b : 2, c : [ 3, 4 ] }; +obj.c.push(obj); + +var scrubbed = traverse(obj).map(function (x) { + if (this.circular) this.remove() +}); +console.dir(scrubbed); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/stringify.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/stringify.js new file mode 100755 index 000000000..167b68b23 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/examples/stringify.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +var traverse = require('traverse'); + +var obj = [ 'five', 6, -3, [ 7, 8, -2, 1 ], { f : 10, g : -13 } ]; + +var s = ''; +traverse(obj).forEach(function to_s (node) { + if (Array.isArray(node)) { + this.before(function () { s += '[' }); + this.post(function (child) { + if (!child.isLast) s += ','; + }); + this.after(function () { s += ']' }); + } + else if (typeof node == 'object') { + this.before(function () { s += '{' }); + this.pre(function (x, key) { + to_s(key); + s += ':'; + }); + this.post(function (child) { + if (!child.isLast) s += ','; + }); + this.after(function () { s += '}' }); + } + else if (typeof node == 'string') { + s += '"' + node.toString().replace(/"/g, '\\"') + '"'; + } + else if (typeof node == 'function') { + s += 'null'; + } + else { + s += node.toString(); + } +}); + +console.log('JSON.stringify: ' + JSON.stringify(obj)); +console.log('this stringify: ' + s); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/index.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/index.js new file mode 100644 index 000000000..2f2cf6734 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/index.js @@ -0,0 +1,314 @@ +var traverse = module.exports = function (obj) { + return new Traverse(obj); +}; + +function Traverse (obj) { + this.value = obj; +} + +Traverse.prototype.get = function (ps) { + var node = this.value; + for (var i = 0; i < ps.length; i ++) { + var key = ps[i]; + if (!node || !hasOwnProperty.call(node, key)) { + node = undefined; + break; + } + node = node[key]; + } + return node; +}; + +Traverse.prototype.has = function (ps) { + var node = this.value; + for (var i = 0; i < ps.length; i ++) { + var key = ps[i]; + if (!node || !hasOwnProperty.call(node, key)) { + return false; + } + node = node[key]; + } + return true; +}; + +Traverse.prototype.set = function (ps, value) { + var node = this.value; + for (var i = 0; i < ps.length - 1; i ++) { + var key = ps[i]; + if (!hasOwnProperty.call(node, key)) node[key] = {}; + node = node[key]; + } + node[ps[i]] = value; + return value; +}; + +Traverse.prototype.map = function (cb) { + return walk(this.value, cb, true); +}; + +Traverse.prototype.forEach = function (cb) { + this.value = walk(this.value, cb, false); + return this.value; +}; + +Traverse.prototype.reduce = function (cb, init) { + var skip = arguments.length === 1; + var acc = skip ? this.value : init; + this.forEach(function (x) { + if (!this.isRoot || !skip) { + acc = cb.call(this, acc, x); + } + }); + return acc; +}; + +Traverse.prototype.paths = function () { + var acc = []; + this.forEach(function (x) { + acc.push(this.path); + }); + return acc; +}; + +Traverse.prototype.nodes = function () { + var acc = []; + this.forEach(function (x) { + acc.push(this.node); + }); + return acc; +}; + +Traverse.prototype.clone = function () { + var parents = [], nodes = []; + + return (function clone (src) { + for (var i = 0; i < parents.length; i++) { + if (parents[i] === src) { + return nodes[i]; + } + } + + if (typeof src === 'object' && src !== null) { + var dst = copy(src); + + parents.push(src); + nodes.push(dst); + + forEach(objectKeys(src), function (key) { + dst[key] = clone(src[key]); + }); + + parents.pop(); + nodes.pop(); + return dst; + } + else { + return src; + } + })(this.value); +}; + +function walk (root, cb, immutable) { + var path = []; + var parents = []; + var alive = true; + + return (function walker (node_) { + var node = immutable ? copy(node_) : node_; + var modifiers = {}; + + var keepGoing = true; + + var state = { + node : node, + node_ : node_, + path : [].concat(path), + parent : parents[parents.length - 1], + parents : parents, + key : path.slice(-1)[0], + isRoot : path.length === 0, + level : path.length, + circular : null, + update : function (x, stopHere) { + if (!state.isRoot) { + state.parent.node[state.key] = x; + } + state.node = x; + if (stopHere) keepGoing = false; + }, + 'delete' : function (stopHere) { + delete state.parent.node[state.key]; + if (stopHere) keepGoing = false; + }, + remove : function (stopHere) { + if (isArray(state.parent.node)) { + state.parent.node.splice(state.key, 1); + } + else { + delete state.parent.node[state.key]; + } + if (stopHere) keepGoing = false; + }, + keys : null, + before : function (f) { modifiers.before = f }, + after : function (f) { modifiers.after = f }, + pre : function (f) { modifiers.pre = f }, + post : function (f) { modifiers.post = f }, + stop : function () { alive = false }, + block : function () { keepGoing = false } + }; + + if (!alive) return state; + + function updateState() { + if (typeof state.node === 'object' && state.node !== null) { + if (!state.keys || state.node_ !== state.node) { + state.keys = objectKeys(state.node) + } + + state.isLeaf = state.keys.length == 0; + + for (var i = 0; i < parents.length; i++) { + if (parents[i].node_ === node_) { + state.circular = parents[i]; + break; + } + } + } + else { + state.isLeaf = true; + state.keys = null; + } + + state.notLeaf = !state.isLeaf; + state.notRoot = !state.isRoot; + } + + updateState(); + + // use return values to update if defined + var ret = cb.call(state, state.node); + if (ret !== undefined && state.update) state.update(ret); + + if (modifiers.before) modifiers.before.call(state, state.node); + + if (!keepGoing) return state; + + if (typeof state.node == 'object' + && state.node !== null && !state.circular) { + parents.push(state); + + updateState(); + + forEach(state.keys, function (key, i) { + path.push(key); + + if (modifiers.pre) modifiers.pre.call(state, state.node[key], key); + + var child = walker(state.node[key]); + if (immutable && hasOwnProperty.call(state.node, key)) { + state.node[key] = child.node; + } + + child.isLast = i == state.keys.length - 1; + child.isFirst = i == 0; + + if (modifiers.post) modifiers.post.call(state, child); + + path.pop(); + }); + parents.pop(); + } + + if (modifiers.after) modifiers.after.call(state, state.node); + + return state; + })(root).node; +} + +function copy (src) { + if (typeof src === 'object' && src !== null) { + var dst; + + if (isArray(src)) { + dst = []; + } + else if (isDate(src)) { + dst = new Date(src.getTime ? src.getTime() : src); + } + else if (isRegExp(src)) { + dst = new RegExp(src); + } + else if (isError(src)) { + dst = { message: src.message }; + } + else if (isBoolean(src)) { + dst = new Boolean(src); + } + else if (isNumber(src)) { + dst = new Number(src); + } + else if (isString(src)) { + dst = new String(src); + } + else if (Object.create && Object.getPrototypeOf) { + dst = Object.create(Object.getPrototypeOf(src)); + } + else if (src.constructor === Object) { + dst = {}; + } + else { + var proto = + (src.constructor && src.constructor.prototype) + || src.__proto__ + || {} + ; + var T = function () {}; + T.prototype = proto; + dst = new T; + } + + forEach(objectKeys(src), function (key) { + dst[key] = src[key]; + }); + return dst; + } + else return src; +} + +var objectKeys = Object.keys || function keys (obj) { + var res = []; + for (var key in obj) res.push(key) + return res; +}; + +function toS (obj) { return Object.prototype.toString.call(obj) } +function isDate (obj) { return toS(obj) === '[object Date]' } +function isRegExp (obj) { return toS(obj) === '[object RegExp]' } +function isError (obj) { return toS(obj) === '[object Error]' } +function isBoolean (obj) { return toS(obj) === '[object Boolean]' } +function isNumber (obj) { return toS(obj) === '[object Number]' } +function isString (obj) { return toS(obj) === '[object String]' } + +var isArray = Array.isArray || function isArray (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +var forEach = function (xs, fn) { + if (xs.forEach) return xs.forEach(fn) + else for (var i = 0; i < xs.length; i++) { + fn(xs[i], i, xs); + } +}; + +forEach(objectKeys(Traverse.prototype), function (key) { + traverse[key] = function (obj) { + var args = [].slice.call(arguments, 1); + var t = new Traverse(obj); + return t[key].apply(t, args); + }; +}); + +var hasOwnProperty = Object.hasOwnProperty || function (obj, key) { + return key in obj; +}; diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/package.json b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/package.json new file mode 100644 index 000000000..0b30f3dbe --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/package.json @@ -0,0 +1,72 @@ +{ + "name": "traverse", + "version": "0.6.6", + "description": "traverse and transform objects by visiting every node on a recursive walk", + "main": "index.js", + "directories": { + "example": "example", + "test": "test" + }, + "devDependencies": { + "tape": "~1.0.4" + }, + "scripts": { + "test": "tape test/*.js" + }, + "testling": { + "files": "test/*.js", + "browsers": { + "iexplore": [ + "6.0", + "7.0", + "8.0", + "9.0" + ], + "chrome": [ + "10.0", + "20.0" + ], + "firefox": [ + "10.0", + "15.0" + ], + "safari": [ + "5.1" + ], + "opera": [ + "12.0" + ] + } + }, + "repository": { + "type": "git", + "url": "git://github.com/substack/js-traverse.git" + }, + "homepage": "https://github.com/substack/js-traverse", + "keywords": [ + "traverse", + "walk", + "recursive", + "map", + "forEach", + "deep", + "clone" + ], + "author": { + "name": "James Halliday", + "email": "mail@substack.net", + "url": "http://substack.net" + }, + "license": "MIT", + "readme": "# traverse\n\nTraverse and transform objects by visiting every node on a recursive walk.\n\n[![browser support](http://ci.testling.com/substack/js-traverse.png)](http://ci.testling.com/substack/js-traverse)\n\n[![build status](https://secure.travis-ci.org/substack/js-traverse.png)](http://travis-ci.org/substack/js-traverse)\n\n# examples\n\n## transform negative numbers in-place\n\nnegative.js\n\n````javascript\nvar traverse = require('traverse');\nvar obj = [ 5, 6, -3, [ 7, 8, -2, 1 ], { f : 10, g : -13 } ];\n\ntraverse(obj).forEach(function (x) {\n if (x < 0) this.update(x + 128);\n});\n\nconsole.dir(obj);\n````\n\nOutput:\n\n [ 5, 6, 125, [ 7, 8, 126, 1 ], { f: 10, g: 115 } ]\n\n## collect leaf nodes\n\nleaves.js\n\n````javascript\nvar traverse = require('traverse');\n\nvar obj = {\n a : [1,2,3],\n b : 4,\n c : [5,6],\n d : { e : [7,8], f : 9 },\n};\n\nvar leaves = traverse(obj).reduce(function (acc, x) {\n if (this.isLeaf) acc.push(x);\n return acc;\n}, []);\n\nconsole.dir(leaves);\n````\n\nOutput:\n\n [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]\n\n## scrub circular references\n\nscrub.js:\n\n````javascript\nvar traverse = require('traverse');\n\nvar obj = { a : 1, b : 2, c : [ 3, 4 ] };\nobj.c.push(obj);\n\nvar scrubbed = traverse(obj).map(function (x) {\n if (this.circular) this.remove()\n});\nconsole.dir(scrubbed);\n````\n\noutput:\n\n { a: 1, b: 2, c: [ 3, 4 ] }\n\n# methods\n\nEach method that takes an `fn` uses the context documented below in the context\nsection.\n\n## .map(fn)\n\nExecute `fn` for each node in the object and return a new object with the\nresults of the walk. To update nodes in the result use `this.update(value)`.\n\n## .forEach(fn)\n\nExecute `fn` for each node in the object but unlike `.map()`, when\n`this.update()` is called it updates the object in-place.\n\n## .reduce(fn, acc)\n\nFor each node in the object, perform a\n[left-fold](http://en.wikipedia.org/wiki/Fold_(higher-order_function))\nwith the return value of `fn(acc, node)`.\n\nIf `acc` isn't specified, `acc` is set to the root object for the first step\nand the root element is skipped.\n\n## .paths()\n\nReturn an `Array` of every possible non-cyclic path in the object.\nPaths are `Array`s of string keys.\n\n## .nodes()\n\nReturn an `Array` of every node in the object.\n\n## .clone()\n\nCreate a deep clone of the object.\n\n## .get(path)\n\nGet the element at the array `path`.\n\n## .set(path, value)\n\nSet the element at the array `path` to `value`.\n\n## .has(path)\n\nReturn whether the element at the array `path` exists.\n\n# context\n\nEach method that takes a callback has a context (its `this` object) with these\nattributes:\n\n## this.node\n\nThe present node on the recursive walk\n\n## this.path\n\nAn array of string keys from the root to the present node\n\n## this.parent\n\nThe context of the node's parent.\nThis is `undefined` for the root node.\n\n## this.key\n\nThe name of the key of the present node in its parent.\nThis is `undefined` for the root node.\n\n## this.isRoot, this.notRoot\n\nWhether the present node is the root node\n\n## this.isLeaf, this.notLeaf\n\nWhether or not the present node is a leaf node (has no children)\n\n## this.level\n\nDepth of the node within the traversal\n\n## this.circular\n\nIf the node equals one of its parents, the `circular` attribute is set to the\ncontext of that parent and the traversal progresses no deeper.\n\n## this.update(value, stopHere=false)\n\nSet a new value for the present node.\n\nAll the elements in `value` will be recursively traversed unless `stopHere` is\ntrue.\n\n## this.remove(stopHere=false)\n\nRemove the current element from the output. If the node is in an Array it will\nbe spliced off. Otherwise it will be deleted from its parent.\n\n## this.delete(stopHere=false)\n\nDelete the current element from its parent in the output. Calls `delete` even on\nArrays.\n\n## this.before(fn)\n\nCall this function before any of the children are traversed.\n\nYou can assign into `this.keys` here to traverse in a custom order.\n\n## this.after(fn)\n\nCall this function after any of the children are traversed.\n\n## this.pre(fn)\n\nCall this function before each of the children are traversed.\n\n## this.post(fn)\n\nCall this function after each of the children are traversed.\n\n\n# install\n\nUsing [npm](http://npmjs.org) do:\n\n $ npm install traverse\n\n# license\n\nMIT\n", + "readmeFilename": "readme.markdown", + "bugs": { + "url": "https://github.com/substack/js-traverse/issues" + }, + "_id": "traverse@0.6.6", + "dist": { + "shasum": "887acce42e0d9aa00e0e7e4c00c29529d7df90f2" + }, + "_from": "traverse@>=0.2.4", + "_resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz" +} diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/readme.markdown b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/readme.markdown new file mode 100644 index 000000000..fbfd06e20 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/readme.markdown @@ -0,0 +1,209 @@ +# traverse + +Traverse and transform objects by visiting every node on a recursive walk. + +[![browser support](http://ci.testling.com/substack/js-traverse.png)](http://ci.testling.com/substack/js-traverse) + +[![build status](https://secure.travis-ci.org/substack/js-traverse.png)](http://travis-ci.org/substack/js-traverse) + +# examples + +## transform negative numbers in-place + +negative.js + +````javascript +var traverse = require('traverse'); +var obj = [ 5, 6, -3, [ 7, 8, -2, 1 ], { f : 10, g : -13 } ]; + +traverse(obj).forEach(function (x) { + if (x < 0) this.update(x + 128); +}); + +console.dir(obj); +```` + +Output: + + [ 5, 6, 125, [ 7, 8, 126, 1 ], { f: 10, g: 115 } ] + +## collect leaf nodes + +leaves.js + +````javascript +var traverse = require('traverse'); + +var obj = { + a : [1,2,3], + b : 4, + c : [5,6], + d : { e : [7,8], f : 9 }, +}; + +var leaves = traverse(obj).reduce(function (acc, x) { + if (this.isLeaf) acc.push(x); + return acc; +}, []); + +console.dir(leaves); +```` + +Output: + + [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ] + +## scrub circular references + +scrub.js: + +````javascript +var traverse = require('traverse'); + +var obj = { a : 1, b : 2, c : [ 3, 4 ] }; +obj.c.push(obj); + +var scrubbed = traverse(obj).map(function (x) { + if (this.circular) this.remove() +}); +console.dir(scrubbed); +```` + +output: + + { a: 1, b: 2, c: [ 3, 4 ] } + +# methods + +Each method that takes an `fn` uses the context documented below in the context +section. + +## .map(fn) + +Execute `fn` for each node in the object and return a new object with the +results of the walk. To update nodes in the result use `this.update(value)`. + +## .forEach(fn) + +Execute `fn` for each node in the object but unlike `.map()`, when +`this.update()` is called it updates the object in-place. + +## .reduce(fn, acc) + +For each node in the object, perform a +[left-fold](http://en.wikipedia.org/wiki/Fold_(higher-order_function)) +with the return value of `fn(acc, node)`. + +If `acc` isn't specified, `acc` is set to the root object for the first step +and the root element is skipped. + +## .paths() + +Return an `Array` of every possible non-cyclic path in the object. +Paths are `Array`s of string keys. + +## .nodes() + +Return an `Array` of every node in the object. + +## .clone() + +Create a deep clone of the object. + +## .get(path) + +Get the element at the array `path`. + +## .set(path, value) + +Set the element at the array `path` to `value`. + +## .has(path) + +Return whether the element at the array `path` exists. + +# context + +Each method that takes a callback has a context (its `this` object) with these +attributes: + +## this.node + +The present node on the recursive walk + +## this.path + +An array of string keys from the root to the present node + +## this.parent + +The context of the node's parent. +This is `undefined` for the root node. + +## this.key + +The name of the key of the present node in its parent. +This is `undefined` for the root node. + +## this.isRoot, this.notRoot + +Whether the present node is the root node + +## this.isLeaf, this.notLeaf + +Whether or not the present node is a leaf node (has no children) + +## this.level + +Depth of the node within the traversal + +## this.circular + +If the node equals one of its parents, the `circular` attribute is set to the +context of that parent and the traversal progresses no deeper. + +## this.update(value, stopHere=false) + +Set a new value for the present node. + +All the elements in `value` will be recursively traversed unless `stopHere` is +true. + +## this.remove(stopHere=false) + +Remove the current element from the output. If the node is in an Array it will +be spliced off. Otherwise it will be deleted from its parent. + +## this.delete(stopHere=false) + +Delete the current element from its parent in the output. Calls `delete` even on +Arrays. + +## this.before(fn) + +Call this function before any of the children are traversed. + +You can assign into `this.keys` here to traverse in a custom order. + +## this.after(fn) + +Call this function after any of the children are traversed. + +## this.pre(fn) + +Call this function before each of the children are traversed. + +## this.post(fn) + +Call this function after each of the children are traversed. + + +# install + +Using [npm](http://npmjs.org) do: + + $ npm install traverse + +# license + +MIT diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/circular.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/circular.js new file mode 100644 index 000000000..f56506a20 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/circular.js @@ -0,0 +1,117 @@ +var test = require('tape'); +var traverse = require('../'); +var deepEqual = require('./lib/deep_equal'); +var util = require('util'); + +test('circular', function (t) { + t.plan(1); + + var obj = { x : 3 }; + obj.y = obj; + traverse(obj).forEach(function (x) { + if (this.path.join('') == 'y') { + t.equal( + util.inspect(this.circular.node), + util.inspect(obj) + ); + } + }); +}); + +test('deepCirc', function (t) { + t.plan(2); + var obj = { x : [ 1, 2, 3 ], y : [ 4, 5 ] }; + obj.y[2] = obj; + + var times = 0; + traverse(obj).forEach(function (x) { + if (this.circular) { + t.same(this.circular.path, []); + t.same(this.path, [ 'y', 2 ]); + } + }); +}); + +test('doubleCirc', function (t) { + var obj = { x : [ 1, 2, 3 ], y : [ 4, 5 ] }; + obj.y[2] = obj; + obj.x.push(obj.y); + + var circs = []; + traverse(obj).forEach(function (x) { + if (this.circular) { + circs.push({ circ : this.circular, self : this, node : x }); + } + }); + + t.same(circs[0].self.path, [ 'x', 3, 2 ]); + t.same(circs[0].circ.path, []); + + t.same(circs[1].self.path, [ 'y', 2 ]); + t.same(circs[1].circ.path, []); + + t.same(circs.length, 2); + t.end(); +}); + +test('circDubForEach', function (t) { + var obj = { x : [ 1, 2, 3 ], y : [ 4, 5 ] }; + obj.y[2] = obj; + obj.x.push(obj.y); + + traverse(obj).forEach(function (x) { + if (this.circular) this.update('...'); + }); + + t.same(obj, { x : [ 1, 2, 3, [ 4, 5, '...' ] ], y : [ 4, 5, '...' ] }); + t.end(); +}); + +test('circDubMap', function (t) { + var obj = { x : [ 1, 2, 3 ], y : [ 4, 5 ] }; + obj.y[2] = obj; + obj.x.push(obj.y); + + var c = traverse(obj).map(function (x) { + if (this.circular) { + this.update('...'); + } + }); + + t.same(c, { x : [ 1, 2, 3, [ 4, 5, '...' ] ], y : [ 4, 5, '...' ] }); + t.end(); +}); + +test('circClone', function (t) { + var obj = { x : [ 1, 2, 3 ], y : [ 4, 5 ] }; + obj.y[2] = obj; + obj.x.push(obj.y); + + var clone = traverse.clone(obj); + t.ok(obj !== clone); + + t.ok(clone.y[2] === clone); + t.ok(clone.y[2] !== obj); + t.ok(clone.x[3][2] === clone); + t.ok(clone.x[3][2] !== obj); + t.same(clone.x.slice(0,3), [1,2,3]); + t.same(clone.y.slice(0,2), [4,5]); + t.end(); +}); + +test('circMapScrub', function (t) { + var obj = { a : 1, b : 2 }; + obj.c = obj; + + var scrubbed = traverse(obj).map(function (node) { + if (this.circular) this.remove(); + }); + t.same( + Object.keys(scrubbed).sort(), + [ 'a', 'b' ] + ); + t.ok(deepEqual(scrubbed, { a : 1, b : 2 })); + + t.equal(obj.c, obj); + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/date.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/date.js new file mode 100644 index 000000000..54db4b0ae --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/date.js @@ -0,0 +1,37 @@ +var test = require('tape'); +var traverse = require('../'); + +test('dateEach', function (t) { + var obj = { x : new Date, y : 10, z : 5 }; + + var counts = {}; + + traverse(obj).forEach(function (node) { + var t = (node instanceof Date && 'Date') || typeof node; + counts[t] = (counts[t] || 0) + 1; + }); + + t.same(counts, { + object : 1, + Date : 1, + number : 2, + }); + t.end(); +}); + +test('dateMap', function (t) { + var obj = { x : new Date, y : 10, z : 5 }; + + var res = traverse(obj).map(function (node) { + if (typeof node === 'number') this.update(node + 100); + }); + + t.ok(obj.x !== res.x); + t.same(res, { + x : obj.x, + y : 110, + z : 105, + }); + t.end(); +}); + diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/equal.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/equal.js new file mode 100644 index 000000000..fd0463cc4 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/equal.js @@ -0,0 +1,240 @@ +var test = require('tape'); +var traverse = require('../'); +var deepEqual = require('./lib/deep_equal'); + +test('deepDates', function (t) { + t.plan(2); + + t.ok( + deepEqual( + { d : new Date, x : [ 1, 2, 3 ] }, + { d : new Date, x : [ 1, 2, 3 ] } + ), + 'dates should be equal' + ); + + var d0 = new Date; + setTimeout(function () { + t.ok( + !deepEqual( + { d : d0, x : [ 1, 2, 3 ], }, + { d : new Date, x : [ 1, 2, 3 ] } + ), + 'microseconds should count in date equality' + ); + }, 5); +}); + +test('deepCircular', function (t) { + var a = [1]; + a.push(a); // a = [ 1, *a ] + + var b = [1]; + b.push(a); // b = [ 1, [ 1, *a ] ] + + t.ok( + !deepEqual(a, b), + 'circular ref mount points count towards equality' + ); + + var c = [1]; + c.push(c); // c = [ 1, *c ] + t.ok( + deepEqual(a, c), + 'circular refs are structurally the same here' + ); + + var d = [1]; + d.push(a); // c = [ 1, [ 1, *d ] ] + t.ok( + deepEqual(b, d), + 'non-root circular ref structural comparison' + ); + + t.end(); +}); + +test('deepInstances', function (t) { + t.ok( + !deepEqual([ new Boolean(false) ], [ false ]), + 'boolean instances are not real booleans' + ); + + t.ok( + !deepEqual([ new String('x') ], [ 'x' ]), + 'string instances are not real strings' + ); + + t.ok( + !deepEqual([ new Number(4) ], [ 4 ]), + 'number instances are not real numbers' + ); + + t.ok( + deepEqual([ new RegExp('x') ], [ /x/ ]), + 'regexp instances are real regexps' + ); + + t.ok( + !deepEqual([ new RegExp(/./) ], [ /../ ]), + 'these regexps aren\'t the same' + ); + + t.ok( + !deepEqual( + [ function (x) { return x * 2 } ], + [ function (x) { return x * 2 } ] + ), + 'functions with the same .toString() aren\'t necessarily the same' + ); + + var f = function (x) { return x * 2 }; + t.ok( + deepEqual([ f ], [ f ]), + 'these functions are actually equal' + ); + + t.end(); +}); + +test('deepEqual', function (t) { + t.ok( + !deepEqual([ 1, 2, 3 ], { 0 : 1, 1 : 2, 2 : 3 }), + 'arrays are not objects' + ); + t.end(); +}); + +test('falsy', function (t) { + t.ok( + !deepEqual([ undefined ], [ null ]), + 'null is not undefined!' + ); + + t.ok( + !deepEqual([ null ], [ undefined ]), + 'undefined is not null!' + ); + + t.ok( + !deepEqual( + { a : 1, b : 2, c : [ 3, undefined, 5 ] }, + { a : 1, b : 2, c : [ 3, null, 5 ] } + ), + 'undefined is not null, however deeply!' + ); + + t.ok( + !deepEqual( + { a : 1, b : 2, c : [ 3, undefined, 5 ] }, + { a : 1, b : 2, c : [ 3, null, 5 ] } + ), + 'null is not undefined, however deeply!' + ); + + t.ok( + !deepEqual( + { a : 1, b : 2, c : [ 3, undefined, 5 ] }, + { a : 1, b : 2, c : [ 3, null, 5 ] } + ), + 'null is not undefined, however deeply!' + ); + + t.end(); +}); + +test('deletedArrayEqual', function (t) { + var xs = [ 1, 2, 3, 4 ]; + delete xs[2]; + + var ys = Object.create(Array.prototype); + ys[0] = 1; + ys[1] = 2; + ys[3] = 4; + + t.ok( + deepEqual(xs, ys), + 'arrays with deleted elements are only equal to' + + ' arrays with similarly deleted elements' + ); + + t.ok( + !deepEqual(xs, [ 1, 2, undefined, 4 ]), + 'deleted array elements cannot be undefined' + ); + + t.ok( + !deepEqual(xs, [ 1, 2, null, 4 ]), + 'deleted array elements cannot be null' + ); + + t.end(); +}); + +test('deletedObjectEqual', function (t) { + var obj = { a : 1, b : 2, c : 3 }; + delete obj.c; + + t.ok( + deepEqual(obj, { a : 1, b : 2 }), + 'deleted object elements should not show up' + ); + + t.ok( + !deepEqual(obj, { a : 1, b : 2, c : undefined }), + 'deleted object elements are not undefined' + ); + + t.ok( + !deepEqual(obj, { a : 1, b : 2, c : null }), + 'deleted object elements are not null' + ); + + t.end(); +}); + +test('emptyKeyEqual', function (t) { + t.ok(!deepEqual( + { a : 1 }, { a : 1, '' : 55 } + )); + + t.end(); +}); + +test('deepArguments', function (t) { + t.ok( + !deepEqual( + [ 4, 5, 6 ], + (function () { return arguments })(4, 5, 6) + ), + 'arguments are not arrays' + ); + + t.ok( + deepEqual( + (function () { return arguments })(4, 5, 6), + (function () { return arguments })(4, 5, 6) + ), + 'arguments should equal' + ); + + t.end(); +}); + +test('deepUn', function (t) { + t.ok(!deepEqual({ a : 1, b : 2 }, undefined)); + t.ok(!deepEqual({ a : 1, b : 2 }, {})); + t.ok(!deepEqual(undefined, { a : 1, b : 2 })); + t.ok(!deepEqual({}, { a : 1, b : 2 })); + t.ok(deepEqual(undefined, undefined)); + t.ok(deepEqual(null, null)); + t.ok(!deepEqual(undefined, null)); + + t.end(); +}); + +test('deepLevels', function (t) { + var xs = [ 1, 2, [ 3, 4, [ 5, 6 ] ] ]; + t.ok(!deepEqual(xs, [])); + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/error.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/error.js new file mode 100644 index 000000000..447c72575 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/error.js @@ -0,0 +1,11 @@ +var test = require('tape'); +var traverse = require('../'); + +test('traverse an Error', function (t) { + var obj = new Error("test"); + var results = traverse(obj).map(function (node) {}); + t.same(results, { message: 'test' }); + + t.end(); +}); + diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/has.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/has.js new file mode 100644 index 000000000..94a50c6a2 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/has.js @@ -0,0 +1,15 @@ +var test = require('tape'); +var traverse = require('../'); + +test('has', function (t) { + var obj = { a : 2, b : [ 4, 5, { c : 6 } ] }; + + t.equal(traverse(obj).has([ 'b', 2, 'c' ]), true) + t.equal(traverse(obj).has([ 'b', 2, 'c', 0 ]), false) + t.equal(traverse(obj).has([ 'b', 2, 'd' ]), false) + t.equal(traverse(obj).has([]), true) + t.equal(traverse(obj).has([ 'a' ]), true) + t.equal(traverse(obj).has([ 'a', 2 ]), false) + + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/instance.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/instance.js new file mode 100644 index 000000000..112f47721 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/instance.js @@ -0,0 +1,17 @@ +var test = require('tape'); +var traverse = require('../'); +var EventEmitter = require('events').EventEmitter; + +test('check instanceof on node elems', function (t) { + var counts = { emitter : 0 }; + + traverse([ new EventEmitter, 3, 4, { ev : new EventEmitter }]) + .forEach(function (node) { + if (node instanceof EventEmitter) counts.emitter ++; + }) + ; + + t.equal(counts.emitter, 2); + + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/interface.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/interface.js new file mode 100644 index 000000000..f454c27f5 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/interface.js @@ -0,0 +1,43 @@ +var test = require('tape'); +var traverse = require('../'); + +test('interface map', function (t) { + var obj = { a : [ 5,6,7 ], b : { c : [8] } }; + + t.same( + traverse.paths(obj) + .sort() + .map(function (path) { return path.join('/') }) + .slice(1) + .join(' ') + , + 'a a/0 a/1 a/2 b b/c b/c/0' + ); + + t.same( + traverse.nodes(obj), + [ + { a: [ 5, 6, 7 ], b: { c: [ 8 ] } }, + [ 5, 6, 7 ], 5, 6, 7, + { c: [ 8 ] }, [ 8 ], 8 + ] + ); + + t.same( + traverse.map(obj, function (node) { + if (typeof node == 'number') { + return node + 1000; + } + else if (Array.isArray(node)) { + return node.join(' '); + } + }), + { a: '5 6 7', b: { c: '8' } } + ); + + var nodes = 0; + traverse.forEach(obj, function (node) { nodes ++ }); + t.same(nodes, 8); + + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/json.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/json.js new file mode 100644 index 000000000..46d55e69c --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/json.js @@ -0,0 +1,49 @@ +var test = require('tape'); +var traverse = require('../'); + +test('json test', function (t) { + var id = 54; + var callbacks = {}; + var obj = { moo : function () {}, foo : [2,3,4, function () {}] }; + + var scrubbed = traverse(obj).map(function (x) { + if (typeof x === 'function') { + callbacks[id] = { id : id, f : x, path : this.path }; + this.update('[Function]'); + id++; + } + }); + + t.equal( + scrubbed.moo, '[Function]', + 'obj.moo replaced with "[Function]"' + ); + + t.equal( + scrubbed.foo[3], '[Function]', + 'obj.foo[3] replaced with "[Function]"' + ); + + t.same(scrubbed, { + moo : '[Function]', + foo : [ 2, 3, 4, "[Function]" ] + }, 'Full JSON string matches'); + + t.same( + typeof obj.moo, 'function', + 'Original obj.moo still a function' + ); + + t.same( + typeof obj.foo[3], 'function', + 'Original obj.foo[3] still a function' + ); + + t.same(callbacks, { + 54: { id: 54, f : obj.moo, path: [ 'moo' ] }, + 55: { id: 55, f : obj.foo[3], path: [ 'foo', '3' ] }, + }, 'Check the generated callbacks list'); + + t.end(); +}); + diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/keys.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/keys.js new file mode 100644 index 000000000..96611408d --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/keys.js @@ -0,0 +1,31 @@ +var test = require('tape'); +var traverse = require('../'); + +test('sort test', function (t) { + var acc = []; + traverse({ + a: 30, + b: 22, + id: 9 + }).forEach(function (node) { + if ((! Array.isArray(node)) && typeof node === 'object') { + this.before(function(node) { + this.keys = Object.keys(node); + this.keys.sort(function(a, b) { + a = [a === "id" ? 0 : 1, a]; + b = [b === "id" ? 0 : 1, b]; + return a < b ? -1 : a > b ? 1 : 0; + }); + }); + } + if (this.isLeaf) acc.push(node); + }); + + t.equal( + acc.join(' '), + '9 30 22', + 'Traversal in a custom order' + ); + + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/leaves.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/leaves.js new file mode 100644 index 000000000..c04ad5f8b --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/leaves.js @@ -0,0 +1,22 @@ +var test = require('tape'); +var traverse = require('../'); + +test('leaves test', function (t) { + var acc = []; + traverse({ + a : [1,2,3], + b : 4, + c : [5,6], + d : { e : [7,8], f : 9 } + }).forEach(function (x) { + if (this.isLeaf) acc.push(x); + }); + + t.equal( + acc.join(' '), + '1 2 3 4 5 6 7 8 9', + 'Traversal in the right(?) order' + ); + + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/lib/deep_equal.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/lib/deep_equal.js new file mode 100644 index 000000000..c75b04c2d --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/lib/deep_equal.js @@ -0,0 +1,96 @@ +var traverse = require('../../'); + +module.exports = function (a, b) { + if (arguments.length !== 2) { + throw new Error( + 'deepEqual requires exactly two objects to compare against' + ); + } + + var equal = true; + var node = b; + + traverse(a).forEach(function (y) { + var notEqual = (function () { + equal = false; + //this.stop(); + return undefined; + }).bind(this); + + //if (node === undefined || node === null) return notEqual(); + + if (!this.isRoot) { + /* + if (!Object.hasOwnProperty.call(node, this.key)) { + return notEqual(); + } + */ + if (typeof node !== 'object') return notEqual(); + node = node[this.key]; + } + + var x = node; + + this.post(function () { + node = x; + }); + + var toS = function (o) { + return Object.prototype.toString.call(o); + }; + + if (this.circular) { + if (traverse(b).get(this.circular.path) !== x) notEqual(); + } + else if (typeof x !== typeof y) { + notEqual(); + } + else if (x === null || y === null || x === undefined || y === undefined) { + if (x !== y) notEqual(); + } + else if (x.__proto__ !== y.__proto__) { + notEqual(); + } + else if (x === y) { + // nop + } + else if (typeof x === 'function') { + if (x instanceof RegExp) { + // both regexps on account of the __proto__ check + if (x.toString() != y.toString()) notEqual(); + } + else if (x !== y) notEqual(); + } + else if (typeof x === 'object') { + if (toS(y) === '[object Arguments]' + || toS(x) === '[object Arguments]') { + if (toS(x) !== toS(y)) { + notEqual(); + } + } + else if (toS(y) === '[object RegExp]' + || toS(x) === '[object RegExp]') { + if (!x || !y || x.toString() !== y.toString()) notEqual(); + } + else if (x instanceof Date || y instanceof Date) { + if (!(x instanceof Date) || !(y instanceof Date) + || x.getTime() !== y.getTime()) { + notEqual(); + } + } + else { + var kx = Object.keys(x); + var ky = Object.keys(y); + if (kx.length !== ky.length) return notEqual(); + for (var i = 0; i < kx.length; i++) { + var k = kx[i]; + if (!Object.hasOwnProperty.call(y, k)) { + notEqual(); + } + } + } + } + }); + + return equal; +}; diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/mutability.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/mutability.js new file mode 100644 index 000000000..3ab90dada --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/mutability.js @@ -0,0 +1,300 @@ +var test = require('tape'); +var traverse = require('../'); +var deepEqual = require('./lib/deep_equal'); + +test('mutate', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse(obj).forEach(function (x) { + if (typeof x === 'number' && x % 2 === 0) { + this.update(x * 10); + } + }); + t.same(obj, res); + t.same(obj, { a : 1, b : 20, c : [ 3, 40 ] }); + t.end(); +}); + +test('mutateT', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse.forEach(obj, function (x) { + if (typeof x === 'number' && x % 2 === 0) { + this.update(x * 10); + } + }); + t.same(obj, res); + t.same(obj, { a : 1, b : 20, c : [ 3, 40 ] }); + t.end(); +}); + +test('map', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse(obj).map(function (x) { + if (typeof x === 'number' && x % 2 === 0) { + this.update(x * 10); + } + }); + t.same(obj, { a : 1, b : 2, c : [ 3, 4 ] }); + t.same(res, { a : 1, b : 20, c : [ 3, 40 ] }); + t.end(); +}); + +test('mapT', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse.map(obj, function (x) { + if (typeof x === 'number' && x % 2 === 0) { + this.update(x * 10); + } + }); + t.same(obj, { a : 1, b : 2, c : [ 3, 4 ] }); + t.same(res, { a : 1, b : 20, c : [ 3, 40 ] }); + t.end(); +}); + +test('clone', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse(obj).clone(); + t.same(obj, res); + t.ok(obj !== res); + obj.a ++; + t.same(res.a, 1); + obj.c.push(5); + t.same(res.c, [ 3, 4 ]); + t.end(); +}); + +test('cloneT', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse.clone(obj); + t.same(obj, res); + t.ok(obj !== res); + obj.a ++; + t.same(res.a, 1); + obj.c.push(5); + t.same(res.c, [ 3, 4 ]); + t.end(); +}); + +test('reduce', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse(obj).reduce(function (acc, x) { + if (this.isLeaf) acc.push(x); + return acc; + }, []); + t.same(obj, { a : 1, b : 2, c : [ 3, 4 ] }); + t.same(res, [ 1, 2, 3, 4 ]); + t.end(); +}); + +test('reduceInit', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse(obj).reduce(function (acc, x) { + if (this.isRoot) assert.fail('got root'); + return acc; + }); + t.same(obj, { a : 1, b : 2, c : [ 3, 4 ] }); + t.same(res, obj); + t.end(); +}); + +test('remove', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + traverse(obj).forEach(function (x) { + if (this.isLeaf && x % 2 == 0) this.remove(); + }); + + t.same(obj, { a : 1, c : [ 3 ] }); + t.end(); +}); + +exports.removeNoStop = function() { + var obj = { a : 1, b : 2, c : { d: 3, e: 4 }, f: 5 }; + + var keys = []; + traverse(obj).forEach(function (x) { + keys.push(this.key) + if (this.key == 'c') this.remove(); + }); + + t.same(keys, [undefined, 'a', 'b', 'c', 'd', 'e', 'f']) + t.end(); +} + +exports.removeStop = function() { + var obj = { a : 1, b : 2, c : { d: 3, e: 4 }, f: 5 }; + + var keys = []; + traverse(obj).forEach(function (x) { + keys.push(this.key) + if (this.key == 'c') this.remove(true); + }); + + t.same(keys, [undefined, 'a', 'b', 'c', 'f']) + t.end(); +} + +test('removeMap', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse(obj).map(function (x) { + if (this.isLeaf && x % 2 == 0) this.remove(); + }); + + t.same(obj, { a : 1, b : 2, c : [ 3, 4 ] }); + t.same(res, { a : 1, c : [ 3 ] }); + t.end(); +}); + +test('delete', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + traverse(obj).forEach(function (x) { + if (this.isLeaf && x % 2 == 0) this.delete(); + }); + + t.ok(!deepEqual( + obj, { a : 1, c : [ 3, undefined ] } + )); + + t.ok(deepEqual( + obj, { a : 1, c : [ 3 ] } + )); + + t.ok(!deepEqual( + obj, { a : 1, c : [ 3, null ] } + )); + t.end(); +}); + +test('deleteNoStop', function (t) { + var obj = { a : 1, b : 2, c : { d: 3, e: 4 } }; + + var keys = []; + traverse(obj).forEach(function (x) { + keys.push(this.key) + if (this.key == 'c') this.delete(); + }); + + t.same(keys, [undefined, 'a', 'b', 'c', 'd', 'e']) + t.end(); +}); + +test('deleteStop', function (t) { + var obj = { a : 1, b : 2, c : { d: 3, e: 4 } }; + + var keys = []; + traverse(obj).forEach(function (x) { + keys.push(this.key) + if (this.key == 'c') this.delete(true); + }); + + t.same(keys, [undefined, 'a', 'b', 'c']) + t.end(); +}); + +test('deleteRedux', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4, 5 ] }; + traverse(obj).forEach(function (x) { + if (this.isLeaf && x % 2 == 0) this.delete(); + }); + + t.ok(!deepEqual( + obj, { a : 1, c : [ 3, undefined, 5 ] } + )); + + t.ok(deepEqual( + obj, { a : 1, c : [ 3 ,, 5 ] } + )); + + t.ok(!deepEqual( + obj, { a : 1, c : [ 3, null, 5 ] } + )); + + t.ok(!deepEqual( + obj, { a : 1, c : [ 3, 5 ] } + )); + + t.end(); +}); + +test('deleteMap', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse(obj).map(function (x) { + if (this.isLeaf && x % 2 == 0) this.delete(); + }); + + t.ok(deepEqual( + obj, + { a : 1, b : 2, c : [ 3, 4 ] } + )); + + var xs = [ 3, 4 ]; + delete xs[1]; + + t.ok(deepEqual( + res, { a : 1, c : xs } + )); + + t.ok(deepEqual( + res, { a : 1, c : [ 3, ] } + )); + + t.ok(deepEqual( + res, { a : 1, c : [ 3 ] } + )); + + t.end(); +}); + +test('deleteMapRedux', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4, 5 ] }; + var res = traverse(obj).map(function (x) { + if (this.isLeaf && x % 2 == 0) this.delete(); + }); + + t.ok(deepEqual( + obj, + { a : 1, b : 2, c : [ 3, 4, 5 ] } + )); + + var xs = [ 3, 4, 5 ]; + delete xs[1]; + + t.ok(deepEqual( + res, { a : 1, c : xs } + )); + + t.ok(!deepEqual( + res, { a : 1, c : [ 3, 5 ] } + )); + + t.ok(deepEqual( + res, { a : 1, c : [ 3 ,, 5 ] } + )); + + t.end(); +}); + +test('objectToString', function (t) { + var obj = { a : 1, b : 2, c : [ 3, 4 ] }; + var res = traverse(obj).forEach(function (x) { + if (typeof x === 'object' && !this.isRoot) { + this.update(JSON.stringify(x)); + } + }); + t.same(obj, res); + t.same(obj, { a : 1, b : 2, c : "[3,4]" }); + t.end(); +}); + +test('stringToObject', function (t) { + var obj = { a : 1, b : 2, c : "[3,4]" }; + var res = traverse(obj).forEach(function (x) { + if (typeof x === 'string') { + this.update(JSON.parse(x)); + } + else if (typeof x === 'number' && x % 2 === 0) { + this.update(x * 10); + } + }); + t.deepEqual(obj, res); + t.deepEqual(obj, { a : 1, b : 20, c : [ 3, 40 ] }); + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/negative.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/negative.js new file mode 100644 index 000000000..91566c80a --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/negative.js @@ -0,0 +1,21 @@ +var traverse = require('../'); +var test = require('tape'); + +test('negative update test', function (t) { + var obj = [ 5, 6, -3, [ 7, 8, -2, 1 ], { f : 10, g : -13 } ]; + var fixed = traverse.map(obj, function (x) { + if (x < 0) this.update(x + 128); + }); + + t.same(fixed, + [ 5, 6, 125, [ 7, 8, 126, 1 ], { f: 10, g: 115 } ], + 'Negative values += 128' + ); + + t.same(obj, + [ 5, 6, -3, [ 7, 8, -2, 1 ], { f: 10, g: -13 } ], + 'Original references not modified' + ); + + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/obj.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/obj.js new file mode 100644 index 000000000..8bcf58aef --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/obj.js @@ -0,0 +1,11 @@ +var test = require('tape'); +var traverse = require('../'); + +test('traverse an object with nested functions', function (t) { + t.plan(1); + + function Cons (x) { + t.equal(x, 10) + }; + traverse(new Cons(10)); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/siblings.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/siblings.js new file mode 100644 index 000000000..c59e55779 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/siblings.js @@ -0,0 +1,37 @@ +var test = require('tape'); +var traverse = require('../'); + +test('siblings', function (t) { + var obj = { a : 1, b : 2, c : [ 4, 5, 6 ] }; + + var res = traverse(obj).reduce(function (acc, x) { + var p = '/' + this.path.join('/'); + if (this.parent) { + acc[p] = { + siblings : this.parent.keys, + key : this.key, + index : this.parent.keys.indexOf(this.key) + }; + } + else { + acc[p] = { + siblings : [], + key : this.key, + index : -1 + } + } + return acc; + }, {}); + + t.same(res, { + '/' : { siblings : [], key : undefined, index : -1 }, + '/a' : { siblings : [ 'a', 'b', 'c' ], key : 'a', index : 0 }, + '/b' : { siblings : [ 'a', 'b', 'c' ], key : 'b', index : 1 }, + '/c' : { siblings : [ 'a', 'b', 'c' ], key : 'c', index : 2 }, + '/c/0' : { siblings : [ '0', '1', '2' ], key : '0', index : 0 }, + '/c/1' : { siblings : [ '0', '1', '2' ], key : '1', index : 1 }, + '/c/2' : { siblings : [ '0', '1', '2' ], key : '2', index : 2 } + }); + + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/stop.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/stop.js new file mode 100644 index 000000000..9ce15b065 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/stop.js @@ -0,0 +1,44 @@ +var test = require('tape'); +var traverse = require('../'); + +test('stop', function (t) { + var visits = 0; + traverse('abcdefghij'.split('')).forEach(function (node) { + if (typeof node === 'string') { + visits ++; + if (node === 'e') this.stop() + } + }); + + t.equal(visits, 5); + t.end(); +}); + +test('stopMap', function (t) { + var s = traverse('abcdefghij'.split('')).map(function (node) { + if (typeof node === 'string') { + if (node === 'e') this.stop() + return node.toUpperCase(); + } + }).join(''); + + t.equal(s, 'ABCDEfghij'); + t.end(); +}); + +test('stopReduce', function (t) { + var obj = { + a : [ 4, 5 ], + b : [ 6, [ 7, 8, 9 ] ] + }; + var xs = traverse(obj).reduce(function (acc, node) { + if (this.isLeaf) { + if (node === 7) this.stop(); + else acc.push(node) + } + return acc; + }, []); + + t.same(xs, [ 4, 5, 6 ]); + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/stringify.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/stringify.js new file mode 100644 index 000000000..f1680d8c9 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/stringify.js @@ -0,0 +1,36 @@ +var test = require('tape'); +var traverse = require('../'); + +test('stringify', function (t) { + var obj = [ 5, 6, -3, [ 7, 8, -2, 1 ], { f : 10, g : -13 } ]; + + var s = ''; + traverse(obj).forEach(function (node) { + if (Array.isArray(node)) { + this.before(function () { s += '[' }); + this.post(function (child) { + if (!child.isLast) s += ','; + }); + this.after(function () { s += ']' }); + } + else if (typeof node == 'object') { + this.before(function () { s += '{' }); + this.pre(function (x, key) { + s += '"' + key + '"' + ':'; + }); + this.post(function (child) { + if (!child.isLast) s += ','; + }); + this.after(function () { s += '}' }); + } + else if (typeof node == 'function') { + s += 'null'; + } + else { + s += node.toString(); + } + }); + + t.equal(s, JSON.stringify(obj)); + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/subexpr.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/subexpr.js new file mode 100644 index 000000000..768260888 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/subexpr.js @@ -0,0 +1,36 @@ +var traverse = require('../'); +var test = require('tape'); + +test('subexpr', function (t) { + var obj = [ 'a', 4, 'b', 5, 'c', 6 ]; + var r = traverse(obj).map(function (x) { + if (typeof x === 'number') { + this.update([ x - 0.1, x, x + 0.1 ], true); + } + }); + + t.same(obj, [ 'a', 4, 'b', 5, 'c', 6 ]); + t.same(r, [ + 'a', [ 3.9, 4, 4.1 ], + 'b', [ 4.9, 5, 5.1 ], + 'c', [ 5.9, 6, 6.1 ], + ]); + t.end(); +}); + +test('block', function (t) { + var obj = [ [ 1 ], [ 2 ], [ 3 ] ]; + var r = traverse(obj).map(function (x) { + if (Array.isArray(x) && !this.isRoot) { + if (x[0] === 5) this.block() + else this.update([ [ x[0] + 1 ] ]) + } + }); + + t.same(r, [ + [ [ [ [ [ 5 ] ] ] ] ], + [ [ [ [ 5 ] ] ] ], + [ [ [ 5 ] ] ], + ]); + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/super_deep.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/super_deep.js new file mode 100644 index 000000000..1eb9e26ed --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/test/super_deep.js @@ -0,0 +1,56 @@ +var test = require('tape'); +var traverse = require('../'); +var deepEqual = require('./lib/deep_equal'); + +test('super_deep', function (t) { + var util = require('util'); + var a0 = make(); + var a1 = make(); + t.ok(deepEqual(a0, a1)); + + a0.c.d.moo = true; + t.ok(!deepEqual(a0, a1)); + + a1.c.d.moo = true; + t.ok(deepEqual(a0, a1)); + + // TODO: this one + //a0.c.a = a1; + //t.ok(!deepEqual(a0, a1)); + t.end(); +}); + +function make () { + var a = { self : 'a' }; + var b = { self : 'b' }; + var c = { self : 'c' }; + var d = { self : 'd' }; + var e = { self : 'e' }; + + a.a = a; + a.b = b; + a.c = c; + + b.a = a; + b.b = b; + b.c = c; + + c.a = a; + c.b = b; + c.c = c; + c.d = d; + + d.a = a; + d.b = b; + d.c = c; + d.d = d; + d.e = e; + + e.a = a; + e.b = b; + e.c = c; + e.d = d; + e.e = e; + + return a; +} diff --git a/test/modules-checked-in/node_modules/hashish/node_modules/traverse/testling/leaves.js b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/testling/leaves.js new file mode 100644 index 000000000..29968dd13 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/node_modules/traverse/testling/leaves.js @@ -0,0 +1,22 @@ +var traverse = require('./'); +var test = require('testling'); + +test('leaves', function (t) { + var obj = { + a : [1,2,3], + b : 4, + c : [5,6], + d : { e : [7,8], f : 9 } + }; + + var acc = []; + traverse(obj).forEach(function (x) { + if (this.isLeaf) acc.push(x); + }); + + t.deepEqual( + acc, [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ], + 'traversal in the proper order' + ); + t.end(); +}); diff --git a/test/modules-checked-in/node_modules/hashish/package.json b/test/modules-checked-in/node_modules/hashish/package.json new file mode 100644 index 000000000..d77f2a614 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/package.json @@ -0,0 +1,46 @@ +{ + "name": "hashish", + "version": "0.0.4", + "description": "Hash data structure manipulation functions", + "main": "./index.js", + "repository": { + "type": "git", + "url": "http://github.com/substack/node-hashish.git" + }, + "keywords": [ + "hash", + "object", + "convenience", + "manipulation", + "data structure" + ], + "author": { + "name": "James Halliday", + "email": "mail@substack.net", + "url": "http://substack.net" + }, + "dependencies": { + "traverse": ">=0.2.4" + }, + "devDependencies": { + "expresso": ">=0.6.0" + }, + "scripts": { + "test": "expresso" + }, + "license": "MIT/X11", + "engine": [ + "node >=0.2.0" + ], + "readme": "Hashish\n=======\n\nHashish is a node.js library for manipulating hash data structures.\nIt is distilled from the finest that ruby, perl, and haskell have to offer by\nway of hash/map interfaces.\n\nHashish provides a chaining interface, where you can do:\n\n var Hash = require('hashish');\n \n Hash({ a : 1, b : 2, c : 3, d : 4 })\n .map(function (x) { return x * 10 })\n .filter(function (x) { return x < 30 })\n .forEach(function (x, key) {\n console.log(key + ' => ' + x);\n })\n ;\n \nOutput:\n\n a => 10\n b => 20\n\nSome functions and attributes in the chaining interface are terminal, like\n`.items` or `.detect()`. They return values of their own instead of the chain\ncontext.\n\nEach function in the chainable interface is also attached to `Hash` in chainless\nform:\n\n var Hash = require('hashish');\n var obj = { a : 1, b : 2, c : 3, d : 4 };\n \n var mapped = Hash.map(obj, function (x) {\n return x * 10\n });\n \n console.dir(mapped);\n\nOutput:\n\n { a: 10, b: 20, c: 30, d: 40 }\n\nIn either case, the 'this' context of the function calls is the same object that\nthe chained functions return, so you can make nested chains.\n\nMethods\n=======\n\nforEach(cb)\n-----------\n\nFor each key/value in the hash, calls `cb(value, key)`.\n\nmap(cb)\n-------\n\nFor each key/value in the hash, calls `cb(value, key)`.\nThe return value of `cb` is the new value at `key` in the resulting hash.\n\nfilter(cb)\n----------\n\nFor each key/value in the hash, calls `cb(value, key)`.\nThe resulting hash omits key/value pairs where `cb` returned a falsy value.\n\ndetect(cb)\n----------\n\nReturns the first value in the hash for which `cb(value, key)` is non-falsy.\nOrder of hashes is not well-defined so watch out for that.\n\nreduce(cb)\n----------\n\nReturns the accumulated value of a left-fold over the key/value pairs.\n\nsome(cb)\n--------\n\nReturns a boolean: whether or not `cb(value, key)` ever returned a non-falsy\nvalue.\n\nupdate(obj1, [obj2, obj3, ...])\n-----------\n\nMutate the context hash, merging the key/value pairs from the passed objects\nand overwriting keys from the context hash if the current `obj` has keys of\nthe same name. Falsy arguments are silently ignored.\n\nupdateAll([ obj1, obj2, ... ])\n------------------------------\n\nLike multi-argument `update()` but operate on an array directly.\n\nmerge(obj1, [obj2, obj3, ...])\n----------\n\nMerge the key/value pairs from the passed objects into the resultant hash\nwithout modifying the context hash. Falsy arguments are silently ignored.\n\nmergeAll([ obj1, obj2, ... ])\n------------------------------\n\nLike multi-argument `merge()` but operate on an array directly.\n\nhas(key)\n--------\n\nReturn whether the hash has a key, `key`.\n\nvaluesAt(keys)\n--------------\n\nReturn an Array with the values at the keys from `keys`.\n\ntap(cb)\n-------\n\nCall `cb` with the present raw hash.\nThis function is chainable.\n\nextract(keys)\n-------------\n\nFilter by including only those keys in `keys` in the resulting hash.\n\nexclude(keys)\n-------------\n\nFilter by excluding those keys in `keys` in the resulting hash.\n\nAttributes\n==========\n\nThese are attributes in the chaining interface and functions in the `Hash.xxx`\ninterface.\n\nkeys\n----\n\nReturn all the enumerable attribute keys in the hash.\n\nvalues\n------\n\nReturn all the enumerable attribute values in the hash.\n\ncompact\n-------\n\nFilter out values which are `=== undefined`.\n\nclone\n-----\n\nMake a deep copy of the hash.\n\ncopy\n----\n\nMake a shallow copy of the hash.\n\nlength\n------\n\nReturn the number of key/value pairs in the hash.\nNote: use `Hash.size()` for non-chain mode.\n\nsize\n----\n\nAlias for `length` since `Hash.length` is masked by `Function.prototype`.\n\nSee Also\n========\n\nSee also [creationix's pattern/hash](http://github.com/creationix/pattern),\nwhich does a similar thing except with hash inputs and array outputs.\n\nInstallation\n============\n\nTo install with [npm](http://github.com/isaacs/npm):\n \n npm install hashish\n\nTo run the tests with [expresso](http://github.com/visionmedia/expresso):\n\n expresso\n", + "readmeFilename": "README.markdown", + "bugs": { + "url": "https://github.com/substack/node-hashish/issues" + }, + "_id": "hashish@0.0.4", + "dist": { + "shasum": "ef5af4bf63fee968e4dc040692c558a8fb4429f6" + }, + "_from": "hashish@*", + "_resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz" +} diff --git a/test/modules-checked-in/node_modules/hashish/test/hash.js b/test/modules-checked-in/node_modules/hashish/test/hash.js new file mode 100644 index 000000000..6afce60a1 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/test/hash.js @@ -0,0 +1,250 @@ +var Hash = require('hashish'); +var assert = require('assert'); + +exports.map = function () { + var ref = { a : 1, b : 2 }; + var items = Hash(ref).map(function (v) { return v + 1 }).items; + var hash = Hash.map(ref, function (v) { return v + 1 }); + assert.deepEqual(ref, { a : 1, b : 2 }); + assert.deepEqual(items, { a : 2, b : 3 }); + assert.deepEqual(hash, { a : 2, b : 3 }); +}; + +exports['cloned map'] = function () { + var ref = { foo : [1,2], bar : [4,5] }; + var hash = Hash(ref).clone.map( + function (v) { v.unshift(v[0] - 1); return v } + ).items; + assert.deepEqual(ref.foo, [1,2]); + assert.deepEqual(ref.bar, [4,5]); + assert.deepEqual(hash.foo, [0,1,2]); + assert.deepEqual(hash.bar, [3,4,5]); +}; + +exports.forEach = function () { + var ref = { a : 5, b : 2, c : 7, 1337 : 'leet' }; + var xs = []; + Hash(ref).forEach(function (x, i) { + xs.push([ i, x ]); + }); + + assert.eql( + xs.map(function (x) { return x[0] }).sort(), + [ '1337', 'a', 'b', 'c' ] + ); + + assert.eql( + xs.map(function (x) { return x[1] }).sort(), + [ 2, 5, 7, 'leet' ] + ); + + var ys = []; + Hash.forEach(ref, function (x, i) { + ys.push([ i, x ]); + }); + + assert.eql(xs.sort(), ys.sort()); +}; + +exports.filter_items = function () { + var ref = { a : 5, b : 2, c : 7, 1337 : 'leet' }; + var items = Hash(ref).filter(function (v, k) { + return v > 5 || k > 5 + }).items; + var hash = Hash.filter(ref, function (v, k) { return v > 5 || k > 5 }); + assert.deepEqual(items, { 1337 : 'leet', c : 7 }); + assert.deepEqual(hash, { 1337 : 'leet', c : 7 }); + assert.deepEqual(ref, { a : 5, b : 2, c : 7, 1337 : 'leet' }); + assert.equal(Hash(ref).length, 4); +}; + +exports.detect = function () { + var h = { a : 5, b : 6, c : 7, d : 8 }; + var hh = Hash(h); + var gt6hh = hh.detect(function (x) { return x > 6 }); + assert.ok(gt6hh == 7 || gt6hh == 8); + var gt6h = Hash.detect(h, function (x) { return x > 6 }); + assert.ok(gt6h == 7 || gt6h == 8); + assert.equal(hh.detect(function (x) { return x > 100 }), undefined); +}; + +exports.reduce = function () { + var ref = { foo : [1,2], bar : [4,5] }; + + var sum1 = Hash(ref).reduce(function (acc, v) { + return acc + v.length + }, 0); + assert.equal(sum1, 4); + + var sum2 = Hash.reduce(ref, function (acc, v) { + return acc + v.length + }, 0); + assert.equal(sum2, 4); +}; + +exports.some = function () { + var h = { a : 5, b : 6, c : 7, d : 8 }; + var hh = Hash(h); + assert.ok(Hash.some(h, function (x) { return x > 7 })); + assert.ok(Hash.some(h, function (x) { return x < 6 })); + assert.ok(!Hash.some(h, function (x) { return x > 10 })); + assert.ok(!Hash.some(h, function (x) { return x < 0 })); + + assert.ok(hh.some(function (x) { return x > 7 })); + assert.ok(hh.some(function (x) { return x < 6 })); + assert.ok(!hh.some(function (x) { return x > 10 })); + assert.ok(!hh.some(function (x) { return x < 0 })); +}; + +exports.update = function () { + var ref = { a : 1, b : 2 }; + var items = Hash(ref).clone.update({ c : 3, a : 0 }).items; + assert.deepEqual(ref, { a : 1, b : 2 }); + assert.deepEqual(items, { a : 0, b : 2, c : 3 }); + + var hash = Hash.update(ref, { c : 3, a : 0 }); + assert.deepEqual(ref, hash); + assert.deepEqual(hash, { a : 0, b : 2, c : 3 }); + + var ref2 = {a: 1}; + var hash2 = Hash.update(ref2, { b: 2, c: 3 }, undefined, { d: 4 }); + assert.deepEqual(ref2, { a: 1, b: 2, c: 3, d: 4 }); +}; + +exports.merge = function () { + var ref = { a : 1, b : 2 }; + var items = Hash(ref).merge({ b : 3, c : 3.14 }).items; + var hash = Hash.merge(ref, { b : 3, c : 3.14 }); + + assert.deepEqual(ref, { a : 1, b : 2 }); + assert.deepEqual(items, { a : 1, b : 3, c : 3.14 }); + assert.deepEqual(hash, { a : 1, b : 3, c : 3.14 }); + + var ref2 = { a : 1 }; + var hash2 = Hash.merge(ref, { b: 2, c: 3 }, undefined, { d: 4 }); + assert.deepEqual(hash2, { a: 1, b: 2, c: 3, d: 4 }); +}; + +exports.has = function () { + var h = { a : 4, b : 5 }; + var hh = Hash(h); + + assert.ok(hh.has('a')); + assert.equal(hh.has('c'), false); + assert.ok(hh.has(['a','b'])); + assert.equal(hh.has(['a','b','c']), false); + + assert.ok(Hash.has(h, 'a')); + assert.equal(Hash.has(h, 'c'), false); + assert.ok(Hash.has(h, ['a','b'])); + assert.equal(Hash.has(h, ['a','b','c']), false); +}; + +exports.valuesAt = function () { + var h = { a : 4, b : 5, c : 6 }; + assert.equal(Hash(h).valuesAt('a'), 4); + assert.equal(Hash(h).valuesAt(['a'])[0], 4); + assert.deepEqual(Hash(h).valuesAt(['a','b']), [4,5]); + assert.equal(Hash.valuesAt(h, 'a'), 4); + assert.deepEqual(Hash.valuesAt(h, ['a']), [4]); + assert.deepEqual(Hash.valuesAt(h, ['a','b']), [4,5]); +}; + +exports.tap = function () { + var h = { a : 4, b : 5, c : 6 }; + var hh = Hash(h); + hh.tap(function (x) { + assert.ok(this === hh) + assert.eql(x, h); + }); + + Hash.tap(h, function (x) { + assert.eql( + Object.keys(this).sort(), + Object.keys(hh).sort() + ); + assert.eql(x, h); + }); +}; + +exports.extract = function () { + var hash = Hash({ a : 1, b : 2, c : 3 }).clone; + var extracted = hash.extract(['a','b']); + assert.equal(extracted.length, 2); + assert.deepEqual(extracted.items, { a : 1, b : 2 }); +}; + +exports.exclude = function () { + var hash = Hash({ a : 1, b : 2, c : 3 }).clone; + var extracted = hash.exclude(['a','b']); + assert.equal(extracted.length, 1); + assert.deepEqual(extracted.items, { c : 3 }); +}; + +exports.concat = function () { + var ref1 = { a : 1, b : 2 }; + var ref2 = { foo : 100, bar : 200 }; + var ref3 = { b : 3, c : 4, bar : 300 }; + + assert.deepEqual( + Hash.concat([ ref1, ref2 ]), + { a : 1, b : 2, foo : 100, bar : 200 } + ); + + assert.deepEqual( + Hash.concat([ ref1, ref2, ref3 ]), + { a : 1, b : 3, c : 4, foo : 100, bar : 300 } + ); +}; + +exports.zip = function () { + var xs = ['a','b','c']; + var ys = [1,2,3,4]; + var h = Hash(xs,ys); + assert.equal(h.length, 3); + assert.deepEqual(h.items, { a : 1, b : 2, c : 3 }); + + var zipped = Hash.zip(xs,ys); + assert.deepEqual(zipped, { a : 1, b : 2, c : 3 }); +}; + +exports.length = function () { + assert.equal(Hash({ a : 1, b : [2,3], c : 4 }).length, 3); + assert.equal(Hash({ a : 1, b : [2,3], c : 4 }).size, 3); + assert.equal(Hash.size({ a : 1, b : [2,3], c : 4 }), 3); +}; + +exports.compact = function () { + var hash = { + a : 1, + b : undefined, + c : false, + d : 4, + e : [ undefined, 4 ], + f : null + }; + var compacted = Hash(hash).compact; + assert.deepEqual( + { + a : 1, + b : undefined, + c : false, + d : 4, + e : [ undefined, 4 ], + f : null + }, + hash, 'compact modified the hash' + ); + assert.deepEqual( + compacted.items, + { + a : 1, + c : false, + d : 4, + e : [ undefined, 4 ], + f : null + } + ); + var h = Hash.compact(hash); + assert.deepEqual(h, compacted.items); +}; diff --git a/test/modules-checked-in/node_modules/hashish/test/property.js b/test/modules-checked-in/node_modules/hashish/test/property.js new file mode 100644 index 000000000..1183c5d03 --- /dev/null +++ b/test/modules-checked-in/node_modules/hashish/test/property.js @@ -0,0 +1,69 @@ +var Hash = require('hashish'); +var assert = require('assert'); +var vm = require('vm'); +var fs = require('fs'); + +var src = fs.readFileSync(__dirname + '/../index.js', 'utf8'); + +exports.defineGetter = function () { + var context = { + module : { exports : {} }, + Object : { + keys : Object.keys, + defineProperty : undefined, + }, + require : require, + }; + context.exports = context.module.exports; + + vm.runInNewContext('(function () {' + src + '})()', context); + var Hash_ = context.module.exports; + + var times = 0; + Hash_.__proto__.__proto__.__defineGetter__ = function () { + times ++; + return Object.__defineGetter__.apply(this, arguments); + }; + + assert.equal(vm.runInNewContext('Object.defineProperty', context), null); + + assert.deepEqual( + Hash_({ a : 1, b : 2, c : 3 }).values, + [ 1, 2, 3 ] + ); + + assert.ok(times > 5); +}; + +exports.defineProperty = function () { + var times = 0; + var context = { + module : { exports : {} }, + Object : { + keys : Object.keys, + defineProperty : function (prop) { + times ++; + if (prop.get) throw new TypeError('engine does not support') + assert.fail('should have asserted by now'); + }, + }, + require : require + }; + context.exports = context.module.exports; + + vm.runInNewContext('(function () {' + src + '})()', context); + var Hash_ = context.module.exports; + + Hash_.__proto__.__proto__.__defineGetter__ = function () { + assert.fail('getter called when a perfectly good' + + ' defineProperty was available' + ); + }; + + assert.deepEqual( + Hash_({ a : 1, b : 2, c : 3 }).values, + [ 1, 2, 3 ] + ); + + assert.equal(times, 1); +}; diff --git a/test/shrinkwrap/package.json b/test/modules-checked-in/package.json similarity index 90% rename from test/shrinkwrap/package.json rename to test/modules-checked-in/package.json index 3b195dd88..d5561df40 100644 --- a/test/shrinkwrap/package.json +++ b/test/modules-checked-in/package.json @@ -7,7 +7,7 @@ "url" : "http://github.com/example/example.git" }, "dependencies": { - "euclidean-distance": "*" + "hashish": "*" }, "engines": { "node": "~0.10.0" diff --git a/test/shrinkwrap/node_modules/euclidean-distance/README.md b/test/shrinkwrap/node_modules/euclidean-distance/README.md deleted file mode 100644 index 139f5ff0b..000000000 --- a/test/shrinkwrap/node_modules/euclidean-distance/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Euclidean Distance - -euclidean-distance is a [browserify](https://github.com/substack/node-browserify#browserify)-friendly npm module -for calculating the [Euclidean distance](http://en.wikipedia.org/wiki/Euclidean_distance#Three_dimensions) -been two points in 2D or 3D space. - - - -## Installation - -``` -npm install euclidean-distance --save -``` - -## Usage - -```js -var d = require('euclidean-distance'); - -d([0,0], [1,0]); -// 1 - -d([0,0], [3,2]); -// 3.605551275463989 - -d([-7,-4,3], [17, 6, 2.5]); -// 26.004807247892 -``` - -## Test - -``` -npm test -``` - -## License - -[WTFPL](http://wtfpl.org/) \ No newline at end of file diff --git a/test/shrinkwrap/node_modules/euclidean-distance/index.js b/test/shrinkwrap/node_modules/euclidean-distance/index.js deleted file mode 100644 index 1204c2e73..000000000 --- a/test/shrinkwrap/node_modules/euclidean-distance/index.js +++ /dev/null @@ -1,23 +0,0 @@ -// http://en.wikipedia.org/wiki/Euclidean_distance#Three_dimensions - -module.exports = function(a, b) { - - // return Math.sqrt( - // Math.pow(a[0]-b[0], 2) + - // Math.pow(a[1]-b[1], 2) + - // Math.pow(a[2]-b[2], 2) - // ) - - // return Math.sqrt( - // [0,1,2].reduce(function(prev, current, i) { - // return prev + Math.pow(a[i]-b[i], 2); - // }, 0) - // ); - - var sum = 0; - var n; - for (n=0; n < a.length; n++) { - sum += Math.pow(a[n]-b[n], 2); - } - return Math.sqrt(sum); -} \ No newline at end of file diff --git a/test/shrinkwrap/node_modules/euclidean-distance/package.json b/test/shrinkwrap/node_modules/euclidean-distance/package.json deleted file mode 100644 index 574ccc7a2..000000000 --- a/test/shrinkwrap/node_modules/euclidean-distance/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "euclidean-distance", - "version": "0.1.0", - "description": "Calculate the Euclidean distance been two points in 2D/3D/nD space.", - "main": "index.js", - "scripts": { - "test": "mocha" - }, - "repository": { - "type": "git", - "url": "https://github.com/zeke/euclidean-distance" - }, - "keywords": [ - "distance", - "space", - "3d", - "2d", - "math", - "euclid", - "color", - "Lab", - "L*a*b*", - "Delta-E", - "dE", - "visualization", - "browser" - ], - "author": { - "name": "zeke" - }, - "license": "WTFPL", - "bugs": { - "url": "https://github.com/zeke/euclidean-distance/issues" - }, - "readme": "# Euclidean Distance\n\neuclidean-distance is a [browserify](https://github.com/substack/node-browserify#browserify)-friendly npm module\nfor calculating the [Euclidean distance](http://en.wikipedia.org/wiki/Euclidean_distance#Three_dimensions)\nbeen two points in 2D or 3D space.\n\n\n\n## Installation\n\n```\nnpm install euclidean-distance --save\n```\n\n## Usage\n\n```js\nvar d = require('euclidean-distance');\n\nd([0,0], [1,0]);\n// 1\n\nd([0,0], [3,2]);\n// 3.605551275463989\n\nd([-7,-4,3], [17, 6, 2.5]);\n// 26.004807247892\n```\n\n## Test\n\n```\nnpm test\n```\n\n## License\n\n[WTFPL](http://wtfpl.org/)", - "readmeFilename": "README.md", - "_id": "euclidean-distance@0.1.0", - "_from": "euclidean-distance@*" -} diff --git a/test/shrinkwrap/node_modules/euclidean-distance/test/indexTest.js b/test/shrinkwrap/node_modules/euclidean-distance/test/indexTest.js deleted file mode 100644 index 80015eadd..000000000 --- a/test/shrinkwrap/node_modules/euclidean-distance/test/indexTest.js +++ /dev/null @@ -1,41 +0,0 @@ -var assert = require("assert") -var euclid = require("../index") - -describe('euclideanDistance', function(){ - - describe('2d', function(){ - - it('returns 1 when points are 1 unit away', function(){ - assert.equal(1, euclid([0,0], [1,0])); - }) - - it('works with non-parallel points', function(){ - var d = euclid([0,0], [3,2]) // 3.605551275463989 - assert.equal(360, Math.floor(d*100)); - }) - - it('handles with non-parallel points', function(){ - var d = euclid([-1,0], [2,2]) // 3.605551275463989 - assert.equal(360, Math.floor(d*100)); - }) - - it('returns 0 when points are the same', function(){ - assert.equal(0, euclid([3,5], [3,5])); - }) - - }) - - describe('3d', function(){ - - it('returns 1 when points are 1 unit away', function(){ - assert.equal(1, euclid([0,0,0], [1,0,0])); - }) - - // http://www.calculatorsoup.com/calculators/geometry-solids/distance-two-points.php - it("works with numbers I didn't make up", function(){ - assert.equal(26, Math.floor(euclid([-7,-4,3], [17, 6, 2.5]))); - }) - - }) - -}) diff --git a/test/shrinkwrap/node_modules/euclidean-distance/test/mocha.opts b/test/shrinkwrap/node_modules/euclidean-distance/test/mocha.opts deleted file mode 100644 index f633acdac..000000000 --- a/test/shrinkwrap/node_modules/euclidean-distance/test/mocha.opts +++ /dev/null @@ -1,2 +0,0 @@ ---reporter spec ---ui tdd diff --git a/test/shrinkwrap/npm-shrinkwrap.json b/test/shrinkwrap/npm-shrinkwrap.json deleted file mode 100644 index c7f0b5505..000000000 --- a/test/shrinkwrap/npm-shrinkwrap.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "node-buildpack-test-app", - "version": "0.0.1", - "dependencies": { - "euclidean-distance": { - "version": "0.1.0", - "from": "euclidean-distance@*" - } - } -} From 396ed4859da0e57b183f4f4e52d0b6c01e7e403b Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 2 Dec 2013 13:30:40 -0800 Subject: [PATCH 090/116] handle semver ranges that contain spaces --- bin/compile | 3 +-- bin/test | 7 +++++++ test/range-with-space/README.md | 1 + test/range-with-space/package.json | 12 ++++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 test/range-with-space/README.md create mode 100644 test/range-with-space/package.json diff --git a/bin/compile b/bin/compile index e8424c6d5..d14ba9ce0 100755 --- a/bin/compile +++ b/bin/compile @@ -19,8 +19,7 @@ trap cat_npm_debug_log ERR semver_range=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .engines.node) # Resolve node version using semver.io -semver_url=http://semver.io/node/resolve/$semver_range -node_version=$(curl --silent $semver_url) +node_version=$(curl --silent --get --data-urlencode "range=${semver_range}" http://semver.io/node/resolve) # Recommend using semver ranges in a safe manner if [ "$semver_range" == "null" ]; then diff --git a/bin/test b/bin/test index 0d13c72e6..7f8217a67 100755 --- a/bin/test +++ b/bin/test @@ -35,6 +35,13 @@ testDangerousRangeGreaterThan() { assertCapturedSuccess } +testRangeWithSpace() { + compile "range-with-space" + assertCaptured "Requested node range: >= 0.8.x" + assertCaptured "Resolved node version: 0.10." + assertCapturedSuccess +} + testStableVersion() { compile "stable-node" assertNotCaptured "PRO TIP: Avoid using semver" diff --git a/test/range-with-space/README.md b/test/range-with-space/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/range-with-space/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/range-with-space/package.json b/test/range-with-space/package.json new file mode 100644 index 000000000..5f95fe6ac --- /dev/null +++ b/test/range-with-space/package.json @@ -0,0 +1,12 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "engines": { + "node": ">= 0.8.x" + } +} From 740457120e25c01c2fc0577cafacb8872c380411 Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 3 Dec 2013 14:46:20 -0800 Subject: [PATCH 091/116] consolidate CONTRIBUTING.md into README and clean house. --- CONTRIBUTING.md | 39 ------------------------- README.md | 78 ++++++++++++++++++++++++++----------------------- 2 files changed, 41 insertions(+), 76 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 46245b42e..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,39 +0,0 @@ -## Testing buildpack changes using Anvil - -[Anvil](https://github.com/ddollar/anvil) is a generic build server for Heroku. - -``` -gem install anvil-cli -``` - -The [heroku-anvil CLI plugin](https://github.com/ddollar/heroku-anvil) is a wrapper for anvil. - -``` -heroku plugins:install https://github.com/ddollar/heroku-anvil -``` - -The [ddollar/test](https://github.com/ddollar/buildpack-test) buildpack runs `bin/test` on your app/buildpack. - -``` -heroku build -b ddollar/test # -b can also point to a local directory -``` - -## Publishing buildpack updates - -``` -heroku plugins:install https://github.com/heroku/heroku-buildpacks - -cd heroku-buildpack-nodejs -git checkout master -heroku buildpacks:publish heroku/nodejs -``` - -- Email [dos@heroku.com](mailto:dos@heroku.com) if changes are significant. -- Add a [changelog item](https://devcenter.heroku.com/admin/changelog_items/new). -- Update [Node Devcenter articles](https://devcenter.heroku.com/admin/articles/owned) as necessary. - -## Keeping up with the Nodeses - -- Run `npm info npm version` to find out the latest available version of npm. -- Follow [@nodejs](https://twitter.com/nodejs) and [@npmjs](https://twitter.com/npmjs) on Twitter. -- Find node-npm version pairings at [nodejs.org/dist/npm-versions.txt](http://nodejs.org/dist/npm-versions.txt) diff --git a/README.md b/README.md index a6ba1cae6..9c8fa87a8 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,45 @@ Heroku Buildpack for Node.js ============================ -This is a [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) for Node.js apps. It will detect your app as Node.js if it has a `package.json` file in the root. It uses npm to install your dependencies, and vendors a version of the Node.js runtime into your slug. +This is the official [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) for Node.js apps. If you fork this repository, **update this README** to explain what your fork does and why it's special. -If you specify a version of node in the [`engines` field of your package.json](https://npmjs.org/doc/json.html#engines), the buildpack will attempt to find the specified version on [nodejs.org/dist](http://nodejs.org/dist/) and download it from our S3 caching proxy. +How it Works +------------ -If you don't specify a version of node, the latest stable version will be used. +Here's a high-level overview of how this buildpack works: -About this Refactor -------------------- +- Uses the [semver.io](http://semver.io) webservice to find the latest version of node that satisfies the [engines.node semver range](https://npmjs.org/doc/json.html#engines) in your package.json. +- Allows any recent version of node to be used, including [pre-release versions](http://semver.io/node.json). +- Uses an [S3 caching proxy](https://github.com/heroku/s3pository#readme) of nodejs.org for faster downloads of the node binary. +- Discourages use of dangerous semver ranges like `*` and `>0.10`. +- Uses the version of `npm` that comes bundled with `node`. +- Puts `node` and `npm` on the `PATH` so they can be executed with [heroku run](https://devcenter.heroku.com/articles/one-off-dynos#an-example-one-off-dyno). +- Caches the `node_modules` directory across builds for fast deploys. +- Doesn't use the cache if `node_modules` is checked into version control. +- Runs `npm rebuild` if `node_modules` is checked into version control. +- Always runs `npm install` to ensure [npm script hooks](https://npmjs.org/doc/misc/npm-scripts.html) are executed. +- Always runs `npm prune` after restoring cached modules to ensure cleanup of unused dependencies. -This branch of the buildpack is intended to replace the [official Node.js buildpack](https://github.com/heroku/heroku-buildpack-nodejs#readme) once it has been tested by some users. To use this buildpack for your node app, simply change your BUILDPACK_URL [config var](https://devcenter.heroku.com/articles/config-vars) and push your app to heroku. +For more technical details, see the [heavily-commented compile script](https://github.com/heroku/heroku-buildpack-nodejs/blob/master/bin/compile). -``` -heroku config:set BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-nodejs#diet -a my-node-app -git commit -am "fakeout" --allow-empty -git push heroku -``` - -Here's a summary of the differences between the current official buildpack and this _diet_ version: - -The old buildpack: - -- Contains a lot of code for compiling node and npm binaries and moving them to S3. This code is orthogonal to the core function of the buildpack, and is only used internally by Node maintainers at Heroku. -- Downloads and compiles node and npm separately. -- Requires manual intervention each time a new version of node or npm is released. -- Does not support pre-release versions of node. -- Uses SCONS to support really old versions of node and npm. -- Maintains S3 manifests of our hand-compiled versions of node and npm. -- Does not cache anything. - -The new buildpack: - -- Uses the latest stable version of node and npm by default. -- Allows any recent version of node to be used, including pre-release versions, as soon as they become available on [nodejs.org/dist](http://nodejs.org/dist/). -- Uses the version of npm that comes bundled with node instead of downloading and compiling them separately. npm has been bundled with node since [v0.6.3 (Nov 2011)](http://blog.nodejs.org/2011/11/25/node-v0-6-3/). This effectively means that node versions `<0.6.3` are no longer supported, and that the `engines.npm` field in package.json is now ignored. -- Makes use of an s3 caching proxy of nodejs.org for faster downloads of the node binaries. -- Makes fewer HTTP requests when resolving node versions. -- Uses an updated version of [node-semver](https://github.com/isaacs/node-semver) for dependency resolution. -- No longer depends on SCONS. -- Caches the `node_modules` directory across builds. -- Runs `npm prune` after restoring cached modules, to ensure that any modules formerly used by your app aren't needlessly installed and/or compiled. Documentation ------------- -For more information about buildpacks and Node.js, see these Dev Center articles: +For more information about using Node.js and buildpacks on Heroku, see these Dev Center articles: - [Heroku Node.js Support](https://devcenter.heroku.com/articles/nodejs-support) - [Getting Started with Node.js on Heroku](https://devcenter.heroku.com/articles/nodejs) - [Buildpacks](https://devcenter.heroku.com/articles/buildpacks) - [Buildpack API](https://devcenter.heroku.com/articles/buildpack-api) + Hacking ------- To make changes to this buildpack, fork it on Github. Push up changes to your fork, then create a new Heroku app to test it, or configure an existing app to use your buildpack: -```sh +``` # Create a new Heroku app that uses your buildpack heroku create --buildpack @@ -69,3 +51,25 @@ heroku config:set BUILDPACK_URL=#your-branch ``` For more detailed information about testing buildpacks, see [CONTRIBUTING.md](CONTRIBUTING.md) + + +Testing +------- + +[Anvil](https://github.com/ddollar/anvil) is a generic build server for Heroku. + +``` +gem install anvil-cli +``` + +The [heroku-anvil CLI plugin](https://github.com/ddollar/heroku-anvil) is a wrapper for anvil. + +``` +heroku plugins:install https://github.com/ddollar/heroku-anvil +``` + +The [ddollar/test](https://github.com/ddollar/buildpack-test) buildpack runs `bin/test` on your app/buildpack. + +``` +heroku build -b ddollar/test # -b can also point to a local directory +``` From 27641610c12ee61974c38b060f461056933a0efa Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 3 Dec 2013 14:57:14 -0800 Subject: [PATCH 092/116] add a section on legacy compatibility --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9c8fa87a8..5dbe7c42a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ Heroku Buildpack for Node.js ============================ -This is the official [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) for Node.js apps. If you fork this repository, **update this README** to explain what your fork does and why it's special. +This is the official [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) for Node.js apps. If you fork this repository, please **update this README** to explain what your fork does and why it's special. + How it Works ------------ -Here's a high-level overview of how this buildpack works: +Here's an overview of what this buildpack does: - Uses the [semver.io](http://semver.io) webservice to find the latest version of node that satisfies the [engines.node semver range](https://npmjs.org/doc/json.html#engines) in your package.json. - Allows any recent version of node to be used, including [pre-release versions](http://semver.io/node.json). @@ -34,6 +35,19 @@ For more information about using Node.js and buildpacks on Heroku, see these Dev - [Buildpack API](https://devcenter.heroku.com/articles/buildpack-api) +Legacy Compatibility +-------------------- + +For most Node.js apps this buildpack should work just fine. If, however, you're unable to deploy using this new version of the buildpack, you can get your app working again by using the legacy branch: + +``` +heroku config:set BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-nodejs#legacy -a my-app +git commit -am "empty" --allow-empty # force a git commit +git push heroku master +``` + +Then please open a support ticket at [help.heroku.com](https://help.heroku.com/) so we can diagnose and get your app running on the default buildpack. + Hacking ------- From b9feca0d071c21618c90f5a3aad3cbc9fca5e624 Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 3 Dec 2013 15:04:19 -0800 Subject: [PATCH 093/116] add empty bin/release file so as not to break things that expect it to be there --- bin/release | 2 ++ 1 file changed, 2 insertions(+) create mode 100755 bin/release diff --git a/bin/release b/bin/release new file mode 100755 index 000000000..b950df397 --- /dev/null +++ b/bin/release @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +# bin/release From ae619742b9b83916c8d757d10b39c43bea4f14ea Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 4 Dec 2013 16:51:34 -0800 Subject: [PATCH 094/116] Set default NODE_ENV to 'production' --- bin/compile | 3 ++- bin/test | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index d14ba9ce0..fb53523c6 100755 --- a/bin/compile +++ b/bin/compile @@ -83,7 +83,8 @@ rm -rf "$build_dir/.npm" # Update the PATH status "Building runtime environment" mkdir -p $build_dir/.profile.d -echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh +echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\";\ +export NODE_ENV=${NODE_ENV:-production}" > $build_dir/.profile.d/nodejs.sh # Post package.json to nomnom service # Use a subshell so failures won't break the build. diff --git a/bin/test b/bin/test index 7f8217a67..793f954eb 100755 --- a/bin/test +++ b/bin/test @@ -60,7 +60,7 @@ testUnstableVersion() { testProfileCreated() { compile "stable-node" assertCaptured "Building runtime environment" - assertFile "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" ".profile.d/nodejs.sh" + assertFile "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\";export NODE_ENV=${NODE_ENV:-production}" ".profile.d/nodejs.sh" assertCapturedSuccess } From 6278ef0102f8d1327a4074b6f50e8ec70ed80286 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 5 Dec 2013 15:11:01 -0800 Subject: [PATCH 095/116] add comment about npm executable --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index d14ba9ce0..f36a0e4cc 100755 --- a/bin/compile +++ b/bin/compile @@ -44,7 +44,7 @@ status "Downloading and installing node" node_url="http://s3pository.heroku.com/node/v$node_version/node-v$node_version-linux-x64.tar.gz" curl $node_url -s -o - | tar xzf - -C $build_dir -# Move node into ./vendor and make it executable +# Move node (and npm) into ./vendor and make them executable mkdir -p $build_dir/vendor mv $build_dir/node-v$node_version-linux-x64 $build_dir/vendor/node chmod +x $build_dir/vendor/node/bin/* From 7080399c8ea3606b4c8144a80340e54f0cf817b5 Mon Sep 17 00:00:00 2001 From: zeke Date: Thu, 5 Dec 2013 15:12:07 -0800 Subject: [PATCH 096/116] be more clear about the effect of `npm prune` --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index f36a0e4cc..2e83b6630 100755 --- a/bin/compile +++ b/bin/compile @@ -68,7 +68,7 @@ fi status "Installing dependencies" npm install --production 2>&1 | indent -status "Pruning unused dependencies" +status "Pruning dependencies not specified in package.json" npm prune 2>&1 | indent status "Caching node_modules directory for future builds" From 18e2af218698961be98b239b34af73141a91d8ee Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 6 Dec 2013 11:12:56 -0800 Subject: [PATCH 097/116] Make the caching messages more clear. --- bin/compile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index 2e83b6630..eac65e3c0 100755 --- a/bin/compile +++ b/bin/compile @@ -56,11 +56,11 @@ cd $build_dir # If node_modules directory is checked into source control then # rebuild any native deps. Otherwise, restore from the build cache. if test -d $build_dir/node_modules; then - status "Using existing node_modules directory" + status "Found existing node_modules directory; skipping cache" status "Rebuilding any native dependencies" npm rebuild 2>&1 | indent elif test -d $cache_dir/node_modules; then - status "Restoring node_modules from cache" + status "Restoring node_modules directory from cache" cp -r $cache_dir/node_modules $build_dir/ fi From f102eb77999f624e5bfab1c1a929d6ffc759a3ab Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 6 Dec 2013 11:41:52 -0800 Subject: [PATCH 098/116] CONTRIBUTING.md doesn't exist anymore. Refer instead to README.md --- bin/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test b/bin/test index 7f8217a67..d42d5f40f 100755 --- a/bin/test +++ b/bin/test @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# See CONTRIBUTING.md for info on running these tests. +# See README.md for info on running these tests. testDetectWithPackageJson() { detect "stable-node" From a06fc9b569fe479102ea8d3182a48dd2eea54d60 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 6 Dec 2013 11:44:49 -0800 Subject: [PATCH 099/116] fix a broken assertion --- bin/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test b/bin/test index d42d5f40f..4309de029 100755 --- a/bin/test +++ b/bin/test @@ -79,7 +79,7 @@ testNodeModulesCached() { testModulesCheckedIn() { compile "modules-checked-in" - assertCaptured "Using existing node_modules directory" + assertCaptured "Found existing node_modules directory; skipping cache" assertCaptured "Rebuilding any native dependencies" assertCapturedSuccess } From 7e5c674ee8ec248771badb44b0854020129b8a31 Mon Sep 17 00:00:00 2001 From: btubbs Date: Fri, 6 Dec 2013 14:45:47 -0800 Subject: [PATCH 100/116] bin/release should emit a valid yaml mapping. --- bin/release | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/release b/bin/release index b950df397..1e2ad1b90 100755 --- a/bin/release +++ b/bin/release @@ -1,2 +1,3 @@ #!/usr/bin/env bash # bin/release +echo "{}" From 18069aa82f02aa3884482c06eb63c1e092a0e46f Mon Sep 17 00:00:00 2001 From: btubbs Date: Fri, 6 Dec 2013 15:37:55 -0800 Subject: [PATCH 101/116] make bin/release comply with published Buildpack API --- bin/release | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/release b/bin/release index 1e2ad1b90..bc379507e 100755 --- a/bin/release +++ b/bin/release @@ -1,3 +1,6 @@ #!/usr/bin/env bash # bin/release -echo "{}" +cat << EOF +addons: [] +default_process_types: {} +EOF From f6d0b9c4795a46fa70ed7abf484e69dc3411cde3 Mon Sep 17 00:00:00 2001 From: Michael Rykov Date: Sat, 7 Dec 2013 01:46:24 -0500 Subject: [PATCH 102/116] Add support for custom .npmrc --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index eac65e3c0..e34e12861 100755 --- a/bin/compile +++ b/bin/compile @@ -66,7 +66,7 @@ fi # Make npm output to STDOUT instead of its default STDERR status "Installing dependencies" -npm install --production 2>&1 | indent +npm install --userconfig $build_dir/.npmrc --production 2>&1 | indent status "Pruning dependencies not specified in package.json" npm prune 2>&1 | indent From 991ceb99492574cc2b2840754783fb4654a59bd9 Mon Sep 17 00:00:00 2001 From: Michael Rykov Date: Sat, 7 Dec 2013 21:30:20 -0500 Subject: [PATCH 103/116] Add a test for custom .npmrc support --- bin/test | 9 ++++++++- test/userconfig/.npmrc | 7 +++++++ test/userconfig/README.md | 1 + test/userconfig/package.json | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 test/userconfig/.npmrc create mode 100644 test/userconfig/README.md create mode 100644 test/userconfig/package.json diff --git a/bin/test b/bin/test index 4309de029..a198ab58c 100755 --- a/bin/test +++ b/bin/test @@ -84,6 +84,13 @@ testModulesCheckedIn() { assertCapturedSuccess } +testUserConfig() { + compile "userconfig" + assertCaptured "https://www.google.com/" + assertCaptured "registry error" + assertCapturedError 1 "" +} + # Pending Tests # testNodeBinariesAddedToPath() { @@ -135,7 +142,7 @@ compile_dir="" compile() { compile_dir=$(mktmpdir) - cp -r ${bp_dir}/test/$1/* ${compile_dir}/ + cp -r ${bp_dir}/test/$1/. ${compile_dir} capture ${bp_dir}/bin/compile ${compile_dir} ${2:-$(mktmpdir)} } diff --git a/test/userconfig/.npmrc b/test/userconfig/.npmrc new file mode 100644 index 000000000..af487f509 --- /dev/null +++ b/test/userconfig/.npmrc @@ -0,0 +1,7 @@ +; Any URL that throws 404s +registry = "https://www.google.com/" +strict-ssl = false +; Ensure fast failure +fetch-retries = 0 +fetch-retry-factor = 0 +fetch-retry-mintimeout = 0 diff --git a/test/userconfig/README.md b/test/userconfig/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/userconfig/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/userconfig/package.json b/test/userconfig/package.json new file mode 100644 index 000000000..5e30e5838 --- /dev/null +++ b/test/userconfig/package.json @@ -0,0 +1,15 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "dependencies": { + "something-not-in-cache": "*" + }, + "engines": { + "node": "~0.10.0" + } +} From bdab33789c7ec147788012fda55f951b73c0c53b Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 9 Dec 2013 11:09:55 -0800 Subject: [PATCH 104/116] only prune when node_modules directory is restored from cache. Fixes #75 --- bin/compile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/compile b/bin/compile index e34e12861..47177e277 100755 --- a/bin/compile +++ b/bin/compile @@ -62,15 +62,15 @@ if test -d $build_dir/node_modules; then elif test -d $cache_dir/node_modules; then status "Restoring node_modules directory from cache" cp -r $cache_dir/node_modules $build_dir/ + + status "Pruning cached dependencies not specified in package.json" + npm prune 2>&1 | indent fi # Make npm output to STDOUT instead of its default STDERR status "Installing dependencies" npm install --userconfig $build_dir/.npmrc --production 2>&1 | indent -status "Pruning dependencies not specified in package.json" -npm prune 2>&1 | indent - status "Caching node_modules directory for future builds" rm -rf $cache_dir mkdir -p $cache_dir From 8c67d739b93d2862f3ba5aa933fc729be194aa74 Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 11 Dec 2013 11:47:08 -0800 Subject: [PATCH 105/116] always use https --- README.md | 4 ++-- bin/compile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5dbe7c42a..d766c5eca 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ How it Works Here's an overview of what this buildpack does: -- Uses the [semver.io](http://semver.io) webservice to find the latest version of node that satisfies the [engines.node semver range](https://npmjs.org/doc/json.html#engines) in your package.json. -- Allows any recent version of node to be used, including [pre-release versions](http://semver.io/node.json). +- Uses the [semver.io](https://semver.io) webservice to find the latest version of node that satisfies the [engines.node semver range](https://npmjs.org/doc/json.html#engines) in your package.json. +- Allows any recent version of node to be used, including [pre-release versions](https://semver.io/node.json). - Uses an [S3 caching proxy](https://github.com/heroku/s3pository#readme) of nodejs.org for faster downloads of the node binary. - Discourages use of dangerous semver ranges like `*` and `>0.10`. - Uses the version of `npm` that comes bundled with `node`. diff --git a/bin/compile b/bin/compile index e34e12861..27e32a447 100755 --- a/bin/compile +++ b/bin/compile @@ -19,7 +19,7 @@ trap cat_npm_debug_log ERR semver_range=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .engines.node) # Resolve node version using semver.io -node_version=$(curl --silent --get --data-urlencode "range=${semver_range}" http://semver.io/node/resolve) +node_version=$(curl --silent --get --data-urlencode "range=${semver_range}" https://semver.io/node/resolve) # Recommend using semver ranges in a safe manner if [ "$semver_range" == "null" ]; then From d93ee4ce87e8b825b4bef39aa55412169e3c8025 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Fri, 22 Nov 2013 00:44:13 -0800 Subject: [PATCH 106/116] Add scripts.start to Procfile from package.json If no Procfile exists and there is a scripts.start in package.json, add the scripts.start script as the 'web' process type to a new Procfile. --- bin/compile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bin/compile b/bin/compile index 920bbe330..855754aa7 100755 --- a/bin/compile +++ b/bin/compile @@ -80,6 +80,17 @@ status "Cleaning up node-gyp and npm artifacts" rm -rf "$build_dir/.node-gyp" rm -rf "$build_dir/.npm" +# Add npm start to Procfile if necessary +if [ ! -e $build_dir/Procfile ]; then + npm_start=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .scripts.start) + + # If a start script is declared, add it to a Procfile + if [ "$npm_start" != "null" ]; then + status "No Procfile present; adding npm start script to Procfile" + echo "web: $npm_start" > $build_dir/Procfile + fi +fi + # Update the PATH status "Building runtime environment" mkdir -p $build_dir/.profile.d From e9266dfec59d3110ff1a651542b7a651f1b08364 Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 11 Dec 2013 12:07:35 -0800 Subject: [PATCH 107/116] add tests around creating a default Procfile --- bin/compile | 11 ++++++----- bin/test | 13 +++++++++++++ .../procfile-absent-npm-start-absent/README.md | 1 + .../package.json | 15 +++++++++++++++ .../README.md | 1 + .../package.json | 18 ++++++++++++++++++ 6 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 test/procfile-absent-npm-start-absent/README.md create mode 100644 test/procfile-absent-npm-start-absent/package.json create mode 100644 test/procfile-absent-npm-start-present/README.md create mode 100644 test/procfile-absent-npm-start-present/package.json diff --git a/bin/compile b/bin/compile index 855754aa7..87fd863ec 100755 --- a/bin/compile +++ b/bin/compile @@ -80,14 +80,15 @@ status "Cleaning up node-gyp and npm artifacts" rm -rf "$build_dir/.node-gyp" rm -rf "$build_dir/.npm" -# Add npm start to Procfile if necessary +# If Procfile is absent, try to create one using `npm start` if [ ! -e $build_dir/Procfile ]; then npm_start=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .scripts.start) - # If a start script is declared, add it to a Procfile - if [ "$npm_start" != "null" ]; then - status "No Procfile present; adding npm start script to Procfile" - echo "web: $npm_start" > $build_dir/Procfile + if [ "$npm_start" == "null" ]; then + protip "Create a Procfile or specify a start script in package.json" + else + status "No Procfile found; adding npm start script to Procfile" + echo "web: npm start" > $build_dir/Procfile fi fi diff --git a/bin/test b/bin/test index a198ab58c..412deadb8 100755 --- a/bin/test +++ b/bin/test @@ -91,6 +91,19 @@ testUserConfig() { assertCapturedError 1 "" } +testProcfileAbsentNpmStartPresent() { + compile "procfile-absent-npm-start-present" + assertCaptured "No Procfile found; adding npm start script to Procfile" + assertFile "web: npm start" "Procfile" + assertCapturedSuccess +} + +testProcfileAbsentNpmStartAbsent() { + compile "procfile-absent-npm-start-absent" + assertCaptured "Create a Procfile or specify a start script" + assertCapturedSuccess +} + # Pending Tests # testNodeBinariesAddedToPath() { diff --git a/test/procfile-absent-npm-start-absent/README.md b/test/procfile-absent-npm-start-absent/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/procfile-absent-npm-start-absent/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/procfile-absent-npm-start-absent/package.json b/test/procfile-absent-npm-start-absent/package.json new file mode 100644 index 000000000..d5561df40 --- /dev/null +++ b/test/procfile-absent-npm-start-absent/package.json @@ -0,0 +1,15 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "dependencies": { + "hashish": "*" + }, + "engines": { + "node": "~0.10.0" + } +} diff --git a/test/procfile-absent-npm-start-present/README.md b/test/procfile-absent-npm-start-present/README.md new file mode 100644 index 000000000..cda334ae1 --- /dev/null +++ b/test/procfile-absent-npm-start-present/README.md @@ -0,0 +1 @@ +A fake README, to keep npm from polluting stderr. \ No newline at end of file diff --git a/test/procfile-absent-npm-start-present/package.json b/test/procfile-absent-npm-start-present/package.json new file mode 100644 index 000000000..b8a6e9143 --- /dev/null +++ b/test/procfile-absent-npm-start-present/package.json @@ -0,0 +1,18 @@ +{ + "name": "node-buildpack-test-app", + "version": "0.0.1", + "description": "node buildpack integration test app", + "repository" : { + "type" : "git", + "url" : "http://github.com/example/example.git" + }, + "dependencies": { + "hashish": "*" + }, + "engines": { + "node": "~0.10.0" + }, + "scripts": { + "start": "echo foo" + } +} From bbc304f9c04058a33e45218289be02c858931d64 Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 11 Dec 2013 14:19:12 -0800 Subject: [PATCH 108/116] update "no Procfile" messaging --- bin/compile | 3 ++- bin/test | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/compile b/bin/compile index 87fd863ec..ed516285d 100755 --- a/bin/compile +++ b/bin/compile @@ -85,9 +85,10 @@ if [ ! -e $build_dir/Procfile ]; then npm_start=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .scripts.start) if [ "$npm_start" == "null" ]; then + status "Procfile not found and npm start script is undefined" protip "Create a Procfile or specify a start script in package.json" else - status "No Procfile found; adding npm start script to Procfile" + status "No Procfile found; Adding npm start to new Procfile" echo "web: npm start" > $build_dir/Procfile fi fi diff --git a/bin/test b/bin/test index 412deadb8..996e33bdc 100755 --- a/bin/test +++ b/bin/test @@ -93,14 +93,14 @@ testUserConfig() { testProcfileAbsentNpmStartPresent() { compile "procfile-absent-npm-start-present" - assertCaptured "No Procfile found; adding npm start script to Procfile" + assertCaptured "No Procfile found; Adding npm start to new Procfile" assertFile "web: npm start" "Procfile" assertCapturedSuccess } testProcfileAbsentNpmStartAbsent() { compile "procfile-absent-npm-start-absent" - assertCaptured "Create a Procfile or specify a start script" + assertCaptured "Create a Procfile or specify a start script in package.json" assertCapturedSuccess } From 94e927bd6863427b26fca053c2fb7190da9422e4 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 13 Dec 2013 11:22:05 -0800 Subject: [PATCH 109/116] Create a default Procfile if server.js is present --- bin/compile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bin/compile b/bin/compile index ed516285d..fc9075a3b 100755 --- a/bin/compile +++ b/bin/compile @@ -84,12 +84,14 @@ rm -rf "$build_dir/.npm" if [ ! -e $build_dir/Procfile ]; then npm_start=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .scripts.start) - if [ "$npm_start" == "null" ]; then - status "Procfile not found and npm start script is undefined" - protip "Create a Procfile or specify a start script in package.json" - else + # If `scripts.start` is set in package.json, or a server.js file + # is present in the app root, then create a default Procfile + if [ "$npm_start" != "null" ] || [ -f $build_dir/server.js ]; then status "No Procfile found; Adding npm start to new Procfile" echo "web: npm start" > $build_dir/Procfile + else + status "Procfile not found and npm start script is undefined" + protip "Create a Procfile or specify a start script in package.json" fi fi From 2dd0ce59f7237f50c1fbd42e528c2cd711a01784 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 13 Dec 2013 14:57:34 -0800 Subject: [PATCH 110/116] pipe nomnom output to /dev/null --- bin/compile | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/compile b/bin/compile index fc9075a3b..f040d4655 100755 --- a/bin/compile +++ b/bin/compile @@ -110,4 +110,5 @@ echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\ --request POST \ --header "content-type: application/json" \ https://nomnom.heroku.com/?request_id=$REQUEST_ID + > /dev/null ) & From 613a6a5d1dd430efab0fc84ad09743d6deefce0a Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 16 Dec 2013 14:21:20 -0800 Subject: [PATCH 111/116] continue the line --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index f040d4655..04d062c48 100755 --- a/bin/compile +++ b/bin/compile @@ -109,6 +109,6 @@ echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\ --silent \ --request POST \ --header "content-type: application/json" \ - https://nomnom.heroku.com/?request_id=$REQUEST_ID + https://nomnom.heroku.com/?request_id=$REQUEST_ID \ > /dev/null ) & From da02c95253b8f8033b73689382142377e5099aa7 Mon Sep 17 00:00:00 2001 From: zeke Date: Tue, 17 Dec 2013 10:49:24 -0800 Subject: [PATCH 112/116] Blow away the cached node_modules directory instead of the entire cache. --- bin/compile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/compile b/bin/compile index 04d062c48..1b14c6120 100755 --- a/bin/compile +++ b/bin/compile @@ -72,7 +72,7 @@ status "Installing dependencies" npm install --userconfig $build_dir/.npmrc --production 2>&1 | indent status "Caching node_modules directory for future builds" -rm -rf $cache_dir +rm -rf $cache_dir/node_modules mkdir -p $cache_dir test -d $build_dir/node_modules && cp -r $build_dir/node_modules $cache_dir/ From a6a58df25c00b2e973fe91c71603887e76943481 Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 18 Dec 2013 10:03:08 -0800 Subject: [PATCH 113/116] add a link to discussion about testing --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d766c5eca..8469bc704 100644 --- a/README.md +++ b/README.md @@ -87,3 +87,5 @@ The [ddollar/test](https://github.com/ddollar/buildpack-test) buildpack runs `bi ``` heroku build -b ddollar/test # -b can also point to a local directory ``` + +For more info on testing, see [Best Practices for Testing Buildpacks](https://discussion.heroku.com/t/best-practices-for-testing-buildpacks/294) on the Heroku discussion forum. From e7a4fff9225693bd7fa417a8a9052533f702795b Mon Sep 17 00:00:00 2001 From: zeke Date: Wed, 18 Dec 2013 10:04:50 -0800 Subject: [PATCH 114/116] remove test cruft --- bin/test | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/bin/test b/bin/test index 996e33bdc..895e0990b 100755 --- a/bin/test +++ b/bin/test @@ -104,34 +104,6 @@ testProcfileAbsentNpmStartAbsent() { assertCapturedSuccess } -# Pending Tests - -# testNodeBinariesAddedToPath() { -# } - -# testNodeModulesRestoredFromCache() { -# } - -# TODO: Figure out how to test stuff like script hooks -# when restoring node_modules from cache -# testScriptHooks() { -# compile "script-hooks" -# assertCaptured "trigger script hooks" -# assertCaptured "preinstall hook message" -# assertCapturedSuccess -# } - -# testWithoutScriptHooks() { -# compile "no-script-hooks" -# assertNotCaptured "trigger script hooks" -# assertCapturedSuccess -# } - -# testInvalidVersion() { -# compile "invalid-node-version" -# assertCapturedError 1 "not found among available versions" -# } - # Utils pushd $(dirname 0) >/dev/null From 2311a12b1a5df874a1571554fe905056231c47e1 Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 20 Dec 2013 15:03:48 -0800 Subject: [PATCH 115/116] roll back the .profile.d NODE_ENV change --- bin/compile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/compile b/bin/compile index eb89c8818..3d1f00f6f 100755 --- a/bin/compile +++ b/bin/compile @@ -98,8 +98,7 @@ fi # Update the PATH status "Building runtime environment" mkdir -p $build_dir/.profile.d -echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\";\ -export NODE_ENV=${NODE_ENV:-production}" > $build_dir/.profile.d/nodejs.sh +echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\";" > $build_dir/.profile.d/nodejs.sh # Post package.json to nomnom service # Use a subshell so failures won't break the build. From c962f1e40ea98e37342261f12b6d2a324b0bc8de Mon Sep 17 00:00:00 2001 From: zeke Date: Fri, 20 Dec 2013 15:06:11 -0800 Subject: [PATCH 116/116] fix NODE_ENV test --- bin/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test b/bin/test index a77132377..e95fb7eec 100755 --- a/bin/test +++ b/bin/test @@ -60,7 +60,7 @@ testUnstableVersion() { testProfileCreated() { compile "stable-node" assertCaptured "Building runtime environment" - assertFile "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\";export NODE_ENV=${NODE_ENV:-production}" ".profile.d/nodejs.sh" + assertFile "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\";" ".profile.d/nodejs.sh" assertCapturedSuccess }