From 3df01c224ac15cb95bf741b34b051428a85819ac Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 09:53:46 -0500 Subject: [PATCH 01/16] Introduces 'package' subcommand `nodenv-package` is the corrolary to nodenv-shell, nodenv-local, and nodenv-global. Right now, it is just a subcommand to show the version selected from package.json. In the future, it should also be used to _set_ the version in package.json. --- bin/JSON.sh | 1 + bin/nodenv-package | 83 ++++++++++++++++++++++++++++++ bin/semver.sh | 1 + test/package.bats | 114 ++++++++++++++++++++++++++++++++++++++++++ test/test_helper.bash | 2 +- 5 files changed, 200 insertions(+), 1 deletion(-) create mode 120000 bin/JSON.sh create mode 100755 bin/nodenv-package create mode 120000 bin/semver.sh create mode 100755 test/package.bats diff --git a/bin/JSON.sh b/bin/JSON.sh new file mode 120000 index 0000000..f3a245b --- /dev/null +++ b/bin/JSON.sh @@ -0,0 +1 @@ +../node_modules/JSON.sh/JSON.sh \ No newline at end of file diff --git a/bin/nodenv-package b/bin/nodenv-package new file mode 100755 index 0000000..00d4dea --- /dev/null +++ b/bin/nodenv-package @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# Summary: Show the local application-specific Node version from package.json +# +# Usage: nodenv package +# +# Shows the highest-installed node version satisfying package.json#engines. +# + +set -e +[ -n "$NODENV_DEBUG" ] && set -x + +abort() { + echo "package-json-engine: $1" >&2 + exit 1 +} + +find_package_json() { + local package_json root="$1" + while ! [[ "$root" =~ ^//[^/]*$ ]]; do + package_json="$root/package.json" + + if [ -f "$package_json" ] && [ -s "$package_json" ]; then + echo "$package_json" + return 0 + fi + [ -n "$root" ] || break + root="${root%/*}" + done + return 1 +} + +extract_expression() { + version_regex='\["engines","node"\][[:space:]]*"([^"]*)"' + # -b -n gives minimal output - see https://github.com/dominictarr/JSON.sh#options + if [[ $(JSON.sh -b -n 2>/dev/null) =~ $version_regex ]]; then + echo "${BASH_REMATCH[1]}" + else + return 1 + fi +} + +matching_version() { + local version_spec=$1 + local -a installed_versions + while IFS= read -r v; do + installed_versions+=( "$v" ) + done < <(nodenv versions --bare --skip-aliases | grep -e '^[[:digit:]]') + + local fast_guess + fast_guess=$(semver.sh -r "$version_spec" "${installed_versions[@]:${#installed_versions[@]}-1}" | tail -n 1) + + # Most #engine version specs just specify a baseline version, + # which means most likely, the highest installed version will satisfy + # This does a first pass with just that single version in hopes it satisfies. + # If so, we can avoid the cost of sh-semver sorting and validating across + # all the installed versions. + if [ -n "$fast_guess" ]; then + echo "$fast_guess" + return 0 + fi + + local match + match=$(semver.sh -r "$version_spec" "${installed_versions[@]}" | tail -n 1) + + if [ -n "$match" ]; then + echo "$match" + else + return 1 + fi +} + +if ! package_json="$(find_package_json "$NODENV_DIR")"; then + abort "no package.json found for this directory" +fi + +if ! version_spec="$(extract_expression <"$package_json")"; then + abort "no engine version configured for this package" +fi + +if ! matching_version "$version_spec"; then + abort "no version found satisfying \`$version_spec'" +fi diff --git a/bin/semver.sh b/bin/semver.sh new file mode 120000 index 0000000..0bde939 --- /dev/null +++ b/bin/semver.sh @@ -0,0 +1 @@ +../node_modules/sh-semver/semver.sh \ No newline at end of file diff --git a/test/package.bats b/test/package.bats new file mode 100755 index 0000000..75b0e0a --- /dev/null +++ b/test/package.bats @@ -0,0 +1,114 @@ +#!/usr/bin/env bats + +load test_helper + +@test 'Recognizes simple node version specified in package.json engines' { + in_package_for_engine 4.2.1 + + run nodenv package + assert_success + assert_output '4.2.1' +} + +@test 'Prefers the greatest installed version matching a range' { + in_package_for_engine '^4.0.0' + + run nodenv package + assert_success + assert_output '4.2.1' +} + +@test 'Ignores non-matching installed versions' { + in_package_for_engine '^1.0.0' + + run nodenv package + assert_failure + assert_output "package-json-engine: no version found satisfying \`^1.0.0'" +} + +@test 'Does not match arbitrary "node" key in package.json' { + in_package_with_babel_env + + run nodenv package + + assert_failure + assert_output 'package-json-engine: no engine version configured for this package' +} + +@test 'Handles missing package.json' { + in_example_package + + run nodenv package + + assert_failure + assert_output 'package-json-engine: no package.json found for this directory' +} + +@test 'Handles unreadable package.json' { + in_example_package + touch package.json + chmod -r package.json + + run nodenv package + + assert_failure + assert_output 'package-json-engine: no package.json found for this directory' +} + +@test 'Handles non-file package.json' { + in_example_package + mkdir package.json + + run nodenv package + + assert_failure + assert_output 'package-json-engine: no package.json found for this directory' +} + +@test 'Handles empty package.json' { + in_example_package + + # empty + touch package.json + run nodenv package + assert_failure + assert_output 'package-json-engine: no package.json found for this directory' +} + +@test 'Handles malformed package.json' { + in_example_package + + # non json + echo "foo" > package.json + run nodenv package + assert_failure + assert_output 'package-json-engine: no engine version configured for this package' + + # malformed + echo "{" > package.json + run nodenv package + assert_failure + assert_output 'package-json-engine: no engine version configured for this package' +} + +@test 'Handles multiple occurrences of "node" key' { + in_example_package + cat << JSON > package.json +{ + "engines": { + "node": "4.2.1" + }, + "presets": [ + ["env", { + "targets": { + "node": "current" + } + }] + ] +} +JSON + + run nodenv package + assert_success + assert_output '4.2.1' +} diff --git a/test/test_helper.bash b/test/test_helper.bash index 474907f..b03d60e 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -9,7 +9,7 @@ setup() { local node_modules_bin=$BATS_TEST_DIRNAME/../node_modules/.bin - export PATH="$node_modules_bin:/usr/bin:/bin:/usr/sbin:/sbin" + export PATH="$BATS_TEST_DIRNAME/../bin:$node_modules_bin:/usr/bin:/bin:/usr/sbin:/sbin" export NODENV_ROOT="$BATS_TEST_DIRNAME/fixtures/nodenv_root" From 50f12a62f1d9cff269be1a59b0471daee435325f Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 14:02:48 -0500 Subject: [PATCH 02/16] Rewrite version-name hook to use new package command Separately; old code had a bug where the first conditional branch would never be executed (the `get_version_respecting_precedence` function never returned non-zero). This is actually a good thing, because we wouldn't _want_ to print anything to STDERR in this hook, anyway. Also, this hook script is sourced by nodenv so it shouldn't have a shebang, nor be executable. Lastly, we rely on nodenv ensuring each plugin's `bin/` dir is added to PATH so we can just invoke `nodenv-package` directly. --- .../version-name/package-json-engine.bash | 16 ++++---- test/version-name-hook.bats | 38 +++++++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) create mode 100755 test/version-name-hook.bats diff --git a/etc/nodenv.d/version-name/package-json-engine.bash b/etc/nodenv.d/version-name/package-json-engine.bash index f443ada..d792170 100644 --- a/etc/nodenv.d/version-name/package-json-engine.bash +++ b/etc/nodenv.d/version-name/package-json-engine.bash @@ -1,11 +1,9 @@ -#!/bin/bash - -# shellcheck source=libexec/nodenv-package-json-engine -source "$(plugin_root)/libexec/nodenv-package-json-engine" +if [ -n "$(nodenv-sh-shell 2>/dev/null)" ] || + [ -n "$(nodenv-local 2>/dev/null)" ]; then + return +fi -if ! NODENV_PACKAGE_JSON_VERSION=$(get_version_respecting_precedence); then - echo "package-json-engine: version satisfying \`$(get_expression_respecting_precedence)' not installed" >&2 - exit 1 -elif [ -n "$NODENV_PACKAGE_JSON_VERSION" ]; then - export NODENV_VERSION="${NODENV_PACKAGE_JSON_VERSION}" +if NODENV_PACKAGE_JSON_VERSION=$(nodenv-package 2>/dev/null) && + [ -n "$NODENV_PACKAGE_JSON_VERSION" ]; then + export NODENV_VERSION=$NODENV_PACKAGE_JSON_VERSION fi diff --git a/test/version-name-hook.bats b/test/version-name-hook.bats new file mode 100755 index 0000000..045d710 --- /dev/null +++ b/test/version-name-hook.bats @@ -0,0 +1,38 @@ +#!/usr/bin/env bats + +load test_helper + +@test 'Prefers nodenv-local over package.json' { + in_package_for_engine 4.2.1 + nodenv local 5.0.0 + + run nodenv version-name + assert_success + assert_output '5.0.0' +} + +@test 'Prefers nodenv-shell over package.json' { + in_package_for_engine 4.2.1 + + NODENV_VERSION=5.0.0 run nodenv version-name + assert_success + assert_output '5.0.0' +} + +@test 'Prefers package.json over nodenv-global' { + in_package_for_engine 4.2.1 + nodenv global 5.0.0 + + run nodenv version-name + assert_success + assert_output '4.2.1' +} + +@test 'Mutes error output from nodenv-package' { + in_example_package + + run nodenv version-name + + assert_success + assert_output 'system' +} From 053c36cc864ed42b63b07668d275af5dd1375e51 Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 14:34:55 -0500 Subject: [PATCH 03/16] Rename nodenv-package-json --- bin/nodenv-package-json | 1 + .../nodenv-package-json | 3 +-- test/{package.bats => package-json.bats} | 22 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) create mode 120000 bin/nodenv-package-json rename bin/nodenv-package => libexec/nodenv-package-json (98%) rename test/{package.bats => package-json.bats} (88%) diff --git a/bin/nodenv-package-json b/bin/nodenv-package-json new file mode 120000 index 0000000..777cd74 --- /dev/null +++ b/bin/nodenv-package-json @@ -0,0 +1 @@ +../libexec/nodenv-package-json \ No newline at end of file diff --git a/bin/nodenv-package b/libexec/nodenv-package-json similarity index 98% rename from bin/nodenv-package rename to libexec/nodenv-package-json index 00d4dea..e8d0b2e 100755 --- a/bin/nodenv-package +++ b/libexec/nodenv-package-json @@ -1,9 +1,8 @@ #!/usr/bin/env bash # +# Usage: nodenv package-json # Summary: Show the local application-specific Node version from package.json # -# Usage: nodenv package -# # Shows the highest-installed node version satisfying package.json#engines. # diff --git a/test/package.bats b/test/package-json.bats similarity index 88% rename from test/package.bats rename to test/package-json.bats index 75b0e0a..cdd7a55 100755 --- a/test/package.bats +++ b/test/package-json.bats @@ -5,7 +5,7 @@ load test_helper @test 'Recognizes simple node version specified in package.json engines' { in_package_for_engine 4.2.1 - run nodenv package + run nodenv package-json assert_success assert_output '4.2.1' } @@ -13,7 +13,7 @@ load test_helper @test 'Prefers the greatest installed version matching a range' { in_package_for_engine '^4.0.0' - run nodenv package + run nodenv package-json assert_success assert_output '4.2.1' } @@ -21,7 +21,7 @@ load test_helper @test 'Ignores non-matching installed versions' { in_package_for_engine '^1.0.0' - run nodenv package + run nodenv package-json assert_failure assert_output "package-json-engine: no version found satisfying \`^1.0.0'" } @@ -29,7 +29,7 @@ load test_helper @test 'Does not match arbitrary "node" key in package.json' { in_package_with_babel_env - run nodenv package + run nodenv package-json assert_failure assert_output 'package-json-engine: no engine version configured for this package' @@ -38,7 +38,7 @@ load test_helper @test 'Handles missing package.json' { in_example_package - run nodenv package + run nodenv package-json assert_failure assert_output 'package-json-engine: no package.json found for this directory' @@ -49,7 +49,7 @@ load test_helper touch package.json chmod -r package.json - run nodenv package + run nodenv package-json assert_failure assert_output 'package-json-engine: no package.json found for this directory' @@ -59,7 +59,7 @@ load test_helper in_example_package mkdir package.json - run nodenv package + run nodenv package-json assert_failure assert_output 'package-json-engine: no package.json found for this directory' @@ -70,7 +70,7 @@ load test_helper # empty touch package.json - run nodenv package + run nodenv package-json assert_failure assert_output 'package-json-engine: no package.json found for this directory' } @@ -80,13 +80,13 @@ load test_helper # non json echo "foo" > package.json - run nodenv package + run nodenv package-json assert_failure assert_output 'package-json-engine: no engine version configured for this package' # malformed echo "{" > package.json - run nodenv package + run nodenv package-json assert_failure assert_output 'package-json-engine: no engine version configured for this package' } @@ -108,7 +108,7 @@ load test_helper } JSON - run nodenv package + run nodenv package-json assert_success assert_output '4.2.1' } From 951c1f8f5161b36cbb509e6bee666da4cd9dcd7c Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 14:49:22 -0500 Subject: [PATCH 04/16] Extract nodenv-package-json-file --- libexec/nodenv-package-json | 33 +++++++++++++++++------------- libexec/nodenv-package-json-file | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 14 deletions(-) create mode 100755 libexec/nodenv-package-json-file diff --git a/libexec/nodenv-package-json b/libexec/nodenv-package-json index e8d0b2e..58dd8e5 100755 --- a/libexec/nodenv-package-json +++ b/libexec/nodenv-package-json @@ -14,21 +14,25 @@ abort() { exit 1 } -find_package_json() { - local package_json root="$1" - while ! [[ "$root" =~ ^//[^/]*$ ]]; do - package_json="$root/package.json" - - if [ -f "$package_json" ] && [ -s "$package_json" ]; then - echo "$package_json" - return 0 - fi - [ -n "$root" ] || break - root="${root%/*}" +READLINK=$(type -p greadlink readlink | head -1) +[ -n "$READLINK" ] || abort "cannot find readlink - are you missing GNU coreutils?" + +abs_dirname() { + local cwd="$PWD" + local path="$1" + + while [ -n "$path" ]; do + cd "${path%/*}" + local name="${path##*/}" + path="$($READLINK "$name" || true)" done - return 1 + + pwd + cd "$cwd" } +bin_path="$(abs_dirname "$0")" + extract_expression() { version_regex='\["engines","node"\][[:space:]]*"([^"]*)"' # -b -n gives minimal output - see https://github.com/dominictarr/JSON.sh#options @@ -69,11 +73,12 @@ matching_version() { fi } -if ! package_json="$(find_package_json "$NODENV_DIR")"; then + +if ! NODENV_PACKAGE_JSON_FILE="$("$bin_path/nodenv-package-json-file")"; then abort "no package.json found for this directory" fi -if ! version_spec="$(extract_expression <"$package_json")"; then +if ! version_spec="$(extract_expression <"$NODENV_PACKAGE_JSON_FILE")"; then abort "no engine version configured for this package" fi diff --git a/libexec/nodenv-package-json-file b/libexec/nodenv-package-json-file new file mode 100755 index 0000000..62f4fac --- /dev/null +++ b/libexec/nodenv-package-json-file @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# Usage: package-file [] +# Summary: Detect the package.json that sets the current nodenv version +# Options: +# Directory from which to find package.json +# [default: ${NODENV_DIR:-PWD}] + +set -e +[ -n "$NODENV_DEBUG" ] && set -x + +target_dir="$1" + +find_package_json() { + local package_json root="$1" + while ! [[ "$root" =~ ^//[^/]*$ ]]; do + package_json="$root/package.json" + + if [ -f "$package_json" ] && [ -s "$package_json" ]; then + echo "$package_json" + return 0 + fi + [ -n "$root" ] || break + root="${root%/*}" + done + return 1 +} + +if [ -n "$target_dir" ]; then + find_package_json "$target_dir" +else + find_package_json "$NODENV_DIR" || { + [ "$NODENV_DIR" != "$PWD" ] && find_package_json "$PWD" + } +fi From 70d610fcf18be7004bce434af1d2f4c5e3aed235 Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 15:29:16 -0500 Subject: [PATCH 05/16] Separate tests specifically for package-json-file --- libexec/nodenv-package-json-file | 4 +- test/package-json-file.bats | 86 ++++++++++++++++++++++++++++++++ test/package-json.bats | 40 --------------- test/test_helper.bash | 1 + 4 files changed, 90 insertions(+), 41 deletions(-) create mode 100755 test/package-json-file.bats diff --git a/libexec/nodenv-package-json-file b/libexec/nodenv-package-json-file index 62f4fac..2c0e2be 100755 --- a/libexec/nodenv-package-json-file +++ b/libexec/nodenv-package-json-file @@ -16,7 +16,9 @@ find_package_json() { while ! [[ "$root" =~ ^//[^/]*$ ]]; do package_json="$root/package.json" - if [ -f "$package_json" ] && [ -s "$package_json" ]; then + if [ -f "$package_json" ] && + [ -r "$package_json" ] && + [ -s "$package_json" ]; then echo "$package_json" return 0 fi diff --git a/test/package-json-file.bats b/test/package-json-file.bats new file mode 100755 index 0000000..bbe2d26 --- /dev/null +++ b/test/package-json-file.bats @@ -0,0 +1,86 @@ +#!/usr/bin/env bats + +load test_helper + +nodenv_package_json_file="$BATS_TEST_DIRNAME/../libexec/nodenv-package-json-file" + +@test 'Looks in provided directory' { + in_example_package + cd .. + + run "$nodenv_package_json_file" "$EXAMPLE_PACKAGE_DIR" + + assert_success + assert_output "$EXAMPLE_PACKAGE_DIR/package.json" +} + +@test 'Looks in $NODENV_DIR by default' { + in_example_package + cd .. + + NODENV_DIR="$EXAMPLE_PACKAGE_DIR" run "$nodenv_package_json_file" + + assert_success + assert_output "$EXAMPLE_PACKAGE_DIR/package.json" +} + +@test 'Falls back to $PWD' { + in_example_package + + run "$nodenv_package_json_file" + + assert_success + assert_output "$EXAMPLE_PACKAGE_DIR/package.json" +} + +@test 'Works up the directory hierarchy' { + in_example_package + mkdir -p "sub/dir" + cd "sub/dir" + + run "$nodenv_package_json_file" + + assert_success + assert_output "$EXAMPLE_PACKAGE_DIR/package.json" +} + +@test 'Exits non-zero when missing package.json' { + in_example_package + rm package.json + + run "$nodenv_package_json_file" + + assert_failure + refute_output +} + +@test 'Treats non-readable same as missing' { + in_example_package + chmod -r package.json + + run "$nodenv_package_json_file" + + assert_failure + refute_output +} + +@test 'Treats non-file same as missing' { + in_example_package + rm package.json + mkdir package.json + + run "$nodenv_package_json_file" + + assert_failure + refute_output +} + +@test 'Treats empty same as missing' { + in_example_package + > package.json + + run "$nodenv_package_json_file" + + assert_failure + refute_output +} diff --git a/test/package-json.bats b/test/package-json.bats index cdd7a55..c08536f 100755 --- a/test/package-json.bats +++ b/test/package-json.bats @@ -35,46 +35,6 @@ load test_helper assert_output 'package-json-engine: no engine version configured for this package' } -@test 'Handles missing package.json' { - in_example_package - - run nodenv package-json - - assert_failure - assert_output 'package-json-engine: no package.json found for this directory' -} - -@test 'Handles unreadable package.json' { - in_example_package - touch package.json - chmod -r package.json - - run nodenv package-json - - assert_failure - assert_output 'package-json-engine: no package.json found for this directory' -} - -@test 'Handles non-file package.json' { - in_example_package - mkdir package.json - - run nodenv package-json - - assert_failure - assert_output 'package-json-engine: no package.json found for this directory' -} - -@test 'Handles empty package.json' { - in_example_package - - # empty - touch package.json - run nodenv package-json - assert_failure - assert_output 'package-json-engine: no package.json found for this directory' -} - @test 'Handles malformed package.json' { in_example_package diff --git a/test/test_helper.bash b/test/test_helper.bash index b03d60e..f068708 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -27,6 +27,7 @@ teardown() { in_example_package() { cd "$EXAMPLE_PACKAGE_DIR" || return 1 + echo '{}' > package.json } in_package_for_engine() { From d6c01f60097385eb9dca90df3a2f5f776f6ca0c3 Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 16:17:23 -0500 Subject: [PATCH 06/16] fixup! Rename nodenv-package-json --- etc/nodenv.d/version-name/package-json-engine.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/nodenv.d/version-name/package-json-engine.bash b/etc/nodenv.d/version-name/package-json-engine.bash index d792170..bc50ef8 100644 --- a/etc/nodenv.d/version-name/package-json-engine.bash +++ b/etc/nodenv.d/version-name/package-json-engine.bash @@ -3,7 +3,7 @@ if [ -n "$(nodenv-sh-shell 2>/dev/null)" ] || return fi -if NODENV_PACKAGE_JSON_VERSION=$(nodenv-package 2>/dev/null) && +if NODENV_PACKAGE_JSON_VERSION=$(nodenv-package-json 2>/dev/null) && [ -n "$NODENV_PACKAGE_JSON_VERSION" ]; then export NODENV_VERSION=$NODENV_PACKAGE_JSON_VERSION fi From 94f6a85286a076a074f1272fc456deaec79f989d Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 16:19:41 -0500 Subject: [PATCH 07/16] Don't export since this script is sourced --- etc/nodenv.d/version-name/package-json-engine.bash | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/etc/nodenv.d/version-name/package-json-engine.bash b/etc/nodenv.d/version-name/package-json-engine.bash index bc50ef8..a2b09e9 100644 --- a/etc/nodenv.d/version-name/package-json-engine.bash +++ b/etc/nodenv.d/version-name/package-json-engine.bash @@ -5,5 +5,6 @@ fi if NODENV_PACKAGE_JSON_VERSION=$(nodenv-package-json 2>/dev/null) && [ -n "$NODENV_PACKAGE_JSON_VERSION" ]; then - export NODENV_VERSION=$NODENV_PACKAGE_JSON_VERSION + # shellcheck disable=2034 + NODENV_VERSION=$NODENV_PACKAGE_JSON_VERSION fi From a41b9f1be4c3435cda44ec63164b3ed20a27b6ae Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 16:58:09 -0500 Subject: [PATCH 08/16] Extract reading of version_spec to own script --- libexec/nodenv-package-json | 12 +---- libexec/nodenv-package-json-file-read | 26 +++++++++++ test/package-json-file-read.bats | 67 +++++++++++++++++++++++++++ test/package-json.bats | 47 ------------------- 4 files changed, 94 insertions(+), 58 deletions(-) create mode 100755 libexec/nodenv-package-json-file-read create mode 100755 test/package-json-file-read.bats diff --git a/libexec/nodenv-package-json b/libexec/nodenv-package-json index 58dd8e5..ad72c3b 100755 --- a/libexec/nodenv-package-json +++ b/libexec/nodenv-package-json @@ -33,16 +33,6 @@ abs_dirname() { bin_path="$(abs_dirname "$0")" -extract_expression() { - version_regex='\["engines","node"\][[:space:]]*"([^"]*)"' - # -b -n gives minimal output - see https://github.com/dominictarr/JSON.sh#options - if [[ $(JSON.sh -b -n 2>/dev/null) =~ $version_regex ]]; then - echo "${BASH_REMATCH[1]}" - else - return 1 - fi -} - matching_version() { local version_spec=$1 local -a installed_versions @@ -78,7 +68,7 @@ if ! NODENV_PACKAGE_JSON_FILE="$("$bin_path/nodenv-package-json-file")"; then abort "no package.json found for this directory" fi -if ! version_spec="$(extract_expression <"$NODENV_PACKAGE_JSON_FILE")"; then +if ! version_spec="$("$bin_path/nodenv-package-json-file-read" "$NODENV_PACKAGE_JSON_FILE")"; then abort "no engine version configured for this package" fi diff --git a/libexec/nodenv-package-json-file-read b/libexec/nodenv-package-json-file-read new file mode 100755 index 0000000..ddc39ba --- /dev/null +++ b/libexec/nodenv-package-json-file-read @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Usage: nodenv package-json-file-read +# +# Summary: Show the engines version-spec from package.json +# Options: +# Package.json file to read from +# + +set -e +[ -n "$NODENV_DEBUG" ] && set -x + +PACKAGE_JSON_FILE=$1 + +[ -f "$PACKAGE_JSON_FILE" ] || exit 1 + +extract_expression() { + local version_regex='\["engines","node"\][[:space:]]*"([^"]*)"' + # -b -n gives minimal output - see https://github.com/dominictarr/JSON.sh#options + if [[ $(JSON.sh -b -n 2>/dev/null) =~ $version_regex ]]; then + echo "${BASH_REMATCH[1]}" + else + return 1 + fi +} + +extract_expression < "$PACKAGE_JSON_FILE" diff --git a/test/package-json-file-read.bats b/test/package-json-file-read.bats new file mode 100755 index 0000000..dba56b8 --- /dev/null +++ b/test/package-json-file-read.bats @@ -0,0 +1,67 @@ +#!/usr/bin/env bats + +load test_helper + +read="$BATS_TEST_DIRNAME/../libexec/nodenv-package-json-file-read" + +@test 'Prints the version spec from engines.node' { + in_example_package + echo '{ "engines": { "node": ">= 8" } }' > package.json + + run $read package.json + + assert_success + assert_output '>= 8' +} + +@test 'Does not match arbitrary "node" key in package.json' { + in_package_with_babel_env + + run $read package.json + + assert_failure + refute_output +} + +@test 'Errors with non-JSON file' { + in_example_package + echo "foo" > package.json + + run $read package.json + + assert_failure + refute_output +} + +@test 'Errors with malformed JSON file' { + in_example_package + echo "{" > package.json + + run $read package.json + + assert_failure + refute_output +} + +@test 'Prints the right "node" key' { + in_example_package + cat << JSON > package.json +{ + "engines": { + "node": "4.2.1" + }, + "presets": [ + ["env", { + "targets": { + "node": "current" + } + }] + ] +} +JSON + + run $read package.json + + assert_success + assert_output '4.2.1' +} diff --git a/test/package-json.bats b/test/package-json.bats index c08536f..d0d41ec 100755 --- a/test/package-json.bats +++ b/test/package-json.bats @@ -25,50 +25,3 @@ load test_helper assert_failure assert_output "package-json-engine: no version found satisfying \`^1.0.0'" } - -@test 'Does not match arbitrary "node" key in package.json' { - in_package_with_babel_env - - run nodenv package-json - - assert_failure - assert_output 'package-json-engine: no engine version configured for this package' -} - -@test 'Handles malformed package.json' { - in_example_package - - # non json - echo "foo" > package.json - run nodenv package-json - assert_failure - assert_output 'package-json-engine: no engine version configured for this package' - - # malformed - echo "{" > package.json - run nodenv package-json - assert_failure - assert_output 'package-json-engine: no engine version configured for this package' -} - -@test 'Handles multiple occurrences of "node" key' { - in_example_package - cat << JSON > package.json -{ - "engines": { - "node": "4.2.1" - }, - "presets": [ - ["env", { - "targets": { - "node": "current" - } - }] - ] -} -JSON - - run nodenv package-json - assert_success - assert_output '4.2.1' -} From 523406e959a8c504c20d05b9328d88c6d8fc6625 Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 22:43:33 -0500 Subject: [PATCH 09/16] Rewrite version-origin hook and tests --- .../version-origin/package-json-engine.bash | 34 ++++++++++++++--- test/version-origin-hook.bats | 38 +++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) create mode 100755 test/version-origin-hook.bats diff --git a/etc/nodenv.d/version-origin/package-json-engine.bash b/etc/nodenv.d/version-origin/package-json-engine.bash index 97c0afd..408fd15 100644 --- a/etc/nodenv.d/version-origin/package-json-engine.bash +++ b/etc/nodenv.d/version-origin/package-json-engine.bash @@ -1,9 +1,31 @@ -#!/bin/bash +if [ -n "$(nodenv-sh-shell 2>/dev/null)" ] || + [ -n "$(nodenv-local 2>/dev/null)" ]; then + return +fi + +READLINK=$(type -p greadlink readlink | head -1) +[ -n "$READLINK" ] || return 1 + +abs_dirname() { + local cwd="$PWD" + local path="$1" + + while [ -n "$path" ]; do + cd "${path%/*}" + local name="${path##*/}" + path="$($READLINK "$name" || true)" + done + + pwd + cd "$cwd" +} -# shellcheck source=libexec/nodenv-package-json-engine -source "$(plugin_root)/libexec/nodenv-package-json-engine" +bin_path="$(abs_dirname "${BASH_SOURCE[0]}")/../../../libexec" -ENGINES_EXPRESSION=$(get_expression_respecting_precedence); -if [ -n "$ENGINES_EXPRESSION" ]; then - export NODENV_VERSION_ORIGIN="package-json-engine matching $ENGINES_EXPRESSION" +if NODENV_PACKAGE_JSON_VERSION=$(nodenv-package-json 2>/dev/null) && + [ -n "$NODENV_PACKAGE_JSON_VERSION" ]; then + NODENV_PACKAGE_JSON_FILE=$("$bin_path/nodenv-package-json-file") + NODENV_PACKAGE_JSON_SPEC=$("$bin_path/nodenv-package-json-file-read" "$NODENV_PACKAGE_JSON_FILE") + # shellcheck disable=2034 + NODENV_VERSION_ORIGIN="satisfying \`$NODENV_PACKAGE_JSON_SPEC' from $NODENV_PACKAGE_JSON_FILE#engines.node" fi diff --git a/test/version-origin-hook.bats b/test/version-origin-hook.bats new file mode 100755 index 0000000..5fdba2e --- /dev/null +++ b/test/version-origin-hook.bats @@ -0,0 +1,38 @@ +#!/usr/bin/env bats + +load test_helper + +@test 'Prefers nodenv-shell over package.json' { + in_package_for_engine 4.2.1 + + NODENV_VERSION=5.0.0 run nodenv version-origin + assert_success + assert_output 'NODENV_VERSION environment variable' +} + +@test 'Prefers nodenv-local over package.json' { + in_package_for_engine 4.2.1 + nodenv local 5.0.0 + + run nodenv version-origin + assert_success + assert_output "$PWD/.node-version" +} + +@test 'Prefers package.json over nodenv-global' { + in_package_for_engine '>= 4' + nodenv global 5.0.0 + + run nodenv version-origin + assert_success + assert_output "satisfying \`>= 4' from $PWD/package.json#engines.node" +} + +@test 'Mutes error output from nodenv-package' { + in_example_package + + run nodenv version-origin + + assert_success + assert_output "$NODENV_ROOT/version" +} From c32b78fcc1e3b1793ce8b01b1f0676f4c9ce1142 Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 23:02:17 -0500 Subject: [PATCH 10/16] Remove old tests --- test/nodenv-package-json-engine.bats | 149 --------------------------- test/package-json-file-read.bats | 13 ++- test/test_helper.bash | 15 --- test/version-name-hook.bats | 10 +- 4 files changed, 17 insertions(+), 170 deletions(-) delete mode 100755 test/nodenv-package-json-engine.bats diff --git a/test/nodenv-package-json-engine.bats b/test/nodenv-package-json-engine.bats deleted file mode 100755 index 47d91d8..0000000 --- a/test/nodenv-package-json-engine.bats +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bats - -load test_helper - -@test 'Recognizes simple node version specified in package.json engines' { - in_package_for_engine 4.2.1 - - run nodenv version - assert_success - assert_output '4.2.1 (set by package-json-engine matching 4.2.1)' -} - -@test 'Prefers the greatest installed version matching a range' { - in_package_for_engine '^4.0.0' - - run nodenv version - assert_success - assert_output '4.2.1 (set by package-json-engine matching ^4.0.0)' -} - -@test 'Ignores non-matching installed versions' { - in_package_for_engine '^1.0.0' - - run nodenv version - # note the command completes successfully - assert_success - assert_output - <<-MSG -package-json-engine: version satisfying \`^1.0.0' not installed - (set by package-json-engine matching ^1.0.0) -MSG -} - -@test 'Prefers nodenv-local over package.json' { - in_package_for_engine 4.2.1 - nodenv local 5.0.0 - - run nodenv version - assert_success - assert_output "5.0.0 (set by $PWD/.node-version)" -} - -@test 'Prefers nodenv-shell over package.json' { - in_package_for_engine 4.2.1 - - NODENV_VERSION=5.0.0 run nodenv version - assert_success - assert_output "5.0.0 (set by NODENV_VERSION environment variable)" -} - -@test 'Prefers package.json over nodenv-global' { - in_package_for_engine 4.2.1 - nodenv global 5.0.0 - - run nodenv version-name - assert_success - assert_output '4.2.1' -} - -@test 'Is not confused by nodenv-shell shadowing nodenv-global' { - in_package_for_engine 4.2.1 - nodenv global 5.0.0 - - NODENV_VERSION=5.0.0 run nodenv version - assert_success - assert_output "5.0.0 (set by NODENV_VERSION environment variable)" -} - -@test 'Does not match arbitrary "node" key in package.json' { - in_package_with_babel_env - - run nodenv version-name - - assert_success - assert_output 'system' -} - -@test 'Handles missing package.json' { - in_example_package - - run nodenv version-name - - assert_success - assert_output 'system' -} - -@test 'Does not fail with unreadable package.json' { - in_example_package - touch package.json - chmod -r package.json - - run nodenv version-name - - assert_success - assert_output 'system' -} - -@test 'Does not fail with non-file package.json' { - in_example_package - mkdir package.json - - run nodenv version-name - - assert_success - assert_output 'system' -} - -@test 'Does not fail with empty or malformed package.json' { - in_example_package - - # empty - touch package.json - run nodenv version-name - assert_success - assert_output 'system' - - # non json - echo "foo" > package.json - run nodenv version-name - assert_success - assert_output 'system' - - # malformed - echo "{" > package.json - run nodenv version-name - assert_success - assert_output 'system' -} - -@test 'Handles multiple occurrences of "node" key' { - in_example_package - cat << JSON > package.json -{ - "engines": { - "node": "4.2.1" - }, - "presets": [ - ["env", { - "targets": { - "node": "current" - } - }] - ] -} -JSON - - run nodenv version-name - assert_success - assert_output '4.2.1' -} diff --git a/test/package-json-file-read.bats b/test/package-json-file-read.bats index dba56b8..cef6a8e 100755 --- a/test/package-json-file-read.bats +++ b/test/package-json-file-read.bats @@ -15,7 +15,18 @@ read="$BATS_TEST_DIRNAME/../libexec/nodenv-package-json-file-read" } @test 'Does not match arbitrary "node" key in package.json' { - in_package_with_babel_env + in_example_package + cat << JSON > package.json +{ + "presets": [ + ["env", { + "targets": { + "node": "current" + } + }] + ] +} +JSON run $read package.json diff --git a/test/test_helper.bash b/test/test_helper.bash index f068708..a2d7011 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -40,18 +40,3 @@ in_package_for_engine() { } JSON } - -in_package_with_babel_env() { - in_example_package - cat << JSON > package.json -{ - "presets": [ - ["env", { - "targets": { - "node": "current" - } - }] - ] -} -JSON -} diff --git a/test/version-name-hook.bats b/test/version-name-hook.bats index 045d710..c98c86b 100755 --- a/test/version-name-hook.bats +++ b/test/version-name-hook.bats @@ -2,19 +2,19 @@ load test_helper -@test 'Prefers nodenv-local over package.json' { +@test 'Prefers nodenv-shell over package.json' { in_package_for_engine 4.2.1 - nodenv local 5.0.0 - run nodenv version-name + NODENV_VERSION=5.0.0 run nodenv version-name assert_success assert_output '5.0.0' } -@test 'Prefers nodenv-shell over package.json' { +@test 'Prefers nodenv-local over package.json' { in_package_for_engine 4.2.1 + nodenv local 5.0.0 - NODENV_VERSION=5.0.0 run nodenv version-name + run nodenv version-name assert_success assert_output '5.0.0' } From c85b5b2ff7ed15e9ccd16aed22367808150a79ee Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 23:03:46 -0500 Subject: [PATCH 11/16] remove old executables --- bin/plugin_root | 26 -------- libexec/nodenv-package-json-engine | 97 ------------------------------ 2 files changed, 123 deletions(-) delete mode 100755 bin/plugin_root delete mode 100755 libexec/nodenv-package-json-engine diff --git a/bin/plugin_root b/bin/plugin_root deleted file mode 100755 index d76a072..0000000 --- a/bin/plugin_root +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# -# Writes the full path to the root of this plugin to standard out. -# Use to source or run files in libexec/ -# -# Adapted from: -# http://www.ostricher.com/2014/10/the-right-way-to-get-the-directory-of-a-bash-script/ -# http://stackoverflow.com/a/12694189/407845 -src="${BASH_SOURCE[0]}" - -# while $src is a symlink, resolve it -while [ -h "$src" ]; do - dir="${src%/*}" - src="$( readlink "$src" )" - - # If $src was a relative symlink (so no "/" as prefix), - # need to resolve it relative to the symlink base directory - [[ $src != /* ]] && src="$dir/$src" -done -dir="${src%/*}" - -if [ -d "$dir" ]; then - echo "$dir/.." -else - echo "$PWD/.." -fi diff --git a/libexec/nodenv-package-json-engine b/libexec/nodenv-package-json-engine deleted file mode 100755 index 58f8db0..0000000 --- a/libexec/nodenv-package-json-engine +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash -# -# If a custom Node version is not already defined, we look -# for a Node version semver expressing in the current tree's package.json. -# If we find a fixed version, we print it out. If we find a range we -# test the installed versions against that range and print the -# greatest matching version. - -# Vendored scripts: -JSON_SH="$(plugin_root)/node_modules/JSON.sh/JSON.sh" -SEMVER="$(plugin_root)/node_modules/sh-semver/semver.sh" - -# Exits non-zero if this plugin should yield precedence -# Gives precedence to local and shell versions. -# Takes precedence over global version. -package_json_has_precedence() { - if [[ ( -z "$(nodenv local 2>/dev/null)" ) - && ( -z "$(nodenv sh-shell 2>/dev/null)" ) ]]; then - return; - else - return 1; - fi -} - -find_package_json_path() { - local package_json root="$1" - while [ -n "$root" ]; do - package_json="$root/package.json" - - if [ -r "$package_json" ] && [ -f "$package_json" ]; then - echo "$package_json" - return - fi - root="${root%/*}" - done -} - -extract_version_from_package_json() { - package_json_path="$1" - version_regex='\["engines","node"\][[:space:]]*"([^"]*)"' - # -b -n gives minimal output - see https://github.com/dominictarr/JSON.sh#options - [[ $("$JSON_SH" -b -n < "$package_json_path" 2>/dev/null) =~ $version_regex ]] - echo "${BASH_REMATCH[1]}" -} - -find_installed_version_matching_expression() { - version_expression="$1" - local -a installed_versions - while IFS= read -r v; do - installed_versions+=( "$v" ) - done < <(nodenv versions --bare --skip-aliases | grep -e '^[[:digit:]]') - - local fast_guess - fast_guess=$("$SEMVER" -r "$version_expression" "${installed_versions[@]:${#installed_versions[@]}-1}" | tail -n 1) - - # Most #engine version specs just specify a baseline version, - # which means most likely, the highest installed version will satisfy - # This does a first pass with just that single version in hopes it satisfies. - # If so, we can avoid the cost of sh-semver sorting and validating across - # all the installed versions. - if [ -n "$fast_guess" ]; then - echo "$fast_guess" - return - fi - - "$SEMVER" -r "$version_expression" "${installed_versions[@]}" | tail -n 1 -} - -get_version_respecting_precedence() { - if ! package_json_has_precedence; then return; fi - - package_json_path=$(find_package_json_path "$PWD") - if [ ! -e "$package_json_path" ]; then return; fi - - version_expression=$( - extract_version_from_package_json "$package_json_path" - ) - if [ -z "$version_expression" ]; then return; fi - - version=$( - find_installed_version_matching_expression "$version_expression" - ) - if [ -z "$version" ]; then return 1; fi - echo "$version" -} - -get_expression_respecting_precedence() { - if ! package_json_has_precedence; then return; fi - - package_json_path=$(find_package_json_path "$PWD") - if [ ! -e "$package_json_path" ]; then return; fi - - version_expression=$( - extract_version_from_package_json "$package_json_path" - ) - echo "$version_expression" -} From 3661e8ad07dcf6d165ef56ddb05f17d5727cfa1a Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 23:09:12 -0500 Subject: [PATCH 12/16] Remove bin from linting Both the 3rd party deps are symlinked to bin, but we don't actually want to lint them. Meanwhile, the only files that exist in bin that _should_ be linted, are symlinks to libexec. So we can just lint them there. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de3f77b..ab4a58e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "libexec" ], "scripts": { - "lint": "git ls-files bin etc libexec test/*.bash | xargs shellcheck", + "lint": "git ls-files etc libexec test/*.bash | xargs shellcheck", "test": "bats ${CI:+--tap} test", "posttest": "npm run lint", "postversion": "npm publish", From e8a3c6b5d5a4c487eaa4215288a7756b4be57a22 Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 23:10:24 -0500 Subject: [PATCH 13/16] return nonzero if we can't find bin_path --- etc/nodenv.d/version-origin/package-json-engine.bash | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/etc/nodenv.d/version-origin/package-json-engine.bash b/etc/nodenv.d/version-origin/package-json-engine.bash index 408fd15..3d0cd88 100644 --- a/etc/nodenv.d/version-origin/package-json-engine.bash +++ b/etc/nodenv.d/version-origin/package-json-engine.bash @@ -11,17 +11,19 @@ abs_dirname() { local path="$1" while [ -n "$path" ]; do - cd "${path%/*}" + cd "${path%/*}" || return 1 local name="${path##*/}" path="$($READLINK "$name" || true)" done pwd - cd "$cwd" + cd "$cwd" || return 1 } bin_path="$(abs_dirname "${BASH_SOURCE[0]}")/../../../libexec" +[ -d "$bin_path" ] || return 1 + if NODENV_PACKAGE_JSON_VERSION=$(nodenv-package-json 2>/dev/null) && [ -n "$NODENV_PACKAGE_JSON_VERSION" ]; then NODENV_PACKAGE_JSON_FILE=$("$bin_path/nodenv-package-json-file") From 57e0df4037ac689127cf3fe45dd27c3ca66f0711 Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Sun, 20 Jan 2019 23:15:52 -0500 Subject: [PATCH 14/16] Explicit bin npm won't include any symlinks in the tarball, so we need to explicitly include the main bin. Pointing to libexec allows npm to make the symlink from node_modules/.bin/ -> libexec/ The other bins aren't necessary because they are provided by npm automaticaly (and included in path that's to npm). (Assuming this is installed globally, which would ensure the bins are in PATH.) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ab4a58e..5c2aa8a 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "bugs": { "url": "https://github.com/nodenv/nodenv-package-json-engine/issues" }, + "bin": "libexec/nodenv-package-json", "directories": { "bin": "bin", "test": "test" From a22ac68ac50727ff11ba1f32bd5562f66ad54daa Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Mon, 21 Jan 2019 11:49:02 -0500 Subject: [PATCH 15/16] Bats expands vars in test names --- test/package-json-file.bats | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/package-json-file.bats b/test/package-json-file.bats index bbe2d26..b215a42 100755 --- a/test/package-json-file.bats +++ b/test/package-json-file.bats @@ -14,7 +14,7 @@ nodenv_package_json_file="$BATS_TEST_DIRNAME/../libexec/nodenv-package-json-file assert_output "$EXAMPLE_PACKAGE_DIR/package.json" } -@test 'Looks in $NODENV_DIR by default' { +@test 'Looks in NODENV_DIR by default' { in_example_package cd .. @@ -24,7 +24,7 @@ nodenv_package_json_file="$BATS_TEST_DIRNAME/../libexec/nodenv-package-json-file assert_output "$EXAMPLE_PACKAGE_DIR/package.json" } -@test 'Falls back to $PWD' { +@test 'Falls back to PWD' { in_example_package run "$nodenv_package_json_file" From fcda693089939b8f34b955e3d57fcb4a2098823d Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Mon, 21 Jan 2019 11:49:25 -0500 Subject: [PATCH 16/16] Unset other nodenv vars These vars are set by nodenv itself, so they'll likely only be set if the tests were run via nodenv (as is the case with `npm t`). Though other maintainers may have some of these set in the shell profile, so we need to unset them regardless. --- test/test_helper.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_helper.bash b/test/test_helper.bash index a2d7011..a0bfc1f 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -5,7 +5,7 @@ load '../node_modules/bats-assert/load' setup() { # common nodenv setup - unset NODENV_VERSION + unset NODENV_VERSION NODENV_DIR NODENV_HOOK_PATH local node_modules_bin=$BATS_TEST_DIRNAME/../node_modules/.bin